吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2438|回复: 12
收起左侧

[Python 转载] [chrome 插件] 26/08/08AI生成 记录历史访问url 查删 本地PY服务端sqlite记录 ve...

[复制链接]
m_h 发表于 2025-9-30 22:30
本帖最后由 m_h 于 2026-8-8 13:20 编辑

2026/08/08
压缩包只更新了服务端 浏览器插件在前面的压缩包内。
更新
更新域名聚合

域名聚合

域名聚合

活跃页面

活跃度

活跃度


捕获.PNG
2026/4/7更新 日期范围显示  搜索

1捕获.PNG
效果如图

插件 manifest_version v3版本

background.js


// 监听网页更新事件
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // 确保是页面加载完成,并且tab.url存在
  if (changeInfo.status === 'complete' && tab.url) {
    // 确保 V3 Service Worker 中 tab.title 或 tab.url 可能为 undefined 的情况
    // 尽管 onUpdated 触发 complete,但 URL 可能还在加载,或者 title 还没获取到
    // 为避免错误,可以稍微延迟一下
    // 或者更可靠的方式是在 content script 中获取,但这会增加复杂度
    // 这里为了简单,我们继续使用tab.url和tab.title,但增加一个检查
    if (tab.url && tab.title) {
      const data = {
        url: tab.url,
        title: tab.title,
        timestamp: new Date().toISOString()
      };

      // 发送到Python服务端
      fetch('http://localhost:6500/api/record', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      }).catch(err => {
        console.error('记录失败:', err);
        // 也许可以考虑在这里给用户一个提示,或者重试机制
      });
    } else {
      console.warn(`Tab ${tabId} updated, but URL or Title is missing. URL: ${tab.url}, Title: ${tab.title}`);
      // 可以在这里尝试在下一tick再获取一次,或者直接忽略
      // 例如:chrome.runtime.lastError 检查fetch操作是否出错
    }
  }
});

// 监听来自popup或其他地方的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "openAdminPage") {
    // 在 V3 Service Worker 中,直接创建标签的方法不变
    chrome.tabs.create({ url: request.url || 'http://localhost:5000' });
    // 如果希望知道创建是否成功,可以添加一个callback
    // sendResponse({ success: true }); // 表示消息已被处理
  }
  // 返回 true 表示异步处理 sendResponse,防止callback被提前垃圾回收
  return true;
});

// Service Worker 的生命周期是事件驱动的。
// 当没有事件发生时,它会休眠。
// 您可以在此处添加一些 Service Worker 相关的逻辑,例如安装时的一些设置。
chrome.runtime.onInstalled.addListener(() => {
  console.log('URL Recorder Extension Installed/Updated.');
  // 可以在此处设置一些初始值,例如:
  // chrome.storage.sync.set({ enableRecording: true });
});

// 注意: 您可能需要重新加载您的插件来应用这些更改。
// 并且,由于 V3 的 Service Worker 是事件驱动的,
// `chrome.tabs.onUpdated` 可能不会像 V2 的 persistent background page 那样一直活跃。
// 如果遇到问题,可能需要更复杂的策略,例如使用 content scripts 来主动收集信息,
// 再通过 message passing 发送给 service worker。
// 但对于简单的URL记录,这个改动应该足够了。

manifest.json

{
  "manifest_version": 3,
  "name": "URL Recorder",
  "version": "1.1",
  "description": "记录浏览历史并同步到服务端",
  "permissions": [
    "tabs",       // 允许访问标签页信息
    "storage",    // 允许使用存储API (如果需要存储一些配置的话)
    "scripting"   // 允许执行脚本(在V3中,即使不注入,有些tab事件的处理也可能间接需要)
  ],
  "host_permissions": [
    "http://localhost:6500/*" // 允许与该URL通信
  ],
  "background": {
    "service_worker": "background.js" // V3 使用 service_worker
  },
  "action": { // V2的 browser_action 被 action 替换
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icon.png",
      "48": "icon.png",
      "128": "icon.png"
    }
  }
}

