网友作品 #000143 · 提交于 2026/08/11
WPS多维表格任务提醒脚本:定时推送企业微信群

作品简介
WPS 多维表格自带简单的定时提醒,但满足不了作者单位“固定工作清单”的推送需求。作者用 AI 编程工具编写了一段多维表格脚本:自动读取表中的工作事项、任务分解、业务牵头人、手机号和“节点目标”周期规则,把“每月适时”“每季度至少1次”等模糊表述转换为具体日期,匹配出今日待完成与明日待办,再按责任人分组推送企业微信群,并通过手机号 @ 到人。
核心亮点
- 自动读取多维表格中的事项、牵头人、手机号与周期规则,无需人工整理
- 模糊规则转换 + 人工兜底:如“每月适时”按每月 25 日、“每半年”按每月 30 日处理
- 支持每日、每周、周末、月底、季度、次月、奇数月等多种周期规则匹配
- 今日、明日待办按责任人分组汇总,@手机号精准提醒到人
- 可注册为云端定时任务自动运行,也支持手动触发,无待办时自动跳过推送
- 运行输出读取条数、命中数、推送结果等信息,状态可溯源
技术与工具
- 运行环境:WPS 多维表格脚本(AirScript),可部署为云端定时任务
- 推送通道:企业微信群机器人 webhook,text 消息携带 mentioned_mobile_list 实现 @ 人
- 所用 Skill:WPSdbsheet;开发与验证:AI 编程类工具(CodeBuddy)
- 水平参考:作者零基础
开发过程
脚本完成后做了逐项验证:注册计划任务、手动触发运行、云端读取在线表格 92 条固定工作清单、命中今日与明日待办共 7 条、推送企业微信群成功,各项测试通过后部署为定时任务自动运行。
适用场景
- 固定工作清单的周期性提醒:月度报表、季度巡检、例会准备等
- 项目团队任务节点追踪,按计划自动向对应负责人推送到期提醒
原始提交内容(点开查看,可复制全文去问 AI)
// ==================== 配置区(按实际修改) ==================== const CONFIG = { sheetName: “表1.2固定工作清单”, fieldWork: “@工作事项”, fieldTask: “@任务分解”, fieldOwner: “@业务牵头人”, // 仅姓名,一事项一人 fieldPhone: “@手机号”, // 独立手机号列 fieldRule: “@节点目标”, webhook: “https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=35272b19-ac5c-45a9-992c-46d87a29ee15”, // 模糊规则人工兜底 ruleOverride: { “每月适时”: “每月25日”, “每月”: “每月25日”, “每季度至少1次”: “每季度20日”, “每半年”: “每月30日” } };
const WEEKDAY_CN = [“周日”, “周一”, “周二”, “周三”, “周四”, “周五”, “周六”]; const QUARTER_FIRST_MONTHS = [1, 4, 7, 10];
// ==================== 工具函数 ==================== function monthLastDay(d) { return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(); } function clampDay(n, last) { return n > last ? last : n; } function pickNum(rule, patterns) { for (let i = 0; i < patterns.length; i++) { const m = rule.match(patterns[i]); if (m) return parseInt(m[1], 10); } return null; }
// 牵头人姓名(手机号已独立成列,不再从姓名里抠) function parseOwner(raw) { const text = String(raw == null ? “” : raw).trim(); if (!text) return { name: “(未指定牵头人)” }; let name = text .replace(/[()()【】[]]/g, “”) .replace(/[,,、;;/\]+/g, “”) .replace(/\s+/g, “”) .trim(); return { name: name || “(未识别)” }; }
// 手机号清洗(独立列) function parsePhone(raw) { const text = String(raw == null ? “” : raw).replace(/[-\s—-()()]/g, “”); const m = text.match(/1[3-9]\d{9}/); return m ? m[0] : “”; }
// 规则命中判定 function matchDate(rawRule, target) { if (!rawRule) return false; let rule = String(rawRule).trim(); if (CONFIG.ruleOverride[rule]) rule = CONFIG.ruleOverride[rule];
const wd = WEEKDAY_CN[target.getDay()]; const day = target.getDate(); const last = monthLastDay(target); const isQFirst = QUARTER_FIRST_MONTHS.indexOf(target.getMonth() + 1) >= 0;
if (rule === “每日”) return true;
const mw = rule.match(/^每周([一二三四五六日])$/); if (mw && wd === “周” + mw[1]) return true;
if (rule === “每周末” && (target.getDay() === 0 || target.getDay() === 6)) return true;
if (rule.indexOf(“周末”) >= 0 && rule.indexOf(“月底”) >= 0) { if (target.getDay() === 0 || target.getDay() === 6 || day === last) return true; }
if (rule.indexOf(“季度”) >= 0 && isQFirst) { const n = pickNum(rule, [/(\d+)日/, /(\d+)号/]); if (n !== null && clampDay(n, last) === day) return true; }
if ((rule.indexOf(“每月底”) >= 0 || rule.indexOf(“月底前”) >= 0) && day === last) return true;
const mm = rule.match(/每月\D*?(\d+)[日号]/); if (mm && clampDay(parseInt(mm[1], 10), last) === day) return true;
if (rule.indexOf(“季度”) < 0 && rule.indexOf(“次月”) < 0 && rule.indexOf(“次年”) < 0) { const n = pickNum(rule, [/(\d+)日前/]); if (n !== null && clampDay(n, last) === day) return true; }
if (rule.indexOf(“次月”) >= 0) { const n = pickNum(rule, [/(\d+)[日号]前/, /(\d+)[日号]/]); if (n !== null && clampDay(n, last) === day) return true; }
if (rule.indexOf(“奇数月”) >= 0) { const n = pickNum(rule, [/(\d+)[日号]前/, /(\d+)[日号]/]); if (n !== null && (target.getMonth() + 1) % 2 === 1 && clampDay(n, last) === day) return true; }
return false; }
// ==================== 读取数据 ==================== function loadRows() { const sheet = Application.Sheets(CONFIG.sheetName); const count = sheet.RecordRange.Count; if (!count || count <= 0) return [];
const values = sheet.RecordRange.Item( “1:” + count, [CONFIG.fieldWork, CONFIG.fieldTask, CONFIG.fieldOwner, CONFIG.fieldPhone, CONFIG.fieldRule] ).Value;
const rows = []; for (let i = 0; i < values.length; i++) { const v = values[i] || []; const work = v[0] == null ? “” : String(v[0]).trim(); const task = v[1] == null ? “” : String(v[1]).trim(); const ownerRaw = v[2] == null ? “” : String(v[2]).trim(); const phoneRaw = v[3] == null ? “” : String(v[3]).trim(); const rule = v[4] == null ? “” : String(v[4]).trim(); if (!work && !task && !ownerRaw && !rule) continue;
const owner = parseOwner(ownerRaw);
rows.push({
work: task || work,
ownerName: owner.name,
ownerPhone: parsePhone(phoneRaw),
rule: rule
});
} return rows; }
// ==================== 拼装消息 ==================== function groupByOwner(items) { const groups = {}; // name -> { works:[], phone:“” } for (let i = 0; i < items.length; i++) { const key = items[i].ownerName || “(未指定牵头人)”; if (!groups[key]) groups[key] = { works: [], phone: “” }; groups[key].works.push(items[i].work || “(事项未填写)”); if (!groups[key].phone && items[i].ownerPhone) groups[key].phone = items[i].ownerPhone; } return groups; }
function buildMessage(today, tomorrow, rows) { const todayHits = []; const todayKeys = {}; for (let i = 0; i < rows.length; i++) { if (matchDate(rows[i].rule, today)) { todayHits.push(rows[i]); todayKeys[rows[i].ownerName + “||” + rows[i].work] = true; } } const tomorrowHits = []; for (let i = 0; i < rows.length; i++) { if (matchDate(rows[i].rule, tomorrow)) { if (todayKeys[rows[i].ownerName + “||” + rows[i].work]) continue; tomorrowHits.push(rows[i]); } }
const lines = []; lines.push( “【工作事项提醒】” + (today.getMonth() + 1) + “月” + today.getDate() + “日(” + WEEKDAY_CN[today.getDay()] + “)” + “ / 明日 “ + (tomorrow.getMonth() + 1) + “月” + tomorrow.getDate() + “日(” + WEEKDAY_CN[tomorrow.getDay()] + “)” );
const mobiles = []; function appendSection(title, hits) { lines.push(“”); lines.push(“— “ + title + “ —”); const groups = groupByOwner(hits); const keys = Object.keys(groups); if (keys.length === 0) { lines.push(“(无)”); return; } for (let i = 0; i < keys.length; i++) { lines.push(“@” + keys[i]); if (groups[keys[i]].phone && mobiles.indexOf(groups[keys[i]].phone) < 0) { mobiles.push(groups[keys[i]].phone); } for (let j = 0; j < groups[keys[i]].works.length; j++) { lines.push(” · “ + groups[keys[i]].works[j]); } } }
appendSection(“今日待完成”, todayHits); appendSection(“明日待办”, tomorrowHits);
return { content: lines.join(“\n”), mobiles: mobiles, todayCount: todayHits.length, tomorrowCount: tomorrowHits.length }; }
// ==================== 推送 ==================== function push(content, mobiles) { const body = { msgtype: “text”, text: { content: content, mentioned_mobile_list: mobiles || [] } }; const resp = HTTP.post(CONFIG.webhook, body, { timeout: 10000 }); return resp.json(); }
// ==================== 主流程 ==================== function main() { const today = new Date(); const tomorrow = new Date(today.getTime() + 24 * 3600 * 1000);
const rows = loadRows(); console.log(“读取事项 “ + rows.length + “ 条”);
const msg = buildMessage(today, tomorrow, rows); console.log(msg.content);
if (msg.todayCount === 0 && msg.tomorrowCount === 0) { return { ok: true, msg: “今明两日无待办,未推送”, count: 0, data: null }; }
const res = push(msg.content, msg.mobiles); if (res && res.errcode === 0) { return { ok: true, msg: “推送成功”, count: msg.todayCount + msg.tomorrowCount, data: { today: msg.todayCount, tomorrow: msg.tomorrowCount, mobiles: msg.mobiles.length } }; } return { ok: false, msg: “推送失败:” + JSON.stringify(res), count: 0, data: null }; }
return main();