chore: remove chrome-extension from tracking and add to gitignore

This commit is contained in:
wizardchen
2026-03-16 21:34:03 +08:00
parent 56a0840aa4
commit 1c752ff059
18 changed files with 2 additions and 9715 deletions
+2 -1
View File
@@ -41,4 +41,5 @@ web/
**/__pycache__
/scripts/scale_dev_jobs.sh
server
frontend/.vite
frontend/.vite
frontend/chrome-extension/
@@ -1,739 +0,0 @@
// 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 = {};
// === 右键菜单 ===
// 防止并发注册
var _menuSetupInProgress = false;
function setupContextMenus() {
if (_menuSetupInProgress) return;
_menuSetupInProgress = true;
chrome.contextMenus.removeAll(function () {
void chrome.runtime.lastError; // 清除可能的 lastError
// 保存选中文字
chrome.contextMenus.create({
id: 'ka-save-selection',
title: '保存到快速笔记 5.0',
contexts: ['selection']
}, function () { void chrome.runtime.lastError; });
// 用选中文字提问
chrome.contextMenus.create({
id: 'ka-ask-selection',
title: '使用知识助理提问',
contexts: ['selection']
}, function () { void chrome.runtime.lastError; });
// 保存图片到快速笔记
chrome.contextMenus.create({
id: 'ka-save-image',
title: '保存图片到快速笔记',
contexts: ['image']
}, function () { void chrome.runtime.lastError; });
_menuSetupInProgress = false;
// 根据登录状态更新菜单文案
updateContextMenuTitle();
});
}
// 插件安装/更新时注册
chrome.runtime.onInstalled.addListener(function () {
setupContextMenus();
});
// Service Worker 每次启动时也注册(确保重载后菜单存在)
setupContextMenus();
// 根据登录类型动态更新右键菜单中"提问"的文案
async function updateContextMenuTitle() {
var data = await chrome.storage.local.get('ka_auth');
var auth = data.ka_auth;
var askTitle = '使用知识助理提问';
if (auth && auth.type === 'wk') {
askTitle = '使用 WeKnora 提问';
} else if (auth && auth.type === 'ka') {
askTitle = '使用知识助理提问';
}
chrome.contextMenus.update('ka-ask-selection', { title: askTitle }, function () {
void chrome.runtime.lastError; // 菜单还未创建时静默忽略
});
}
chrome.contextMenus.onClicked.addListener(function (info, tab) {
// 处理图片保存(不需要 selectionText)
if (info.menuItemId === 'ka-save-image') {
var imgUrl = info.srcUrl;
if (!imgUrl) return;
var title = '图片收藏 - ' + (tab.title || '未知页面');
var clip = {
type: 'image-clip',
content: '![图片](' + imgUrl + ')',
title: title,
meta: { url: tab.url || '', title: tab.title || '', imageUrl: imgUrl }
};
saveClip(clip).then(function () {
if (tab && tab.id) {
chrome.tabs.sendMessage(tab.id, {
type: 'SHOW_NOTIFICATION',
payload: { msg: '图片已保存到快速笔记', status: 'success' }
}).catch(function () {});
}
});
return;
}
if (!info.selectionText) return;
if (info.menuItemId === 'ka-save-selection') {
var title = '选中文本 - ' + (tab.title || '未知页面');
var clip = {
type: 'select-clip',
content: info.selectionText,
title: title,
meta: { url: tab.url || '', title: tab.title || '' }
};
saveClip(clip).then(function () {
if (tab && tab.id) {
chrome.tabs.sendMessage(tab.id, {
type: 'SHOW_NOTIFICATION',
payload: { msg: '已保存到快速笔记', status: 'success' }
}).catch(function () {});
}
});
}
if (info.menuItemId === 'ka-ask-selection') {
// 打开侧边栏并将选中文字作为问题
if (tab && tab.id) {
chrome.storage.local.set({
ka_pending_query: { query: info.selectionText, ts: Date.now() }
});
chrome.sidePanel.open({ tabId: tab.id }).catch(function () {});
}
}
});
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
handleMessage(msg, sender).then(function (result) {
sendResponse(result);
}).catch(function (err) {
sendResponse({ success: false, error: err.message || '未知错误' });
});
return true;
});
async function handleMessage(msg, sender) {
switch (msg.type) {
case 'GET_AUTH':
return getAuth();
case 'SET_AUTH':
return setAuth(msg.payload);
case 'CLEAR_AUTH':
return clearAuth();
case 'GET_CONFIG':
return getConfig();
case 'SET_CONFIG':
return setConfig(msg.payload);
case 'SAVE_NOTE':
return saveNote(msg.payload);
case 'GET_NOTES':
return getNotes();
case 'SAVE_CLIP':
return saveClip(msg.payload);
case 'GET_CLIPS':
return getClips();
case 'DELETE_CLIP':
return deleteClip(msg.payload);
case 'DELETE_NOTE':
return deleteNote(msg.payload);
case 'UPDATE_CLIP':
return updateClip(msg.payload);
case 'UPDATE_NOTE':
return updateNote(msg.payload);
case 'INJECT_SCRIPT':
return injectScript(msg.payload.tabId);
case 'ASK_WEKNORA':
// 打开侧边栏并传递选中的文字作为问题
if (sender && sender.tab && sender.tab.id) {
await chrome.sidePanel.open({ tabId: sender.tab.id });
// 存储待处理的问题,sidepanel 加载后会读取
await chrome.storage.local.set({
ka_pending_query: { query: msg.payload.text, ts: Date.now() }
});
}
return { success: true };
case 'SAVE_SELECTION':
// 从气泡保存选中文字
return saveClip(msg.payload);
case 'SAVE_IMAGE':
// 从气泡保存图片
return saveClip(msg.payload);
case 'CAPTURE_SCREENSHOT':
// 截取当前标签页可见区域
try {
var tabId = sender && sender.tab && sender.tab.id;
if (!tabId) return { success: false, error: '无法获取标签页' };
var dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'jpeg', quality: 90 });
return { success: true, dataUrl: dataUrl };
} 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: '未知消息类型' };
}
}
// === Auth ===
async function getAuth() {
var data = await chrome.storage.local.get('ka_auth');
return { success: true, data: data.ka_auth || null };
}
async function setAuth(auth) {
await chrome.storage.local.set({ ka_auth: auth });
updateContextMenuTitle();
return { success: true };
}
async function clearAuth() {
await chrome.storage.local.remove('ka_auth');
updateContextMenuTitle();
return { success: true };
}
// === Config (WeKnora) ===
async function getConfig() {
var data = await chrome.storage.local.get('ka_config');
return { success: true, data: data.ka_config || { baseUrl: '', apiKey: '' } };
}
async function setConfig(config) {
await chrome.storage.local.set({ ka_config: config });
return { success: true };
}
// === Notes (Markdown) ===
async function saveNote(note) {
var data = await chrome.storage.local.get('ka_notes');
var notes = data.ka_notes || [];
note.id = Date.now().toString();
note.createdAt = new Date().toISOString();
notes.unshift(note);
if (notes.length > 100) notes = notes.slice(0, 100);
await chrome.storage.local.set({ ka_notes: notes });
return { success: true, data: note };
}
async function getNotes() {
var data = await chrome.storage.local.get('ka_notes');
return { success: true, data: data.ka_notes || [] };
}
// === Clips (网页截取收藏) ===
async function saveClip(clip) {
try {
var data = await chrome.storage.local.get('ka_clips');
var clips = data.ka_clips || [];
// 如果传入了已有 id,说明是编辑已有笔记,执行更新而非新增
if (clip.id) {
var found = false;
for (var i = 0; i < clips.length; i++) {
if (clips[i].id === clip.id) {
// 保留原始创建时间和其他元数据,只更新内容相关字段
clips[i].content = clip.content;
if (clip.title) clips[i].title = clip.title;
if (clip.type) clips[i].type = clip.type;
clips[i].updatedAt = new Date().toISOString();
clip = clips[i]; // 返回完整的更新后记录
found = true;
break;
}
}
// 如果在 ka_clips 中没找到,再到 ka_notes 中查找并更新
if (!found) {
var notesData = await chrome.storage.local.get('ka_notes');
var notes = notesData.ka_notes || [];
for (var j = 0; j < notes.length; j++) {
if (notes[j].id === clip.id) {
notes[j].content = clip.content;
if (clip.title) notes[j].title = clip.title;
if (clip.type) notes[j].type = clip.type;
notes[j].updatedAt = new Date().toISOString();
clip = notes[j];
found = true;
await chrome.storage.local.set({ ka_notes: notes });
break;
}
}
}
if (found) {
await chrome.storage.local.set({ ka_clips: clips });
return { success: true, data: clip };
}
// 没找到原记录,当作新建处理(fallthrough)
}
// 新建记录
clip.id = Date.now().toString();
clip.createdAt = new Date().toISOString();
clips.unshift(clip);
if (clips.length > 200) clips = clips.slice(0, 200);
await chrome.storage.local.set({ ka_clips: clips });
return { success: true, data: clip };
} catch (err) {
// 如果保存失败(可能是截图太大),尝试去掉截图再保存
if (clip.screenshot) {
try {
delete clip.screenshot;
var data2 = await chrome.storage.local.get('ka_clips');
var clips2 = data2.ka_clips || [];
clips2.unshift(clip);
if (clips2.length > 200) clips2 = clips2.slice(0, 200);
await chrome.storage.local.set({ ka_clips: clips2 });
return { success: true, data: clip, warning: '截图过大已省略,仅保存文字' };
} catch (err2) {
return { success: false, error: '保存失败: ' + (err2.message || '存储空间不足') };
}
}
return { success: false, error: '保存失败: ' + (err.message || '未知错误') };
}
}
async function getClips() {
var data = await chrome.storage.local.get('ka_clips');
return { success: true, data: data.ka_clips || [] };
}
async function deleteClip(payload) {
var data = await chrome.storage.local.get('ka_clips');
var clips = data.ka_clips || [];
clips = clips.filter(function (c) { return c.id !== payload.id; });
await chrome.storage.local.set({ ka_clips: clips });
return { success: true };
}
async function deleteNote(payload) {
var data = await chrome.storage.local.get('ka_notes');
var notes = data.ka_notes || [];
notes = notes.filter(function (n) { return n.id !== payload.id; });
await chrome.storage.local.set({ ka_notes: notes });
return { success: true };
}
async function updateClip(payload) {
var data = await chrome.storage.local.get('ka_clips');
var clips = data.ka_clips || [];
var found = false;
for (var i = 0; i < clips.length; i++) {
if (clips[i].id === payload.id) {
clips[i].content = payload.content;
if (payload.title) clips[i].title = payload.title;
clips[i].updatedAt = new Date().toISOString();
found = true;
break;
}
}
if (!found) return { success: false, error: '未找到对应记录' };
await chrome.storage.local.set({ ka_clips: clips });
return { success: true };
}
async function updateNote(payload) {
var data = await chrome.storage.local.get('ka_notes');
var notes = data.ka_notes || [];
var found = false;
for (var i = 0; i < notes.length; i++) {
if (notes[i].id === payload.id) {
notes[i].content = payload.content;
if (payload.title) notes[i].title = payload.title;
notes[i].updatedAt = new Date().toISOString();
found = true;
break;
}
}
if (!found) return { success: false, error: '未找到对应记录' };
await chrome.storage.local.set({ ka_notes: notes });
return { success: true };
}
// === Inject content script ===
async function injectScript(tabId) {
try {
await chrome.scripting.executeScript({ target: { tabId: tabId }, files: ['content.js'] });
await chrome.scripting.insertCSS({ target: { tabId: tabId }, files: ['content.css'] });
return { success: true };
} catch (e) {
return { success: false, error: e.message };
}
}
// === Commands ===
chrome.commands.onCommand.addListener(async function (cmd, tab) {
if (!tab || !tab.id) return;
if (cmd === 'open-sidepanel') {
await chrome.sidePanel.open({ tabId: tab.id });
}
if (cmd === 'quick-ask') {
await chrome.sidePanel.open({ tabId: tab.id });
}
if (cmd === 'select-clip') {
// 快捷键触发选择剪藏:先确保 content script 已注入,再发消息
try {
await chrome.tabs.sendMessage(tab.id, { type: 'SELECT_CLIP' });
} catch (e) {
// content script 未注入,先注入再发送
try {
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
await chrome.scripting.insertCSS({ target: { tabId: tab.id }, files: ['content.css'] });
setTimeout(function () {
chrome.tabs.sendMessage(tab.id, { type: 'SELECT_CLIP' }).catch(function () {});
}, 300);
} catch (injectErr) {
// 无法注入的页面(如 chrome:// 页面),忽略
}
}
}
});
@@ -1,623 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>网页内容收藏 - 快速笔记 5.0</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root { --primary: #07C160; --primary-dark: #06ad54; --bg: #f5f6f7; --white: #fff; --text: #1a1a1a; --text2: #555; --text3: #999; --border: #eaeaea; --sidebar-w: 220px; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; }
/* 顶部导航 */
.top-bar { background: var(--white); border-bottom: 1px solid var(--border); padding: 0 32px; height: 56px; display: flex; align-items: center; gap: 16px; position: sticky; top: 0; z-index: 10; flex-shrink: 0; }
.top-bar-logo { display: flex; align-items: center; gap: 10px; }
.top-bar-logo-icon { width: 32px; height: 32px; background: var(--primary); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 15px; font-weight: 700; }
.top-bar-logo-icon svg { width: 18px; height: 18px; color: #fff; }
.top-bar-logo h1 { font-size: 16px; font-weight: 700; color: var(--text); }
.top-bar-divider { width: 1px; height: 20px; background: var(--border); }
.top-bar-title { font-size: 14px; color: var(--text2); }
.top-bar-spacer { flex: 1; }
.top-bar-actions { display: flex; gap: 8px; }
.top-bar-btn { height: 34px; padding: 0 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--white); font-size: 13px; color: var(--text2); cursor: pointer; display: flex; align-items: center; gap: 6px; transition: all 0.15s; }
.top-bar-btn:hover { border-color: var(--primary); color: var(--primary); }
.top-bar-btn svg { width: 14px; height: 14px; }
/* ===== 主体左右布局 ===== */
.main-layout { flex: 1; display: flex; overflow: hidden; }
/* 左侧分类栏 */
.sidebar { width: var(--sidebar-w); background: var(--white); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto; }
.sidebar-title { font-size: 11px; font-weight: 600; color: var(--text3); letter-spacing: 0.5px; text-transform: uppercase; padding: 20px 20px 10px; }
.sidebar-list { list-style: none; padding: 0 8px; flex: 1; }
.sidebar-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; cursor: pointer; font-size: 13px; color: var(--text2); transition: all 0.15s; margin-bottom: 2px; user-select: none; }
.sidebar-item:hover { background: #f5f5f5; color: var(--text); }
.sidebar-item.active { background: rgba(7, 193, 96, 0.08); color: var(--primary); font-weight: 600; }
.sidebar-item .si-icon { width: 32px; height: 32px; border-radius: 9px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: all 0.15s; }
.sidebar-item .si-icon svg { width: 16px; height: 16px; }
.sidebar-item.active .si-icon { background: rgba(7, 193, 96, 0.12); }
.sidebar-item .si-icon-all { background: #f0f0f0; color: var(--text3); }
.sidebar-item.active .si-icon-all { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
.sidebar-item .si-icon-select { background: #f0f7ff; color: #5b9bd5; }
.sidebar-item.active .si-icon-select { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
.sidebar-item .si-icon-smart { background: #fff7ed; color: #e8913a; }
.sidebar-item.active .si-icon-smart { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
.sidebar-item .si-icon-markdown { background: #f0f0ff; color: #6c6ce0; }
.sidebar-item.active .si-icon-markdown { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
.sidebar-item .si-label { flex: 1; }
.sidebar-item .si-count { font-size: 11px; color: var(--text3); background: #f0f0f0; padding: 1px 7px; border-radius: 10px; font-weight: 500; min-width: 22px; text-align: center; transition: all 0.15s; }
.sidebar-item.active .si-count { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
/* 右侧内容区 */
.content-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* 内容顶部工具栏 */
.content-toolbar { display: flex; align-items: center; gap: 12px; padding: 18px 28px 0; flex-shrink: 0; }
.content-toolbar-title { font-size: 18px; font-weight: 700; color: var(--text); }
.content-toolbar-count { font-size: 13px; color: var(--text3); font-weight: 400; margin-left: 2px; }
.content-toolbar-spacer { flex: 1; }
.content-search { height: 36px; width: 240px; border: 1px solid var(--border); border-radius: 8px; padding: 0 12px 0 34px; font-size: 13px; outline: none; transition: border-color 0.2s; background: var(--white) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") 12px center no-repeat; }
.content-search:focus { border-color: var(--primary); }
/* 内容滚动区 */
.content-scroll { flex: 1; overflow-y: auto; padding: 16px 28px 40px; }
/* 空状态 */
.empty-state { text-align: center; padding: 80px 20px; }
.empty-state-icon { width: 80px; height: 80px; background: #e8f8ee; border-radius: 20px; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; }
.empty-state-icon svg { width: 36px; height: 36px; color: var(--primary); }
.empty-state h2 { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
.empty-state p { font-size: 14px; color: var(--text3); line-height: 1.6; }
/* 收藏卡片列表 — 网格卡片布局 */
.clip-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
.clip-card {
background: var(--white);
border-radius: 16px;
border: none;
overflow: visible;
transition: all 0.25s ease;
display: flex;
flex-direction: column;
min-height: 200px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 1px rgba(0, 0, 0, 0.04);
}
.clip-card:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.05);
transform: translateY(-3px);
}
.clip-card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px 0;
}
.clip-card-favicon {
width: 20px;
height: 20px;
border-radius: 4px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.clip-card-favicon svg { width: 12px; height: 12px; color: var(--text3); }
.clip-card-title {
font-size: 14px;
font-weight: 600;
color: var(--text);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.clip-card-time {
font-size: 11px;
color: var(--text3);
white-space: nowrap;
}
.clip-card-body {
padding: 10px 20px 8px;
flex: 1;
min-height: 0;
overflow: hidden;
}
.clip-card-content {
font-size: 13px;
color: var(--text2);
line-height: 1.65;
max-height: 140px;
overflow: hidden;
position: relative;
white-space: pre-wrap;
word-break: break-word;
cursor: pointer;
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 65%, transparent 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 65%, transparent 100%);
}
.clip-card-content.expanded {
max-height: none;
-webkit-mask-image: none;
mask-image: none;
}
.clip-card-fade {
display: none;
}
.clip-card-footer {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 20px 12px;
margin-top: auto;
border-top: 1px solid #f0f0f0;
}
.clip-card-tag {
font-size: 10px;
padding: 2px 8px;
border-radius: 4px;
background: rgba(7, 193, 96, 0.08);
color: var(--primary);
flex-shrink: 0;
}
.clip-card-footer-spacer {
flex: 1;
}
.clip-card-actions { position: relative; flex-shrink: 0; }
.clip-more-btn {
width: 26px;
height: 26px;
border: none;
background: transparent;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text3);
transition: color 0.15s;
}
.clip-more-btn:hover { color: var(--text); background: #f5f5f5; }
.clip-more-btn svg { width: 18px; height: 18px; }
.clip-more-menu {
position: absolute;
bottom: 100%;
right: 0;
margin-bottom: 4px;
background: var(--white);
border-radius: 10px;
box-shadow: 0 6px 24px rgba(0,0,0,0.15);
padding: 4px 0;
min-width: 120px;
display: none;
z-index: 100;
}
.clip-more-menu.show { display: block; }
/* 打开菜单时提升当前卡片层级,避免被相邻卡片遮挡 */
.clip-card.menu-open {
z-index: 60;
position: relative;
}
.clip-more-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
font-size: 13px;
color: var(--text2);
cursor: pointer;
border: none;
background: none;
width: 100%;
text-align: left;
transition: all 0.12s;
white-space: nowrap;
}
.clip-more-menu-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.clip-more-menu-item.menu-delete:hover { background: #fff0f0; color: #e44; }
.clip-more-menu-item svg { width: 14px; height: 14px; flex-shrink: 0; }
/* 编辑态时卡片跨满一行 */
.clip-card.editing {
grid-column: 1 / -1;
min-height: auto;
}
/* === 编辑遮罩(黑色半透明蒙版) === */
.edit-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 300;
display: none;
animation: edit-overlay-in 0.25s ease;
backdrop-filter: blur(2px);
}
.edit-overlay.show { display: block; }
@keyframes edit-overlay-in { from { opacity: 0; } to { opacity: 1; } }
/* === 编辑浮层卡片 === */
.clip-card.editing {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 310;
width: 92vw;
max-width: 1100px;
max-height: 90vh;
overflow-y: auto;
border-radius: 18px;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.08);
border-color: transparent;
animation: edit-card-in 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
}
@keyframes edit-card-in {
from { opacity: 0; transform: translate(-50%, -46%) scale(0.95); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
.clip-card.editing:hover {
transform: translate(-50%, -50%);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.08);
}
/* === 选择剪藏卡片 — 截图缩略图 === */
.clip-screenshot-thumb {
width: calc(100% - 28px);
margin: 6px 14px 0;
border-radius: 12px;
display: block;
object-fit: cover;
max-height: 180px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.05);
}
.clip-screenshot-thumb:hover {
opacity: 0.92;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 1px rgba(0, 0, 0, 0.06);
}
/* === 选择剪藏编辑态 — 左图右编辑器双栏 === */
.clip-edit-split {
display: flex;
gap: 16px;
padding: 16px 18px 0;
}
.clip-edit-split-left {
flex: 0 0 45%;
max-width: 45%;
display: flex;
flex-direction: column;
gap: 8px;
}
.clip-edit-split-left img {
width: 100%;
border-radius: 12px;
border: none;
object-fit: contain;
max-height: 500px;
background: #f8f8f8;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08), 0 0 1px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.2s ease;
}
.clip-edit-split-left img:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 1px rgba(0, 0, 0, 0.06);
}
.clip-edit-split-left .split-label {
font-size: 11px;
color: var(--text3);
text-align: center;
}
.clip-edit-split-right {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.clip-edit-split-right .clip-edit-card {
flex: 1;
display: flex;
flex-direction: column;
}
.clip-edit-split-right .clip-edit-textarea {
flex: 1;
min-height: 360px;
}
/* 速记标签色 */
.clip-card-tag.tag-markdown { background: rgba(59,125,216,0.1); color: #3b7dd8; }
.clip-card-tag.tag-image { background: rgba(233,89,80,0.1); color: #e95950; }
.clip-card-tag.tag-text { background: rgba(134,142,150,0.1); color: #868e96; }
/* 图片分类图标 */
.sidebar-item .si-icon-image { background: #fef0f0; color: #e95950; }
.sidebar-item.active .si-icon-image { background: rgba(7, 193, 96, 0.12); color: var(--primary); }
/* Markdown 渲染样式 */
.md-rendered { white-space: normal; }
.md-rendered h1 { font-size: 20px; font-weight: 700; margin: 0 0 10px; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 6px; }
.md-rendered h2 { font-size: 17px; font-weight: 700; margin: 14px 0 8px; color: var(--text); }
.md-rendered h3 { font-size: 15px; font-weight: 600; margin: 12px 0 6px; color: var(--text); }
.md-rendered h4 { font-size: 14px; font-weight: 600; margin: 10px 0 4px; color: var(--text); }
.md-rendered p { margin: 6px 0; line-height: 1.7; }
.md-rendered ul, .md-rendered ol { margin: 6px 0; padding-left: 20px; }
.md-rendered li { margin: 3px 0; line-height: 1.6; }
.md-rendered code { font-family: "SF Mono","Fira Code","Menlo",monospace; background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; color: #d63384; }
.md-rendered pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 14px 16px; overflow-x: auto; margin: 8px 0; font-size: 12px; line-height: 1.5; }
.md-rendered pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
.md-rendered blockquote { border-left: 3px solid var(--primary); padding: 6px 14px; margin: 8px 0; background: #f8faf8; color: var(--text2); font-size: 13px; border-radius: 0 6px 6px 0; }
.md-rendered hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
.md-rendered a { color: var(--primary); text-decoration: none; }
.md-rendered a:hover { text-decoration: underline; }
.md-rendered strong { font-weight: 700; }
.md-rendered em { font-style: italic; }
.md-rendered table { width: 100%; border-collapse: collapse; margin: 8px 0; font-size: 13px; }
.md-rendered th, .md-rendered td { border: 1px solid var(--border); padding: 6px 10px; text-align: left; }
.md-rendered th { background: #f5f6f7; font-weight: 600; }
.md-rendered img { max-width: 100%; border-radius: 6px; margin: 6px 0; }
/* 编辑态 */
.clip-card-edit-area { display: none; padding: 0 18px 14px; }
.clip-card.editing .clip-card-header { display: none; }
.clip-card.editing .clip-card-body { display: none; }
.clip-card.editing .clip-card-footer { display: none; }
.clip-card.editing .clip-card-edit-area { display: block; }
/* 有分栏布局时,padding 由分栏自身控制 */
.clip-card.editing .clip-card-edit-area:has(.clip-edit-split) { padding: 0 0 14px; }
/* 编辑浮层标题栏 */
.clip-edit-header {
display: none;
align-items: center;
gap: 10px;
padding: 16px 20px 8px;
}
.clip-card.editing .clip-edit-header { display: flex; }
.clip-edit-header-title {
font-size: 15px;
font-weight: 600;
color: var(--text);
flex: 1;
}
.clip-edit-header-close {
width: 32px;
height: 32px;
border: none;
background: #f0f0f0;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text3);
transition: all 0.15s;
flex-shrink: 0;
}
.clip-edit-header-close:hover { background: #e5e5e5; color: var(--text); }
.clip-edit-header-close svg { width: 16px; height: 16px; }
.clip-edit-card {
background: #f8fcf9;
border-radius: 14px;
overflow: hidden;
border: 1.5px solid #edf8f1;
}
.clip-edit-textarea {
width: 100%;
min-height: 300px;
border: none;
background: transparent;
padding: 14px 16px;
font-family: "SF Mono","Fira Code","Menlo","Consolas",monospace;
font-size: 13px;
line-height: 1.6;
color: var(--text);
resize: none;
outline: none;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
.clip-edit-textarea::placeholder { color: #c0c0c0; }
/* 编辑工具栏 */
.clip-edit-toolbar { display: flex; align-items: center; padding: 2px 8px; gap: 0; border-top: 1px solid #edf8f1; }
.clip-edit-toolbar-group { display: flex; align-items: center; gap: 0; }
.clip-edit-toolbar-divider { width: 1px; height: 14px; background: #e0e8e2; margin: 0 6px; flex-shrink: 0; }
.clip-edit-tool-btn { width: 28px; height: 28px; border: none; background: transparent; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #999; transition: all 0.15s; padding: 0; }
.clip-edit-tool-btn:hover { background: rgba(7,193,96,0.08); color: #555; }
.clip-edit-tool-btn:active { background: rgba(7,193,96,0.15); }
.clip-edit-tool-btn svg { width: 15px; height: 15px; }
.clip-edit-toolbar-spacer { flex: 1; }
.clip-edit-actions { display: flex; align-items: center; gap: 6px; }
.clip-edit-btn {
height: 28px;
padding: 0 14px;
border: none;
border-radius: 14px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.clip-edit-btn.cancel {
background: #eee;
color: var(--text2);
}
.clip-edit-btn.cancel:hover { background: #e0e0e0; color: var(--text); }
.clip-edit-btn.save {
background: var(--primary);
color: #fff;
}
.clip-edit-btn.save:hover { background: var(--primary-dark); box-shadow: 0 2px 8px rgba(7,193,96,0.25); }
.clip-edit-btn.save:disabled { background: #ddd; box-shadow: none; cursor: not-allowed; }
.clip-edit-tool-btn.clip-preview-active { background: rgba(7,193,96,0.12); color: var(--primary); }
/* 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); }
/* === 图片灯箱 Lightbox === */
.lightbox-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 500;
display: none;
align-items: center;
justify-content: center;
cursor: zoom-out;
animation: lightbox-fadein 0.2s ease;
}
.lightbox-overlay.show { display: flex; }
@keyframes lightbox-fadein { from { opacity: 0; } to { opacity: 1; } }
.lightbox-img {
max-width: 90vw;
max-height: 90vh;
border-radius: 0;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.4);
cursor: default;
object-fit: contain;
animation: lightbox-scalein 0.25s ease;
}
@keyframes lightbox-scalein { from { transform: scale(0.85); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.lightbox-close {
position: absolute;
top: 20px;
right: 28px;
width: 40px;
height: 40px;
border: none;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 20px;
transition: background 0.15s;
}
.lightbox-close:hover { background: rgba(255, 255, 255, 0.3); }
.lightbox-close svg { width: 20px; height: 20px; }
/* 让编辑态截图有可点击放大提示 */
.clip-edit-split-left img { cursor: zoom-in; }
</style>
</head>
<body>
<div class="top-bar">
<div class="top-bar-logo">
<div class="top-bar-logo-icon">S</div>
<h1>快速笔记 5.0</h1>
</div>
<div class="top-bar-divider"></div>
<span class="top-bar-title">内容收藏</span>
<div class="top-bar-spacer"></div>
<div class="top-bar-actions">
<button class="top-bar-btn" id="btn-refresh">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
刷新
</button>
<button class="top-bar-btn" id="btn-clear-all">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
清空全部
</button>
</div>
</div>
<div class="main-layout">
<!-- 左侧分类栏 -->
<aside class="sidebar">
<div class="sidebar-title">分类</div>
<ul class="sidebar-list">
<li class="sidebar-item active" data-filter="all">
<div class="si-icon si-icon-all">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
</div>
<span class="si-label">全部内容</span>
<span class="si-count" id="count-all">0</span>
</li>
<li class="sidebar-item" data-filter="select-clip">
<div class="si-icon si-icon-select">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3h5v2H5v3H3V3zm13 0h5v5h-2V5h-3V3zM3 16v5h5v-2H5v-3H3zm18 0v5h-5v-2h3v-3h2z"/></svg>
</div>
<span class="si-label">截图</span>
<span class="si-count" id="count-select">0</span>
</li>
<li class="sidebar-item" data-filter="smart-clip">
<div class="si-icon si-icon-smart">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div>
<span class="si-label">智能剪藏</span>
<span class="si-count" id="count-smart">0</span>
</li>
<li class="sidebar-item" data-filter="markdown">
<div class="si-icon si-icon-markdown">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
</div>
<span class="si-label">速记</span>
<span class="si-count" id="count-markdown">0</span>
</li>
<li class="sidebar-item" data-filter="image-clip">
<div class="si-icon si-icon-image">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</div>
<span class="si-label">图片收藏</span>
<span class="si-count" id="count-image">0</span>
</li>
</ul>
</aside>
<!-- 右侧内容区 -->
<div class="content-area">
<div class="content-toolbar">
<span class="content-toolbar-title" id="content-title">全部内容</span>
<span class="content-toolbar-count">(<strong id="clips-total">0</strong> 条)</span>
<div class="content-toolbar-spacer"></div>
<input type="text" class="content-search" id="search-input" placeholder="搜索收藏内容...">
</div>
<div class="content-scroll">
<div class="clip-list" id="clip-list">
<!-- 动态生成 -->
</div>
<div class="empty-state" id="empty-state" style="display:none;">
<div class="empty-state-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
</div>
<h2>暂无收藏内容</h2>
<p>使用插件的「选择剪藏」或「智能剪藏」功能<br>截取网页内容后会自动保存到这里</p>
</div>
</div>
</div>
</div>
<!-- 隐藏的筛选 select(供 JS 兼容使用) -->
<select id="filter-type" style="display:none;">
<option value="all">全部类型</option>
<option value="select-clip">截图</option>
<option value="smart-clip">智能剪藏</option>
<option value="markdown">速记笔记</option>
<option value="image-clip">图片收藏</option>
</select>
<!-- 编辑态背景遮罩 -->
<div class="edit-overlay" id="edit-overlay"></div>
<div class="toast" id="toast"></div>
<!-- 图片灯箱 -->
<div class="lightbox-overlay" id="lightbox">
<button class="lightbox-close" id="lightbox-close" title="关闭">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<img class="lightbox-img" id="lightbox-img" src="" alt="原图预览">
</div>
<script src="clips.js"></script>
</body>
</html>
@@ -1,746 +0,0 @@
(function () {
'use strict';
function $(id) { return document.getElementById(id); }
var clipList = $('clip-list');
var emptyState = $('empty-state');
var totalEl = $('clips-total');
var searchInput = $('search-input');
var filterType = $('filter-type');
var contentTitle = $('content-title');
var allClips = []; // 每条记录增加 _source 字段: 'ka_clips' | 'ka_notes'
// === 图片灯箱 ===
var lightbox = $('lightbox');
var lightboxImg = $('lightbox-img');
var lightboxClose = $('lightbox-close');
function openLightbox(src) {
lightboxImg.src = src;
lightbox.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeLightbox() {
lightbox.classList.remove('show');
lightboxImg.src = '';
document.body.style.overflow = '';
}
// 点击遮罩关闭(不点击图片本身)
lightbox.addEventListener('click', function (e) {
if (e.target === lightbox) closeLightbox();
});
lightboxClose.addEventListener('click', closeLightbox);
// ESC 关闭
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && lightbox.classList.contains('show')) {
closeLightbox();
}
});
// 分类名称映射
var categoryLabels = {
'all': '全部内容',
'select-clip': '截图',
'smart-clip': '智能剪藏',
'markdown': '速记笔记',
'image-clip': '图片收藏'
};
// === Toast ===
function toast(msg, type) {
var el = $('toast');
el.textContent = msg;
el.className = 'toast show' + (type ? ' ' + type : '');
setTimeout(function () { el.classList.remove('show'); }, 2000);
}
// === 轻量级 Markdown → HTML 渲染 ===
function renderMarkdown(text) {
if (!text) return '';
var html = text;
// 代码块 (```...```) — 先处理,避免内部被其他规则干扰
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
return '<pre><code>' + escapeHtml(code.replace(/\n$/, '')) + '</code></pre>';
});
// 行内代码
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// 表格(简单支持)
html = html.replace(/((?:\|.+\|\n)+)/g, function (tableBlock) {
var rows = tableBlock.trim().split('\n');
if (rows.length < 2) return tableBlock;
var out = '<table>';
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 += '<tr>';
cells.forEach(function (cell) {
out += '<' + tag + '>' + cell.trim() + '</' + tag + '>';
});
out += '</tr>';
});
out += '</table>';
return out;
});
// 将剩余文本按行处理
var lines = html.split('\n');
var result = [];
var inList = false;
var listType = '';
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
// 已渲染的 <pre>/<table> 块直接保留
if (line.indexOf('<pre>') !== -1 || line.indexOf('<table>') !== -1) {
if (inList) { result.push('</' + listType + '>'); inList = false; }
result.push(line);
continue;
}
// 标题
if (/^####\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h4>' + RegExp.$1 + '</h4>'); continue; }
if (/^###\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h3>' + RegExp.$1 + '</h3>'); continue; }
if (/^##\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h2>' + RegExp.$1 + '</h2>'); continue; }
if (/^#\s+(.+)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<h1>' + RegExp.$1 + '</h1>'); continue; }
// 分隔线
if (/^---+$/.test(line.trim())) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<hr>'); continue; }
// 引用
if (/^>\s*(.*)/.test(line)) { if (inList) { result.push('</' + listType + '>'); inList = false; } result.push('<blockquote>' + RegExp.$1 + '</blockquote>'); continue; }
// 无序列表
if (/^[\-\*]\s+(.+)/.test(line)) {
if (!inList || listType !== 'ul') {
if (inList) result.push('</' + listType + '>');
result.push('<ul>'); inList = true; listType = 'ul';
}
result.push('<li>' + RegExp.$1 + '</li>');
continue;
}
// 有序列表
if (/^\d+\.\s+(.+)/.test(line)) {
if (!inList || listType !== 'ol') {
if (inList) result.push('</' + listType + '>');
result.push('<ol>'); inList = true; listType = 'ol';
}
result.push('<li>' + RegExp.$1 + '</li>');
continue;
}
// 其他:关闭列表,空行跳过,非空行包裹 <p>
if (inList) { result.push('</' + listType + '>'); inList = false; }
if (line.trim() === '') { continue; }
result.push('<p>' + line + '</p>');
}
if (inList) result.push('</' + listType + '>');
html = result.join('\n');
// 行内样式(在所有块级元素处理完之后)
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
return html;
}
// === 加载收藏列表 ===
function loadClips() {
chrome.runtime.sendMessage({ type: 'GET_CLIPS' }, function (resp) {
if (resp && resp.success) {
allClips = (resp.data || []).map(function (c) { c._source = 'ka_clips'; return c; });
// 同时加载 ka_notes 中的旧数据(向后兼容)
chrome.runtime.sendMessage({ type: 'GET_NOTES' }, function (notesResp) {
if (notesResp && notesResp.success && notesResp.data) {
notesResp.data.forEach(function (note) {
// 避免重复(相同 id 的数据已在 ka_clips 中)
var exists = allClips.some(function (c) { return c.id === note.id; });
if (!exists) {
note._source = 'ka_notes';
allClips.push(note);
}
});
}
// 按时间排序
allClips.sort(function (a, b) {
return new Date(b.createdAt || 0) - new Date(a.createdAt || 0);
});
renderClips();
});
}
});
}
// === 编辑工具栏辅助函数 ===
function clipInsertMd(textarea, before, after, placeholder) {
textarea.focus();
var start = textarea.selectionStart;
var end = textarea.selectionEnd;
var text = textarea.value;
var selected = text.substring(start, end);
var insert = selected || placeholder || '';
textarea.value = text.substring(0, start) + before + insert + (after || '') + text.substring(end);
textarea.selectionStart = start + before.length;
textarea.selectionEnd = start + before.length + insert.length;
textarea.dispatchEvent(new Event('input'));
}
function clipInsertLinePrefix(textarea, prefix) {
textarea.focus();
var start = textarea.selectionStart;
var text = textarea.value;
var lineStart = text.lastIndexOf('\n', start - 1) + 1;
textarea.value = text.substring(0, lineStart) + prefix + text.substring(lineStart);
textarea.selectionStart = textarea.selectionEnd = start + prefix.length;
textarea.dispatchEvent(new Event('input'));
}
// === 渲染列表 ===
function renderClips() {
var keyword = searchInput.value.trim().toLowerCase();
var typeFilter = filterType.value;
// 更新右侧标题
contentTitle.textContent = categoryLabels[typeFilter] || '全部内容';
// 更新左侧分类计数
updateCategoryCounts();
var filtered = allClips.filter(function (clip) {
if (typeFilter !== 'all' && clip.type !== typeFilter) return false;
if (keyword) {
var text = ((clip.title || '') + ' ' + (clip.content || '')).toLowerCase();
return text.indexOf(keyword) !== -1;
}
return true;
});
totalEl.textContent = filtered.length;
if (filtered.length === 0) {
clipList.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
// Markdown 编辑工具栏 HTML(复用)
function buildEditToolbarHtml() {
return '<div class="clip-edit-toolbar">'
+ ' <div class="clip-edit-toolbar-group">'
+ ' <button class="clip-edit-tool-btn" data-md="bold" title="加粗 **文本**"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg></button>'
+ ' </div>'
+ ' <div class="clip-edit-toolbar-divider"></div>'
+ ' <div class="clip-edit-toolbar-group">'
+ ' <button class="clip-edit-tool-btn" data-md="heading" title="标题 ## 文本"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4H5z"/></svg></button>'
+ ' <button class="clip-edit-tool-btn" data-md="ul" title="无序列表 - 项目"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg></button>'
+ ' <button class="clip-edit-tool-btn" data-md="ol" title="有序列表 1. 项目"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg></button>'
+ ' </div>'
+ ' <div class="clip-edit-toolbar-divider"></div>'
+ ' <div class="clip-edit-toolbar-group">'
+ ' <button class="clip-edit-tool-btn" data-md="link" title="插入链接 [文本](url)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>'
+ ' <button class="clip-edit-tool-btn" data-md="image" title="插入图片 ![描述](url)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></button>'
+ ' </div>'
+ ' <div class="clip-edit-toolbar-divider"></div>'
+ ' <div class="clip-edit-toolbar-group">'
+ ' <button class="clip-edit-tool-btn" data-md="preview" title="预览 Markdown 效果"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button>'
+ ' </div>'
+ ' <div class="clip-edit-toolbar-spacer"></div>'
+ ' <div class="clip-edit-actions">'
+ ' <button class="clip-edit-btn cancel">取消</button>'
+ ' <button class="clip-edit-btn save">保存</button>'
+ ' </div>'
+ ' </div>';
}
var html = '';
filtered.forEach(function (clip) {
var url = (clip.meta && clip.meta.url) || '';
var time = formatTime(clip.createdAt);
var isMarkdown = clip.type === 'markdown';
var isSmartClip = clip.type === 'smart-clip';
var isImageClip = clip.type === 'image-clip';
var isSelectClip = clip.type === 'select-clip';
var hasScreenshot = isSelectClip && clip.screenshot;
// 类型标签
var typeBadge, tagClass;
if (isMarkdown) {
typeBadge = '速记';
tagClass = 'clip-card-tag tag-markdown';
} else if (isSmartClip) {
typeBadge = '智能剪藏';
tagClass = 'clip-card-tag';
} else if (isImageClip) {
typeBadge = '图片收藏';
tagClass = 'clip-card-tag tag-image';
} else if (hasScreenshot) {
typeBadge = '截图';
tagClass = 'clip-card-tag';
} else {
typeBadge = '文本';
tagClass = 'clip-card-tag tag-text';
}
// 内容区
var contentHtml;
if (hasScreenshot) {
// 选择剪藏有截图:显示缩略图
contentHtml = '<img class="clip-screenshot-thumb" src="' + clip.screenshot + '" alt="截图">';
} else {
// 所有非截图内容统一使用 markdown 渲染(速记笔记、智能剪藏、选中文字保存、图片收藏)
contentHtml = '<div class="clip-card-content md-rendered">' + renderMarkdown(clip.content || '') + '</div>';
}
// 编辑区标题栏
var editHeaderHtml = '<div class="clip-edit-header">'
+ '<span class="clip-edit-header-title">编辑 · ' + escapeHtml(typeBadge) + '</span>'
+ '<button class="clip-edit-header-close" title="关闭"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>'
+ '</div>';
// 编辑区:选择剪藏有截图时用左右分栏布局
var editAreaHtml;
if (hasScreenshot) {
editAreaHtml = '<div class="clip-card-edit-area">'
+ editHeaderHtml
+ '<div class="clip-edit-split">'
+ ' <div class="clip-edit-split-left">'
+ ' <img src="' + clip.screenshot + '" alt="截图原图">'
+ ' <span class="split-label">截图预览(点击放大)</span>'
+ ' </div>'
+ ' <div class="clip-edit-split-right">'
+ ' <div class="clip-edit-card">'
+ ' <textarea class="clip-edit-textarea" placeholder="# 标题&#10;&#10;在这里编辑截图中的文字…"></textarea>'
+ buildEditToolbarHtml()
+ ' </div>'
+ ' </div>'
+ '</div>'
+ '</div>';
} else {
editAreaHtml = '<div class="clip-card-edit-area">'
+ editHeaderHtml
+ ' <div class="clip-edit-card">'
+ ' <textarea class="clip-edit-textarea" placeholder="# 标题&#10;&#10;在这里编辑内容…"></textarea>'
+ buildEditToolbarHtml()
+ ' </div>'
+ '</div>';
}
html += '<div class="clip-card" data-id="' + clip.id + '" data-source="' + (clip._source || 'ka_clips') + '">'
+ '<div class="clip-card-header">'
+ ' <span class="clip-card-time">' + time + '</span>'
+ '</div>'
+ '<div class="clip-card-body">'
+ ' ' + contentHtml
+ '</div>'
+ editAreaHtml
+ '<div class="clip-card-footer">'
+ ' <span class="' + tagClass + '">' + typeBadge + '</span>'
+ ' <div class="clip-card-footer-spacer"></div>'
+ ' <div class="clip-card-actions">'
+ ' <button class="clip-more-btn" title="更多操作"><svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg></button>'
+ ' <div class="clip-more-menu">'
+ (url ? ' <button class="clip-more-menu-item menu-open-link" data-url="' + escapeHtml(url) + '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>打开链接</button>' : '')
+ ' <button class="clip-more-menu-item menu-edit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>编辑</button>'
+ ' <button class="clip-more-menu-item menu-copy"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>复制</button>'
+ ' <button class="clip-more-menu-item menu-delete"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>删除</button>'
+ ' </div>'
+ ' </div>'
+ '</div>'
+ '</div>';
});
clipList.innerHTML = html;
// 绑定三点菜单
clipList.querySelectorAll('.clip-more-btn').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var menu = btn.nextElementSibling;
var card = btn.closest('.clip-card');
// 先关闭其他已打开的菜单
clipList.querySelectorAll('.clip-more-menu.show').forEach(function (m) {
if (m !== menu) {
m.classList.remove('show');
var otherCard = m.closest('.clip-card');
if (otherCard) otherCard.classList.remove('menu-open');
}
});
// 切换当前菜单
var isOpen = menu.classList.toggle('show');
if (isOpen) {
card.classList.add('menu-open');
} else {
card.classList.remove('menu-open');
}
});
});
// 点击其他地方关闭菜单
document.addEventListener('click', function () {
clipList.querySelectorAll('.clip-more-menu.show').forEach(function (m) {
m.classList.remove('show');
var card = m.closest('.clip-card');
if (card) card.classList.remove('menu-open');
});
});
// 绑定复制事件
clipList.querySelectorAll('.menu-copy').forEach(function (btn) {
btn.addEventListener('click', function () {
var card = btn.closest('.clip-card');
var id = card.dataset.id;
var clip = allClips.find(function (c) { return c.id === id; });
if (clip) {
navigator.clipboard.writeText(clip.content || '').then(function () {
toast('已复制到剪贴板', 'success');
});
}
});
});
// 绑定删除事件
clipList.querySelectorAll('.menu-delete').forEach(function (btn) {
btn.addEventListener('click', function () {
var card = btn.closest('.clip-card');
var id = card.dataset.id;
var source = card.dataset.source;
if (confirm('确定要删除这条收藏吗?')) {
var msgType = source === 'ka_notes' ? 'DELETE_NOTE' : 'DELETE_CLIP';
chrome.runtime.sendMessage({ type: msgType, payload: { id: id } }, function () {
allClips = allClips.filter(function (c) { return c.id !== id; });
renderClips();
toast('已删除', 'success');
});
}
});
});
// 绑定打开链接事件
clipList.querySelectorAll('.menu-open-link').forEach(function (btn) {
btn.addEventListener('click', function () {
var linkUrl = btn.getAttribute('data-url');
if (linkUrl) {
window.open(linkUrl, '_blank');
}
});
});
// 绑定编辑事件
clipList.querySelectorAll('.menu-edit').forEach(function (btn) {
btn.addEventListener('click', function () {
var card = btn.closest('.clip-card');
enterEditMode(card);
});
});
// 双击卡片内容进入编辑模式
clipList.querySelectorAll('.clip-card').forEach(function (card) {
card.addEventListener('dblclick', function (e) {
// 如果已在编辑态,或点击的是按钮/链接/菜单,不触发
if (card.classList.contains('editing')) return;
var tag = e.target.tagName;
if (tag === 'BUTTON' || tag === 'A' || tag === 'TEXTAREA' || tag === 'INPUT') return;
if (e.target.closest('.clip-card-actions')) return;
if (e.target.closest('.clip-more-menu')) return;
enterEditMode(card);
});
});
// 点击卡片内容展开/收起(非 markdown 类型才有折叠效果)
clipList.querySelectorAll('.clip-card-content:not(.md-rendered)').forEach(function (el) {
el.addEventListener('click', function () {
el.classList.toggle('expanded');
var fade = el.querySelector('.clip-card-fade');
if (fade) fade.style.display = el.classList.contains('expanded') ? 'none' : '';
});
});
// Markdown 卡片点击展开/收起(无 fade 渐变)
clipList.querySelectorAll('.clip-card-content.md-rendered').forEach(function (el) {
el.addEventListener('click', function () {
el.classList.toggle('expanded');
});
});
// 编辑态截图 → 点击打开灯箱查看原图(列表缩略图不放大)
clipList.querySelectorAll('.clip-edit-split-left img').forEach(function (img) {
img.addEventListener('click', function (e) {
e.stopPropagation();
if (img.src) openLightbox(img.src);
});
});
}
// === 进入编辑模式(独立函数,供菜单和双击调用) ===
var editOverlay = $('edit-overlay');
var _editingCard = null; // 当前编辑中的卡片
function exitEditMode() {
if (_editingCard) {
_editingCard.classList.remove('editing');
_editingCard = null;
}
editOverlay.classList.remove('show');
document.body.style.overflow = '';
}
// 点击遮罩退出编辑
editOverlay.addEventListener('click', exitEditMode);
// ESC 退出编辑
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && _editingCard) {
// 如果灯箱正在显示,优先关闭灯箱
if (lightbox.classList.contains('show')) return;
exitEditMode();
}
});
function enterEditMode(card) {
if (!card || card.classList.contains('editing')) return;
var id = card.dataset.id;
var clip = allClips.find(function (c) { return c.id === id; });
if (!clip) return;
// 关闭其他菜单
clipList.querySelectorAll('.clip-more-menu.show').forEach(function (m) {
m.classList.remove('show');
var c = m.closest('.clip-card');
if (c) c.classList.remove('menu-open');
});
// 如果已有其他卡片在编辑,先退出
if (_editingCard && _editingCard !== card) {
_editingCard.classList.remove('editing');
}
// 显示遮罩 + 进入编辑态
_editingCard = card;
editOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
card.classList.add('editing');
var textarea = card.querySelector('.clip-edit-textarea');
textarea.value = clip.content || '';
textarea.style.height = 'auto';
textarea.style.height = Math.max(300, textarea.scrollHeight) + 'px';
textarea.focus();
// 输入时自动调整高度
textarea.oninput = function () {
textarea.style.height = 'auto';
textarea.style.height = Math.max(300, textarea.scrollHeight) + 'px';
};
// 标题栏关闭按钮
var headerClose = card.querySelector('.clip-edit-header-close');
if (headerClose) {
headerClose.onclick = function () { exitEditMode(); };
}
// 工具栏按钮事件
card.querySelectorAll('.clip-edit-tool-btn[data-md]').forEach(function (toolBtn) {
toolBtn.onclick = function (e) {
e.preventDefault();
var action = toolBtn.getAttribute('data-md');
switch (action) {
case 'bold':
clipInsertMd(textarea, '**', '**', '粗体文本');
break;
case 'heading':
clipInsertLinePrefix(textarea, '## ');
break;
case 'ul':
clipInsertLinePrefix(textarea, '- ');
break;
case 'ol':
clipInsertLinePrefix(textarea, '1. ');
break;
case 'link':
clipInsertMd(textarea, '[', '](url)', '链接文本');
break;
case 'image':
clipInsertMd(textarea, '![', '](url)', '图片描述');
break;
case 'preview':
if (!textarea.value.trim() && textarea.style.display !== 'none') { toast('请先输入内容'); return; }
var previewArea = card.querySelector('.clip-edit-preview');
if (!previewArea) {
previewArea = document.createElement('div');
previewArea.className = 'clip-edit-preview md-rendered';
previewArea.style.cssText = 'padding:12px 14px;font-size:13px;line-height:1.7;color:#555;overflow-y:auto;min-height:120px;display:none;';
textarea.parentNode.insertBefore(previewArea, textarea.nextSibling);
}
var isPreviewing = textarea.style.display === 'none';
if (isPreviewing) {
textarea.style.display = '';
previewArea.style.display = 'none';
toolBtn.classList.remove('clip-preview-active');
textarea.focus();
} else {
previewArea.innerHTML = renderMarkdown(textarea.value);
textarea.style.display = 'none';
previewArea.style.display = 'block';
toolBtn.classList.add('clip-preview-active');
}
break;
}
};
});
// 取消按钮
var cancelBtn = card.querySelector('.clip-edit-btn.cancel');
cancelBtn.onclick = function () {
exitEditMode();
};
// 保存按钮
var saveBtn = card.querySelector('.clip-edit-btn.save');
saveBtn.onclick = function () {
var newContent = textarea.value;
var source = card.dataset.source;
var storageKey = source === 'ka_notes' ? 'ka_notes' : 'ka_clips';
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
chrome.storage.local.get(storageKey, function (data) {
var items = data[storageKey] || [];
var found = false;
for (var i = 0; i < items.length; i++) {
if (items[i].id === id) {
items[i].content = newContent;
items[i].updatedAt = new Date().toISOString();
found = true;
break;
}
}
if (!found) {
saveBtn.disabled = false;
saveBtn.textContent = '保存';
toast('未找到对应记录', '');
return;
}
var setData = {};
setData[storageKey] = items;
chrome.storage.local.set(setData, function () {
saveBtn.disabled = false;
saveBtn.textContent = '保存';
if (chrome.runtime.lastError) {
toast('保存失败,请重试', '');
return;
}
clip.content = newContent;
if (clip.type === 'markdown') {
clip.title = newContent.split('\n')[0].replace(/^#+\s*/, '').substring(0, 50) || '未命名笔记';
}
exitEditMode();
renderClips();
toast('已保存', 'success');
});
});
};
}
// === 工具函数 ===
function escapeHtml(s) {
var d = document.createElement('div');
d.textContent = s || '';
return d.innerHTML;
}
function formatTime(iso) {
if (!iso) return '';
var d = new Date(iso);
var now = new Date();
var diff = now - d;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前';
if (diff < 604800000) return Math.floor(diff / 86400000) + ' 天前';
var m = d.getMonth() + 1;
var day = d.getDate();
var h = d.getHours().toString().padStart(2, '0');
var min = d.getMinutes().toString().padStart(2, '0');
return m + '月' + day + '日 ' + h + ':' + min;
}
// === 分类计数更新 ===
function updateCategoryCounts() {
var countAll = allClips.length;
var countSelect = 0;
var countSmart = 0;
var countMarkdown = 0;
var countImage = 0;
allClips.forEach(function (c) {
if (c.type === 'select-clip') countSelect++;
else if (c.type === 'smart-clip') countSmart++;
else if (c.type === 'markdown') countMarkdown++;
else if (c.type === 'image-clip') countImage++;
});
$('count-all').textContent = countAll;
$('count-select').textContent = countSelect;
$('count-smart').textContent = countSmart;
$('count-markdown').textContent = countMarkdown;
if ($('count-image')) $('count-image').textContent = countImage;
}
// === 左侧分类点击 ===
document.querySelectorAll('.sidebar-item').forEach(function (item) {
item.addEventListener('click', function () {
document.querySelectorAll('.sidebar-item').forEach(function (i) { i.classList.remove('active'); });
item.classList.add('active');
filterType.value = item.getAttribute('data-filter');
renderClips();
});
});
// === 事件绑定 ===
searchInput.addEventListener('input', renderClips);
$('btn-refresh').addEventListener('click', function () {
loadClips();
toast('已刷新', 'success');
});
$('btn-clear-all').addEventListener('click', function () {
if (allClips.length === 0) return;
if (confirm('确定要清空所有收藏吗?此操作不可恢复。')) {
// 同时清空 ka_clips 和 ka_notes
chrome.storage.local.set({ ka_clips: [], ka_notes: [] }, function () {
allClips = [];
renderClips();
toast('已清空', 'success');
});
}
});
// === 监听 storage 变化,实时刷新列表 ===
// 当其他页面保存了新内容时,列表自动更新
if (chrome && chrome.storage && chrome.storage.onChanged) {
chrome.storage.onChanged.addListener(function (changes, area) {
if (area === 'local' && (changes.ka_clips || changes.ka_notes)) {
loadClips();
}
});
}
// === 初始化 ===
loadClips();
})();
@@ -1,261 +0,0 @@
/* 快速笔记 5.0 - 内容脚本样式 */
/* === 截屏式区域框选 === */
#ka-clip-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2147483646;
cursor: crosshair;
user-select: none;
-webkit-user-select: none;
}
/* 暗幕遮罩(四块拼合) */
.ka-clip-dim {
position: fixed;
background: rgba(0, 0, 0, 0.45);
z-index: -1;
pointer-events: none;
transition: none;
}
/* 选区矩形 */
.ka-clip-selection {
position: fixed;
border: 2px solid #07C160;
background: transparent;
z-index: 1;
pointer-events: none;
box-shadow: 0 0 0 1px rgba(7, 193, 96, 0.3), 0 0 20px rgba(7, 193, 96, 0.1);
}
/* 选区角标尺寸拖拽点 */
.ka-clip-handle {
position: absolute;
width: 10px;
height: 10px;
background: #fff;
border: 2px solid #07C160;
border-radius: 2px;
z-index: 2;
pointer-events: auto !important;
cursor: pointer;
}
.ka-clip-handle-nw { top: -6px; left: -6px; cursor: nw-resize; }
.ka-clip-handle-n { top: -6px; left: 50%; margin-left: -5px; cursor: n-resize; }
.ka-clip-handle-ne { top: -6px; right: -6px; cursor: ne-resize; }
.ka-clip-handle-e { top: 50%; margin-top: -5px; right: -6px; cursor: e-resize; }
.ka-clip-handle-se { bottom: -6px; right: -6px; cursor: se-resize; }
.ka-clip-handle-s { bottom: -6px; left: 50%; margin-left: -5px; cursor: s-resize; }
.ka-clip-handle-sw { bottom: -6px; left: -6px; cursor: sw-resize; }
.ka-clip-handle-w { top: 50%; margin-top: -5px; left: -6px; cursor: w-resize; }
/* 提示文字 */
.ka-clip-tip {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.75);
color: #fff;
padding: 14px 28px;
border-radius: 12px;
font-size: 15px;
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
z-index: 2;
pointer-events: none;
backdrop-filter: blur(4px);
letter-spacing: 0.5px;
}
/* 确认工具栏 */
.ka-clip-toolbar {
position: fixed;
display: flex;
align-items: center;
gap: 8px;
z-index: 3;
pointer-events: auto;
animation: ka-fade-in 0.15s ease;
}
@keyframes ka-fade-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.ka-clip-size {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
background: rgba(0, 0, 0, 0.55);
padding: 4px 10px;
border-radius: 6px;
font-family: -apple-system, "SF Mono", "Menlo", monospace;
backdrop-filter: blur(4px);
}
.ka-clip-btn {
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 13px;
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
backdrop-filter: blur(8px);
}
.ka-clip-btn kbd {
font-size: 10px;
padding: 1px 5px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.15);
font-family: inherit;
}
.ka-clip-btn-cancel {
background: rgba(255, 255, 255, 0.9);
color: #555;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.ka-clip-btn-cancel:hover {
background: #fff;
}
.ka-clip-btn-cancel kbd {
background: rgba(0, 0, 0, 0.08);
}
.ka-clip-btn-confirm {
background: #07C160;
color: #fff;
box-shadow: 0 2px 12px rgba(7, 193, 96, 0.35);
}
.ka-clip-btn-confirm:hover {
background: #06ad54;
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.4);
}
/* === 通知 === */
.ka-notification {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
border-radius: 10px;
font-size: 14px;
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
z-index: 2147483647;
transform: translateX(120%);
transition: transform 0.3s ease;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.ka-notification-show {
transform: translateX(0);
}
.ka-notification-success {
background: #07C160;
color: #fff;
}
.ka-notification-error {
background: #e44;
color: #fff;
}
.ka-notification-info {
background: #333;
color: #fff;
}
/* === 选中文字气泡 === */
.ka-sel-bubble {
position: absolute;
display: flex;
align-items: center;
gap: 0;
background: #07C160;
border-radius: 10px;
box-shadow: 0 6px 24px rgba(7, 193, 96, 0.3), 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 2147483645;
padding: 4px;
animation: ka-bubble-in 0.18s ease;
user-select: none;
-webkit-user-select: none;
}
@keyframes ka-bubble-in {
from { opacity: 0; transform: translateY(6px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.ka-sel-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border: none;
background: transparent;
border-radius: 8px;
font-size: 13px;
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
color: #fff;
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
line-height: 1;
}
.ka-sel-btn svg {
width: 15px;
height: 15px;
flex-shrink: 0;
color: #fff;
}
.ka-sel-btn-save:hover {
background: rgba(255, 255, 255, 0.18);
}
.ka-sel-btn-ask:hover {
background: rgba(255, 255, 255, 0.18);
}
.ka-sel-divider {
width: 1px;
height: 18px;
background: rgba(255, 255, 255, 0.3);
flex-shrink: 0;
}
/* 气泡下方小三角(默认朝下) */
.ka-sel-arrow {
position: absolute;
bottom: -6px;
left: 50%;
margin-left: -6px;
width: 12px;
height: 12px;
background: #07C160;
transform: rotate(45deg);
box-shadow: 3px 3px 4px rgba(0, 0, 0, 0.06);
z-index: -1;
}
/* 气泡在选区下方时,三角朝上 */
.ka-sel-arrow.ka-sel-arrow-top {
bottom: auto;
top: -6px;
box-shadow: -2px -2px 4px rgba(0, 0, 0, 0.06);
}
@@ -1,876 +0,0 @@
(function () {
'use strict';
// 防止重复注入 — 用版本号区分
var KA_VERSION = 4;
if (window.__kaContentVersion === KA_VERSION) {
// 已注入过相同版本,但确保消息监听器仍然存在
if (typeof window.ensureMessageListener === 'function') {
window.ensureMessageListener();
}
return;
}
window.__kaContentVersion = KA_VERSION;
// === 智能剪藏 ===
function smartClip() {
var content = extractMainContent();
var title = document.title || location.hostname;
chrome.runtime.sendMessage({
type: 'SAVE_CLIP',
payload: {
type: 'smart-clip',
content: '## ' + title + '\n\n' + content + '\n\n---\n来源: ' + location.href,
title: '智能剪藏 - ' + title,
meta: { url: location.href, title: title }
}
}, function (resp) {
if (resp && resp.success) {
showNotification('智能剪藏成功', 'success');
chrome.runtime.sendMessage({
type: 'CLIP_RESULT',
payload: { title: '智能剪藏 - ' + title, content: content }
});
} else {
showNotification('剪藏失败', 'error');
}
});
}
function extractMainContent() {
var selectors = ['article', 'main', '[role="main"]', '.post-content', '.article-content', '.entry-content', '.content', '#content'];
var el = null;
for (var i = 0; i < selectors.length; i++) {
el = document.querySelector(selectors[i]);
if (el && el.textContent.trim().length > 100) break;
el = null;
}
if (!el) {
var paras = document.querySelectorAll('p');
var texts = [];
paras.forEach(function (p) {
var t = p.textContent.trim();
if (t.length > 30) texts.push(t);
});
return texts.join('\n\n') || document.body.innerText.substring(0, 3000);
}
var clone = el.cloneNode(true);
var removes = clone.querySelectorAll('script, style, nav, header, footer, .ads, .ad, .advertisement, .sidebar, .comment, .comments, [role="navigation"], [role="banner"]');
removes.forEach(function (r) { r.remove(); });
return clone.textContent.trim().substring(0, 5000);
}
// === 选择剪藏 — 截屏式区域框选 ===
var clipState = {
active: false,
overlay: null,
startX: 0,
startY: 0,
rect: null,
toolbar: null,
dimTop: null,
dimBottom: null,
dimLeft: null,
dimRight: null
};
function startSelectClip() {
if (clipState.active) return;
clipState.active = true;
// 创建全屏遮罩
var overlay = document.createElement('div');
overlay.id = 'ka-clip-overlay';
overlay.innerHTML =
'<div class="ka-clip-dim ka-clip-dim-top"></div>' +
'<div class="ka-clip-dim ka-clip-dim-bottom"></div>' +
'<div class="ka-clip-dim ka-clip-dim-left"></div>' +
'<div class="ka-clip-dim ka-clip-dim-right"></div>' +
'<div class="ka-clip-selection"></div>' +
'<div class="ka-clip-tip">拖拽鼠标框选要截取的区域,松开后确认</div>';
document.body.appendChild(overlay);
clipState.overlay = overlay;
clipState.dimTop = overlay.querySelector('.ka-clip-dim-top');
clipState.dimBottom = overlay.querySelector('.ka-clip-dim-bottom');
clipState.dimLeft = overlay.querySelector('.ka-clip-dim-left');
clipState.dimRight = overlay.querySelector('.ka-clip-dim-right');
clipState.rect = overlay.querySelector('.ka-clip-selection');
// 初始暗幕覆盖全屏
var w = window.innerWidth;
var h = window.innerHeight;
setDims(0, 0, w, h);
clipState.rect.style.display = 'none';
overlay.addEventListener('mousedown', onClipMouseDown);
document.addEventListener('keydown', onClipKeyDown);
}
function setDims(x, y, x2, y2) {
var w = window.innerWidth;
var h = window.innerHeight;
// 确保有效范围
var left = Math.min(x, x2);
var top = Math.min(y, y2);
var right = Math.max(x, x2);
var bottom = Math.max(y, y2);
// 如果选区大小为0,全屏暗幕
if (right - left < 2 || bottom - top < 2) {
clipState.dimTop.style.cssText = 'top:0;left:0;width:' + w + 'px;height:' + h + 'px;';
clipState.dimBottom.style.cssText = 'display:none;';
clipState.dimLeft.style.cssText = 'display:none;';
clipState.dimRight.style.cssText = 'display:none;';
return;
}
// 上方暗幕
clipState.dimTop.style.cssText = 'top:0;left:0;width:' + w + 'px;height:' + top + 'px;';
// 下方暗幕
clipState.dimBottom.style.cssText = 'top:' + bottom + 'px;left:0;width:' + w + 'px;height:' + (h - bottom) + 'px;';
// 左侧暗幕
clipState.dimLeft.style.cssText = 'top:' + top + 'px;left:0;width:' + left + 'px;height:' + (bottom - top) + 'px;';
// 右侧暗幕
clipState.dimRight.style.cssText = 'top:' + top + 'px;left:' + right + 'px;width:' + (w - right) + 'px;height:' + (bottom - top) + 'px;';
}
function onClipMouseDown(e) {
if (e.button !== 0) return;
// 如果点击在工具栏或 handle 上,不要重新框选
if (e.target.closest && (e.target.closest('.ka-clip-toolbar') || e.target.closest('.ka-clip-handle'))) return;
e.preventDefault();
e.stopPropagation();
// 移除之前的工具栏和 handles
if (clipState.toolbar) {
clipState.toolbar.remove();
clipState.toolbar = null;
}
var oldHandles = clipState.overlay.querySelectorAll('.ka-clip-handle');
oldHandles.forEach(function (h) { h.remove(); });
clipState.startX = e.clientX;
clipState.startY = e.clientY;
clipState.rect.style.display = 'block';
clipState.rect.style.left = e.clientX + 'px';
clipState.rect.style.top = e.clientY + 'px';
clipState.rect.style.width = '0';
clipState.rect.style.height = '0';
// 隐藏提示
var tip = clipState.overlay.querySelector('.ka-clip-tip');
if (tip) tip.style.display = 'none';
document.addEventListener('mousemove', onClipMouseMove);
document.addEventListener('mouseup', onClipMouseUp);
}
function onClipMouseMove(e) {
e.preventDefault();
var x = Math.min(e.clientX, clipState.startX);
var y = Math.min(e.clientY, clipState.startY);
var w = Math.abs(e.clientX - clipState.startX);
var h = Math.abs(e.clientY - clipState.startY);
clipState.rect.style.left = x + 'px';
clipState.rect.style.top = y + 'px';
clipState.rect.style.width = w + 'px';
clipState.rect.style.height = h + 'px';
setDims(x, y, x + w, y + h);
}
function onClipMouseUp(e) {
document.removeEventListener('mousemove', onClipMouseMove);
document.removeEventListener('mouseup', onClipMouseUp);
var x = Math.min(e.clientX, clipState.startX);
var y = Math.min(e.clientY, clipState.startY);
var w = Math.abs(e.clientX - clipState.startX);
var h = Math.abs(e.clientY - clipState.startY);
if (w < 10 || h < 10) {
// 太小,忽略
clipState.rect.style.display = 'none';
setDims(0, 0, window.innerWidth, window.innerHeight);
var tip = clipState.overlay.querySelector('.ka-clip-tip');
if (tip) tip.style.display = 'block';
return;
}
// 允许拖拽调整大小 — 添加 resize handles
addResizeHandles(x, y, w, h);
// 显示确认工具栏
showClipToolbar(x, y, w, h);
}
function addResizeHandles(x, y, w, h) {
// 给选区添加 8 个拖拽点
var handles = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
handles.forEach(function (pos) {
var handle = document.createElement('div');
handle.className = 'ka-clip-handle ka-clip-handle-' + pos;
handle.dataset.pos = pos;
clipState.rect.appendChild(handle);
handle.addEventListener('mousedown', function (e) {
e.preventDefault();
e.stopPropagation();
startResize(pos, e);
});
});
}
function startResize(pos, e) {
var startX = e.clientX;
var startY = e.clientY;
var origLeft = parseInt(clipState.rect.style.left);
var origTop = parseInt(clipState.rect.style.top);
var origW = parseInt(clipState.rect.style.width);
var origH = parseInt(clipState.rect.style.height);
function onMove(ev) {
ev.preventDefault();
var dx = ev.clientX - startX;
var dy = ev.clientY - startY;
var newLeft = origLeft, newTop = origTop, newW = origW, newH = origH;
if (pos.indexOf('e') !== -1) newW = Math.max(20, origW + dx);
if (pos.indexOf('w') !== -1) { newW = Math.max(20, origW - dx); newLeft = origLeft + dx; }
if (pos.indexOf('s') !== -1) newH = Math.max(20, origH + dy);
if (pos.indexOf('n') !== -1) { newH = Math.max(20, origH - dy); newTop = origTop + dy; }
clipState.rect.style.left = newLeft + 'px';
clipState.rect.style.top = newTop + 'px';
clipState.rect.style.width = newW + 'px';
clipState.rect.style.height = newH + 'px';
setDims(newLeft, newTop, newLeft + newW, newTop + newH);
// 更新工具栏位置
if (clipState.toolbar) {
clipState.toolbar.style.left = newLeft + 'px';
clipState.toolbar.style.top = (newTop + newH + 8) + 'px';
}
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.removeEventListener('mousemove', onClipMouseMove);
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
}
function showClipToolbar(x, y, w, h) {
if (clipState.toolbar) clipState.toolbar.remove();
var bar = document.createElement('div');
bar.className = 'ka-clip-toolbar';
bar.style.left = x + 'px';
bar.style.top = (y + h + 8) + 'px';
// 选区尺寸提示
bar.innerHTML =
'<span class="ka-clip-size">' + Math.round(w) + ' × ' + Math.round(h) + '</span>' +
'<button class="ka-clip-btn ka-clip-btn-cancel">取消 <kbd>Esc</kbd></button>' +
'<button class="ka-clip-btn ka-clip-btn-confirm">确认截取 <kbd>↵</kbd></button>';
clipState.overlay.appendChild(bar);
clipState.toolbar = bar;
bar.querySelector('.ka-clip-btn-cancel').addEventListener('click', cancelClip);
bar.querySelector('.ka-clip-btn-confirm').addEventListener('click', confirmClip);
}
function onClipKeyDown(e) {
if (!clipState.active) return;
if (e.key === 'Escape') {
cancelClip();
} else if (e.key === 'Enter') {
confirmClip();
}
}
function confirmClip() {
if (!clipState.rect) return;
var rectLeft = parseInt(clipState.rect.style.left);
var rectTop = parseInt(clipState.rect.style.top);
var rectW = parseInt(clipState.rect.style.width);
var rectH = parseInt(clipState.rect.style.height);
if (isNaN(rectW) || isNaN(rectH) || rectW < 10 || rectH < 10) {
showNotification('选区太小,请重新框选', 'error');
return;
}
// 1. 先提取选区内的文字内容
var content = extractTextInRect(
rectLeft + window.scrollX,
rectTop + window.scrollY,
rectW,
rectH
);
// 2. 保存裁剪参数(异步回调中使用)
var cropLeft = rectLeft;
var cropTop = rectTop;
var cropW = rectW;
var cropH = rectH;
// 3. 逐个隐藏截图工具元素,确保截图干净
// 先隐藏选区框(绿色边框)、resize handles(小方块)、工具栏
if (clipState.rect) clipState.rect.style.display = 'none';
if (clipState.toolbar) clipState.toolbar.style.display = 'none';
// 隐藏所有 handles(以防有残留的)
if (clipState.overlay) {
var handles = clipState.overlay.querySelectorAll('.ka-clip-handle');
handles.forEach(function (h) { h.style.display = 'none'; });
// 隐藏提示文字
var tip = clipState.overlay.querySelector('.ka-clip-tip');
if (tip) tip.style.display = 'none';
}
// 最后隐藏整个 overlay(包括暗幕遮罩)
if (clipState.overlay) clipState.overlay.style.display = 'none';
// 4. 双重 requestAnimationFrame 确保浏览器完成渲染后再截图
requestAnimationFrame(function () {
requestAnimationFrame(function () {
chrome.runtime.sendMessage({ type: 'CAPTURE_SCREENSHOT' }, function (resp) {
// 检查连接错误
var lastErr = chrome.runtime.lastError;
// 立即清除遮罩
cancelClip();
if (lastErr || !resp || !resp.success || !resp.dataUrl) {
// 截图失败,回退到纯文字保存
doSaveSelectClip(content, null);
return;
}
// 5. 用 canvas 裁剪选区部分
var img = new Image();
img.onload = function () {
var dpr = window.devicePixelRatio || 1;
var canvas = document.createElement('canvas');
canvas.width = cropW * dpr;
canvas.height = cropH * dpr;
var ctx = canvas.getContext('2d');
ctx.drawImage(img,
cropLeft * dpr, cropTop * dpr, cropW * dpr, cropH * dpr,
0, 0, cropW * dpr, cropH * dpr
);
var croppedDataUrl = canvas.toDataURL('image/jpeg', 0.8);
doSaveSelectClip(content, croppedDataUrl);
};
img.onerror = function () {
doSaveSelectClip(content, null);
};
img.src = resp.dataUrl;
});
});
});
}
// 保存选择剪藏数据(独立函数,不依赖 clipState)
function doSaveSelectClip(textContent, screenshotDataUrl) {
// 只要有截图或有文字,就保存
var hasText = textContent && textContent.trim().length > 0;
var hasScreenshot = !!screenshotDataUrl;
if (!hasText && !hasScreenshot) {
showNotification('选区内没有可保存的内容', 'error');
return;
}
var title = document.title || location.hostname;
var clipData = {
type: 'select-clip',
content: hasText ? textContent.trim() : '(截图内容)',
title: '截图 - ' + title,
meta: { url: location.href, title: title, timestamp: Date.now() }
};
if (hasScreenshot) {
clipData.screenshot = screenshotDataUrl;
}
chrome.runtime.sendMessage({
type: 'SAVE_CLIP',
payload: clipData
}, function (resp) {
var err = chrome.runtime.lastError;
if (err) {
showNotification('保存失败: ' + err.message, 'error');
return;
}
if (resp && resp.success) {
showNotification('内容已截取保存', 'success');
chrome.runtime.sendMessage({
type: 'CLIP_RESULT',
payload: { title: clipData.title, content: clipData.content }
});
} else {
showNotification('保存失败: ' + ((resp && resp.error) || '未知错误'), 'error');
}
});
}
function extractTextInRect(absX, absY, w, h) {
// 隐藏遮罩以免干扰元素查找
if (clipState.overlay) clipState.overlay.style.display = 'none';
var texts = [];
var walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
var node;
while (node = walker.nextNode()) {
var text = node.textContent.trim();
if (!text) continue;
var range = document.createRange();
range.selectNodeContents(node);
var rects = range.getClientRects();
for (var i = 0; i < rects.length; i++) {
var r = rects[i];
var nodeAbsX = r.left + window.scrollX;
var nodeAbsY = r.top + window.scrollY;
// 检查是否在选区内(有交集)
if (nodeAbsX + r.width > absX &&
nodeAbsX < absX + w &&
nodeAbsY + r.height > absY &&
nodeAbsY < absY + h) {
texts.push(text);
break;
}
}
}
if (clipState.overlay) clipState.overlay.style.display = '';
// 去重并保持顺序
var seen = {};
var unique = [];
texts.forEach(function (t) {
if (!seen[t]) {
seen[t] = true;
unique.push(t);
}
});
return unique.join('\n');
}
function cancelClip() {
clipState.active = false;
document.removeEventListener('keydown', onClipKeyDown);
document.removeEventListener('mousemove', onClipMouseMove);
document.removeEventListener('mouseup', onClipMouseUp);
if (clipState.overlay) {
clipState.overlay.remove();
clipState.overlay = null;
}
clipState.rect = null;
clipState.toolbar = null;
clipState.dimTop = null;
clipState.dimBottom = null;
clipState.dimLeft = null;
clipState.dimRight = null;
}
// === 页面内通知 ===
function showNotification(msg, type) {
var n = document.createElement('div');
n.className = 'ka-notification ka-notification-' + (type || 'info');
n.textContent = msg;
document.body.appendChild(n);
requestAnimationFrame(function () {
n.classList.add('ka-notification-show');
});
setTimeout(function () {
n.classList.remove('ka-notification-show');
setTimeout(function () { n.remove(); }, 300);
}, 2500);
}
// === 消息监听 ===
function ensureMessageListener() {
// 移除旧版监听器(如果有)
if (window.__kaMessageListener) {
try { chrome.runtime.onMessage.removeListener(window.__kaMessageListener); } catch (e) {}
}
window.__kaMessageListener = function (msg, sender, sendResponse) {
if (msg.type === 'SMART_CLIP') {
smartClip();
sendResponse({ success: true });
}
if (msg.type === 'SELECT_CLIP') {
startSelectClip();
sendResponse({ success: true });
}
if (msg.type === 'SHOW_NOTIFICATION' && msg.payload) {
showNotification(msg.payload.msg, msg.payload.status);
sendResponse({ success: true });
}
return true;
};
chrome.runtime.onMessage.addListener(window.__kaMessageListener);
}
// === 选中文字气泡 ===
var selBubble = null;
var selBubbleHideTimer = null;
function removeSelBubble() {
if (selBubble) {
selBubble.remove();
selBubble = null;
}
if (selBubbleHideTimer) {
clearTimeout(selBubbleHideTimer);
selBubbleHideTimer = null;
}
}
function createSelBubble(text, rect) {
removeSelBubble();
var bubble = document.createElement('div');
bubble.className = 'ka-sel-bubble';
// 保存到笔记按钮
var saveBtn = document.createElement('button');
saveBtn.className = 'ka-sel-btn ka-sel-btn-save';
saveBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>保存到笔记';
// 分隔线
var divider = document.createElement('div');
divider.className = 'ka-sel-divider';
// 问 WeKnora 按钮
var askBtn = document.createElement('button');
askBtn.className = 'ka-sel-btn ka-sel-btn-ask';
askBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>问 WeKnora';
bubble.appendChild(saveBtn);
bubble.appendChild(divider);
bubble.appendChild(askBtn);
// 小三角
var arrow = document.createElement('div');
arrow.className = 'ka-sel-arrow';
bubble.appendChild(arrow);
document.body.appendChild(bubble);
selBubble = bubble;
// 定位:在选区上方居中
var bubbleW = bubble.offsetWidth;
var bubbleH = bubble.offsetHeight;
var left = rect.left + rect.width / 2 - bubbleW / 2 + window.scrollX;
var top = rect.top - bubbleH - 10 + window.scrollY;
// 防止溢出左右
if (left < 8) left = 8;
if (left + bubbleW > document.documentElement.scrollWidth - 8) {
left = document.documentElement.scrollWidth - bubbleW - 8;
}
// 如果上方空间不够,放到下方
if (rect.top - bubbleH - 10 < 0) {
top = rect.bottom + 10 + window.scrollY;
arrow.className = 'ka-sel-arrow ka-sel-arrow-top';
}
bubble.style.left = left + 'px';
bubble.style.top = top + 'px';
// "问 WeKnora" — 发送到 sidepanel
askBtn.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
chrome.runtime.sendMessage({
type: 'ASK_WEKNORA',
payload: { text: text }
}, function () {
void chrome.runtime.lastError;
});
removeSelBubble();
window.getSelection().removeAllRanges();
});
// "保存到笔记" — 保存到 ka_clips
saveBtn.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var clipTitle = '选中文本 - ' + (document.title || location.hostname);
var clipPayload = {
type: 'select-clip',
content: text,
title: clipTitle,
meta: { url: location.href, title: document.title || '' }
};
function doSave(retryCount) {
chrome.runtime.sendMessage({
type: 'SAVE_SELECTION',
payload: clipPayload
}, function (saveResp) {
var err = chrome.runtime.lastError;
if (err) {
// service worker 可能休眠,重试一次
if (retryCount < 1) {
setTimeout(function () { doSave(retryCount + 1); }, 500);
} else {
showNotification('保存失败: ' + err.message, 'error');
}
return;
}
if (saveResp && saveResp.success) {
showNotification('已保存到笔记', 'success');
} else {
showNotification('保存失败: ' + ((saveResp && saveResp.error) || '未知错误'), 'error');
}
});
}
doSave(0);
removeSelBubble();
window.getSelection().removeAllRanges();
});
// 点击气泡外关闭
setTimeout(function () {
document.addEventListener('mousedown', onBubbleOutsideClick);
}, 50);
}
function onBubbleOutsideClick(e) {
if (selBubble && !selBubble.contains(e.target)) {
removeSelBubble();
document.removeEventListener('mousedown', onBubbleOutsideClick);
}
}
// 监听鼠标抬起 → 检测选中文字
document.addEventListener('mouseup', function (e) {
// 如果正在框选剪藏或点击在气泡内,不触发
if (clipState.active) return;
if (selBubble && selBubble.contains(e.target)) return;
if (imgBubble && imgBubble.contains(e.target)) return;
// 延迟一点让 selection 更新
setTimeout(function () {
var sel = window.getSelection();
var text = sel ? sel.toString().trim() : '';
if (text.length < 2) {
// 没有选中有效文字,移除气泡
removeSelBubble();
document.removeEventListener('mousedown', onBubbleOutsideClick);
return;
}
// 获取选区矩形
if (sel.rangeCount > 0) {
var range = sel.getRangeAt(0);
var rects = range.getClientRects();
if (rects.length > 0) {
// 使用第一个 rect 的顶部和整体范围来定位
var boundRect = range.getBoundingClientRect();
createSelBubble(text, boundRect);
}
}
}, 10);
});
// === 右键图片气泡 ===
var imgBubble = null;
function removeImgBubble() {
if (imgBubble) {
imgBubble.remove();
imgBubble = null;
}
}
function createImgBubble(imgSrc, imgAlt, rect) {
removeImgBubble();
removeSelBubble(); // 同时移除文字气泡
var bubble = document.createElement('div');
bubble.className = 'ka-sel-bubble ka-img-bubble';
// 保存图片按钮
var saveBtn = document.createElement('button');
saveBtn.className = 'ka-sel-btn ka-sel-btn-save';
saveBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>保存图片到笔记';
// 分隔线
var divider = document.createElement('div');
divider.className = 'ka-sel-divider';
// 问 WeKnora 按钮
var askBtn = document.createElement('button');
askBtn.className = 'ka-sel-btn ka-sel-btn-ask';
askBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>问 WeKnora';
bubble.appendChild(saveBtn);
bubble.appendChild(divider);
bubble.appendChild(askBtn);
// 小三角
var arrow = document.createElement('div');
arrow.className = 'ka-sel-arrow';
bubble.appendChild(arrow);
document.body.appendChild(bubble);
imgBubble = bubble;
// 定位:在图片上方居中
var bubbleW = bubble.offsetWidth;
var bubbleH = bubble.offsetHeight;
var left = rect.left + rect.width / 2 - bubbleW / 2 + window.scrollX;
var top = rect.top - bubbleH - 10 + window.scrollY;
// 防止溢出左右
if (left < 8) left = 8;
if (left + bubbleW > document.documentElement.scrollWidth - 8) {
left = document.documentElement.scrollWidth - bubbleW - 8;
}
// 如果上方空间不够,放到下方
if (rect.top - bubbleH - 10 < 0) {
top = rect.bottom + 10 + window.scrollY;
arrow.className = 'ka-sel-arrow ka-sel-arrow-top';
}
bubble.style.left = left + 'px';
bubble.style.top = top + 'px';
// "保存图片到笔记"
saveBtn.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var clipTitle = '图片收藏 - ' + (document.title || location.hostname);
chrome.runtime.sendMessage({
type: 'SAVE_IMAGE',
payload: {
type: 'image-clip',
content: '![' + (imgAlt || '图片') + '](' + imgSrc + ')',
title: clipTitle,
meta: { url: location.href, title: document.title || '', imageUrl: imgSrc }
}
}, function (saveResp) {
void chrome.runtime.lastError;
if (saveResp && saveResp.success) {
showNotification('图片已保存到笔记', 'success');
} else {
showNotification('保存失败', 'error');
}
});
removeImgBubble();
});
// "问 WeKnora" — 发送图片描述到 sidepanel
askBtn.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var question = '请描述这张图片的内容: ' + imgSrc;
chrome.runtime.sendMessage({
type: 'ASK_WEKNORA',
payload: { text: question }
}, function () {
void chrome.runtime.lastError;
});
removeImgBubble();
});
// 点击气泡外关闭
setTimeout(function () {
document.addEventListener('mousedown', onImgBubbleOutsideClick);
}, 50);
}
function onImgBubbleOutsideClick(e) {
if (imgBubble && !imgBubble.contains(e.target)) {
removeImgBubble();
document.removeEventListener('mousedown', onImgBubbleOutsideClick);
}
}
// 监听右键点击图片 → 弹出气泡
document.addEventListener('contextmenu', function (e) {
// 如果正在框选剪藏,不触发
if (clipState.active) return;
var target = e.target;
// 检查是否右键点击了图片
if (target.tagName === 'IMG' && target.src) {
// 延迟一点,让浏览器原生右键菜单先出现
setTimeout(function () {
var imgRect = target.getBoundingClientRect();
createImgBubble(target.src, target.alt || '', imgRect);
}, 100);
} else {
// 不是图片,移除图片气泡
removeImgBubble();
}
});
// === Option + Command 快捷键直接触发选择剪藏 ===
// Chrome 扩展 commands API 不支持 Alt+Command 组合,所以在页面层监听
var optCmdState = { altDown: false, metaDown: false };
document.addEventListener('keydown', function (e) {
// 如果正在剪藏中,不响应
if (clipState.active) return;
// 记录按键状态
if (e.key === 'Alt') optCmdState.altDown = true;
if (e.key === 'Meta') optCmdState.metaDown = true;
// 同时按住 Option(Alt) + Command(Meta) 触发选择剪藏
if (optCmdState.altDown && optCmdState.metaDown) {
e.preventDefault();
optCmdState.altDown = false;
optCmdState.metaDown = false;
startSelectClip();
}
});
document.addEventListener('keyup', function (e) {
if (e.key === 'Alt') optCmdState.altDown = false;
if (e.key === 'Meta') optCmdState.metaDown = false;
});
// 窗口失焦时重置状态,防止按键卡住
window.addEventListener('blur', function () {
optCmdState.altDown = false;
optCmdState.metaDown = false;
});
// 暴露到 window 上以便防重复注入时调用
window.ensureMessageListener = ensureMessageListener;
ensureMessageListener();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 B

@@ -1,76 +0,0 @@
{
"manifest_version": 3,
"name": "快速笔记 5.0",
"version": "5.0.0",
"description": "快速笔记 5.0 - 知识库问答、网页内容采集、Markdown 速记",
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"default_title": "快速笔记 5.0"
},
"side_panel": {
"default_path": "sidepanel.html"
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": ["content.css"],
"run_at": "document_idle"
}
],
"permissions": [
"storage",
"activeTab",
"sidePanel",
"scripting",
"contextMenus",
"tabs"
],
"web_accessible_resources": [
{
"resources": ["weknora.html", "weknora.js", "clips.html", "clips.js"],
"matches": ["<all_urls>"]
}
],
"host_permissions": [
"<all_urls>"
],
"commands": {
"open-sidepanel": {
"suggested_key": {
"default": "Ctrl+Shift+K",
"mac": "Command+Shift+K"
},
"description": "打开侧边栏"
},
"quick-ask": {
"suggested_key": {
"default": "Ctrl+Shift+A",
"mac": "Command+Shift+A"
},
"description": "快速提问"
},
"select-clip": {
"suggested_key": {
"default": "Ctrl+Shift+S",
"mac": "Command+Shift+S"
},
"description": "选择剪藏"
}
}
}
@@ -1,978 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>快速笔记 5.0</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root { --primary: #07C160; --primary-dark: #06ad54; --bg: #f5f6f7; --white: #fff; --text: #333; --text2: #666; --text3: #999; --border: #e5e5e5; --shadow: 0 2px 12px rgba(0,0,0,0.08); --radius: 12px; }
body { width: 380px; max-height: 680px; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); overflow: hidden; }
/* Pages */
.page { display: none; flex-direction: column; overflow: hidden; }
.page.active { display: flex; }
/* === Page 1: 选择登录 === */
#pg-login { align-items: center; justify-content: center; background: linear-gradient(135deg, #07C160 0%, #06ad54 100%); color: #fff; flex: 1; min-height: 480px; }
#pg-login .logo { width: 72px; height: 72px; background: rgba(255,255,255,0.2); border-radius: 18px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; }
#pg-login .logo img { width: 48px; height: 48px; }
#pg-login h1 { font-size: 22px; font-weight: 600; margin-bottom: 6px; }
#pg-login .subtitle { font-size: 13px; opacity: 0.85; margin-bottom: 40px; }
.login-btn { width: 280px; height: 48px; border: none; border-radius: 24px; font-size: 15px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: all 0.2s; margin-bottom: 14px; }
.login-btn:active { transform: scale(0.97); }
#btn-ka { background: #fff; color: var(--primary); }
#btn-ka:hover { background: #f0faf4; }
#btn-wk { background: rgba(255,255,255,0.18); color: #fff; border: 1.5px solid rgba(255,255,255,0.5); }
#btn-wk:hover { background: rgba(255,255,255,0.28); }
.login-btn svg { width: 20px; height: 20px; }
/* === Page 2: 快速笔记 5.0 登录(扫码 + API Key 双模式) === */
#pg-ka-login { background: var(--white); min-height: 480px; }
.ka-header { padding: 16px 20px 0; display: flex; align-items: center; gap: 10px; }
.ka-back { width: 32px; height: 32px; border: none; background: #f5f5f5; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); transition: color 0.12s; }
.ka-back:hover { color: var(--text); }
.ka-back svg { width: 18px; height: 18px; }
.ka-title { font-size: 15px; font-weight: 600; color: var(--text); }
/* 登录方式 Tab */
.ka-tabs { display: flex; margin: 16px 20px 0; background: #f3f3f3; border-radius: 8px; padding: 3px; }
.ka-tab { flex: 1; height: 34px; border: none; border-radius: 6px; background: transparent; font-size: 13px; font-weight: 500; color: var(--text3); cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 5px; transition: all 0.15s; }
.ka-tab.active { background: #fff; color: var(--text); }
.ka-tab svg { width: 14px; height: 14px; }
/* 登录内容区 */
.ka-body { padding: 20px; }
.ka-panel { display: none; }
.ka-panel.active { display: flex; flex-direction: column; align-items: center; }
/* 扫码面板 */
.qr-wrap { display: flex; flex-direction: column; align-items: center; padding: 6px 0; }
.qr-box { width: 200px; height: 200px; background: #fff; border: 1px solid #eee; border-radius: 12px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; margin-bottom: 14px; }
.qr-box canvas { width: 180px; height: 180px; cursor: pointer; border-radius: 6px; }
.qr-box canvas:active { opacity: 0.7; }
.qr-box .qr-placeholder { width: 180px; height: 180px; background: #f8f8f8; border-radius: 6px; display: flex; align-items: center; justify-content: center; }
.qr-tip { font-size: 13px; font-weight: 500; color: var(--text); margin-bottom: 4px; }
.qr-sub { font-size: 12px; color: var(--text3); margin-bottom: 14px; }
.qr-status { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text3); padding: 6px 14px; background: #f5f5f5; border-radius: 6px; }
.qr-status .dot { width: 6px; height: 6px; border-radius: 50%; background: #ccc; animation: pulse-dot 1.2s ease infinite; }
.qr-status.scanned { color: var(--primary); background: rgba(7,193,96,0.06); }
.qr-status.scanned .dot { background: var(--primary); }
@keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
.qr-refresh { margin-top: 10px; border: none; background: none; color: var(--primary); font-size: 12px; cursor: pointer; padding: 4px 10px; border-radius: 6px; transition: background 0.12s; }
.qr-refresh:hover { background: rgba(7,193,96,0.06); }
/* API Key 面板 */
.ka-form { width: 100%; }
.ka-form .form-group { margin-bottom: 14px; }
.ka-form .form-group label { display: block; font-size: 13px; font-weight: 500; color: var(--text2); margin-bottom: 5px; }
.ka-form .form-group input { width: 100%; height: 42px; border: 1px solid #eee; border-radius: 8px; padding: 0 12px; font-size: 13px; outline: none; transition: border-color 0.12s; }
.ka-form .form-group input:focus { border-color: #ccc; }
.ka-form .form-group input::placeholder { color: #ccc; }
.ka-test-msg { font-size: 12px; text-align: center; min-height: 16px; margin-bottom: 8px; }
.ka-test-msg.ok { color: var(--primary); }
.ka-test-msg.err { color: #e44; }
.ka-form .btn-primary { width: 100%; }
/* === Page 3: WeKnora 配置 === */
#pg-weknora { background: var(--white); min-height: 480px; }
.wk-header { padding: 24px 20px 16px; text-align: center; }
.wk-header h2 { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
.wk-header p { font-size: 13px; color: var(--text3); }
.wk-form { padding: 0 20px; flex: 1; }
.form-group { margin-bottom: 18px; }
.form-group label { display: block; font-size: 13px; font-weight: 500; color: var(--text2); margin-bottom: 6px; }
.form-group input { width: 100%; height: 44px; border: 1.5px solid var(--border); border-radius: 10px; padding: 0 14px; font-size: 14px; outline: none; transition: border-color 0.2s; }
.form-group input:focus { border-color: var(--primary); }
.form-group input::placeholder { color: #ccc; }
.wk-actions { padding: 0 20px 24px; display: flex; flex-direction: column; gap: 10px; }
.btn-primary { height: 46px; border: none; border-radius: 23px; background: var(--primary); color: #fff; font-size: 15px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.btn-primary:hover { background: var(--primary-dark); }
.btn-primary:disabled { background: #ccc; cursor: not-allowed; }
.btn-secondary { height: 40px; border: 1.5px solid var(--border); border-radius: 20px; background: transparent; color: var(--text2); font-size: 14px; cursor: pointer; transition: all 0.2s; }
.btn-secondary:hover { border-color: var(--text3); }
.wk-test-result { font-size: 12px; text-align: center; min-height: 20px; margin-top: 4px; }
.wk-test-result.ok { color: var(--primary); }
.wk-test-result.err { color: #e44; }
.wk-help { display: flex; align-items: center; gap: 6px; padding: 10px 14px; font-size: 12px; color: #8a7340; background: #fefaf0; border-radius: 10px; margin-top: 4px; }
.wk-help svg { width: 14px; height: 14px; flex-shrink: 0; color: #c9a23c; }
.wk-help a { color: var(--primary); text-decoration: none; font-weight: 500; }
.wk-help a:hover { text-decoration: underline; }
/* === Page 4: 主界面 === */
#pg-main { background: #fff; overflow-y: auto; }
/* 顶栏 */
.main-top { padding: 12px 18px; display: flex; align-items: center; gap: 12px; }
.main-avatar { width: 38px; height: 38px; border-radius: 50%; overflow: hidden; flex-shrink: 0; }
.main-avatar img { width: 100%; height: 100%; object-fit: cover; }
.main-avatar .letter-avatar { width: 100%; height: 100%; background: var(--primary); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 600; border-radius: 50%; }
.main-info { flex: 1; min-width: 0; }
.main-name { font-size: 14px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.main-badge { display: inline-block; font-size: 10px; padding: 2px 8px; border-radius: 6px; margin-top: 2px; font-weight: 500; }
.badge-ka { background: rgba(7,193,96,0.12); color: var(--primary); }
.badge-wk { background: rgba(7,193,96,0.12); color: var(--primary); }
.main-actions { position: relative; }
.more-btn { width: 30px; height: 30px; border: none; background: transparent; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); transition: color 0.15s; }
.more-btn:hover { color: var(--text); }
.more-btn svg { width: 20px; height: 20px; }
.more-menu { position: absolute; top: 100%; right: 0; margin-top: 4px; background: var(--white); border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,0.1); padding: 4px 0; min-width: 140px; display: none; z-index: 50; }
.more-menu.show { display: block; }
.more-menu-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; font-size: 13px; color: var(--text2); cursor: pointer; transition: all 0.12s; border: none; background: none; width: 100%; text-align: left; }
.more-menu-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.more-menu-item svg { width: 16px; height: 16px; flex-shrink: 0; }
/* 主内容区 */
.main-content { flex: 1; display: flex; flex-direction: column; padding: 14px 18px 14px; overflow-y: auto; }
/* ===== 知识采集 — 卡片 + 浮动胶囊 ===== */
.collect-wrap {
position: relative;
padding-bottom: 30px; /* 胶囊按钮露出空间 */
margin-bottom: 6px;
perspective: 1000px;
}
/* 翻转时隐藏胶囊 */
.collect-wrap.flipped .collect-fab {
opacity: 0;
pointer-events: none;
transform: translateX(-50%) scale(0.8);
}
/* 翻转时不需要底部占位 */
.collect-wrap.flipped {
padding-bottom: 0;
}
.collect-card-flipper {
position: relative;
width: 100%;
min-height: 220px;
transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1);
transform-style: preserve-3d;
}
.collect-wrap.flipped .collect-card-flipper {
transform: rotateY(180deg);
}
.collect-card-front,
.collect-card-back {
position: absolute;
top: 0; left: 0;
width: 100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
border-radius: 18px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.06), 0 4px 24px rgba(0, 0, 0, 0.10);
background: var(--white);
overflow: hidden;
min-height: 220px;
}
.collect-card-front {
z-index: 2;
cursor: pointer;
transition: box-shadow 0.25s ease;
}
.collect-card-front:hover {
box-shadow: 0 0 12px rgba(0, 0, 0, 0.08), 0 8px 32px rgba(0, 0, 0, 0.14);
}
.collect-card-back {
transform: rotateY(180deg);
z-index: 1;
cursor: default;
display: flex;
flex-direction: column;
height: 100%; /* 填满 flipper 高度 */
}
/* flipper 高度由可见面的内容驱动 */
.collect-card-flipper { min-height: 220px; }
/* 空状态 */
.collect-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
padding: 60px 16px 70px;
color: #b5c9bc;
}
.collect-empty svg { width: 30px; height: 30px; opacity: 0.4; }
.collect-empty span { font-size: 13px; font-weight: 500; color: #aebfb4; }
.collect-empty-sub { font-size: 11px; font-weight: 400; color: #c8d5cc; }
/* 有内容 */
.collect-content { padding: 14px 16px 16px; }
.collect-content-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.collect-content-meta { display: flex; align-items: center; gap: 8px; }
.latest-clip-tag {
font-size: 10px;
padding: 2px 7px;
border-radius: 4px;
background: rgba(7, 193, 96, 0.12);
color: var(--primary);
font-weight: 600;
}
.latest-clip-tag.tag-markdown { background: rgba(59,125,216,0.12); color: #3b7dd8; }
.latest-clip-tag.tag-image { background: rgba(233,89,80,0.12); color: #e95950; }
.latest-clip-tag.tag-text { background: rgba(134,142,150,0.12); color: #868e96; }
.latest-clip-time { font-size: 10px; color: #bbb; }
.collect-content-more {
display: flex;
align-items: center;
gap: 2px;
border: none;
background: #f5f6f7;
font-size: 11px;
color: var(--text3);
cursor: pointer;
padding: 3px 8px 3px 10px;
border-radius: 10px;
transition: all 0.15s;
}
.collect-content-more:hover { color: var(--primary); background: rgba(7,193,96,0.08); }
.collect-content-more svg { width: 12px; height: 12px; }
.collect-content-body {
font-size: 13px;
color: var(--text2);
line-height: 1.65;
max-height: 130px;
overflow: hidden;
word-break: break-word;
}
/* 只在内容溢出时才显示底部渐隐 */
.collect-content-body.overflowing {
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 88%, transparent 100%);
mask-image: linear-gradient(to bottom, #000 0%, #000 88%, transparent 100%);
}
.collect-content-body img {
max-width: 100%;
border-radius: 8px;
max-height: 110px;
object-fit: cover;
}
/* ===== 浮动椭圆胶囊按钮 ===== */
.collect-fab {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 6px;
background: rgba(255, 255, 255, 0.75);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
padding: 5px 6px;
border-radius: 50px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
z-index: 5;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.collect-fab-btn {
width: 38px;
height: 38px;
border: none;
border-radius: 50%;
background: rgba(245, 246, 247, 0.85);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.18s ease;
color: #666;
}
.collect-fab-btn svg { width: 17px; height: 17px; fill: currentColor; stroke: none; }
.collect-fab-btn:hover {
background: rgba(7, 193, 96, 0.1);
color: var(--primary);
transform: scale(1.08);
}
.collect-fab-btn:active { transform: scale(0.94); }
/* 中间主按钮高亮 */
.collect-fab-btn.fab-primary {
width: 42px;
height: 42px;
background: var(--primary);
color: #fff;
box-shadow: 0 3px 12px rgba(7, 193, 96, 0.3);
}
.collect-fab-btn.fab-primary:hover {
background: var(--primary-dark);
color: #fff;
box-shadow: 0 4px 16px rgba(7, 193, 96, 0.4);
transform: scale(1.08);
}
.collect-fab-btn.fab-primary svg { width: 19px; height: 19px; }
/* Markdown 速记 — 卡片背面 */
.note-back-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px 0;
flex-shrink: 0;
}
.note-back-title {
font-size: 12px;
font-weight: 600;
color: var(--primary);
display: flex;
align-items: center;
gap: 5px;
}
.note-back-title svg { width: 13px; height: 13px; }
.note-back-close {
width: 26px;
height: 26px;
border: none;
background: #f5f6f7;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text3);
transition: all 0.15s;
}
.note-back-close:hover { background: #eee; color: var(--text); }
.note-back-close svg { width: 14px; height: 14px; }
.note-card { background: transparent; border-radius: 0; overflow: hidden; border: none; flex: 1; display: flex; flex-direction: column; min-height: 0; }
.note-input { width: 100%; border: none; background: transparent; padding: 10px 14px; font-size: 13px; font-family: "SF Mono", "Fira Code", "Menlo", monospace; resize: none; outline: none; line-height: 1.6; color: var(--text); overflow-y: auto; flex: 1; min-height: 0; }
.note-input::placeholder { color: #c0c0c0; }
.note-preview { display: none; padding: 12px 14px; font-size: 13px; line-height: 1.7; color: var(--text2); overflow-y: auto; flex: 1; min-height: 0; }
.note-preview h1 { font-size: 18px; font-weight: 700; margin: 0 0 8px; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: 5px; }
.note-preview h2 { font-size: 16px; font-weight: 700; margin: 10px 0 6px; color: var(--text); }
.note-preview h3 { font-size: 14px; font-weight: 600; margin: 8px 0 4px; color: var(--text); }
.note-preview h4 { font-size: 13px; font-weight: 600; margin: 6px 0 3px; color: var(--text); }
.note-preview p { margin: 4px 0; line-height: 1.7; }
.note-preview ul, .note-preview ol { margin: 4px 0; padding-left: 18px; }
.note-preview li { margin: 2px 0; line-height: 1.6; }
.note-preview code { font-family: "SF Mono","Fira Code","Menlo",monospace; background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; color: #d63384; }
.note-preview pre { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 10px 12px; overflow-x: auto; margin: 6px 0; font-size: 12px; line-height: 1.5; }
.note-preview pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
.note-preview blockquote { border-left: 3px solid var(--primary); padding: 4px 12px; margin: 6px 0; background: #f8faf8; color: var(--text2); font-size: 13px; border-radius: 0 6px 6px 0; }
.note-preview hr { border: none; border-top: 1px solid var(--border); margin: 8px 0; }
.note-preview a { color: var(--primary); text-decoration: none; }
.note-preview a:hover { text-decoration: underline; }
.note-preview strong { font-weight: 700; }
.note-preview em { font-style: italic; }
.note-preview table { width: 100%; border-collapse: collapse; margin: 6px 0; font-size: 12px; }
.note-preview th, .note-preview td { border: 1px solid var(--border); padding: 4px 8px; text-align: left; }
.note-preview th { background: #f5f6f7; font-weight: 600; }
.note-preview img { max-width: 100%; border-radius: 6px; margin: 4px 0; }
.note-tool-btn.active { background: rgba(7,193,96,0.12); color: var(--primary); }
.note-toolbar { display: flex; align-items: center; padding: 2px 8px; gap: 0; border-top: 1px solid #edf8f1; flex-shrink: 0; }
.note-toolbar-group { display: flex; align-items: center; gap: 0; }
.note-toolbar-divider { width: 1px; height: 14px; background: #e0e8e2; margin: 0 6px; flex-shrink: 0; }
.note-tool-btn { width: 28px; height: 28px; border: none; background: transparent; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: #999; transition: all 0.15s; padding: 0; }
.note-tool-btn:hover { background: rgba(7,193,96,0.08); color: #555; }
.note-tool-btn:active { background: rgba(7,193,96,0.15); }
.note-tool-btn svg { width: 15px; height: 15px; }
.note-toolbar-spacer { flex: 1; }
.note-bottom { display: flex; align-items: center; justify-content: flex-end; padding: 4px 10px 8px; }
.note-save { height: 28px; width: 28px; border: none; border-radius: 50%; background: #ddd; color: #fff; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; }
.note-save.active { background: var(--primary); }
.note-save:hover.active { background: var(--primary-dark); box-shadow: 0 2px 8px rgba(7,193,96,0.25); }
.note-save:disabled { background: #ddd; box-shadow: none; cursor: not-allowed; }
.note-save svg { width: 14px; height: 14px; }
/* ===== 知识问答 — 单行输入栏 ===== */
/* 底部输入区域整体包裹 */
.chat-bar-wrapper {
background: var(--white);
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06), 0 0 1px rgba(0, 0, 0, 0.04);
margin-top: 4px;
overflow: visible;
position: relative;
transition: box-shadow 0.2s;
}
.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;
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-kb:hover { background: #eaebed; color: var(--text); }
.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; }
/* 输入框 */
.chat-bar-input {
flex: 1;
border: none;
background: transparent;
font-size: 13px;
font-family: inherit;
outline: none;
color: var(--text);
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; }
/* 发送按钮 */
.chat-bar-send {
display: flex;
align-items: center;
justify-content: center;
border: none;
background: #e8e8e8;
color: #aaa;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.chat-bar-send svg { width: 14px; height: 14px; }
.chat-bar-send.active { background: var(--primary); color: #fff; }
.chat-bar-send.active:hover { background: var(--primary-dark); }
.chat-send-btn { width: 30px; height: 30px; border: none; border-radius: 50%; background: #ddd; color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.25s; flex-shrink: 0; }
.chat-send-btn.active { background: var(--primary); }
.chat-send-btn:hover.active { background: var(--primary-dark); }
.chat-send-btn svg { width: 14px; height: 14px; }
/* 知识库合并下拉菜单 */
.kb-dropdown { position: relative; display: inline-block; }
.kb-dropdown-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: 200px; display: none; z-index: 100; max-height: 70vh; overflow-y: auto; }
.kb-dropdown-menu.show { display: block; }
.kb-dropdown-item { padding: 9px 14px; font-size: 13px; color: var(--text); cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.15s; }
.kb-dropdown-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.kb-dropdown-item.selected { color: var(--primary); font-weight: 500; }
.kb-dropdown-item .kb-radio { width: 16px; height: 16px; border: 2px solid #ccc; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all 0.15s; }
.kb-dropdown-item.selected .kb-radio { border-color: var(--primary); }
.kb-dropdown-item.selected .kb-radio::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--primary); }
.kb-dropdown-item:hover .kb-radio { border-color: var(--primary); }
.kb-dropdown-item svg { width: 14px; height: 14px; }
/* 下拉菜单分隔线和分组标题 */
.kb-dropdown-divider { height: 1px; background: #f0f0f0; margin: 4px 12px; }
.kb-dropdown-section { padding: 6px 14px 2px; font-size: 10px; font-weight: 600; color: #bbb; letter-spacing: 0.5px; text-transform: uppercase; }
/* 模式选项 */
.kb-mode-item { padding: 8px 14px; font-size: 13px; color: var(--text); cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.15s; }
.kb-mode-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.kb-mode-item.selected { color: var(--primary); font-weight: 500; }
.kb-mode-item .kb-radio { width: 14px; height: 14px; border: 2px solid #ccc; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: all 0.15s; }
.kb-mode-item.selected .kb-radio { border-color: var(--primary); }
.kb-mode-item.selected .kb-radio::after { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--primary); }
.kb-mode-item:hover .kb-radio { border-color: var(--primary); }
.kb-mode-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); 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); }
</style>
</head>
<body>
<!-- Page 1: 登录选择 -->
<div id="pg-login" class="page active">
<div class="logo">
<img src="icons/icon48.png" alt="快速笔记 5.0">
</div>
<h1>快速笔记 5.0</h1>
<p class="subtitle">知识库问答 · 网页采集 · Markdown 速记</p>
<button id="btn-ka" class="login-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
快速笔记 5.0 登录
</button>
<button id="btn-wk" class="login-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
WeKnora 自建服务
</button>
</div>
<!-- Page 2: 快速笔记 5.0 登录(扫码 + API Key) -->
<div id="pg-ka-login" class="page">
<div class="ka-header">
<button class="ka-back" id="btn-ka-back" title="返回">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<span class="ka-title">快速笔记 5.0 登录</span>
</div>
<!-- 登录方式 Tab -->
<div class="ka-tabs">
<button class="ka-tab active" data-tab="qr">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><circle cx="17.5" cy="17.5" r="3.5"/></svg>
微信扫码
</button>
<button class="ka-tab" data-tab="apikey">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
服务地址
</button>
</div>
<div class="ka-body">
<!-- 扫码登录面板 -->
<div class="ka-panel active" id="panel-qr">
<div class="qr-wrap">
<div class="qr-box" id="qr-box">
<div class="qr-placeholder" id="qr-canvas-area"></div>
</div>
<div class="qr-tip">微信扫码登录</div>
<div class="qr-sub">打开微信扫描二维码,或点击二维码模拟登录</div>
<div class="qr-status" id="qr-status">
<span class="dot"></span>
<span id="qr-status-text">等待扫码…</span>
</div>
<button class="qr-refresh" id="btn-qr-refresh" style="display:none;">刷新二维码</button>
</div>
</div>
<!-- API Key 登录面板 -->
<div class="ka-panel" id="panel-apikey">
<div class="ka-form">
<div class="form-group">
<label>服务地址</label>
<input type="url" id="ka-url" placeholder="https://your-server.com/api">
</div>
<div class="form-group">
<label>API Key</label>
<input type="password" id="ka-key" placeholder="请输入 API Key">
</div>
<div class="ka-test-msg" id="ka-test-msg"></div>
<button class="btn-primary" id="btn-ka-save">保存并登录</button>
</div>
</div>
</div>
</div>
<!-- Page 3: WeKnora 配置 -->
<div id="pg-weknora" class="page">
<div class="wk-header">
<h2>WeKnora 自建服务</h2>
<p>请填写您的服务配置信息</p>
</div>
<div class="wk-form">
<div class="form-group">
<label>服务地址</label>
<input type="url" id="wk-url" placeholder="https://your-server.com/api">
</div>
<div class="form-group">
<label>API Key</label>
<input type="password" id="wk-key" placeholder="请输入 API Key">
</div>
<div class="wk-test-result" id="wk-test-msg"></div>
<div class="wk-help">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<span>不知道如何获取?查看 <a href="#" id="wk-help-link" target="_blank">配置指南</a></span>
</div>
</div>
<div class="wk-actions">
<button class="btn-primary" id="btn-wk-save">保存并登录</button>
<button class="btn-secondary" id="btn-wk-back">返回</button>
</div>
</div>
<!-- Page 4: 主界面 -->
<div id="pg-main" class="page">
<!-- 顶栏 -->
<div class="main-top">
<div class="main-avatar" id="main-avatar">
<div class="letter-avatar" id="main-letter" style="display:none;">S</div>
<img id="main-avatar-img" src="" alt="" style="display:none;">
</div>
<div class="main-info">
<div class="main-name" id="main-name">用户</div>
<span class="main-badge badge-ka" id="main-badge">快速笔记 5.0</span>
</div>
<div class="main-actions">
<button class="more-btn" id="btn-more" title="更多">
<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
</button>
<div class="more-menu" id="more-menu">
<button class="more-menu-item" id="btn-clips">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 016.5 2H19v16H6.5a2.5 2.5 0 000 5H19"/></svg>
笔记
</button>
<button class="more-menu-item" id="btn-sidebar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
对话面板
</button>
<button class="more-menu-item" id="btn-settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
设置
</button>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- ★ 知识采集 — 翻转卡片 + 浮动按钮 -->
<div class="collect-wrap" id="collect-wrap">
<div class="collect-card-flipper" id="card-flipper">
<!-- 正面:最新笔记内容 -->
<div class="collect-card-front" id="latest-clip">
<!-- 无内容时的占位 -->
<div class="collect-empty" id="latest-clip-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="9" y1="8" x2="17" y2="8"/><line x1="9" y1="12" x2="15" y2="12"/></svg>
<span>暂无采集内容</span>
<span class="collect-empty-sub">使用下方按钮开始采集知识</span>
</div>
<!-- 有内容时 -->
<div class="collect-content" id="latest-clip-content" style="display:none;">
<div class="collect-content-top">
<div class="collect-content-meta">
<span class="latest-clip-tag" id="latest-clip-tag"></span>
<span class="latest-clip-time" id="latest-clip-time"></span>
</div>
<button class="collect-content-more" id="btn-latest-more" title="查看全部">
全部
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</div>
<div class="collect-content-body" id="latest-clip-body"></div>
</div>
</div>
<!-- 背面:速记输入框 -->
<div class="collect-card-back" id="note-back">
<div class="note-back-header">
<div class="note-back-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
Markdown 速记
</div>
<button class="note-back-close" id="btn-note-close" title="取消">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="note-card">
<textarea class="note-input" id="note-input" placeholder="# 标题&#10;&#10;在这里快速记录…"></textarea>
<div class="note-preview" id="note-preview"></div>
<div class="note-toolbar">
<!-- 文字格式组 -->
<div class="note-toolbar-group">
<button class="note-tool-btn" data-md="bold" title="加粗 **文本**">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"/></svg>
</button>
</div>
<div class="note-toolbar-divider"></div>
<!-- 块级元素组 -->
<div class="note-toolbar-group">
<button class="note-tool-btn" data-md="heading" title="标题 ## 文本">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4H5z"/></svg>
</button>
<button class="note-tool-btn" data-md="ul" title="无序列表 - 项目">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>
</button>
<button class="note-tool-btn" data-md="ol" title="有序列表 1. 项目">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>
</button>
</div>
<div class="note-toolbar-divider"></div>
<!-- 插入组 -->
<div class="note-toolbar-group">
<button class="note-tool-btn" data-md="link" title="插入链接 [文本](url)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
</button>
<button class="note-tool-btn" data-md="image" title="插入图片 ![描述](url)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</button>
</div>
<div class="note-toolbar-divider"></div>
<!-- 预览 -->
<div class="note-toolbar-group">
<button class="note-tool-btn" id="btn-note-preview" data-md="preview" title="预览 Markdown 效果">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
<div class="note-toolbar-spacer"></div>
<!-- 保存按钮 -->
<button class="note-save active" id="btn-note-save" title="保存笔记">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
</button>
</div>
</div>
</div>
</div>
<!-- 浮动椭圆胶囊按钮 -->
<div class="collect-fab" id="collect-fab">
<button class="collect-fab-btn" id="btn-smart-clip" title="智剪">
<svg viewBox="0 0 24 24"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</button>
<button class="collect-fab-btn fab-primary" id="btn-select-clip" title="剪藏">
<svg viewBox="0 0 24 24"><path d="M3 3h5v2H5v3H3V3zm13 0h5v5h-2V5h-3V3zM3 16v5h5v-2H5v-3H3zm18 0v5h-5v-2h3v-3h2zM7 7h10v10H7V7z"/></svg>
</button>
<button class="collect-fab-btn" id="btn-quick-note" title="速记">
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
</button>
</div>
</div>
<div class="popup-image-previews" id="popup-image-previews" style="display:none;"></div>
<div class="chat-bar-wrapper">
<div class="chat-bar-toolbar">
<div class="kb-dropdown" id="kb-dropdown">
<button class="chat-bar-kb" id="btn-kb-select">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
<span id="kb-name">全部</span>
<svg class="chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="kb-dropdown-menu" id="kb-menu">
<div class="kb-dropdown-item selected" data-kb="all">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
全部知识库
</div>
<div class="kb-dropdown-item" data-kb="quick-note">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
快速笔记
</div>
<div class="kb-dropdown-item" data-kb="web-collect">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
网页收藏
</div>
<div class="kb-dropdown-item" data-kb="product-doc">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
产品文档
</div>
<div class="kb-dropdown-item" data-kb="tech-spec">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
技术规范
</div>
</div>
</div>
<button class="chat-bar-mode" id="btn-mode-select">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
<span id="mode-name">快速回答</span>
<svg class="chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="popup-mode-menu" id="popup-mode-menu">
<div class="kb-mode-item selected" data-mode="fast">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
快速
</div>
<div class="kb-mode-item" data-mode="deep">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
深度
</div>
<div class="kb-mode-item" data-mode="precise">
<span class="kb-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
精确
</div>
</div>
</div>
<div class="chat-bar">
<button class="popup-image-btn" id="popup-image-btn" style="display:none;" title="上传图片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</button>
<textarea class="chat-bar-input" id="chat-input" placeholder="向知识库提问…" autocomplete="off" rows="1"></textarea>
<button class="chat-bar-send" id="btn-chat-send">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
</button>
</div>
</div>
<input type="file" id="popup-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
</div>
</div>
<!-- 设置面板 -->
<div class="settings-overlay" id="settings-overlay">
<div class="settings-panel">
<div class="settings-handle"></div>
<div class="settings-title">
<span>设置</span>
<button class="settings-close" id="btn-settings-close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div id="settings-content">
<div class="settings-item">
<span class="settings-label">账号类型</span>
<span class="settings-value" id="set-type">-</span>
</div>
<div class="settings-item">
<span class="settings-label">用户微信</span>
<span class="settings-value" id="set-name">-</span>
</div>
<div class="settings-item" id="set-url-row" style="display:none;">
<span class="settings-label">服务地址</span>
<span class="settings-value" id="set-url">-</span>
</div>
<div class="settings-item" id="set-key-row" style="display:none;">
<span class="settings-label">API Key</span>
<span class="settings-value" id="set-key">-</span>
</div>
<div class="settings-item settings-item-clickable" id="set-clip-kb-row">
<span class="settings-label">剪藏知识库</span>
<span class="settings-value" id="set-clip-kb">未选择 ›</span>
</div>
<div class="settings-item">
<span class="settings-label">版本</span>
<span class="settings-value">v5.0.0</span>
</div>
</div>
<button class="settings-logout" id="btn-logout">退出登录</button>
</div>
</div>
<!-- 剪藏知识库选择面板 -->
<div class="settings-overlay" id="clip-kb-overlay">
<div class="settings-panel" style="max-height:300px;display:flex;flex-direction:column;width:300px;padding:14px 16px;">
<div class="settings-title" style="display:flex;align-items:center;justify-content:space-between;font-size:14px;margin-bottom:8px;">
<span>选择剪藏知识库</span>
<button id="clip-kb-close" style="border:none;background:none;font-size:18px;color:#999;cursor:pointer;padding:0 4px;">×</button>
</div>
<div class="clip-kb-desc" style="font-size:11px;color:#999;margin-bottom:8px;">剪藏的网页内容将存储到此知识库</div>
<div id="clip-kb-list" style="flex:1;overflow-y:auto;"></div>
<div id="clip-kb-empty" style="display:none;text-align:center;padding:16px 0;">
<div style="font-size:13px;color:#999;margin-bottom:8px;">暂无知识库</div>
<div style="font-size:12px;color:#bbb;margin-bottom:12px;">请前往 WeKnora 创建知识库后使用剪藏功能</div>
<button id="clip-kb-goto" style="height:32px;padding:0 16px;border:none;border-radius:16px;background:#07C160;color:#fff;font-size:12px;cursor:pointer;">前往创建</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script src="popup.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -1,127 +0,0 @@
# 知识助理 - Chrome 浏览器扩展插件需求文档
## 1. 产品概述
**产品名称**:知识助理
**产品形态**:Chrome 浏览器扩展插件(Manifest V3)
**核心定位**:知识库问答 + 网页内容快速采集 + Markdown 速记
## 2. 设计规范
| 项目 | 说明 |
|------|------|
| 设计风格 | 扁平化 |
| 主体色 | `#07C160` |
| 图标 | 绿色背景 + 白色线条书本图标 |
| 字体 | 系统默认(PingFang SC / Microsoft YaHei) |
## 3. 功能模块
### 3.1 双账号登录
插件安装后首页展示两种账号登录入口:
#### 3.1.1 知识助理账号
- 使用微信头像账号体系
- 点击后**自动登录**(读取微信授权信息,无需手动操作)
- 登录过程展示头像和账号信息的加载过渡
#### 3.1.2 WeKnora 自建服务账号
- 需要手动填写**服务地址**和 **API Key**
- 提供「测试连接」功能,验证配置是否正确
- 填写完成并验证通过后点击「保存并登录」进入主界面
### 3.2 登录后主界面
登录成功后进入主界面,顶部显示:
| 元素 | 说明 |
|------|------|
| 头像 | 知识助理账号显示微信头像;WeKnora 账号显示字母头像(蓝底白字 S) |
| 账号名 | 显示用户昵称或微信号 |
| 账号标签 | 标识当前是「知识助理」还是「WeKnora」 |
| 设置按钮 | 点击弹出设置面板 |
| 退出按钮 | 退出当前账号,返回登录选择页 |
### 3.3 设置面板
- 点击设置按钮后,以**底部弹出面板(Bottom Sheet)**形式展示
- 内容包含:
- 用户头像与账号信息
- 知识助理账号:显示微信昵称、微信号
- WeKnora 账号:显示服务地址、API Key(可编辑修改)
### 3.4 知识库对话
主界面核心功能区,提供**聊天输入框**:
- 文本输入区域,支持多行输入
- 工具栏按钮:知识库选择、快速模式切换
- 发送按钮(有内容时激活为绿色)
- 发送后打开侧边栏对话面板,展示问答结果
- 支持快捷键发送(Enter 发送,Shift+Enter 换行)
### 3.5 网页内容采集
#### 3.5.1 保存当前页面
- 选择目标知识库
- 一键保存当前页面 URL 到知识库
#### 3.5.2 智能剪藏(全窗口截取)
- 自动提取当前页面的核心内容
- AI 智能识别正文、去除广告和导航等干扰信息
- 提取结果保存到知识库
#### 3.5.3 选择剪藏(部分窗口截取)
- 用户在页面上手动框选/圈选想要保存的区域
- 支持选中文字内容提取
- 选中内容保存到知识库
### 3.6 Markdown 快速记录
- 支持在插件内快速编写 Markdown 内容
- 编写完成后保存到指定知识库
- 作为快速笔记/速记工具使用
## 4. 交互流程
```
安装插件
│
▼
首页:选择登录方式
├── 知识助理 → 自动获取微信信息 → 自动登录 → 主界面
└── WeKnora → 填写服务地址 + API Key → 登录 → 主界面
主界面
├── 对话输入框 → 发送问题 → 侧边栏显示回答
├── 保存当前页面 → 选择知识库 → 保存
├── 智能剪藏 → 自动提取页面内容 → 保存
├── 选择剪藏 → 手动框选区域 → 保存
├── Markdown 速记 → 编写内容 → 保存到知识库
├── 设置 → 查看/修改账号信息
└── 退出 → 返回登录选择页
```
## 5. 快捷键
| 快捷键 | 功能 |
|--------|------|
| `⌘/Ctrl + Shift + K` | 打开侧边栏 |
| `⌘/Ctrl + Shift + A` | 快速提问 |
## 6. 技术栈
| 项目 | 技术选型 |
|------|----------|
| 扩展标准 | Chrome Extension Manifest V3 |
| 前端 | HTML + CSS + JS(外部文件,符合 CSP) |
| 状态存储 | chrome.storage.local |
| 消息通信 | chrome.runtime.sendMessage |
| 侧边栏 | chrome.sidePanel API |
| 内容脚本 | content.js + content.css |
@@ -1,634 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>快速笔记 5.0 - 对话</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root { --primary: #07C160; --primary-dark: #06ad54; --bg: #f5f6f7; --white: #fff; --text: #333; --text2: #666; --text3: #999; --border: #e5e5e5; --shadow: 0 2px 12px rgba(0,0,0,0.08); --radius: 12px; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; }
/* ===== Header ===== */
.header {
background: var(--white);
padding: 14px 18px;
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
z-index: 10;
position: relative;
}
.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; 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; }
/* ===== Messages ===== */
.messages { flex: 1; overflow-y: auto; padding: 18px 16px; display: flex; flex-direction: column; gap: 14px; }
.msg {
max-width: 85%;
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.6;
word-wrap: break-word;
overflow: hidden;
flex-shrink: 0;
}
.msg-user {
align-self: flex-end;
background: var(--primary);
color: #fff;
border-bottom-right-radius: 6px;
box-shadow: 0 2px 10px rgba(7, 193, 96, 0.2);
}
.msg-bot {
align-self: flex-start;
background: var(--white);
color: var(--text);
border-bottom-left-radius: 6px;
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08);
}
.msg-bot pre { background: #f5f5f5; padding: 10px 12px; border-radius: 8px; overflow-x: auto; font-size: 12px; margin: 8px 0; }
.msg-bot code { font-family: "SF Mono", "Fira Code", monospace; font-size: 13px; }
.msg-time { font-size: 11px; color: var(--text3); text-align: center; padding: 4px 0; }
.msg-typing {
align-self: flex-start;
background: var(--white);
padding: 14px 20px;
border-radius: 16px;
border-bottom-left-radius: 6px;
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08);
display: none;
}
.msg-typing.show { display: flex; }
.typing-dots { display: flex; gap: 5px; }
.typing-dots span { width: 6px; height: 6px; background: var(--text3); border-radius: 50%; animation: typing-bounce 1.4s infinite; }
.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing-bounce { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-6px); } }
/* ===== Welcome ===== */
.welcome {
text-align: center;
padding: 36px 20px;
background: var(--white);
border-radius: 18px;
box-shadow: 0 0 8px rgba(0,0,0,0.04), 0 4px 20px rgba(0,0,0,0.08);
margin: 8px 0;
}
.welcome-icon { width: 56px; height: 56px; background: #e8f8ee; border-radius: 16px; display: flex; align-items: center; justify-content: center; margin: 0 auto 14px; }
.welcome-icon svg { width: 28px; height: 28px; color: var(--primary); }
.welcome h2 { font-size: 16px; font-weight: 600; margin-bottom: 6px; }
.welcome p { font-size: 13px; color: var(--text3); line-height: 1.5; }
.quick-btns { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 16px; }
.quick-btn {
padding: 7px 16px;
border: none;
border-radius: 50px;
background: #f5f6f7;
font-size: 12px;
color: var(--text2);
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.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 {
padding: 10px 14px 14px;
flex-shrink: 0;
background: transparent;
}
/* 整体包裹卡片 */
.sp-chat-bar-wrapper {
background: var(--white);
border-radius: 16px;
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 20px rgba(0,0,0,0.10);
overflow: visible;
position: relative;
transition: box-shadow 0.2s;
}
.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;
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-kb:hover { background: #eaebed; color: var(--text); }
.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; }
/* 输入框 */
.sp-chat-bar-input {
flex: 1;
border: none;
background: transparent;
font-size: 13px;
font-family: inherit;
outline: none;
color: var(--text);
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; }
/* 发送按钮 — 圆形 */
.sp-chat-bar-send {
display: flex;
align-items: center;
justify-content: center;
border: none;
background: #e8e8e8;
color: #aaa;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
transition: all 0.2s;
flex-shrink: 0;
}
.sp-chat-bar-send svg { width: 14px; height: 14px; }
.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;
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: 200px;
display: none;
z-index: 100;
max-height: 70vh;
overflow-y: auto;
}
.sp-kb-dropdown-menu.show { display: block; }
.sp-dropdown-item {
padding: 9px 14px;
font-size: 13px;
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.15s;
}
.sp-dropdown-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.sp-dropdown-item.selected { color: var(--primary); font-weight: 500; }
.sp-dropdown-item .sp-radio {
width: 16px; height: 16px;
border: 2px solid #ccc;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.sp-dropdown-item.selected .sp-radio { border-color: var(--primary); }
.sp-dropdown-item.selected .sp-radio::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--primary); }
.sp-dropdown-item:hover .sp-radio { border-color: var(--primary); }
.sp-dropdown-item svg { width: 14px; height: 14px; }
/* 下拉菜单分隔线和分组标题 */
.sp-dropdown-divider { height: 1px; background: #f0f0f0; margin: 4px 12px; }
.sp-dropdown-section { padding: 6px 14px 2px; font-size: 10px; font-weight: 600; color: #bbb; letter-spacing: 0.5px; text-transform: uppercase; }
/* 模式选项 */
.sp-mode-item {
padding: 8px 14px;
font-size: 13px;
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.15s;
}
.sp-mode-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.sp-mode-item.selected { color: var(--primary); font-weight: 500; }
.sp-mode-item .sp-radio {
width: 14px; height: 14px;
border: 2px solid #ccc;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.sp-mode-item.selected .sp-radio { border-color: var(--primary); }
.sp-mode-item.selected .sp-radio::after { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--primary); }
.sp-mode-item:hover .sp-radio { border-color: var(--primary); }
.sp-mode-item svg { width: 14px; height: 14px; }
/* 附件选项 */
.sp-attach-item {
padding: 8px 14px;
font-size: 13px;
color: var(--text2);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.15s;
}
.sp-attach-item:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.sp-attach-item svg { width: 14px; height: 14px; }
/* ===== Clips ===== */
.clip-card {
background: var(--white);
border-radius: 16px;
padding: 14px 16px;
box-shadow: 0 0 6px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.08);
align-self: flex-start;
max-width: 90%;
transition: box-shadow 0.2s;
}
.clip-card:hover { box-shadow: 0 0 8px rgba(0,0,0,0.06), 0 6px 24px rgba(0,0,0,0.12); }
.clip-card-title { font-size: 12px; color: var(--primary); font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; gap: 5px; }
.clip-card-title svg { flex-shrink: 0; }
.clip-card-content { font-size: 13px; color: var(--text); line-height: 1.65; max-height: 200px; overflow-y: auto; }
.clip-actions { display: flex; gap: 8px; margin-top: 10px; }
.clip-action-btn {
padding: 5px 14px;
border: none;
border-radius: 50px;
background: #f5f6f7;
font-size: 11px;
color: var(--text2);
cursor: pointer;
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; }
</style>
</head>
<body>
<div class="header">
<div class="header-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<div class="header-title-wrap">
<h1>快速笔记 5.0</h1>
<div class="header-session-title" id="sp-session-title"></div>
</div>
<button class="header-btn" id="btn-clear" title="清空对话">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</div>
<div class="messages" id="messages">
<div class="welcome" id="welcome">
<div class="welcome-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h2>你好,有什么可以帮助你?</h2>
<p>你可以向我提问知识库中的任何问题</p>
<div class="quick-btns">
<button class="quick-btn" data-q="帮我总结最近的笔记">总结笔记</button>
<button class="quick-btn" data-q="知识库中有哪些内容?">知识库概览</button>
<button class="quick-btn" data-q="搜索相关文档">搜索文档</button>
</div>
</div>
<div class="msg-typing" id="typing">
<div class="typing-dots">
<span></span><span></span><span></span>
</div>
</div>
</div>
<div class="input-area">
<div class="sp-image-previews" id="sp-image-previews" style="display:none;"></div>
<div class="sp-chat-bar-wrapper">
<div class="sp-chat-bar-toolbar">
<button class="sp-chat-bar-kb" id="sp-btn-kb">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
<span id="sp-kb-name">全部</span>
<svg class="sp-chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<button class="sp-chat-bar-mode" id="sp-btn-mode">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
<span id="sp-mode-name">快速回答</span>
<svg class="sp-chat-bar-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
</div>
<div class="sp-chat-bar">
<button class="sp-chat-bar-img" id="sp-image-btn" style="display:none;" title="上传图片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</button>
<textarea class="sp-chat-bar-input" id="sp-input" placeholder="向知识库提问…" autocomplete="off" rows="1"></textarea>
<button class="sp-chat-bar-send" id="sp-send">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/></svg>
</button>
</div>
</div>
<input type="file" id="sp-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
<!-- 合并下拉菜单 -->
<div class="sp-kb-dropdown-menu" id="sp-kb-menu">
<div class="sp-dropdown-item selected" data-kb="all">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
全部知识库
</div>
<div class="sp-dropdown-item" data-kb="quick-note">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
快速笔记
</div>
<div class="sp-dropdown-item" data-kb="web-collect">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
网页收藏
</div>
<div class="sp-dropdown-item" data-kb="product-doc">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
产品文档
</div>
<div class="sp-dropdown-item" data-kb="tech-spec">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
技术规范
</div>
</div>
<!-- 模式下拉菜单(独立) -->
<div class="sp-mode-dropdown-menu" id="sp-mode-menu">
<div class="sp-mode-item selected" data-mode="fast">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
快速
</div>
<div class="sp-mode-item" data-mode="deep">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
深度
</div>
<div class="sp-mode-item" data-mode="precise">
<span class="sp-radio"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
精确
</div>
</div>
</div>
<script src="sidepanel.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -1,387 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WeKnora - 知识库管理</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root { --primary: #07C160; --primary-dark: #06ad54; --bg: #f5f6f7; --white: #fff; --text: #1a1a1a; --text2: #555; --text3: #999; --border: #eaeaea; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: var(--white); color: var(--text); height: 100vh; display: flex; overflow: hidden; }
/* === 左侧导航 === */
.sidebar { width: 220px; background: #fafbfc; border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
.sidebar-logo { padding: 20px 20px 24px; display: flex; align-items: center; justify-content: space-between; }
.sidebar-logo h1 { font-size: 18px; font-weight: 700; color: var(--text); letter-spacing: 1px; }
.sidebar-logo-btn { width: 30px; height: 30px; border: none; background: transparent; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); }
.sidebar-logo-btn:hover { background: #eee; color: var(--text); }
.sidebar-logo-btn svg { width: 16px; height: 16px; }
.sidebar-nav { flex: 1; padding: 0 12px; }
.nav-item { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; font-size: 14px; color: var(--text2); cursor: pointer; transition: all 0.15s; margin-bottom: 2px; position: relative; }
.nav-item:hover { background: #f0f0f0; color: var(--text); }
.nav-item.active { background: var(--primary); color: #fff; }
.nav-item svg { width: 18px; height: 18px; flex-shrink: 0; }
.nav-item .nav-plus { position: absolute; right: 10px; width: 18px; height: 18px; color: var(--primary); }
.nav-item.active .nav-plus { color: rgba(255,255,255,0.7); }
.sidebar-user { padding: 16px; border-top: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
.sidebar-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--primary); color: #fff; display: flex; align-items: center; justify-content: center; font-size: 15px; font-weight: 600; flex-shrink: 0; }
.sidebar-user-info { flex: 1; min-width: 0; }
.sidebar-user-name { font-size: 13px; font-weight: 600; color: var(--text); }
.sidebar-user-email { font-size: 11px; color: var(--text3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sidebar-user-arrow { width: 16px; height: 16px; color: var(--text3); }
/* === 中间内容区 === */
.main-area { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); min-width: 0; }
/* 内容顶部 tabs */
.content-top { padding: 20px 24px 0; }
.content-tabs { display: flex; gap: 16px; margin-bottom: 16px; }
.content-tab { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px 12px; border-radius: 10px; font-size: 11px; color: var(--text3); cursor: pointer; transition: all 0.15s; min-width: 48px; }
.content-tab:hover { background: #f5f5f5; color: var(--text2); }
.content-tab.active { background: rgba(7,193,96,0.08); color: var(--primary); }
.content-tab svg { width: 18px; height: 18px; }
.content-header { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
.content-header h2 { font-size: 22px; font-weight: 700; }
.content-header-btn { width: 28px; height: 28px; border: none; background: rgba(7,193,96,0.08); border-radius: 7px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--primary); }
.content-header-btn svg { width: 15px; height: 15px; }
.content-desc { font-size: 13px; color: var(--text3); margin-bottom: 20px; }
/* 空状态 */
.content-body { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px; position: relative; }
.empty-illustration { width: 140px; height: 100px; margin-bottom: 20px; position: relative; }
.empty-box { width: 120px; height: 50px; background: linear-gradient(180deg, #e8f8ee 0%, #d4f2e0 100%); border-radius: 8px; position: absolute; bottom: 0; left: 10px; display: flex; align-items: center; justify-content: center; gap: 6px; }
.empty-box-dot { width: 8px; height: 8px; border-radius: 50%; }
.empty-box-dot:nth-child(1) { background: #2b6bff; }
.empty-box-dot:nth-child(2) { background: #2b6bff; }
.empty-box-dot:nth-child(3) { background: #e0e0e0; }
.empty-arrows { position: absolute; top: 0; left: 50%; transform: translateX(-50%); display: flex; gap: 4px; }
.empty-arrow { width: 20px; height: 30px; }
.empty-title { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 6px; }
.empty-desc { font-size: 13px; color: var(--text3); margin-bottom: 20px; text-align: center; }
.empty-btn { height: 40px; padding: 0 24px; border: none; border-radius: 20px; background: var(--primary); color: #fff; font-size: 14px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 8px; transition: all 0.2s; }
.empty-btn:hover { background: var(--primary-dark); box-shadow: 0 4px 12px rgba(7,193,96,0.25); }
.empty-btn svg { width: 16px; height: 16px; }
/* 右下角 FAB */
.fab-btn { position: absolute; right: 24px; bottom: 24px; width: 44px; height: 44px; border: none; border-radius: 50%; background: var(--primary); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 16px rgba(7,193,96,0.3); transition: all 0.2s; }
.fab-btn:hover { transform: scale(1.08); box-shadow: 0 6px 20px rgba(7,193,96,0.35); }
.fab-btn svg { width: 22px; height: 22px; }
/* === 右侧对话面板 === */
.chat-panel { width: 360px; display: flex; flex-direction: column; flex-shrink: 0; background: var(--white); }
/* 对话面板头部 */
.chat-panel-header { padding: 14px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; }
.chat-panel-icon { font-size: 16px; }
.chat-panel-title { font-size: 14px; font-weight: 600; flex: 1; }
.chat-panel-actions { display: flex; gap: 2px; }
.chat-panel-btn { width: 30px; height: 30px; border: none; background: transparent; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); }
.chat-panel-btn:hover { background: #f5f5f5; color: var(--text); }
.chat-panel-btn svg { width: 16px; height: 16px; }
/* 对话内容 */
.chat-panel-brand { padding: 16px; }
.chat-panel-brand-name { font-size: 15px; font-weight: 700; color: var(--primary); margin-bottom: 20px; }
.chat-panel-brand-bar { display: flex; gap: 6px; }
.brand-bar-btn { width: 30px; height: 30px; border: 1px solid var(--border); border-radius: 8px; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--text3); }
.brand-bar-btn:hover { border-color: var(--primary); color: var(--primary); }
.brand-bar-btn svg { width: 14px; height: 14px; }
.chat-panel-body { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 30px; text-align: center; }
.chat-panel-body h3 { font-size: 16px; font-weight: 600; margin-bottom: 8px; color: var(--text); }
.chat-panel-body p { font-size: 13px; color: var(--text3); line-height: 1.5; }
/* 对话输入框 */
.chat-panel-input { padding: 12px 14px; border-top: 1px solid var(--border); }
.chat-input-card { background: #f7f8fa; border-radius: 12px; border: 1.5px solid rgba(0,0,0,0.06); transition: border-color 0.2s; }
.chat-input-card:focus-within { border-color: rgba(7,193,96,0.3); }
.chat-input-card textarea { width: 100%; min-height: 36px; max-height: 80px; border: none; background: transparent; padding: 10px 14px 4px; font-size: 13px; font-family: inherit; resize: none; outline: none; line-height: 1.5; color: var(--text); }
.chat-input-card textarea::placeholder { color: #bbb; }
.chat-input-toolbar { display: flex; align-items: center; padding: 4px 8px 8px; gap: 2px; }
.chat-tool-btn { height: 26px; padding: 0 8px; border: none; background: transparent; border-radius: 6px; font-size: 11px; color: var(--text3); cursor: pointer; display: flex; align-items: center; gap: 4px; transition: all 0.15s; white-space: nowrap; }
.chat-tool-btn:hover { background: rgba(7,193,96,0.06); color: var(--primary); }
.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; }
</style>
</head>
<body>
<!-- 左侧导航 -->
<div class="sidebar">
<div class="sidebar-logo">
<h1>WEKNORA</h1>
<button class="sidebar-logo-btn" title="复制">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
<div class="sidebar-nav">
<div class="nav-item active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
知识库
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
知识搜索
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
智能体
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="12" r="4"/><circle cx="17" cy="12" r="4"/></svg>
共享空间
</div>
<div class="nav-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
对话
<svg class="nav-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</div>
<div class="nav-item" id="nav-clips">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
网页内容收藏
</div>
</div>
<div class="sidebar-user">
<div class="sidebar-avatar" id="wk-user-letter">U</div>
<div class="sidebar-user-info">
<div class="sidebar-user-name" id="wk-user-name">加载中...</div>
<div class="sidebar-user-email" id="wk-user-email"></div>
</div>
<svg class="sidebar-user-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</div>
</div>
<!-- 中间内容区 -->
<div class="main-area">
<div class="content-top">
<div class="content-tabs">
<div class="content-tab">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
全部
</div>
<div class="content-tab active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
我的
</div>
<div class="content-tab">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
协作
</div>
</div>
<div class="content-header">
<h2>知识库</h2>
<button class="content-header-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
</button>
</div>
<div class="content-desc">管理和组织您的知识库,支持文档类型和问答型知识库</div>
</div>
<div class="content-body">
<!-- 空状态插图 -->
<div class="empty-illustration">
<svg width="140" height="100" viewBox="0 0 140 100">
<!-- 箭头 -->
<circle cx="50" cy="8" r="5" fill="#c8e6c9"/>
<circle cx="70" cy="4" r="4" fill="#e0e0e0"/>
<circle cx="90" cy="10" r="6" fill="#a5d6a7"/>
<path d="M55 20 L50 30 M70 18 L70 30 M85 22 L82 30" stroke="#07C160" stroke-width="2" fill="none" stroke-linecap="round"/>
<polygon points="47,30 53,30 50,36" fill="#07C160"/>
<polygon points="67,30 73,30 70,36" fill="#07C160"/>
<polygon points="79,30 85,30 82,36" fill="#07C160"/>
<!-- 盒子 -->
<rect x="15" y="45" width="110" height="50" rx="8" fill="url(#boxGrad)"/>
<defs><linearGradient id="boxGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#e8f8ee"/><stop offset="100%" stop-color="#d0f0db"/></linearGradient></defs>
<circle cx="50" cy="72" r="5" fill="#2b6bff"/>
<circle cx="65" cy="72" r="5" fill="#2b6bff"/>
<circle cx="80" cy="72" r="5" fill="#e0e0e0"/>
</svg>
</div>
<div class="empty-title">暂无知识库</div>
<div class="empty-desc">点击左侧快捷操作"新建知识库"按钮创建第一个知识库</div>
<button class="empty-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
新建知识库
</button>
<button class="fab-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
</button>
</div>
</div>
<!-- 右侧对话面板 -->
<div class="chat-panel">
<div class="chat-panel-header">
<span class="chat-panel-icon">🐱</span>
<span class="chat-panel-title">WeKnora Assistant</span>
<div class="chat-panel-actions">
<button class="chat-panel-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></button>
<button class="chat-panel-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg></button>
</div>
</div>
<div class="chat-panel-brand">
<div class="chat-panel-brand-name">WeKnora</div>
<div class="chat-panel-brand-bar">
<button class="brand-bar-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button class="brand-bar-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
<button class="brand-bar-btn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></button>
</div>
</div>
<div class="chat-panel-body">
<h3>开始对话</h3>
<p>选择知识库后,输入问题开始智能问答</p>
</div>
<div class="chat-panel-input">
<div class="wk-image-previews" id="wk-image-previews" style="display:none;"></div>
<div class="chat-input-card">
<textarea placeholder="输入问题..." rows="1"></textarea>
<div class="chat-input-toolbar">
<button class="wk-image-upload-btn" id="wk-image-btn" style="display:none;" title="上传图片">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</button>
<button class="chat-tool-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
普通
</button>
<button class="chat-tool-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
</button>
<button class="chat-tool-btn mode-active">
<svg viewBox="0 0 24 24" fill="currentColor" style="width:8px;height:8px"><circle cx="12" cy="12" r="12"/></svg>
知识库
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:10px;height:10px"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<button class="chat-tool-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9"/></svg>
yueyue
</button>
</div>
</div>
</div>
<input type="file" id="wk-image-input" accept="image/jpeg,image/png,image/gif,image/webp" multiple style="display:none;">
</div>
<script src="weknora.js"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff