@yionr/plan-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/cli.mjs +175 -0
  2. package/package.json +21 -0
  3. package/scripts/collect_monthly_data.py +126 -0
  4. package/scripts/config.py +38 -0
  5. package/scripts/create_next_week.py +85 -0
  6. package/scripts/generate_monthly_md.py +91 -0
  7. package/scripts/get_current_week.py +79 -0
  8. package/scripts/get_user_info.py +85 -0
  9. package/scripts/query_weekly_records.py +90 -0
  10. package/scripts/update_completion.py +69 -0
  11. package/skills/sab-monthly-summary/SKILL.md +85 -0
  12. package/skills/sab-monthly-summary/scripts/collect_monthly_data.py +126 -0
  13. package/skills/sab-monthly-summary/scripts/config.py +41 -0
  14. package/skills/sab-monthly-summary/scripts/create_next_week.py +85 -0
  15. package/skills/sab-monthly-summary/scripts/generate_monthly_md.py +91 -0
  16. package/skills/sab-monthly-summary/scripts/get_current_week.py +79 -0
  17. package/skills/sab-monthly-summary/scripts/get_user_info.py +85 -0
  18. package/skills/sab-monthly-summary/scripts/query_weekly_records.py +90 -0
  19. package/skills/sab-monthly-summary/scripts/update_completion.py +69 -0
  20. package/skills/sab-weekly-plan/SKILL.md +109 -0
  21. package/skills/sab-weekly-plan/scripts/collect_monthly_data.py +126 -0
  22. package/skills/sab-weekly-plan/scripts/config.py +41 -0
  23. package/skills/sab-weekly-plan/scripts/create_next_week.py +85 -0
  24. package/skills/sab-weekly-plan/scripts/generate_monthly_md.py +91 -0
  25. package/skills/sab-weekly-plan/scripts/get_current_week.py +79 -0
  26. package/skills/sab-weekly-plan/scripts/get_user_info.py +85 -0
  27. package/skills/sab-weekly-plan/scripts/query_weekly_records.py +90 -0
  28. package/skills/sab-weekly-plan/scripts/update_completion.py +69 -0
