@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.
- package/cli.mjs +175 -0
- package/package.json +21 -0
- package/scripts/collect_monthly_data.py +126 -0
- package/scripts/config.py +38 -0
- package/scripts/create_next_week.py +85 -0
- package/scripts/generate_monthly_md.py +91 -0
- package/scripts/get_current_week.py +79 -0
- package/scripts/get_user_info.py +85 -0
- package/scripts/query_weekly_records.py +90 -0
- package/scripts/update_completion.py +69 -0
- package/skills/sab-monthly-summary/SKILL.md +85 -0
- package/skills/sab-monthly-summary/scripts/collect_monthly_data.py +126 -0
- package/skills/sab-monthly-summary/scripts/config.py +41 -0
- package/skills/sab-monthly-summary/scripts/create_next_week.py +85 -0
- package/skills/sab-monthly-summary/scripts/generate_monthly_md.py +91 -0
- package/skills/sab-monthly-summary/scripts/get_current_week.py +79 -0
- package/skills/sab-monthly-summary/scripts/get_user_info.py +85 -0
- package/skills/sab-monthly-summary/scripts/query_weekly_records.py +90 -0
- package/skills/sab-monthly-summary/scripts/update_completion.py +69 -0
- package/skills/sab-weekly-plan/SKILL.md +109 -0
- package/skills/sab-weekly-plan/scripts/collect_monthly_data.py +126 -0
- package/skills/sab-weekly-plan/scripts/config.py +41 -0
- package/skills/sab-weekly-plan/scripts/create_next_week.py +85 -0
- package/skills/sab-weekly-plan/scripts/generate_monthly_md.py +91 -0
- package/skills/sab-weekly-plan/scripts/get_current_week.py +79 -0
- package/skills/sab-weekly-plan/scripts/get_user_info.py +85 -0
- package/skills/sab-weekly-plan/scripts/query_weekly_records.py +90 -0
- 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()
|