popup.html

<!DOCTYPE html>
<html>
<head>
  <title>URL Recorder</title>
</head>
<body>
  <h3>历史记录管理</h3>
  <a href="http://localhost:6500" target="_blank">打开管理页面</a>

  <script>
    // 如果需要通过popup来控制background.js,可以在这里添加脚本
    // 例如:当用户点击链接时,可以发送一个消息给 background script
    document.querySelector('a').addEventListener('click', (event) => {
      // 阻止默认跳转,然后通过background script打开
      // event.preventDefault();
      // chrome.runtime.sendMessage({ action: "openAdminPage", url: 'http://localhost:5000' });
      // 在这个例子中,链接直接跳转也能工作,所以无需阻止默认行为。
      console.log("Admin page link clicked.");
    });
  </script>
</body>
</html>

py服务端

requirements.txt   可能不全 有BUG 剩下的 找不到可以问AI

flask==1.1.4
markupsafe<2.0.0
werkzeug<2.0.0
jinja2==2.11.3  # 明确指定兼容版本
flask-cors==3.0.10  # 这是最后一个支持 Python 3.5 的主要版本

app.py


from flask import Flask, request, jsonify, render_template
from flask_cors import CORS
import sqlite3
from datetime import datetime

# **注意:测试端口改为 6501**
APP_PORT = 6501

app = Flask(__name__)
CORS(app)  # 允许所有跨域请求

# 初始化数据库(SQLite)
def init_db():
    conn = sqlite3.connect('database.db')
    c = conn.cursor()
    # 确保 history 表存在
    c.execute('''CREATE TABLE IF NOT EXISTS history
                 (id INTEGER PRIMARY KEY AUTOINCREMENT,
                  url TEXT NOT NULL UNIQUE,
                  title TEXT,
                  timestamp DATETIME)''')
    conn.commit()
    conn.close()

# 记录浏览历史API (保持不变,但现在运行在 6501 端口)
@app.route('/api/record', methods=['POST'])
def record_history():
    if not request.json or 'url' not in request.json:
        return jsonify({'error': 'Missing data'}), 400

    data = {
        'url': request.json['url'],
        'title': request.json.get('title', ''),
        'timestamp': datetime.now().isoformat()
    }
    conn = sqlite3.connect('database.db')
    c = conn.cursor()
    try:
        # 使用 INSERT OR IGNORE 更优雅地处理UNIQUE约束
        c.execute("INSERT OR IGNORE INTO history (url, title, timestamp) VALUES (?, ?, ?)",
                 (data['url'], data['title'], data['timestamp']))
        if c.rowcount == 0:
             return jsonify({'error': 'URL already exists (Ignored)'}), 409

        conn.commit()
        return jsonify({'status': 'success'}), 201
    except Exception as e:
        # 其他异常处理
        return jsonify({'error': str(e)}), 500
    finally:
        conn.close()

