告别周报烦恼:我用 200 行代码打造了一个 AI 工时助手

痛点:每到周五写周报,总要翻遍整个工时系统,手动复制粘贴、整理汇总,耗时又枯燥。

解法:一个浏览器插件,自动抓取工时数据,一键生成 AI 总结。本文将完整分享实现思路与核心代码。


一、效果预览

安装脚本后,打开工时管理系统,你会看到:

  1. 自动捕获:页面加载后自动拦截接口数据,无需手动操作
  2. 浮动面板:右下角出现两个渐变按钮
  3. 一键复制:点击即可复制整理好的周报内容
  4. AI 总结:调用本地 Ollama 大模型,智能提炼关键成果


二、核心原理:如何"拦截"接口数据

现代网页应用通常通过 XMLHttpRequestFetch API 与服务器通信。我们的核心思路是:"劫持"这些原生方法,在数据返回时插一脚

2.1 拦截 XMLHttpRequest

javascript 复制代码
// 保存原始方法
const originalXhrOpen = XMLHttpRequest.prototype.open;
const originalXhrSend = XMLHttpRequest.prototype.send;

// 重写 open 方法,记录请求的 URL
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
    this._url = url;  // 保存 URL 供后续判断
    this._method = method;
    return originalXhrOpen.apply(this, [method, url, ...rest]);
};

// 重写 send 方法,监听特定接口的响应
XMLHttpRequest.prototype.send = function(...args) {
    // 判断是否是目标工时接口
    if (this._url && this._url.includes('/pms/project/job/time/getJobTimeCalendar')) {
        this.addEventListener('load', function() {
            const response = this.responseText;
            const data = JSON.parse(response);

            if (data.code === 200) {
                // 存储到 localStorage,方便后续使用
                localStorage.setItem('capturedJobTimeData', JSON.stringify(data.data));
                console.log('[工时助手] 数据已捕获');
            }
        });
    }
    return originalXhrSend.apply(this, args);
};

2.2 拦截 Fetch API

现代应用越来越多使用 fetch,同样需要处理:

javascript 复制代码
const originalFetch = window.fetch;

window.fetch = function(input, init) {
    const url = typeof input === 'string' ? input : input.url;

    return originalFetch.apply(this, arguments).then(response => {
        if (url && url.includes(targetUrl)) {
            // 注意:fetch 返回的 Response 只能读取一次,需要克隆
            const clonedResponse = response.clone();

            clonedResponse.json().then(data => {
                if (data.code === 200) {
                    capturedData = data.data;
                    console.log('[工时助手] Fetch 捕获到数据');
                }
            });
        }
        return response;  // 必须返回原始响应
    });
};

知识点response.clone() 是必需的,因为 Response body 是流式读取的,消费后就不能再次读取。克隆一份专门用于脚本处理,不影响页面原有逻辑。


三、数据处理:从原始 JSON 到周报文本

捕获到的是原始日历数据,需要:

  1. 筛选本周(周一至周五)的数据
  2. 按项目名称分组汇总
  3. 格式化为易读的周报格式

3.1 计算本周日期范围

javascript 复制代码
function getCurrentWeekDates() {
    const today = new Date();
    const dayOfWeek = today.getDay(); // 0=周日, 1=周一...

    // 计算周一的偏移量(周日时回退6天到上周一)
    const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;

    const dates = [];
    for (let i = 0; i < 5; i++) {  // 周一到周五
        const date = new Date(today);
        date.setDate(today.getDate() + mondayOffset + i);
        dates.push(formatDate(date));  // 格式化为 YYYY-MM-DD
    }
    return dates;
}

function formatDate(date) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
}

3.2 数据分组与格式化

javascript 复制代码
function processData(data) {
    const weekDates = getCurrentWeekDates();
    const projectMap = new Map();

    // 遍历本周每一天
    weekDates.forEach(dateStr => {
        // 在返回的数组中找到对应日期的数据
        const dayData = data.find(item => item.date === dateStr);

        if (dayData && dayData.list) {
            dayData.list.forEach(item => {
                const projectName = item.projectShortName || '其他';
                const content = item.remark || '';

                if (content) {
                    // 按项目分组存储
                    if (projectMap.has(projectName)) {
                        projectMap.get(projectName).push(content);
                    } else {
                        projectMap.set(projectName, [content]);
                    }
                }
            });
        }
    });

    // 生成周报文本:一、项目A:xxx;二、项目B:xxx
    let result = '';
    let index = 1;
    projectMap.forEach((contents, projectName) => {
        const mergedContent = contents.join('\n');
        result += `${numberToChinese(index)}、${projectName}:${mergedContent}\n`;
        index++;
    });

    return result.trim();
}


---

## 四、AI 集成:调用本地 Ollama 大模型

为了不让周报看起来只是机械堆砌,脚本集成了 **Ollama** 本地大模型,自动提炼关键成果。

### 4.1 Ollama 配置

```javascript
const OLLAMA_URL = 'http://localhost:11434/api/generate';
const MODEL_NAME = 'qwen2.5:3b';  // 可自行替换为其他模型

Ollama 是一个在本地运行大语言模型的工具,无需联网,保护数据隐私。安装后执行 ollama run qwen2.5:3b 即可使用。

4.2 调用 AI 进行总结

javascript 复制代码
async function summarizeWithAI(content) {
    // 精心设计的提示词,指导 AI 的输出格式
    const prompt = `请将以下本周工作内容进行智能总结,要求:
1. 按项目维度汇总工作内容
2. 合并相似的工作项
3. 提炼关键工作成果
4. 使用简洁、专业的语言
5. 输出格式保持清晰易读

原始工作内容:
${content}

