chinaweal-claude-code/skills/work-weekly-report/scripts/work-weekly-report.js

681 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* 工作周报管理工具
* 用于管理个人工作周报,包括制定计划、记录工作、查询周报等
*/
const fs = require('fs');
const path = require('path');
// 数据文件路径
const DATA_DIR = path.join(__dirname, '..', 'data');
const DATA_FILE = path.join(DATA_DIR, 'weekly-reports.json');
// 确保数据目录存在
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
/**
* 生成 UUID
*/
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 获取本地日期字符串 YYYY-MM-DD
*/
function toLocalDateString(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}`;
}
/**
* 获取当前日期字符串
*/
function getCurrentDateStr() {
const now = new Date();
return now.toISOString().split('T')[0];
}
/**
* 获取当前时间 ISO 字符串
*/
function getCurrentISO() {
return new Date().toISOString();
}
/**
* 判断是否为法定工作日(周一至周五)
*/
function isWorkday(dateStr) {
const date = new Date(dateStr);
const day = date.getDay();
return day >= 1 && day <= 5;
}
/**
* 获取日期所在周的第一天(周一)
*/
function getWeekStartDate(dateStr) {
const date = new Date(dateStr);
const day = date.getDay();
// 计算到周一的天数
const diff = day === 0 ? -6 : 1 - day;
const monday = new Date(date);
monday.setDate(date.getDate() + diff);
return toLocalDateString(monday);
}
/**
* 获取日期所在周的周五
*/
function getWeekEndDate(weekStartDate) {
const monday = new Date(weekStartDate);
const friday = new Date(monday);
friday.setDate(monday.getDate() + 4);
return toLocalDateString(friday);
}
/**
* 获取 ISO 周编号 (YYYY-Www)
*/
function getWeekNumber(dateStr) {
const date = new Date(dateStr);
// ISO 周码每年的第一个周四所在的周为第1周
const thursday = new Date(date);
thursday.setDate(date.getDate() + (4 - (date.getDay() === 0 ? 7 : date.getDay())));
const yearStart = new Date(thursday.getFullYear(), 0, 1);
const weekNum = Math.ceil(((thursday - yearStart) / 86400000 + 1) / 7);
return `${thursday.getFullYear()}-W${String(weekNum).padStart(2, '0')}`;
}
/**
* 获取当前工作周信息
*/
function getCurrentWeek() {
const today = getCurrentDateStr();
return getWeekInfo(today);
}
/**
* 获取指定日期所在的工作周信息
*/
function getWeekInfo(dateStr) {
const weekStart = getWeekStartDate(dateStr);
const weekEnd = getWeekEndDate(weekStart);
const weekNumber = getWeekNumber(dateStr);
return {
weekNumber,
startDate: weekStart,
endDate: weekEnd
};
}
/**
* 解析周标识为周编号
*/
function parseWeekIdentifier(identifier) {
if (!identifier || identifier === '本周' || identifier === '当前周') {
return getCurrentWeek().weekNumber;
}
if (identifier === '上周') {
const today = new Date();
today.setDate(today.getDate() - 7);
return getWeekNumber(today.toISOString().split('T')[0]);
}
if (identifier === '上上周') {
const today = new Date();
today.setDate(today.getDate() - 14);
return getWeekNumber(today.toISOString().split('T')[0]);
}
if (identifier === '下周') {
const today = new Date();
today.setDate(today.getDate() + 7);
return getWeekNumber(today.toISOString().split('T')[0]);
}
// 假设是 YYYY-Www 格式
if (/^\d{4}-W\d{2}$/.test(identifier)) {
return identifier;
}
throw new Error(`无法解析周标识: ${identifier}`);
}
/**
* 加载数据文件
*/
function loadData() {
if (!fs.existsSync(DATA_FILE)) {
const initial = {
meta: {
version: '1.0.0',
createdAt: getCurrentISO(),
updatedAt: getCurrentISO()
},
weeklyReports: {}
};
fs.writeFileSync(DATA_FILE, JSON.stringify(initial, null, 2), 'utf8');
return initial;
}
const content = fs.readFileSync(DATA_FILE, 'utf8');
return JSON.parse(content);
}
/**
* 保存数据文件
*/
function saveData(data) {
data.meta.updatedAt = getCurrentISO();
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
}
/**
* 从周编号反推该周的周一日期
* 使用 ISO 8601 标准每年第一个周四所在的周为第1周W1从该周的周一开始
*/
function getWeekStartFromWeekNumber(year, weekNum) {
const MS_PER_DAY = 24 * 60 * 60 * 1000;
// 找到该年1月4日ISO 8601 定义包含该年第一个周四的那一周是W1
const jan4 = new Date(year, 0, 4);
const jan4Day = jan4.getDay(); // 0=Sun, 1=Mon, ..., 6=Sat
// 找到W1的周一
let firstMonday;
if (jan4Day === 1) {
// 1月4日是周一 -> W1周一是1月4日
firstMonday = jan4;
} else if (jan4Day === 0) {
// 1月4日是周日 -> W1周一是上周一12月X日
// 1月4日 - 7天 = 12月28日上上个周一再 -1天 = 12月29日上周一
firstMonday = new Date(jan4.getTime() - 7 * MS_PER_DAY);
// 但这已经是12月28了而1月4日是周日那上周一是12月29日
// 所以实际上 1月4日 - 8天 = 12月27... 这不对
// 让我重新想:
// 如果1月4日是周日那这一周是 Dec 28 - Jan 3 (W1 ends Jan 3)
// 所以W1的周一是 Dec 28
// 那么 Dec 28 + 7 = Jan 4W2从 Jan 5开始
// 所以 W1 of 2026 = Dec 28 - Jan 3
// W1 starts on Monday Dec 28
// Let me recalculate: Jan 4 - 6 = Dec 29 (that's Monday if Jan 4 is Sunday)
// Wait, if Jan 4 is Sunday, then Jan 4 - 1 = Jan 3 (Saturday)
// Jan 4 - 2 = Jan 2, ..., Jan 4 - 7 = Dec 28
// But that's 7 days back, not 6. And Dec 28 is Monday?
// Let me just use getTime()
const daysToSubtract = jan4Day === 0 ? 6 : jan4Day - 1;
firstMonday = new Date(jan4.getTime() - daysToSubtract * MS_PER_DAY);
} else {
// 1月4日是 Tue(2), Wed(3), Thu(4), Fri(5), Sat(6) -> 找到上溯到周一
// 如果1月4日是周三那周一就是1月4日 - 2天
// daysToSubtract = 4 - 1 = 3, 1月4日 - 3天 = 1月1日 = 周一 ✓
const daysToSubtract = jan4Day - 1;
firstMonday = new Date(jan4.getTime() - daysToSubtract * MS_PER_DAY);
}
// 计算目标周的周一
const targetMonday = new Date(firstMonday.getTime() + (weekNum - 1) * 7 * MS_PER_DAY);
return targetMonday;
}
/**
* 获取或创建指定周的周报对象
*/
function getOrCreateWeeklyReport(weekNumber) {
const data = loadData();
if (!data.weeklyReports[weekNumber]) {
const [yearStr, weekPart] = weekNumber.split('-W');
const year = parseInt(yearStr, 10);
const weekNum = parseInt(weekPart, 10);
const targetMonday = getWeekStartFromWeekNumber(year, weekNum);
const targetFriday = new Date(targetMonday);
targetFriday.setDate(targetMonday.getDate() + 4);
data.weeklyReports[weekNumber] = {
weekNumber,
weekStartDate: toLocalDateString(targetMonday),
weekEndDate: toLocalDateString(targetFriday),
plan: [],
records: {},
summary: ''
};
}
return { data, report: data.weeklyReports[weekNumber] };
}
/**
* 制定/更新周计划
*/
function planWeek(weekNumber, tasks, priority = 'medium') {
const { data, report } = getOrCreateWeeklyReport(weekNumber);
const now = getCurrentISO();
for (const taskDesc of tasks) {
const planItem = {
id: generateUUID(),
description: taskDesc,
expectedDate: null,
priority: priority,
status: 'pending',
createdAt: now,
updatedAt: now
};
report.plan.push(planItem);
}
saveData(data);
return report;
}
/**
* 添加工作记录
*/
function addRecord(date, content, status, options = {}) {
const { hours, planItemId } = options;
if (!isWorkday(date)) {
throw new Error(`${date} 不是法定工作日(周一至周五)`);
}
const weekInfo = getWeekInfo(date);
const { data, report } = getOrCreateWeeklyReport(weekInfo.weekNumber);
if (!report.records[date]) {
report.records[date] = [];
}
const record = {
id: generateUUID(),
content,
date,
status,
hours: hours || null,
planItemId: planItemId || null,
createdAt: getCurrentISO(),
updatedAt: getCurrentISO()
};
report.records[date].push(record);
// 如果有关联的计划项,自动更新其状态
if (planItemId) {
const planItem = report.plan.find(p => p.id === planItemId);
if (planItem) {
planItem.status = status === 'completed' ? 'completed' :
status === 'in-progress' ? 'in-progress' : planItem.status;
planItem.updatedAt = getCurrentISO();
}
}
saveData(data);
return report;
}
/**
* 获取未完成的工作项
*/
function getPendingItems(weekIdentifier) {
const weekNumber = parseWeekIdentifier(weekIdentifier);
const { data, report } = getOrCreateWeeklyReport(weekNumber);
const pending = report.plan.filter(p => p.status === 'pending' || p.status === 'in-progress');
// 按优先级排序
const priorityOrder = { high: 0, medium: 1, low: 2 };
pending.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
return pending;
}
/**
* 查询周报
*/
function queryWeek(weekIdentifier, format = 'detail') {
const weekNumber = parseWeekIdentifier(weekIdentifier);
const { report } = getOrCreateWeeklyReport(weekNumber);
if (format === 'summary') {
return formatWeekSummary(report);
}
return report;
}
/**
* 格式化周报摘要
*/
function formatWeekSummary(report) {
const total = report.plan.length;
const completed = report.plan.filter(p => p.status === 'completed').length;
const pending = report.plan.filter(p => p.status === 'pending' || p.status === 'in-progress');
let output = `周报摘要: ${report.weekNumber} (${report.weekStartDate}${report.weekEndDate})\n`;
output += '================================================================\n\n';
output += `计划完成率: ${completed}/${total}`;
if (total > 0) {
output += ` (${Math.round(completed / total * 100)}%)`;
}
output += '\n\n';
if (report.plan.length > 0) {
output += '计划项:\n';
for (const item of report.plan) {
const statusIcon = item.status === 'completed' ? '[√]' : '[ ]';
const priorityTag = item.priority === 'high' ? '[高]' : item.priority === 'low' ? '[低]' : '[中]';
output += ` ${statusIcon} ${priorityTag} ${item.description}`;
if (item.expectedDate) {
output += ` (预计: ${item.expectedDate})`;
}
output += '\n';
}
output += '\n';
}
if (Object.keys(report.records).length > 0) {
output += '每日记录:\n';
const sortedDates = Object.keys(report.records).sort();
for (const date of sortedDates) {
const records = report.records[date];
const hours = records.reduce((sum, r) => sum + (r.hours || 0), 0);
const content = records.map(r => r.content).join('; ');
output += ` - ${date}: ${content}`;
if (hours > 0) {
output += ` (${hours}h)`;
}
output += '\n';
}
output += '\n';
}
if (pending.length > 0) {
output += '未完成工作:\n';
for (const item of pending) {
output += ` - ${item.description}\n`;
}
output += '\n';
}
if (report.summary) {
output += `总结: ${report.summary}\n`;
}
return output;
}
/**
* 更新计划项状态
*/
function updatePlanItemStatus(weekNumber, planItemId, status) {
const { data, report } = getOrCreateWeeklyReport(weekNumber);
const planItem = report.plan.find(p => p.id === planItemId);
if (!planItem) {
throw new Error(`未找到计划项: ${planItemId}`);
}
planItem.status = status;
planItem.updatedAt = getCurrentISO();
saveData(data);
return report;
}
/**
* 设置周报总结
*/
function setSummary(weekNumber, summary) {
const { data, report } = getOrCreateWeeklyReport(weekNumber);
report.summary = summary;
saveData(data);
return report;
}
/**
* 删除计划项
*/
function removePlanItem(weekNumber, planItemId) {
const { data, report } = getOrCreateWeeklyReport(weekNumber);
const index = report.plan.findIndex(p => p.id === planItemId);
if (index === -1) {
throw new Error(`未找到计划项: ${planItemId}`);
}
report.plan.splice(index, 1);
saveData(data);
return report;
}
/**
* 获取工作周的所有工作日
*/
function getWorkdays(weekNumber) {
const { report } = getOrCreateWeeklyReport(weekNumber);
const workdays = [];
const current = new Date(report.weekStartDate);
const end = new Date(report.weekEndDate);
while (current <= end) {
workdays.push(current.toISOString().split('T')[0]);
current.setDate(current.getDate() + 1);
}
return workdays;
}
// === CLI 模式 ===
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
// 已知的标志列表
const FLAGS = ['--week', '--tasks', '--priority', '--date', '--content', '--status', '--hours', '--plan-item', '--format'];
function getArgValue(flag) {
const index = args.indexOf(flag);
if (index !== -1 && index + 1 < args.length) {
const nextArg = args[index + 1];
// 如果下一个参数也是标志返回null
if (FLAGS.includes(nextArg)) {
return null;
}
return nextArg;
}
// 支持 --flag=value 格式
const combined = args.find(arg => arg.startsWith(`${flag}=`));
if (combined) {
return combined.split('=')[1];
}
return null;
}
function getRemainingArgs(flag) {
const index = args.indexOf(flag);
if (index === -1) return [];
const result = [];
let i = index + 1;
while (i < args.length) {
const arg = args[i];
// 如果遇到另一个标志,停止
if (FLAGS.includes(arg)) {
break;
}
result.push(arg);
i++;
}
return result;
}
try {
switch (command) {
case 'plan': {
const week = getArgValue('--week') || getCurrentWeek().weekNumber;
const tasks = getRemainingArgs('--tasks');
if (tasks.length === 0) {
console.error('错误: 请提供任务列表 (--tasks "任务1" "任务2" ...)');
process.exit(1);
}
const priority = getArgValue('--priority') || 'medium';
const report = planWeek(week, tasks, priority);
console.log(`已在 ${report.weekNumber} 创建 ${tasks.length} 个计划项`);
console.log('\n计划项:');
report.plan.slice(-tasks.length).forEach((item, i) => {
console.log(` ${i + 1}. [${item.priority}] ${item.description} (${item.id})`);
});
break;
}
case 'record': {
const date = getArgValue('--date') || getCurrentDateStr();
const content = getArgValue('--content');
if (!content) {
console.error('错误: 请提供工作内容 (--content "内容")');
process.exit(1);
}
const status = getArgValue('--status') || 'in-progress';
const hours = getArgValue('--hours') ? parseFloat(getArgValue('--hours')) : null;
const planItemId = getArgValue('--plan-item');
const report = addRecord(date, content, status, { hours, planItemId });
console.log(`已在 ${date} 添加工作记录`);
console.log(`${report.weekNumber} 当前共有 ${report.plan.length} 个计划项`);
break;
}
case 'pending': {
const week = getArgValue('--week') || getCurrentWeek().weekNumber;
const pending = getPendingItems(week);
const currentWeek = getCurrentWeek();
console.log(`本周未完成的工作 (${week})\n`);
console.log('========================================\n');
if (pending.length === 0) {
console.log('所有任务已完成!');
} else {
const high = pending.filter(p => p.priority === 'high');
const medium = pending.filter(p => p.priority === 'medium');
const low = pending.filter(p => p.priority === 'low');
if (high.length > 0) {
console.log('[高优先级]');
high.forEach(p => console.log(` - ${p.description}`));
console.log();
}
if (medium.length > 0) {
console.log('[中优先级]');
medium.forEach(p => console.log(` - ${p.description}`));
console.log();
}
if (low.length > 0) {
console.log('[低优先级]');
low.forEach(p => console.log(` - ${p.description}`));
console.log();
}
}
console.log(`${pending.length} 项未完成任务`);
break;
}
case 'query': {
const week = getArgValue('--week') || '本周';
const format = getArgValue('--format') || 'detail';
const weekNumber = parseWeekIdentifier(week);
const result = queryWeek(weekNumber, format);
console.log(typeof result === 'string' ? result : JSON.stringify(result, null, 2));
break;
}
case 'update-status': {
const weekIdentifier = getArgValue('--week') || '本周';
const planItemId = getArgValue('--plan-item');
const status = getArgValue('--status');
if (!planItemId || !status) {
console.error('错误: 请提供计划项ID (--plan-item) 和状态 (--status)');
process.exit(1);
}
const weekNumber = parseWeekIdentifier(weekIdentifier);
const report = updatePlanItemStatus(weekNumber, planItemId, status);
console.log(`已更新计划项 ${planItemId} 状态为 ${status}`);
console.log(`${report.weekNumber} 计划完成率: ${report.plan.filter(p => p.status === 'completed').length}/${report.plan.length}`);
break;
}
case 'summary': {
const weekIdentifier = getArgValue('--week') || '本周';
const content = getArgValue('--content');
if (!content) {
console.error('错误: 请提供总结内容 (--content "内容")');
process.exit(1);
}
const weekNumber = parseWeekIdentifier(weekIdentifier);
const report = setSummary(weekNumber, content);
console.log(`已在 ${weekNumber} 设置周报总结`);
break;
}
case 'remove-plan': {
const weekIdentifier = getArgValue('--week') || '本周';
const planItemId = getArgValue('--plan-item');
if (!planItemId) {
console.error('错误: 请提供计划项ID (--plan-item)');
process.exit(1);
}
const weekNumber = parseWeekIdentifier(weekIdentifier);
const report = removePlanItem(weekNumber, planItemId);
console.log(`已删除计划项 ${planItemId}`);
console.log(`${report.weekNumber} 剩余 ${report.plan.length} 个计划项`);
break;
}
case 'week-info': {
const date = getArgValue('--date') || getCurrentDateStr();
const info = getWeekInfo(date);
console.log(`日期 ${date} 所在工作周:`);
console.log(` 周编号: ${info.weekNumber}`);
console.log(` 周一: ${info.startDate}`);
console.log(` 周五: ${info.endDate}`);
break;
}
default:
console.log('用法:');
console.log(' node work-weekly-report.js plan --tasks "任务1" "任务2" [--week YYYY-Www] [--priority high|medium|low]');
console.log(' node work-weekly-report.js record --content "内容" [--date YYYY-MM-DD] [--status completed|in-progress|blocked] [--hours N] [--plan-item UUID]');
console.log(' node work-weekly-report.js pending [--week YYYY-Www]');
console.log(' node work-weekly-report.js query [--week YYYY-Www] [--format detail|summary]');
console.log(' node work-weekly-report.js update-status --plan-item UUID --status STATUS [--week YYYY-Www]');
console.log(' node work-weekly-report.js summary --content "总结" [--week YYYY-Www]');
console.log(' node work-weekly-report.js remove-plan --plan-item UUID [--week YYYY-Www]');
console.log(' node work-weekly-report.js week-info [--date YYYY-MM-DD]');
console.log('\n周标识: YYYY-Www, 本周, 上周, 上上周, 下周');
}
} catch (error) {
console.error('错误:', error.message);
process.exit(1);
}
}
// 导出模块
module.exports = {
getCurrentWeek,
getWeekInfo,
planWeek,
addRecord,
getPendingItems,
queryWeek,
updatePlanItemStatus,
setSummary,
removePlanItem,
getWorkdays,
parseWeekIdentifier,
isWorkday
};