# **修改后的:获取历史记录API (支持分页和搜索)**
@app.route('/api/history', methods=['GET'])
def get_history():
    conn = sqlite3.connect('database.db')
    conn.row_factory = sqlite3.Row
    c = conn.cursor()

    # --- 1. 获取和验证分页参数 ---
    page = request.args.get('page', 1, type=int) 
    per_page = request.args.get('per_page', 100, type=int) 

    # 限制每页数量,防止恶意请求过大的数据量
    PER_PAGE_MAX = 500
    if per_page < 1 or per_page > PER_PAGE_MAX:
        per_page = 100

    # --- 2. 获取和处理搜索参数 ---
    search_term = request.args.get('search', '')

    # --- 3. 构造 WHERE 和 ORDER BY 子句 ---
    where_conditions = []
    param_list = []

    if search_term:
        # 模糊搜索 URL 或 Title
        where_conditions.append("(url LIKE ? OR title LIKE ?)")
        search_like = f"%{search_term}%"
        param_list.extend([search_like, search_like])

    where_clause = "WHERE " + " AND ".join(where_conditions) if where_conditions else ""

    # 获取排序参数
    sort_by = request.args.get('sort', 'timestamp') # 默认排序
    order = request.args.get('order', 'desc') # 默认倒序

    valid_columns = ['id', 'url', 'title', 'timestamp']
    sort_by = sort_by if sort_by in valid_columns else 'timestamp'
    order = order if order.lower() in ['asc', 'desc'] else 'desc'

    order_clause = f"ORDER BY {sort_by} {order}"

    # --- 4. 获取总记录数 (带搜索条件) ---
    count_query = f"SELECT COUNT(*) FROM history {where_clause}"
    c.execute(count_query, param_list)
    total_records = c.fetchone()[0]

    # --- 5. 构造分页查询 ---

    offset = (page - 1) * per_page

    # 纠正页码,防止超出范围
    last_page = (total_records + per_page - 1) // per_page if total_records > 0 else 1
    if page < 1:
        page = 1
    if page > last_page:
        page = last_page
        offset = (page - 1) * per_page

    limit_clause = f"LIMIT {per_page} OFFSET {offset}"

    # 最终查询
    main_query = f"SELECT * FROM history {where_clause} {order_clause} {limit_clause}"

    c.execute(main_query, param_list)
    rows = c.fetchall()
    conn.close()

    # --- 6. 返回分页结果 ---
    return jsonify({
        'data': [dict(row) for row in rows],
        'meta': {
            'total_records': total_records,
            'per_page': per_page,
            'current_page': page,
            'last_page': last_page
        }
    })

# 批量删除API (保持不变)
@app.route('/api/delete', methods=['POST'])
def delete_records():
    try:
        data = request.get_json()
        if not data or 'ids' not in data:
            return jsonify({'error': 'Missing IDs'}), 400

        ids = data['ids']
        if not isinstance(ids, list) or not all(isinstance(i, int) for i in ids):
            return jsonify({'error': 'IDs must be a list of integers'}), 400

        # 安全参数化查询
        placeholders = ','.join(['?'] * len(ids))
        query = "DELETE FROM history WHERE id IN ({})".format(placeholders)

        conn = sqlite3.connect('database.db')
        c = conn.cursor()
        c.execute(query, ids)
        conn.commit()
        conn.close()

        return jsonify({'deleted': len(ids)}), 200
    except Exception as e:
        return jsonify({'error': str(e)}), 500

# 管理页面(不再传递数据,让前端JS去请求API)
@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    init_db()

    app.run(host='0.0.0.0', port=APP_PORT, debug=True)

templates\index.html