package/cli.mjs ADDED
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import inquirer from 'inquirer';
5
+ import chalk from 'chalk';
6
+ import { execSync } from 'child_process';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('plan-kit')
18
+ .description('伟星SAB股份大洋数字化发展部周计划/月总结工具')
19
+ .version('1.0.0');
20
+
21
+ program
22
+ .command('setup')
23
+ .description('交互式安装周计划/月总结 skill')
24
+ .action(async () => {
25
+ console.log(chalk.blue.bold('\n伟星SAB股份大洋数字化发展部周计划/月总结工具\n'));
26
+
27
+ // Step 1: 检查飞书 CLI 安装与认证
28
+ console.log(chalk.cyan('▸ 检查飞书 CLI...'));
29
+ let hasLarkCli = false;
30
+ let hasAuth = false;
31
+ try {
32
+ execSync('lark-cli --version 2>&1', { encoding: 'utf8', timeout: 10000 });
33
+ hasLarkCli = true;
34
+ console.log(chalk.green(' ✓ lark-cli 已安装'));
35
+ } catch {
36
+ console.log(chalk.yellow(' ✗ lark-cli 未安装'));
37
+ }
38
+
39
+ if (!hasLarkCli) {
40
+ const { installLark } = await inquirer.prompt([{
41
+ type: 'confirm',
42
+ name: 'installLark',
43
+ message: '是否现在安装飞书 CLI?(将运行 npx @larksuite/cli@latest install)',
44
+ default: true,
45
+ }]);
46
+ if (installLark) {
47
+ console.log(chalk.cyan('\n正在启动飞书 CLI 安装向导...\n'));
48
+ execSync('npx @larksuite/cli@latest install', { stdio: 'inherit' });
49
+ hasLarkCli = true;
50
+ } else {
51
+ console.log(chalk.red('未安装飞书 CLI,无法继续。'));
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ // 检测认证状态(auth status 直接输出 JSON,无 --as 参数)
57
+ console.log(chalk.cyan('▸ 检查飞书 CLI 认证状态...'));
58
+ try {
59
+ const authResult = execSync('lark-cli auth status 2>&1', { encoding: 'utf8', timeout: 10000 }).trim();
60
+ const authJson = JSON.parse(authResult);
61
+ const userIdentity = authJson.identities && authJson.identities.user;
62
+ if (userIdentity && userIdentity.status === 'ready' && userIdentity.tokenStatus === 'valid') {
63
+ hasAuth = true;
64
+ const userName = userIdentity.userName || '(未知)';
65
+ console.log(chalk.green(` ✓ 已认证(${userName})`));
66
+ } else {
67
+ throw new Error('not logged in');
68
+ }
69
+ } catch {
70
+ console.log(chalk.yellow(' ✗ 未认证或认证已过期'));
71
+ }
72
+
73
+ if (!hasAuth) {
74
+ // 飞书 CLI 的 config init 和 auth login 是交互式 TUI,
75
+ // 嵌套在 execSync 中可能导致键盘输入卡住(尤其 Windows)。
76
+ // 改为引导用户在独立终端中手动执行。
77
+ console.log(chalk.yellow('\n⚠ 飞书 CLI 未认证,请在新终端中运行以下命令完成配置:\n'));
78
+ console.log(chalk.cyan(' # 1. 初始化应用配置(需要 App ID 和 App Secret)'));
79
+ console.log(chalk.cyan(' lark-cli config init --new'));
80
+ console.log(chalk.cyan(''));
81
+ console.log(chalk.cyan(' # 2. 登录认证'));
82
+ console.log(chalk.cyan(' lark-cli auth login'));
83
+ console.log(chalk.cyan(''));
84
+ console.log(chalk.yellow(' 配置完成后,按任意键继续安装 skill...\n'));
85
+
86
+ // 等待用户按键
87
+ process.stdin.setRawMode(true);
88
+ await new Promise(resolve => {
89
+ process.stdin.once('data', () => {
90
+ process.stdin.setRawMode(false);
91
+ resolve();
92
+ });
93
+ });
94
+
95
+ // 重新检测认证
96
+ try {
97
+ const recheck = execSync('lark-cli auth status 2>&1', { encoding: 'utf8', timeout: 10000 }).trim();
98
+ const recheckJson = JSON.parse(recheck);
99
+ const recheckUser = recheckJson.identities && recheckJson.identities.user;
100
+ if (recheckUser && recheckUser.status === 'ready' && recheckUser.tokenStatus === 'valid') {
101
+ hasAuth = true;
102
+ console.log(chalk.green(' ✓ 认证成功!\n'));
103
+ } else {
104
+ console.log(chalk.yellow(' ⚠ 仍检测到未认证,继续安装但脚本可能无法正常工作。\n'));
105
+ }
106
+ } catch {
107
+ console.log(chalk.yellow(' ⚠ 认证检测失败,继续安装但脚本可能无法正常工作。\n'));
108
+ }
109
+ }
110
+
111
+ // Step 2: 选择 Code Agent
112
+ const { agentDirs } = await inquirer.prompt([{
113
+ type: 'checkbox',
114
+ name: 'agentDirs',
115
+ message: '选择要安装到哪些 Code Agent 的 skills 目录:',
116
+ choices: [
117
+ { name: `Claude Code (${path.join(HOME, '.claude/skills/')})`, value: path.join(HOME, '.claude/skills') },
118
+ { name: `Cursor (${path.join(HOME, '.cursor/skills/')})`, value: path.join(HOME, '.cursor/skills') },
119
+ { name: `Codex (${path.join(HOME, '.codex/skills/')})`, value: path.join(HOME, '.codex/skills') },
120
+ { name: '自定义路径(稍后输入)', value: '__custom__' },
121
+ ],
122
+ validate: (a) => a.length > 0 || '请至少选择一个',
123
+ }]);
124
+
125
+ let installDirs = agentDirs.filter(d => d !== '__custom__');
126
+ if (agentDirs.includes('__custom__')) {
127
+ const { customDir } = await inquirer.prompt([{
128
+ type: 'input',
129
+ name: 'customDir',
130
+ message: '请输入自定义 skills 目录路径:',
131
+ validate: (i) => i.trim().length > 0 || '路径不能为空',
132
+ }]);
133
+ installDirs.push(customDir.trim());
134
+ }
135
+
136
+ // Step 3: 选 Skill(周计划为必装,月总结可选)
137
+ console.log(chalk.cyan('\n▸ 安装 Skill'));
138
+ console.log(chalk.gray(' 周计划 (sab-weekly-plan) — 默认必装'));
139
+
140
+ const { installSummary } = await inquirer.prompt([{
141
+ type: 'confirm',
142
+ name: 'installSummary',
143
+ message: '是否同时安装月总结 (sab-monthly-summary)?',
144
+ default: true,
145
+ }]);
146
+
147
+ const selectedSkills = ['sab-weekly-plan'];
148
+ if (installSummary) {
149
+ selectedSkills.push('sab-monthly-summary');
150
+ }
151
+
152
+ // Step 4: 安装 Skills(每个 skill 目录 = SKILL.md + scripts/)
153
+ for (const dir of installDirs) {
154
+ console.log(chalk.cyan('\n▸ 安装 Skill 到'), dir, '...');
155
+ fs.mkdirSync(dir, { recursive: true });
156
+
157
+ for (const skillName of selectedSkills) {
158
+ const skillSrc = path.join(__dirname, 'skills', skillName);
159
+ const dstDir = path.join(dir, skillName);
160
+ if (fs.existsSync(dstDir)) {
161
+ fs.rmSync(dstDir, { recursive: true });
162
+ }
163
+ fs.cpSync(skillSrc, dstDir, { recursive: true });
164
+ console.log(chalk.green(' ✓'), skillName, '(含 scripts/)');
165
+ }
166
+ }
167
+
168
+ console.log(chalk.green.bold('\n✓ 安装完成!\n'));
169
+ console.log('使用方式:');
170
+ console.log(' - 对 Claude 说「写本周计划」或「反馈本周完成情况」');
171
+ console.log(' - 对 Claude 说「生成X月工作总结」生成月总结');
172
+ console.log('');
173
+ });
174
+
175
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@yionr/plan-kit",
3
+ "version": "1.0.0",
4
+ "description": "伟星SAB股份大洋数字化发展部周计划/月总结工具",
5
+ "bin": {
6
+ "plan-kit": "cli.mjs"
7
+ },
8
+ "files": [
9
+ "cli.mjs",
10
+ "scripts/",
11
+ "skills/",
12
+ "templates/"
13
+ ],
14
+ "dependencies": {
15
+ "commander": "^12.0.0",
16
+ "inquirer": "^9.0.0",
17
+ "chalk": "^5.0.0"
18
+ },
19
+ "license": "UNLICENSED",
20
+ "private": false
21
+ }
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env python3
2
+ """汇总指定月份所有周计划记录。
3
+
4
+ 用法: python3 collect_monthly_data.py --open-id ou_xxx --month 7
5
+ 输出: JSON
6
+ """
7
+
8
+ import json
9
+ import subprocess
10
+ import sys
11
+ import argparse
12
+ import os
13
+
14
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
15
+ import config
16
+
17
+
18
+ def run_lark(args):
19
+ result = subprocess.run(
20
+ ["lark-cli"] + args,
21
+ capture_output=True,
22
+ text=True,
23
+ timeout=30,
24
+ )
25
+ try:
26
+ data = json.loads(result.stdout)
27
+ except json.JSONDecodeError:
28
+ print(json.dumps({"error": "parse_failed", "stdout": result.stdout[:500], "stderr": result.stderr[:500]}, ensure_ascii=False))
29
+ sys.exit(1)
30
+
31
+ if not data.get("ok"):
32
+ print(json.dumps({"error": "lark_cli_failed", "detail": data.get("error", {})}, ensure_ascii=False))
33
+ sys.exit(1)
34
+ return data
35
+
36
+
37
+ def safe_val(arr, default=""):
38
+ if isinstance(arr, list):
39
+ return arr[0] if arr else default
40
+ if arr is None:
41
+ return default
42
+ return arr
43
+
44
+
45
+ def main():
46
+ parser = argparse.ArgumentParser()
47
+ parser.add_argument("--open-id", required=True)
48
+ parser.add_argument("--month", required=True, type=int)
49
+ args = parser.parse_args()
50
+
51
+ month = args.month
52
+
53
+ # 先查所有记录(按责任人筛选,内存中再按月份过滤)
54
+ data = run_lark([
55
+ "base", "+record-list",
56
+ "--base-token", config.BASE_TOKEN,
57
+ "--table-id", config.TABLE_IDS["周计划"],
58
+ "--filter-json", json.dumps(
59
+ {"logic": "and", "conditions": [
60
+ ["责任人", "intersects", [{"id": args.open_id}]],
61
+ ]},
62
+ ensure_ascii=False
63
+ ),
64
+ "--as", "user",
65
+ "--format", "json",
66
+ ])
67
+
68
+ records = data["data"]["data"]
69
+ field_ids = data["data"]["field_id_list"]
70
+
71
+ idx_content = field_ids.index("fldjdmxh69")
72
+ idx_completion = field_ids.index("fldF9ebqDi")
73
+ idx_remark = field_ids.index("fldu5uAqGU")
74
+ idx_hours = field_ids.index("fldQXhORVG")
75
+ idx_week = field_ids.index("fldAivwku1")
76
+
77
+ all_records = []
78
+ seen_ids = set()
79
+
80
+ for i, rec in enumerate(records):
81
+ record_id = rec.get("_record_id") if isinstance(rec, dict) else None
82
+ record_id = record_id or (data["data"].get("record_id_list") or [None])[i] or "rec_{}".format(i)
83
+ if record_id in seen_ids:
84
+ continue
85
+ seen_ids.add(record_id)
86
+
87
+ week_val = safe_val(rec[idx_week])
88
+ # 过滤:只保留目标月份的记录
89
+ if week_val.startswith("{}月".format(month)):
90
+ all_records.append({
91
+ "record_id": record_id,
92
+ "content": safe_val(rec[idx_content]),
93
+ "completion_status": safe_val(rec[idx_completion]),
94
+ "remark": safe_val(rec[idx_remark]),
95
+ "hours": safe_val(rec[idx_hours], None),
96
+ "week": week_val,
97
+ })
98
+
99
+ # 提取临时工作(从备注中识别)
100
+ temp_work = []
101
+ skip_keywords = ["未完成", "延期", "搁置", "等待"]
102
+ for rec in all_records:
103
+ remark = rec.get("remark", "")
104
+ if remark and len(remark) > 10 and not any(k in remark for k in skip_keywords):
105
+ temp_work.append({
106
+ "record_id": rec["record_id"],
107
+ "content": remark,
108
+ "source_week": rec["week"],
109
+ })
110
+
111
+ user_data = run_lark(["contact", "+get-user", "--as", "user"])
112
+ name = user_data["data"]["user"]["name"]
113
+
114
+ result = {
115
+ "name": name,
116
+ "month": month,
117
+ "records": all_records,
118
+ "temp_work": temp_work,
119
+ "total_records": len(all_records),
120
+ "temp_work_count": len(temp_work),
121
+ }
122
+ print(json.dumps(result, ensure_ascii=False))
123
+
124
+
125
+ if __name__ == "__main__":
126
+ main()
@@ -0,0 +1,38 @@
1
+ """共享配置"""
2
+
3
+ BASE_TOKEN = "HBR3bvERVaooCUsFfxcc0fYLndd"
4
+
5
+ TABLE_IDS = {
6
+ "部门人员": "tblxsJA46bTLlUEE",
7
+ "周计划": "tblN6nUNSwah6j2v",
8
+ "月计划": "tblLb0SsT38HFf8X",
9
+ }
10
+
11
+ DEFAULT_DEPT = "信息技术"
12
+
13
+ # 人员字段映射
14
+ PERSONNEL_FIELDS = {
15
+ "人员": "fldbGCCdNV",
16
+ "所属组": "flddUR4K4c",
17
+ "人名": "fldrI663hx",
18
+ }
19
+
20
+ # 周计划可写字段
21
+ WEEKLY_WRITABLE_FIELDS = [
22
+ "责任人", # fldOavYzSr, user
23
+ "月周", # fldAivwku1, select
24
+ "工作计划内容", # fldjdmxh69, text
25
+ "工时", # fldQXhORVG, number
26
+ "完成情况", # fldF9ebqDi, select
27
+ "备注", # fldu5uAqGU, text
28
+ "计划来源", # fldIvgrltV, select
29
+ "业务组", # fldgFNrrqP, select
30
+ ]
31
+
32
+ # 周计划只读字段(不要写)
33
+ WEEKLY_READONLY_FIELDS = [
34
+ "创建时间", # fldJrGqC4Y, created_at
35
+ "修改人", # fldMZjLGsr, updated_by
36
+ "创建人", # fldnYVOlV6, created_by
37
+ "最后更新时间", # fldzy7XDqO, updated_at
38
+ ]
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ """创建下周周计划记录:迁移未完成项 + 新建用户口述的计划。
3
+
4
+ 用法:
5
+ python3 create_next_week.py \
6
+ --open-id ou_xxx \
7
+ --group 移动 \
8
+ --next-week "7月第5周" \
9
+ --migrated '[{"content":"xxx","hours":8}]' \
10
+ --new '[{"content":"yyy","hours":4}]'
11
+ """
12
+
13
+ import json
14
+ import subprocess
15
+ import sys
16
+ import argparse
17
+ import os
18
+
19
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
20
+ import config
21
+
22
+
23
+ def main():
24
+ parser = argparse.ArgumentParser()
25
+ parser.add_argument("--open-id", required=True)
26
+ parser.add_argument("--group", required=True)
27
+ parser.add_argument("--next-week", required=True)
28
+ parser.add_argument("--migrated", default="[]")
29
+ parser.add_argument("--new", default="[]")
30
+ args = parser.parse_args()
31
+
32
+ try:
33
+ migrated = json.loads(args.migrated)
34
+ new_items = json.loads(args.new)
35
+ except json.JSONDecodeError as e:
36
+ print(json.dumps({"ok": False, "error": "invalid_json", "detail": str(e)}, ensure_ascii=False))
37
+ sys.exit(1)
38
+
39
+ all_items = migrated + new_items
40
+ if not all_items:
41
+ print(json.dumps({"ok": True, "created": 0}, ensure_ascii=False))
42
+ return
43
+
44
+ fields = ["责任人", "月周", "工作计划内容", "工时", "业务组", "计划来源"]
45
+ rows = []
46
+ for item in all_items:
47
+ row = [
48
+ [{"id": args.open_id}],
49
+ args.next_week,
50
+ item.get("content", ""),
51
+ item.get("hours"),
52
+ args.group,
53
+ item.get("source", "新增计划"),
54
+ ]
55
+ rows.append(row)
56
+
57
+ payload = json.dumps({"fields": fields, "rows": rows}, ensure_ascii=False)
58
+
59
+ result = subprocess.run(
60
+ [
61
+ "lark-cli", "base", "+record-batch-create",
62
+ "--base-token", config.BASE_TOKEN,
63
+ "--table-id", config.TABLE_IDS["周计划"],
64
+ "--json", payload,
65
+ "--as", "user",
66
+ ],
67
+ capture_output=True,
68
+ text=True,
69
+ timeout=30,
70
+ )
71
+
72
+ try:
73
+ data = json.loads(result.stdout)
74
+ except json.JSONDecodeError:
75
+ data = json.loads(result.stderr) if result.stderr else {"ok": False}
76
+
77
+ if data.get("ok"):
78
+ print(json.dumps({"ok": True, "created": len(rows)}, ensure_ascii=False))
79
+ else:
80
+ print(json.dumps({"ok": False, "error": data.get("error", {})}, ensure_ascii=False))
81
+ sys.exit(1)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ main()
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """按模板生成月总结 Markdown。
3
+
4
+ 用法: python3 generate_monthly_md.py --data '<json_from_collect>'
5
+ 输出: Markdown 文本到 stdout
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ import argparse
11
+
12
+
13
+ def main():
14
+ parser = argparse.ArgumentParser()
15
+ parser.add_argument("--data", required=True)
16
+ args = parser.parse_args()
17
+
18
+ try:
19
+ data = json.loads(args.data)
20
+ except json.JSONDecodeError as e:
21
+ print("ERROR: invalid JSON: {}".format(e), file=sys.stderr)
22
+ sys.exit(1)
23
+
24
+ name = data.get("name", "")
25
+ month = data.get("month", "")
26
+ records = data.get("records", [])
27
+ temp_work = data.get("temp_work", [])
28
+ year = 2026
29
+
30
+ lines = []
31
+ lines.append("# 信息技术科 {}年{}月工作总结\n".format(year, month))
32
+ lines.append(name + "\n")
33
+
34
+ # 一、工作计划完成情况
35
+ lines.append("## 一、工作计划完成情况(包括计划外完成)\n")
36
+
37
+ table_rows = []
38
+ idx = 0
39
+
40
+ for rec in records:
41
+ idx += 1
42
+ completion = rec.get("completion_status", "")
43
+ remark = rec.get("remark", "")
44
+ follow_up = ""
45
+ if completion == "未完成":
46
+ follow_up = "移至下月继续跟进"
47
+
48
+ table_rows.append("| {} | {} | {} | {} | {} |".format(
49
+ idx, rec.get("content", ""), completion, remark, follow_up
50
+ ))
51
+
52
+ # 临时工作也插入表格
53
+ for tw in temp_work:
54
+ idx += 1
55
+ table_rows.append("| {} | {} | {} | {} | {} |".format(
56
+ idx, tw.get("content", ""), "(临时工作)", tw.get("content", "")[:50], "-"
57
+ ))
58
+
59
+ if table_rows:
60
+ lines.append("| 序号 | 内容 | 完成情况 | 原因说明 | 跟进措施 |")
61
+ lines.append("|------|------|---------|---------|---------|")
62
+ lines.extend(table_rows)
63
+ lines.append("")
64
+
65
+ # 二、重点工作回顾
66
+ lines.append("## 二、重点工作回顾\n")
67
+ completed = [r for r in records if r.get("completion_status") == "完成"]
68
+ if completed:
69
+ lines.append("<!-- AI: 从以下已完成项中提炼1-3个重点工作 -->")
70
+ for r in completed[:5]:
71
+ lines.append("<!-- - {} -->".format(r.get("content", "")))
72
+ lines.append("(待 AI 根据上述材料生成)\n")
73
+
74
+ # 三、存在问题及分析
75
+ lines.append("## 三、存在问题及分析\n")
76
+ undone = [r for r in records if r.get("completion_status") == "未完成"]
77
+ if undone:
78
+ lines.append("<!-- AI: 从以下未完成项汇总共性问题 -->")
79
+ for r in undone:
80
+ lines.append("<!-- - {}: {} -->".format(r.get("content", ""), r.get("remark", "")))
81
+ lines.append("(待 AI 根据上述材料生成)\n")
82
+
83
+ # 四、下月工作重点
84
+ lines.append("## 四、下月工作重点\n")
85
+ lines.append("(待 AI 根据上述材料生成)\n")
86
+
87
+ print("\n".join(lines))
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ """计算当前周和下周的月周值,输出 JSON 到 stdout。
3
+
4
+ 月周归属规则:
5
+ - 以周一为周起始
6
+ - 一周中在某个月天数 >= 4 天 → 归属该月
7
+ - 该月第一周从归属该月的第一个周一开始编号
8
+
9
+ 用法: python3 get_current_week.py [--date YYYY-MM-DD]
10
+ """
11
+
12
+ import json
13
+ import sys
14
+ from datetime import date, timedelta
15
+
16
+
17
+ def get_monday(d):
18
+ """返回日期 d 所在周的周一。"""
19
+ return d - timedelta(days=d.weekday())
20
+
21
+
22
+ def count_month_days(week_monday):
23
+ """统计一周在各个月的天数。"""
24
+ counts = {}
25
+ for i in range(7):
26
+ day = week_monday + timedelta(days=i)
27
+ counts[day.month] = counts.get(day.month, 0) + 1
28
+ return counts
29
+
30
+
31
+ def get_yuezhou(week_monday):
32
+ """根据周一日期,计算对应的月周字符串。"""
33
+ counts = count_month_days(week_monday)
34
+ target_month = max(counts, key=counts.get)
35
+
36
+ if counts[target_month] < 4:
37
+ target_month = (week_monday + timedelta(days=3)).month
38
+
39
+ year = week_monday.year
40
+ first_day = date(year, target_month, 1)
41
+ cursor = get_monday(first_day)
42
+
43
+ week_num = 0
44
+ while cursor <= week_monday + timedelta(days=7):
45
+ cursor_counts = count_month_days(cursor)
46
+ assigned = max(cursor_counts, key=cursor_counts.get)
47
+
48
+ if assigned == target_month:
49
+ week_num += 1
50
+ if cursor == week_monday:
51
+ return "{}月第{}周".format(target_month, week_num)
52
+
53
+ cursor += timedelta(days=7)
54
+
55
+ return None
56
+
57
+
58
+ def main():
59
+ specified_date = None
60
+ for i, arg in enumerate(sys.argv):
61
+ if arg == "--date" and i + 1 < len(sys.argv):
62
+ specified_date = date.fromisoformat(sys.argv[i + 1])
63
+ break
64
+
65
+ today = specified_date or date.today()
66
+ this_monday = get_monday(today)
67
+ next_monday = this_monday + timedelta(days=7)
68
+
69
+ result = {
70
+ "this_week": get_yuezhou(this_monday),
71
+ "next_week": get_yuezhou(next_monday),
72
+ "this_monday": this_monday.isoformat(),
73
+ "next_monday": next_monday.isoformat(),
74
+ }
75
+ print(json.dumps(result, ensure_ascii=False))
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()