请输出总结后的工作内容:`;

    return new Promise((resolve, reject) => {
        // GM_xmlhttpRequest 是油猴脚本特有的跨域请求 API
        GM_xmlhttpRequest({
            method: 'POST',
            url: OLLAMA_URL,
            headers: {
                'Content-Type': 'application/json'
            },
            data: JSON.stringify({
                model: MODEL_NAME,
                prompt: prompt,
                stream: false,  // 非流式,等待完整响应
                options: {
                    temperature: 0.3,  // 低温度 = 更确定性的输出
                    max_tokens: 800
                }
            }),
            onload: function(response) {
                const result = JSON.parse(response.responseText);
                if (result.response) {
                    resolve(result.response);
                }
            },
            onerror: function(error) {
                reject('请求失败,请确认 Ollama 服务是否运行');
            },
            timeout: 60000  // 本地模型可能较慢,给足时间
        });
    });
}

五、UI 交互:纯原生 DOM 构建浮动面板

脚本没有依赖任何 UI 库,完全使用原生 DOM API 创建界面,保证轻量和兼容性。

5.1 创建渐变按钮

javascript 复制代码
function createButtonPanel() {
    const panel = document.createElement('div');
    panel.id = 'tm-button-panel';
    panel.style.cssText = `
        position: fixed;
        bottom: 10px;
        right: 10px;
        z-index: 999999;
        display: flex;
        flex-direction: column;
        gap: 8px;
    `;

    // 查看原始数据按钮(青绿渐变)
    const viewBtn = document.createElement('button');
    viewBtn.innerText = '📋 查看本周工时';
    viewBtn.style.cssText = getButtonStyle('#11998e', '#38ef7d');
    viewBtn.addEventListener('click', () => {
        const text = localStorage.getItem('formattedWorkContent');
        if (text) showDataModal(text, '原始格式');
    });

    // AI 总结按钮(粉紫渐变)
    const aiBtn = document.createElement('button');
    aiBtn.innerText = '🤖 AI 智能总结';
    aiBtn.style.cssText = getButtonStyle('#f093fb', '#f5576c');
    aiBtn.addEventListener('click', async () => {
        showLoadingModal();  // 显示加载动画
        try {
            const summary = await summarizeWithAI(formattedText);
            closeLoadingModal();
            showDataModal(summary, 'AI 智能总结');
        } catch (err) {
            showNotification('AI 总结失败: ' + err, true);
        }
    });

    panel.appendChild(viewBtn);
    panel.appendChild(aiBtn);
    document.body.appendChild(panel);
}

function getButtonStyle(color1, color2) {
    return `
        padding: 10px 16px;
        background: linear-gradient(135deg, ${color1} 0%, ${color2} 100%);
        color: white;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 13px;
        font-weight: bold;
        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
        transition: all 0.3s ease;
    `;
}

六、完整脚本使用指南

6.1 安装步骤

  1. 安装浏览器扩展 :Chrome/Edge 商店搜索 Tampermonkey(篡改猴)

  2. 新建脚本:点击扩展图标 → "添加新脚本"

  3. 粘贴代码:将完整脚本粘贴到编辑器中,保存(Ctrl+S)

6.2 配置 Ollama(可选)

如需 AI 总结功能:

bash 复制代码
# 1. 安装 Ollama(https://ollama.com)
# 2. 拉取模型
ollama pull qwen2.5:3b

# 3. 启动服务(保持终端运行)
ollama run qwen2.5:3b

6.3 使用流程

步骤 操作 效果
1 登录工时系统 脚本自动在后台捕获数据
2 点击"📋 查看本周工时" 弹出原始数据弹窗
3 点击"📝 复制到剪贴板" 内容已复制,可直接粘贴到周报
4 (可选)点击"🤖 AI 智能总结" 等待 AI 处理后生成精炼版本

七、技术亮点总结

技术点 实现方式 价值
接口拦截 重写 XHR/Fetch 原型方法 无侵入式获取数据,无需后端配合
数据持久化 localStorage 缓存 页面刷新不丢失,支持延迟查看
AI 集成 Ollama 本地大模型 数据不出本机,保护隐私
跨域请求 GM_xmlhttpRequest 油猴脚本特权 API,突破浏览器 CORS 限制
零依赖 UI 原生 DOM + CSS 体积小、加载快、无冲突

结语

技术服务的终极目标是解决真实问题。一个 200 行的油猴脚本,可能看起来不够"高大上",但它每周为你节省半小时,让你从重复劳动中解放出来------这就是技术赋能的价值。

如果你也有类似的数据整理痛点,不妨动手试试,或许一个脚本就能改变你的工作流。


相关资源

相关推荐
Mahut2 小时前
我们是怎么用 TanStack 全家桶的
前端·javascript·架构
FreeBuf_2 小时前
Claude浏览器扩展漏洞允许通过任意网站实现零点击XSS提示注入
前端·网络·xss
AlunYegeer2 小时前
【JAVA】网关的管理原理和微服务的Interceptor区分
java·服务器·前端
sensen_kiss2 小时前
CAN302 电子商务技术 Pt.2 深入了解HTML和CSS
前端·css·学习·html
说实话起个名字真难啊2 小时前
前端JS审计:渗透测试的“破局之钥”
开发语言·前端·javascript·测试工具
吴声子夜歌2 小时前
TypeScript——编译器和编译选项
前端·javascript·typescript
herogus丶2 小时前
【Chrome插件】页面自动化助手使用介绍
前端·chrome·自动化
Traced back2 小时前
[特殊字符] Vue3 常用指令大全
前端·javascript·vue.js
Highcharts.js2 小时前
在React中使用图表库时,优先选择组件化方案可以降低开发复杂度
前端·javascript·react.js·数据可视化·highcharts