<!DOCTYPE html>
<html>
<head>
    <title>浏览历史管理</title>
    <style>
        table { width: 100%; border-collapse: collapse; table-layout: fixed; }
        th, td { padding: 8px; border: 1px solid #ddd; word-wrap: break-word; }
        th { cursor: default; background: #f5f5f5; } 

        /* 针对可点击的排序列 */
        th[onclick], #id-sortable-header {
            cursor: pointer;
        }

        .controls { margin-bottom: 20px; display: flex; gap: 15px; align-items: center; }
        .pagination { margin-top: 15px; display: flex; gap: 10px; align-items: center; }
        .pagination button:disabled { opacity: 0.5; cursor: not-allowed; }

        /* 排序指示图标 */
        .sort-icon::after { margin-left: 5px; content: ' \2195'; opacity: 0.4; }
        .sorted-asc::after { content: ' \25B2'; color: green; opacity: 1; }
        .sorted-desc::after { content: ' \25BC'; color: red; opacity: 1; }
    </style>
</head>
<body>
    <h1>浏览历史记录</h1>

    <div class="controls">
        <input type="text" id="search-input" placeholder="搜索 URL 或标题">
        <button id="search-btn">搜索</button>
        <label for="per-page-select">每页显示:</label>
        <select id="per-page-select">
            <option value="50">50</option>
            <option value="100">100</option>
            <option value="250">250</option>
            <option value="500">500</option>
        </select>
        <button id="delete-selected">删除选中</button>
    </div>

    <table>
        <thead>
            <tr>
                <!-- ID 列:复选框和排序分离 -->
                <th style="width: 100px;">
                    <input type="checkbox" id="select-all">
                    <span id="id-sortable-header" data-column="id">ID <span id="sort-id" class="sort-icon"></span></span>
                </th>
                <!-- 其他列仍使用 onclick -->
                <th data-column="url">URL <span id="sort-url" class="sort-icon"></span></th>
                <th data-column="title">标题 <span id="sort-title" class="sort-icon"></span></th>
                <th data-column="timestamp">访问时间 <span id="sort-timestamp" class="sort-icon"></span></th>
                <th style="width: 50px;"></th> <!-- 占位列 -->
            </tr>
        </thead>
        <tbody id="history-body">
            <!-- 数据将通过 JavaScript 填充 -->
        </tbody>
    </table>

    <div class="pagination">
        <button id="prev-btn" disabled>上一页</button>
        <span id="page-info"></span>
        <button id="next-btn" disabled>下一页</button>
        <span id="total-info"></span>
    </div>

    <script>
        // --- 状态管理 ---
        const API_URL = '/api/history';
        let currentState = {
            page: 1,
            // 从本地存储加载每页数量,默认 100
            per_page: localStorage.getItem('per_page') ? parseInt(localStorage.getItem('per_page')) : 100, 
            search: '',
            sort: 'timestamp', // 默认按时间排序
            order: 'desc'      // 默认倒序
        };

        // --- DOM 元素引用 ---
        const historyBody = document.getElementById('history-body');
        const selectAllCheckbox = document.getElementById('select-all');
        const deleteBtn = document.getElementById('delete-selected');
        const searchInput = document.getElementById('search-input');
        const searchBtn = document.getElementById('search-btn');
        const perPageSelect = document.getElementById('per-page-select');
        const prevBtn = document.getElementById('prev-btn');
        const nextBtn = document.getElementById('next-btn');
        const pageInfoSpan = document.getElementById('page-info');
        const totalInfoSpan = document.getElementById('total-info');

        // 初始化每页数量选择框
        if (perPageSelect.querySelector(`option[value="${currentState.per_page}"]`)) {
            perPageSelect.value = currentState.per_page;
        } else {
            currentState.per_page = 100;
        }

        // --- 核心数据获取函数 ---
        async function fetchHistory() {
            historyBody.innerHTML = '<tr><td colspan="5">加载中...</td></tr>';
            selectAllCheckbox.checked = false; // 加载新数据时取消全选

            const url = new URL(API_URL, window.location.origin);
            url.searchParams.set('page', currentState.page);
            url.searchParams.set('per_page', currentState.per_page);
            url.searchParams.set('sort', currentState.sort);
            url.searchParams.set('order', currentState.order);
            if (currentState.search) {
                url.searchParams.set('search', currentState.search);
            }

            try {
                const response = await fetch(url.toString());
                const result = await response.json();

                if (response.ok) {
                    renderTable(result.data);
                    renderPagination(result.meta);
                    updateSortIndicators();
                } else {
                    console.error("加载数据 API 错误:", result);
                    historyBody.innerHTML = `<tr><td colspan="5">API 错误: ${JSON.stringify(result)}</td></tr>`;
                }

            } catch (error) {
                console.error("加载数据请求失败:", error);
                historyBody.innerHTML = '<tr><td colspan="5">加载历史记录失败,请检查后端服务是否运行在 6501 端口。</td></tr>';
            }
        }

        // --- 渲染函数 ---
        function renderTable(historyItems) {
            historyBody.innerHTML = ''; 
            if (historyItems.length === 0) {
                historyBody.innerHTML = '<tr><td colspan="5">没有找到记录。</td></tr>';
                return;
            }

            historyItems.forEach(item => {
                const row = historyBody.insertRow();
                row.innerHTML = `
                    <td>
                        <!-- value 必须是数据库 ID -->
                        <input type="checkbox" class="row-checkbox" value="${item.id}"> 
                        ${item.id}
                    </td>
                    <td><a href="${item.url}" title="${item.url}" target="_blank">${truncate(item.url, 50)}</a></td>
                    <td>${truncate(item.title || '无标题', 30)}</td>
                    <td>${item.timestamp}</td>
                    <td></td> 
                `;
            });
        }

        function renderPagination(meta) {
            const { total_records, per_page, current_page, last_page } = meta;

            const start = total_records > 0 ? (current_page - 1) * per_page + 1 : 0;
            let end = current_page * per_page;
            if (end > total_records) {
                end = total_records;
            }

            pageInfoSpan.textContent = `第 ${current_page} / ${last_page} 页`;
            totalInfoSpan.textContent = `(显示 ${start}-${end} 条,共 ${total_records} 条)`;

            prevBtn.disabled = current_page <= 1;
            nextBtn.disabled = current_page >= last_page;
        }

        // --- 排序处理 (已修复逻辑) ---
        function sortTable(column) {
            if (currentState.sort === column) {
                // 如果是同一列,切换方向
                currentState.order = currentState.order === 'asc' ? 'desc' : 'asc';
            } else {
                // 如果是新列,确定新的初始排序方向。
                currentState.sort = column;

                // 默认策略:
                // 如果新列是 'timestamp',从 'desc' 开始(最新)
                // 否则,从 'asc' 开始(ID 等从小到大,更明显)
                currentState.order = (column === 'timestamp') ? 'desc' : 'asc'; 
            }
            currentState.page = 1; 
            fetchHistory();
        }

        function updateSortIndicators() {
            document.querySelectorAll('.sort-icon').forEach(icon => {
                icon.className = 'sort-icon'; 
            });

            const currentIcon = document.getElementById(`sort-${currentState.sort}`);
            if (currentIcon) {
                currentIcon.classList.add(currentState.order === 'asc' ? 'sorted-asc' : 'sorted-desc');
            }
        }

        // --- 事件处理函数 ---
        function handlePageChange(direction) {
            if (direction === 'next') {
                currentState.page += 1;
            } else if (direction === 'prev') {
                currentState.page -= 1;
            }
            fetchHistory();
        }

        function handleSearch() {
            currentState.search = searchInput.value.trim();
            currentState.page = 1; 
            fetchHistory();
        }

        function handlePerPageChange(e) {
            currentState.per_page = parseInt(e.target.value);
            localStorage.setItem('per_page', currentState.per_page); 
            currentState.page = 1;
            fetchHistory();
        }

        // --- 批量删除逻辑 (保持不变) ---
        async function handleDeleteSelected() {
            const checkboxes = document.querySelectorAll('#history-body .row-checkbox:checked');
            const ids = Array.from(checkboxes).map(cb => parseInt(cb.value));

            if (ids.length > 0) {
                if (!confirm(`确定要删除选中的 ${ids.length} 条记录吗?`)) return;

                try {
                    const response = await fetch('/api/delete', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ ids })
                    });
                    const data = await response.json();
                    if (data.deleted > 0) {
                        alert(`成功删除了 ${data.deleted} 条记录。`);
                        fetchHistory(); 
                    } else {
                        alert('删除请求成功,但没有记录被删除。');
                    }
                } catch (error) {
                    console.error('删除请求失败:', error);
                    alert('删除请求失败,请检查网络或后端服务。');
                }
            } else {
                alert('请选择至少一条记录。');
            }
        }

        // --- 实用工具函数 ---
        function truncate(str, n) {
            if (str && str.length > n) {
                return str.substring(0, n - 1) + '...';
            }
            return str;
        }

        // --- 初始化和绑定事件 ---
        document.addEventListener('DOMContentLoaded', function() {
            // 绑定分页和搜索事件
            prevBtn.addEventListener('click', () => handlePageChange('prev'));
            nextBtn.addEventListener('click', () => handlePageChange('next'));
            searchBtn.addEventListener('click', handleSearch);
            searchInput.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') handleSearch();
            });
            perPageSelect.addEventListener('change', handlePerPageChange);
            deleteBtn.addEventListener('click', handleDeleteSelected);

            // 全选/反选逻辑 (作用于当前页)
            selectAllCheckbox.addEventListener('change', function(e) {
              document.querySelectorAll('#history-body .row-checkbox').forEach(checkbox => {
                checkbox.checked = e.target.checked;
              });
            });

            // 绑定 ID 排序事件 (修复 ID 排序问题)
            const idHeader = document.getElementById('id-sortable-header');
            idHeader.addEventListener('click', () => {
                sortTable('id');
            });

            // 将 sortTable 暴露给全局 (用于其他 th 的 onclick)
            window.sortTable = sortTable; 

            // 初次加载数据
            fetchHistory();
        });

    </script>
</body>
</html>

如果不能用 就告诉我  我发个压缩包得了 !


我不提供编译exe哈。我都是py环境直接用



梦想标签收纳盒 python 服务端版
https://www.52pojie.cn/thread-2101224-1-1.html
AI.zip (27.95 KB, 下载次数: 2)

AI.chrome.Fav.V2.zip (55.72 KB, 下载次数: 1)

chrome.Favserver.test.zip

72.54 KB, 下载次数: 0, 下载积分: 吾爱币 -1 CB

更新域名聚合活跃

免费评分

参与人数 4吾爱币 +4 热心值 +2 收起 理由
tzxinqing + 1 + 1 用心讨论,共获提升!
lgc81034 + 1 谢谢@Thanks!
tohyueyun + 1 用心讨论,共获提升!
lwj1367 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

sona124311 发表于 2025-10-1 09:03
强烈推荐论坛嵌入这个插件
 楼主| m_h 发表于 2025-10-1 10:18
132580 发表于 2025-10-1 09:50
你好,作者没太看明白,历史记录浏览器不是自带吗 ,另外onetab插件不就是 可以收纳标签页吗  还是说使用  ...

浏览器带的 不好操作 ,简单来说 删除就很麻烦。
自己记录 想怎么折腾 怎么找 都好找  我没做时间区域搜索  但是 PY代码在 找AI就行了 我觉得没必要所以没搞。
梦想收藏夹  是网络版 联网的 py服务端
BrutusScipio 发表于 2025-9-30 23:49
梦想标签收纳盒那种图是不是链接贴错了 再说这是根据域名匹配站点吗?是的话自定义规则匹配子域名
 楼主| m_h 发表于 2025-10-1 00:54
BrutusScipio 发表于 2025-9-30 23:49
梦想标签收纳盒那种图是不是链接贴错了 再说这是根据域名匹配站点吗?是的话自定义规则匹配子域名

截图是搜索 52 其实 所有域名都记录  如果不是都记录 那就没必要这样搞了  几万 url今天找 AI更新了下 搜索 分页 排序。就发上来了
132580 发表于 2025-10-1 09:50
你好,作者没太看明白,历史记录浏览器不是自带吗 ,另外onetab插件不就是 可以收纳标签页吗  还是说使用 把端口穿透出去 多机使用
nulla2011 发表于 2025-10-1 11:11
你这个还得开个本地服务器,为什么不用 chrome 插件的网页呢
 楼主| m_h 发表于 2025-10-1 11:35
nulla2011 发表于 2025-10-1 11:11
你这个还得开个本地服务器,为什么不用 chrome 插件的网页呢

很多本地服务器啊,做系统   跟着我走啊。  可以保留N年的记录   浏览器能做到 但是。不好查
yxf515321 发表于 2025-10-1 18:34
这样不太好吧,我的秘密会被暴露的
影风 发表于 2025-10-1 18:55
能否把插件以压缩包形式或crx形式上传0.0
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-10-24 03:13

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表