skill-forge-hub 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +94 -2
- package/annual-report/SKILL.md +31 -0
- package/annual-report/resources/content-rules.md +40 -0
- package/annual-report/resources/data-collection.md +25 -0
- package/annual-report/resources/date-parsing.md +18 -0
- package/annual-report/resources/disk-and-html.md +28 -0
- package/annual-report/resources/industry-dictionary.md +19 -0
- package/annual-report/resources/usage-guide.md +26 -0
- package/annual-report/user-config.md +37 -0
- package/article-to-html/SKILL.md +239 -0
- package/article-to-html/design-system.md +358 -0
- package/article-to-html/scripts/download_roboto.py +38 -0
- package/article-to-html/scripts/extract_docx.py +68 -0
- package/article-to-html/scripts/inline_fonts.py +32 -0
- package/article-to-html/scripts/install_sourcehan.py +78 -0
- package/cli.js +350 -0
- package/daily-report/SKILL.md +31 -0
- package/daily-report/resources/content-rules.md +82 -0
- package/daily-report/resources/data-collection.md +40 -0
- package/daily-report/resources/date-parsing.md +21 -0
- package/daily-report/resources/disk-and-html.md +14 -0
- package/daily-report/resources/industry-dictionary.md +19 -0
- package/daily-report/resources/usage-guide.md +65 -0
- package/daily-report/user-config.md +36 -0
- package/package.json +33 -4
- package/spec-doc2spec/SKILL.md +64 -0
- package/spec-doc2spec/resources/checklist.md +25 -0
- package/spec-doc2spec/resources/images.md +27 -0
- package/spec-doc2spec/resources/structure.md +27 -0
- package/spec-doc2spec/resources/usage-guide.md +32 -0
- package/spec-spec2doc/SKILL.md +62 -0
- package/spec-spec2doc/resources/checklist.md +24 -0
- package/spec-spec2doc/resources/images-and-template.md +14 -0
- package/spec-spec2doc/resources/pdf-path.md +21 -0
- package/spec-spec2doc/resources/usage-guide.md +32 -0
- package/template/SKILL.md +42 -0
- package/template/resources/README.md +20 -0
- package/weekly-report/SKILL.md +31 -0
- package/weekly-report/resources/content-rules.md +82 -0
- package/weekly-report/resources/data-collection.md +41 -0
- package/weekly-report/resources/date-parsing.md +20 -0
- package/weekly-report/resources/disk-and-html.md +14 -0
- package/weekly-report/resources/industry-dictionary.md +19 -0
- package/weekly-report/resources/usage-guide.md +68 -0
- package/weekly-report/user-config.md +37 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract text, tables, and images from a .docx file.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python3 extract_docx.py input.docx [--media-dir /tmp/docx-extract] > /tmp/extracted.json
|
|
6
|
+
|
|
7
|
+
Output JSON:
|
|
8
|
+
{
|
|
9
|
+
"texts": ["段落1", ...], # 正文段落(不含表格内文本)
|
|
10
|
+
"tables": [["单元格", ...], ...], # 表格内容,行优先(每行一个数组)
|
|
11
|
+
"media": ["word/media/image1.png", ...], # 图片在 zip 内的路径
|
|
12
|
+
"media_dir": "/tmp/docx-extract" or null # 图片落盘目录,供 Read 工具查看
|
|
13
|
+
}
|
|
14
|
+
"""
|
|
15
|
+
import sys
|
|
16
|
+
import os
|
|
17
|
+
import json
|
|
18
|
+
import zipfile
|
|
19
|
+
import xml.etree.ElementTree as ET
|
|
20
|
+
|
|
21
|
+
W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def para_text(p):
|
|
25
|
+
return ''.join(n.text or '' for n in p.iter(W + 't')).strip()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cell_text(tc):
|
|
29
|
+
return ''.join(n.text or '' for n in tc.iter(W + 't')).strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(path):
|
|
33
|
+
z = zipfile.ZipFile(path)
|
|
34
|
+
tree = ET.fromstring(z.read('word/document.xml'))
|
|
35
|
+
body = tree.find(W + 'body')
|
|
36
|
+
|
|
37
|
+
texts, tables = [], []
|
|
38
|
+
for el in body:
|
|
39
|
+
if el.tag == W + 'p':
|
|
40
|
+
t = para_text(el)
|
|
41
|
+
if t:
|
|
42
|
+
texts.append(t)
|
|
43
|
+
elif el.tag == W + 'tbl':
|
|
44
|
+
rows = []
|
|
45
|
+
for tr in el.findall(W + 'tr'):
|
|
46
|
+
rows.append([cell_text(tc) for tc in tr.findall(W + 'tc')])
|
|
47
|
+
tables.append(rows)
|
|
48
|
+
|
|
49
|
+
media = [n for n in z.namelist() if n.startswith('word/media/') and not n.endswith('/')]
|
|
50
|
+
|
|
51
|
+
media_dir = None
|
|
52
|
+
if '--media-dir' in sys.argv:
|
|
53
|
+
media_dir = sys.argv[sys.argv.index('--media-dir') + 1]
|
|
54
|
+
os.makedirs(media_dir, exist_ok=True)
|
|
55
|
+
for n in media:
|
|
56
|
+
with open(os.path.join(media_dir, os.path.basename(n)), 'wb') as f:
|
|
57
|
+
f.write(z.read(n))
|
|
58
|
+
|
|
59
|
+
print(json.dumps({'texts': texts, 'tables': tables, 'media': media,
|
|
60
|
+
'media_dir': media_dir}, ensure_ascii=False, indent=1))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == '__main__':
|
|
64
|
+
if len(sys.argv) < 2:
|
|
65
|
+
print('Usage: python3 extract_docx.py input.docx [--media-dir /path]',
|
|
66
|
+
file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
main(sys.argv[1])
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Embed Roboto woff2 fonts as base64 data URIs to produce a self-contained HTML.
|
|
3
|
+
|
|
4
|
+
Replaces url('assets/fonts/roboto-latin-{400,500,700,900}-normal.woff2')
|
|
5
|
+
with inline base64 data URIs, so the output HTML needs no external files.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 inline_fonts.py input.html output.html /path/to/assets/fonts
|
|
9
|
+
"""
|
|
10
|
+
import sys
|
|
11
|
+
import base64
|
|
12
|
+
|
|
13
|
+
WEIGHTS = ['400', '500', '700', '900']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(src, dst, fontdir):
|
|
17
|
+
html = open(src, encoding='utf-8').read()
|
|
18
|
+
for w in WEIGHTS:
|
|
19
|
+
path = f"{fontdir.rstrip('/')}/roboto-latin-{w}-normal.woff2"
|
|
20
|
+
data = 'data:font/woff2;base64,' + base64.b64encode(open(path, 'rb').read()).decode()
|
|
21
|
+
html = html.replace(f"url('assets/fonts/roboto-latin-{w}-normal.woff2')",
|
|
22
|
+
f"url('{data}')")
|
|
23
|
+
open(dst, 'w', encoding='utf-8').write(html)
|
|
24
|
+
print(f'OK: {dst} ({len(html)} bytes, 4 Roboto weights embedded)')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == '__main__':
|
|
28
|
+
if len(sys.argv) < 4:
|
|
29
|
+
print('Usage: python3 inline_fonts.py input.html output.html /path/to/assets/fonts',
|
|
30
|
+
file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
main(sys.argv[1], sys.argv[2], sys.argv[3])
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Detect and install Source Han Sans SC (思源黑体) on macOS/Windows.
|
|
3
|
+
|
|
4
|
+
Checks common font directories first; if missing, downloads the official
|
|
5
|
+
Adobe Fonts release (2.005R, SIL OFL — free for commercial use), verifies
|
|
6
|
+
SHA-256, and installs the OTF weights to the user font directory.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 install_sourcehan.py
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import shutil
|
|
14
|
+
import zipfile
|
|
15
|
+
import hashlib
|
|
16
|
+
import platform
|
|
17
|
+
import tempfile
|
|
18
|
+
import urllib.request
|
|
19
|
+
|
|
20
|
+
OFFICIAL = 'https://github.com/adobe-fonts/source-han-sans/releases/download/2.005R/09_SourceHanSansSC.zip'
|
|
21
|
+
MIRROR = 'https://ghfast.top/' + OFFICIAL # GitHub 直连超时时使用
|
|
22
|
+
SHA256 = 'ef7364f7ac2564be1ae9c1d74276de2653fe38b73449070398c4fc0b7e032ff1'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def font_dirs():
|
|
26
|
+
if platform.system() == 'Darwin':
|
|
27
|
+
return [os.path.expanduser('~/Library/Fonts'), '/Library/Fonts']
|
|
28
|
+
if platform.system() == 'Windows':
|
|
29
|
+
return [os.path.join(os.environ.get('LOCALAPPDATA', os.path.expanduser('~')),
|
|
30
|
+
'Microsoft', 'Windows', 'Fonts')]
|
|
31
|
+
return [os.path.expanduser('~/.fonts')]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def installed():
|
|
35
|
+
for d in font_dirs():
|
|
36
|
+
if os.path.isdir(d):
|
|
37
|
+
for f in os.listdir(d):
|
|
38
|
+
if f.lower().startswith('sourcehansans'):
|
|
39
|
+
return True
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
if installed():
|
|
45
|
+
print('OK: Source Han Sans SC already installed, nothing to do')
|
|
46
|
+
return
|
|
47
|
+
target = font_dirs()[0]
|
|
48
|
+
os.makedirs(target, exist_ok=True)
|
|
49
|
+
tmp = tempfile.mkdtemp(prefix='sourcehan-')
|
|
50
|
+
zip_path = os.path.join(tmp, 'sourcehan.zip')
|
|
51
|
+
for url in (MIRROR, OFFICIAL):
|
|
52
|
+
try:
|
|
53
|
+
print('downloading:', url)
|
|
54
|
+
urllib.request.urlretrieve(url, zip_path)
|
|
55
|
+
break
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print('download failed:', e)
|
|
58
|
+
else:
|
|
59
|
+
print('ERROR: both sources failed, install manually from', OFFICIAL)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
h = hashlib.sha256(open(zip_path, 'rb').read()).hexdigest()
|
|
62
|
+
if h != SHA256:
|
|
63
|
+
print('ERROR: SHA-256 mismatch:', h, 'expected', SHA256)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
with zipfile.ZipFile(zip_path) as z:
|
|
66
|
+
for n in z.namelist():
|
|
67
|
+
if n.lower().endswith('.otf') and 'license' not in n.lower():
|
|
68
|
+
base = os.path.basename(n)
|
|
69
|
+
with z.open(n) as f, open(os.path.join(target, base), 'wb') as out:
|
|
70
|
+
out.write(f.read())
|
|
71
|
+
print('installed:', base)
|
|
72
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
73
|
+
print('DONE: 思源黑体已安装到', target, '— 重启浏览器后生效')
|
|
74
|
+
print('NOTE: 严禁执行 atsutil databases -removeUser(会破坏字体数据库)')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == '__main__':
|
|
78
|
+
main()
|
package/cli.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* SkillForge CLI —— 技能锻造坊安装器
|
|
5
|
+
*
|
|
6
|
+
* skill-forge ls 列出全部技能(分类 + 名称 + 说明)
|
|
7
|
+
* skill-forge init 交互式挑选技能,装进 AI 助手的技能目录
|
|
8
|
+
*
|
|
9
|
+
* 零依赖,Node >= 14。
|
|
10
|
+
*/
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const os = require("os");
|
|
14
|
+
const readline = require("readline");
|
|
15
|
+
|
|
16
|
+
const PKG_DIR = __dirname;
|
|
17
|
+
const VERSION = require(path.join(PKG_DIR, "package.json")).version;
|
|
18
|
+
|
|
19
|
+
// 分类映射与 README「技能分类」表一致;新增技能时请同步
|
|
20
|
+
const CATEGORIES = [
|
|
21
|
+
{ name: "报表汇演", en: "Reports", skills: ["daily-report", "weekly-report", "annual-report"] },
|
|
22
|
+
{ name: "文档装裱", en: "Documents", skills: ["article-to-html"] },
|
|
23
|
+
{ name: "工程提效", en: "Engineering", skills: ["spec-doc2spec", "spec-spec2doc"] },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// 常见 AI 助手的用户级技能目录(宿主中立,init 时自动探测)
|
|
27
|
+
const KNOWN_SKILL_DIRS = [
|
|
28
|
+
"~/.qoder-cn/skills",
|
|
29
|
+
"~/.claude/skills",
|
|
30
|
+
"~/.cursor/skills",
|
|
31
|
+
"~/.windsurf/skills",
|
|
32
|
+
"~/.trae/skills",
|
|
33
|
+
"~/.codex/skills",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// 包内不视为技能的目录
|
|
37
|
+
const SKIP_DIRS = new Set(["template", "node_modules", ".git", ".github"]);
|
|
38
|
+
|
|
39
|
+
// ---------- 工具 ----------
|
|
40
|
+
|
|
41
|
+
function expandHome(p) {
|
|
42
|
+
if (p === "~") return os.homedir();
|
|
43
|
+
if (p.startsWith("~/")) return path.join(os.homedir(), p.slice(2));
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseFrontmatter(text) {
|
|
48
|
+
const m = text.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
49
|
+
if (!m) return {};
|
|
50
|
+
const fm = {};
|
|
51
|
+
const lines = m[1].split("\n");
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const kv = lines[i].match(/^([A-Za-z-]+):\s*(.*)$/);
|
|
54
|
+
if (!kv) continue;
|
|
55
|
+
const key = kv[1];
|
|
56
|
+
let val = kv[2].trim();
|
|
57
|
+
if (val === ">" || val === "|") {
|
|
58
|
+
// 折叠多行 description
|
|
59
|
+
const parts = [];
|
|
60
|
+
while (i + 1 < lines.length && /^\s{2,}\S/.test(lines[i + 1])) {
|
|
61
|
+
i++;
|
|
62
|
+
parts.push(lines[i].trim());
|
|
63
|
+
}
|
|
64
|
+
val = parts.join(" ");
|
|
65
|
+
}
|
|
66
|
+
fm[key] = val;
|
|
67
|
+
}
|
|
68
|
+
return fm;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scanSkills() {
|
|
72
|
+
const skills = [];
|
|
73
|
+
for (const entry of fs.readdirSync(PKG_DIR)) {
|
|
74
|
+
if (SKIP_DIRS.has(entry) || entry.startsWith(".")) continue;
|
|
75
|
+
const skillDir = path.join(PKG_DIR, entry);
|
|
76
|
+
const skillMd = path.join(skillDir, "SKILL.md");
|
|
77
|
+
if (!fs.statSync(skillDir).isDirectory() || !fs.existsSync(skillMd)) continue;
|
|
78
|
+
const fm = parseFrontmatter(fs.readFileSync(skillMd, "utf8"));
|
|
79
|
+
if (!fm.name || fm.name === "skill-name") continue;
|
|
80
|
+
skills.push({ dir: entry, name: fm.name, description: fm.description || "" });
|
|
81
|
+
}
|
|
82
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
83
|
+
return skills;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function categoryOf(name) {
|
|
87
|
+
for (const c of CATEGORIES) {
|
|
88
|
+
if (c.skills.includes(name)) return c.name;
|
|
89
|
+
}
|
|
90
|
+
return "未分类";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function shortDesc(desc, max = 70) {
|
|
94
|
+
// 按中文句号、英文句号+空格、换行切句;避免 (.docx) 这类点被误切
|
|
95
|
+
const first = desc.split(/\n|。|\.\s/)[0].trim();
|
|
96
|
+
return first.length > max ? first.slice(0, max) + "…" : first;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 升级时保留的用户文件:内置名单(user-config.md)+ 技能目录 .preserve 清单(每行一个文件名,# 注释)
|
|
100
|
+
const BUILTIN_PRESERVE = new Set(["user-config.md"]);
|
|
101
|
+
|
|
102
|
+
function loadPreserveSet(skillDir) {
|
|
103
|
+
const set = new Set(BUILTIN_PRESERVE);
|
|
104
|
+
const pf = path.join(skillDir, ".preserve");
|
|
105
|
+
if (fs.existsSync(pf)) {
|
|
106
|
+
for (const line of fs.readFileSync(pf, "utf8").split(/\r?\n/)) {
|
|
107
|
+
const t = line.trim();
|
|
108
|
+
if (t && !t.startsWith("#")) set.add(t.toLowerCase());
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return set;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function copyDir(src, dest, preserve, hits) {
|
|
115
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
116
|
+
for (const entry of fs.readdirSync(src)) {
|
|
117
|
+
const s = path.join(src, entry);
|
|
118
|
+
const d = path.join(dest, entry);
|
|
119
|
+
if (fs.statSync(s).isDirectory()) copyDir(s, d, preserve, hits);
|
|
120
|
+
else if (preserve.has(entry.toLowerCase()) && fs.existsSync(d)) hits.push(d);
|
|
121
|
+
else fs.copyFileSync(s, d);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ask(question) {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
128
|
+
rl.question(question, (ans) => {
|
|
129
|
+
rl.close();
|
|
130
|
+
resolve(ans.trim());
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------- ls ----------
|
|
136
|
+
|
|
137
|
+
function cmdLs() {
|
|
138
|
+
const skills = scanSkills();
|
|
139
|
+
if (skills.length === 0) {
|
|
140
|
+
console.log("未发现任何技能。");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.log("\n技能锻造坊 SkillForge · 技能清单\n");
|
|
144
|
+
let idx = 0;
|
|
145
|
+
const mapped = new Set(CATEGORIES.reduce((acc, c) => acc.concat(c.skills), []));
|
|
146
|
+
for (const cat of CATEGORIES) {
|
|
147
|
+
const list = cat.skills.map((n) => skills.find((s) => s.name === n)).filter(Boolean);
|
|
148
|
+
if (list.length === 0) continue;
|
|
149
|
+
console.log(`【${cat.name} ${cat.en}】`);
|
|
150
|
+
for (const s of list) {
|
|
151
|
+
idx++;
|
|
152
|
+
console.log(` [${idx}] ${s.name.padEnd(16)} ${shortDesc(s.description)}`);
|
|
153
|
+
}
|
|
154
|
+
console.log("");
|
|
155
|
+
}
|
|
156
|
+
const rest = skills.filter((s) => !mapped.has(s.name));
|
|
157
|
+
if (rest.length > 0) {
|
|
158
|
+
console.log("【未分类】");
|
|
159
|
+
for (const s of rest) {
|
|
160
|
+
idx++;
|
|
161
|
+
console.log(` [${idx}] ${s.name.padEnd(16)} ${shortDesc(s.description)}`);
|
|
162
|
+
}
|
|
163
|
+
console.log("");
|
|
164
|
+
}
|
|
165
|
+
console.log(`共 ${skills.length} 件技能。安装:skill-forge init\n`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------- init ----------
|
|
169
|
+
|
|
170
|
+
function detectTargetDirs() {
|
|
171
|
+
const found = [];
|
|
172
|
+
for (const dir of KNOWN_SKILL_DIRS) {
|
|
173
|
+
const p = expandHome(dir);
|
|
174
|
+
if (fs.existsSync(p)) found.push(p);
|
|
175
|
+
}
|
|
176
|
+
return found;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function selectSkillsInteractive(skills) {
|
|
180
|
+
const numbered = [];
|
|
181
|
+
const mapped = new Set(CATEGORIES.reduce((acc, c) => acc.concat(c.skills), []));
|
|
182
|
+
let text = "\n可选技能(编号多选,逗号分隔;a=全选;q=退出):\n\n";
|
|
183
|
+
for (const cat of CATEGORIES) {
|
|
184
|
+
const list = cat.skills.map((n) => skills.find((s) => s.name === n)).filter(Boolean);
|
|
185
|
+
if (list.length === 0) continue;
|
|
186
|
+
text += `【${cat.name} ${cat.en}】\n`;
|
|
187
|
+
for (const s of list) {
|
|
188
|
+
numbered.push(s);
|
|
189
|
+
text += ` [${numbered.length}] ${s.name.padEnd(16)} ${shortDesc(s.description)}\n`;
|
|
190
|
+
}
|
|
191
|
+
text += "\n";
|
|
192
|
+
}
|
|
193
|
+
const rest = skills.filter((s) => !mapped.has(s.name));
|
|
194
|
+
if (rest.length > 0) {
|
|
195
|
+
text += "【未分类】\n";
|
|
196
|
+
for (const s of rest) {
|
|
197
|
+
numbered.push(s);
|
|
198
|
+
text += ` [${numbered.length}] ${s.name.padEnd(16)} ${shortDesc(s.description)}\n`;
|
|
199
|
+
}
|
|
200
|
+
text += "\n";
|
|
201
|
+
}
|
|
202
|
+
console.log(text);
|
|
203
|
+
const ans = await ask("你的选择:");
|
|
204
|
+
if (/^q$/i.test(ans)) return [];
|
|
205
|
+
if (/^a$/i.test(ans)) return skills;
|
|
206
|
+
const chosen = [];
|
|
207
|
+
for (const part of ans.split(/[,,\s]+/)) {
|
|
208
|
+
if (!part) continue;
|
|
209
|
+
const i = parseInt(part, 10);
|
|
210
|
+
if (!isNaN(i) && i >= 1 && i <= numbered.length && !chosen.includes(numbered[i - 1])) {
|
|
211
|
+
chosen.push(numbered[i - 1]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return chosen;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function cmdInit(argv) {
|
|
218
|
+
const skills = scanSkills();
|
|
219
|
+
if (skills.length === 0) {
|
|
220
|
+
console.log("未发现任何技能。");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 1. 目标目录
|
|
225
|
+
let target = argv.target ? expandHome(argv.target) : null;
|
|
226
|
+
if (!target) {
|
|
227
|
+
const detected = detectTargetDirs();
|
|
228
|
+
if (detected.length === 1) {
|
|
229
|
+
target = detected[0];
|
|
230
|
+
console.log(`检测到技能目录:${target}`);
|
|
231
|
+
} else if (detected.length > 1 && process.stdin.isTTY) {
|
|
232
|
+
console.log("检测到多个技能目录:");
|
|
233
|
+
detected.forEach((d, i) => console.log(` [${i + 1}] ${d}`));
|
|
234
|
+
const ans = await ask("选择目标目录编号(回车取第一个):");
|
|
235
|
+
const i = parseInt(ans, 10) - 1;
|
|
236
|
+
target = detected[isNaN(i) || i < 0 || i >= detected.length ? 0 : i];
|
|
237
|
+
} else if (detected.length > 1) {
|
|
238
|
+
target = detected[0];
|
|
239
|
+
console.log(`检测到多个技能目录,非交互环境取第一个:${target}`);
|
|
240
|
+
} else {
|
|
241
|
+
console.log("未检测到常见 AI 助手的技能目录。");
|
|
242
|
+
console.log("请用 --target 指定,例如:skill-forge init --target ~/.claude/skills");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 2. 选择技能
|
|
248
|
+
let chosen;
|
|
249
|
+
if (argv.skills) {
|
|
250
|
+
const wanted = argv.skills.split(",").map((x) => x.trim());
|
|
251
|
+
chosen = skills.filter((s) => wanted.includes(s.name));
|
|
252
|
+
if (chosen.length === 0) {
|
|
253
|
+
console.log(`错误:--skills 指定的技能不存在。可用技能:${skills.map((s) => s.name).join(", ")}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
} else if (process.stdin.isTTY) {
|
|
257
|
+
chosen = await selectSkillsInteractive(skills);
|
|
258
|
+
if (chosen.length === 0) {
|
|
259
|
+
console.log("未选择任何技能,退出。");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
console.log("错误:非交互环境必须显式指定 --skills,例如:--skills daily-report,weekly-report");
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 3. 确认
|
|
268
|
+
if (!argv.yes) {
|
|
269
|
+
console.log(`\n将安装 ${chosen.length} 件技能到:${target}`);
|
|
270
|
+
chosen.forEach((s) => console.log(` - ${s.name}`));
|
|
271
|
+
if (process.stdin.isTTY) {
|
|
272
|
+
const ans = await ask("确认安装?(y/N):");
|
|
273
|
+
if (!/^y/i.test(ans)) {
|
|
274
|
+
console.log("已取消。");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 4. 复制
|
|
281
|
+
for (const s of chosen) {
|
|
282
|
+
const src = path.join(PKG_DIR, s.dir);
|
|
283
|
+
const dest = path.join(target, s.dir);
|
|
284
|
+
const existed = fs.existsSync(dest);
|
|
285
|
+
const hits = [];
|
|
286
|
+
copyDir(src, dest, loadPreserveSet(src), hits);
|
|
287
|
+
let note = existed ? "已更新" : "已安装";
|
|
288
|
+
if (hits.length > 0) {
|
|
289
|
+
const names = hits.map((h) => path.basename(h)).join("、");
|
|
290
|
+
note += `(保留你的本地修改未覆盖:${names})`;
|
|
291
|
+
}
|
|
292
|
+
console.log(`${note}:${s.name} → ${dest}`);
|
|
293
|
+
}
|
|
294
|
+
console.log(`\n完成。共 ${chosen.length} 件技能。`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------- 入口 ----------
|
|
298
|
+
|
|
299
|
+
function printHelp() {
|
|
300
|
+
console.log(`技能锻造坊 SkillForge v${VERSION}
|
|
301
|
+
|
|
302
|
+
用法:
|
|
303
|
+
skill-forge ls 列出全部技能(分类 + 名称 + 说明)
|
|
304
|
+
skill-forge init 交互式挑选技能并安装到 AI 助手技能目录
|
|
305
|
+
skill-forge init --target <目录> 指定技能目录(默认自动探测常见助手目录)
|
|
306
|
+
skill-forge init --skills a,b 跳过选择,直接安装指定技能(适合脚本)
|
|
307
|
+
skill-forge init --yes 跳过确认,直接安装
|
|
308
|
+
skill-forge --version 显示版本
|
|
309
|
+
|
|
310
|
+
升级保护:目标目录已存在的 user-config.md(及技能 .preserve 清单声明的文件)不会被覆盖,你的本地配置永久保留
|
|
311
|
+
|
|
312
|
+
示例:
|
|
313
|
+
skill-forge ls
|
|
314
|
+
skill-forge init
|
|
315
|
+
skill-forge init --target ~/.claude/skills --skills daily-report,weekly-report`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function parseArgs(args) {
|
|
319
|
+
const argv = { target: null, skills: null, yes: false };
|
|
320
|
+
const rest = [];
|
|
321
|
+
for (let i = 0; i < args.length; i++) {
|
|
322
|
+
const a = args[i];
|
|
323
|
+
if (a === "--target" || a === "-t") argv.target = args[++i];
|
|
324
|
+
else if (a.startsWith("--target=")) argv.target = a.slice(9);
|
|
325
|
+
else if (a === "--skills" || a === "-s") argv.skills = args[++i];
|
|
326
|
+
else if (a.startsWith("--skills=")) argv.skills = a.slice(9);
|
|
327
|
+
else if (a === "--yes" || a === "-y") argv.yes = true;
|
|
328
|
+
else rest.push(a);
|
|
329
|
+
}
|
|
330
|
+
return { argv, rest };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function main() {
|
|
334
|
+
const { argv, rest } = parseArgs(process.argv.slice(2));
|
|
335
|
+
const cmd = rest[0];
|
|
336
|
+
if (!cmd) return printHelp();
|
|
337
|
+
if (cmd === "ls" || cmd === "list") return cmdLs();
|
|
338
|
+
if (cmd === "init" || cmd === "install") return cmdInit(argv);
|
|
339
|
+
if (cmd === "--version" || cmd === "-v" || cmd === "version") {
|
|
340
|
+
console.log(VERSION);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") return printHelp();
|
|
344
|
+
printHelp();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
main().catch((e) => {
|
|
348
|
+
console.error("出错了:", e.message);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: daily-report
|
|
3
|
+
description: 生成工作日报(默认完成任务/明日计划/风险阻塞点三段结构,中文顿号序号),适配各行业(内置 12 行业职业词典,可指定角色身份)。数据来源灵活:各仓库 git 提交聚合、纯文字、图片或文件整理,无 git 也适用。支持自定义模板(可永久或临时指定)。支持自然语言指定日期(今天/昨天/上周三/10月20号等)。当用户请求写日报、今日日报、昨天的日报、生成日报、/daily-report,或询问日报用法(日报怎么用/日报使用说明/日报有哪些使用方式)时使用。
|
|
4
|
+
allowed-tools: Bash, Read, Write
|
|
5
|
+
disable-model-invocation: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 日报生成
|
|
9
|
+
|
|
10
|
+
聚合各仓库 git 提交 + 本次会话口述内容生成三段式日报(今日完成任务/明日计划/风险阻塞点),对话输出 md 正文 + 落盘 md/txt/html/content-png 并自动打开 html。
|
|
11
|
+
|
|
12
|
+
## 生成流程
|
|
13
|
+
|
|
14
|
+
0. **读用户配置**(DISK/FORMATS/DIR/OPEN_HTML/TEMPLATE/ROLE/CONFIRM、自定义模板)→ 先 Read 同目录 `user-config.md`
|
|
15
|
+
1. **解析日期**(自然语言 → 目标日期 YYYY-MM-DD)→ 读 `resources/date-parsing.md`
|
|
16
|
+
2. **收集数据**(git 提交 + 会话口述 + 口头补充,合并去重)→ 读 `resources/data-collection.md`
|
|
17
|
+
3. **内容归类与润色**(聚合来源 / 补充任务 / 纯文字模式 / 输出格式 / 写作风格)→ 读 `resources/content-rules.md`
|
|
18
|
+
4. **行业词条**(话术含职业词时按词典取词)→ 读 `resources/industry-dictionary.md`
|
|
19
|
+
5. **落盘与截图**(md/txt/html/content-png + 自动打开 html)→ 读 `resources/disk-and-html.md`
|
|
20
|
+
6. **使用咨询**(用户只问「日报怎么用」等用法、未要求生成)→ 读 `resources/usage-guide.md`,将其内容原样输出(首行标题改为「日报生成器使用指南」),不收集 git、不生成报告、不落盘、不开浏览器
|
|
21
|
+
|
|
22
|
+
## ⚙️ 配置与自定义模板
|
|
23
|
+
|
|
24
|
+
**执行本 skill 前必须先 Read 同目录 `user-config.md`**:默认配置(DISK/FORMATS/DIR/OPEN_HTML/TEMPLATE/ROLE/CONFIRM)与自定义模板都在该文件中,以它为准。升级技能时 user-config.md 不会被覆盖,你的修改永久保留。
|
|
25
|
+
|
|
26
|
+
## 规则速览
|
|
27
|
+
|
|
28
|
+
- 完成任务 3-6 点、每点 20-40 字;序号用中文顿号(1、2、3)
|
|
29
|
+
- 三段兜底写「暂无」;计划点带进度百分比与预估用时;风险阻塞点三环(问题+影响+建议)
|
|
30
|
+
- 用时单位 XH(小时),估算值注明「约」
|
|
31
|
+
- 完整规则(聚合来源 / 纯文字模式 / 写作风格约束 / 使用提示)见 `resources/content-rules.md`
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 内容规则
|
|
2
|
+
|
|
3
|
+
## 输出内容与数据来源
|
|
4
|
+
- 三段结构:今日完成任务(3-6 点)/ 明日计划 / 风险阻塞点
|
|
5
|
+
- 任务点格式:1、任务描述(量化数据:文件数/行数/提交次数)- 进度XX% - 用时XH
|
|
6
|
+
- 任务来源(默认):各仓库当日 git 提交自动聚合 + 本次会话中用户已提及/口述的工作内容 + 用户口头补充,全部合并去重;仅当用户明确说"只整理这段文字/只按这段"时才忽略 git 与会话上下文、只用指定文字(见「纯文字整理模式」)
|
|
7
|
+
- 「明日计划」取材双通道:①用户口述/文字补充的计划(优先并入);②一次生成连续多天日报时(本次批量生成中后面日期的日报,或 reports 目录中已存在的后续日报),从后一天日报的「今日完成任务」中抽取任务作为当天「明日计划」(抽取数量灵活、不设死上限:后续完成任务多且连续性强时可多抽至 4-5 条,少则抽 1-2 条,与后续日报内容形成呼应即可),描述改写为计划语气(完成态改进行态:如「完成 XX 重构」→「推进 XX 重构」「继续 XX 开发」,去掉量化数据/进度/用时);批量多天时优先保证跨天连续性:抽取时尽量让当天「明日计划」与后一天「今日完成任务」形成「计划→完成」的同主题承接链(如 11.05 计划「推进底部导航重构」→ 11.06 完成「完成底部导航重构」,同一条工作线前后呼应,跨天任务尽量串成连续的工作链);批量中最后一天无后续日报时,该段按口述或「暂无」处理
|
|
8
|
+
- 量化数据取自 git --stat(文件数、增删行数);用时为合理估算(git 不记录耗时,估算值注明"约",可口头修正)
|
|
9
|
+
- 数据收集范围(按当前系统自动选择,另一系统的仓库自动跳过):macOS = NearCast / nearcast-server / nearcast-go / nearcast-admin / SmartCanteenConsumerMachineApp / TrustPay 共 6 个仓库;Windows = srf-app / srf-manage / srf-operation / srf-service / srf-web-user / srf-web-operation / srf-web-wx / srf-web-h5-teacher / srf-web-uop / Spec-Core-Smart-Canteen 共 10 个仓库;无当日提交的仓库不写入
|
|
10
|
+
- HTML 版为参考图式深蓝面板卡片美化样式(实心深蓝卡片 + 细密水平条纹 + 上下边缘高光线 + 左缘节色条,保留 ask 同款发光动效):顶部「一键复制」按钮(复制纯文本版)+ TL;DR 摘要行、「今日完成任务」标题右侧带目标日期徽章 + 完成任务节内当日节奏条(上午/下午/晚上时段条,hover 显示时段提交明细)+ 节奏条下量化指标行(提交次数/涉及文件数/代码行数)+ 进度条终点圆点与流星扫描填充 + 液态玻璃卡片(@supports 兜底)+ CRT 扫描光带 + 全页 3D 微倾斜(±0.75°)、页脚署名 GENERATED BY SPECCORE TEAM(日期动态跟随目标日期),并随 FORMATS 默认落一张内容区截图 content-png(卡片整体 + 四周留白 + 日期徽章)
|
|
11
|
+
|
|
12
|
+
## 用户补充任务
|
|
13
|
+
|
|
14
|
+
用户调用时可能口头追加任务(如"再补充一下:上午改了角标接口,下午跟测试联调了"),按以下规则处理:
|
|
15
|
+
|
|
16
|
+
- **触发**:话术含"补充/加上/还有/另外/以及/对了"等追加词,或直接口述任务内容(说"整理成日报/只整理这段文字/只按这段/不用 git"时走「纯文字整理模式」,不是追加)
|
|
17
|
+
- **分段**:补充内容带分段词时按段并入——"明日计划/计划:xxx"→ 明日计划段;"风险/阻塞:xxx"→ 风险阻塞点段(说"风险暂无/无风险"→ 风险段写「暂无」);不带分段词默认并入"今日完成任务"
|
|
18
|
+
- **拆条**:按语义主题拆分,一个主题一条;拆分信号:时间词(上午/下午/晚上)、追加连接词、分号顿号列举、数字列举(1、2、3)
|
|
19
|
+
- **润色**:口语整理为规范任务描述(动词开头、说清对象与结果);用户未给量化数据时,优先从 git 提交补文件数/行数,无记录则省略量化、只写进度与用时(估算值注明"约")
|
|
20
|
+
- **去重**:与 git 聚合任务合并时,同一件事只保留一条
|
|
21
|
+
- **格式**:与 git 聚合条目同格式,接在"今日完成任务"下继续编号
|
|
22
|
+
|
|
23
|
+
## 纯文字整理模式(用户贴一段文字,无 git 提交)
|
|
24
|
+
|
|
25
|
+
用户调用时直接贴一段文字并说"整理成日报/根据这段生成日报"(可能不是开发岗位,没有任何 git 提交),按以下规则处理:
|
|
26
|
+
|
|
27
|
+
- **触发**:话术含"整理/根据这段/以下内容/帮我梳理"等 + 附了一段多句文字/图片/文件,或明说"只整理这段文字/只按这段/不用 git/别收集 git"(与"写日报 + 补充一下"的区别:文字/图片/文件是唯一主源、跳过 git 与会话上下文,不是追加)
|
|
28
|
+
- **数据来源**:用户文字/图片/文件为主源,**跳过 git 收集**(不跑 git 命令)且忽略本次会话其它工作内容;图片用 Read 视觉理解提取图中文字信息,md/txt 直接读文本,docx/pdf 优先尝试 pandoc 转文本(无 pandoc 则请用户粘贴文字);用户是开发且内容提到代码相关工作时,可仍收集 git 用于补量化数据
|
|
29
|
+
- **三段归类**(用户文字没按段写明时用信号词归类):
|
|
30
|
+
- 含"明天/明日/接下来/计划/准备/打算/将要/待办" → 明日计划
|
|
31
|
+
- 含"风险/阻塞/问题/卡住/困难/待定/依赖/遗留" → 风险阻塞点
|
|
32
|
+
- 其余 → 今日完成任务;全段无任何信号词 → 全部归入今日完成任务
|
|
33
|
+
- 用户已明确分段("今日完成:A;明日计划:B;风险:C")→ 按用户分段,不猜
|
|
34
|
+
- **拆条**:按语义主题拆分,一个主题一条;拆分信号:时间词(上午/下午/晚上)、连接词(还有/另外/以及)、分号顿号列举、数字列举(1、2、3)、换行/段落边界
|
|
35
|
+
- **润色**:口语整理为规范任务描述(动词开头、说清对象与结果),每点 20-40 字
|
|
36
|
+
- **量化数据**:无 git 提交 → 不写文件数/行数/提交次数;格式退化为「1、任务描述 - 进度XX% - 用时XH」
|
|
37
|
+
- **进度与用时**:文字没给进度时,完成任务默认 100%、计划默认 0%;用时/预估用时仅在用户文字给出时才写,没给就省略(不瞎编估算值)
|
|
38
|
+
- **风险阻塞点**:只列描述
|
|
39
|
+
- **日期/落盘/HTML**:与常规流程一致(默认今天、md/txt/html 三文件 + 内容区截图落盘、html 自动打开)
|
|
40
|
+
|
|
41
|
+
## 输出格式
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
# 今日日报 - YYYYMMDD
|
|
45
|
+
|
|
46
|
+
> 生成时间:YYYY-MM-DD HH:MM:SS
|
|
47
|
+
|
|
48
|
+
## 今日完成任务
|
|
49
|
+
1、任务描述(量化数据:文件数/行数/提交次数)- 进度XX% - 用时XH
|
|
50
|
+
|
|
51
|
+
## 明日计划
|
|
52
|
+
1、任务描述 - 进度XX% - 预估用时XH
|
|
53
|
+
|
|
54
|
+
## 风险阻塞点
|
|
55
|
+
- 阻塞描述
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 规则
|
|
59
|
+
|
|
60
|
+
- 完成任务 3-6 点,每点 20-40 字;序号用中文顿号(1、2、)
|
|
61
|
+
- 聚合后任务数超上限(6 点)时,同类小任务合并压缩到上限内;用户说「精简点/太长了」→ 条目数取下限、每点压缩到 20 字左右;说「详细点」→ 条目数取上限、每点写足 40 字
|
|
62
|
+
- 三段兜底:完成任务默认进度 100%;完成任务/明日计划/风险阻塞点无任何内容时该段写「暂无」(默认聚合时明日计划无 git 来源,用户没给计划就写「暂无」)
|
|
63
|
+
- 计划每点带进度百分比与预估用时
|
|
64
|
+
- 风险阻塞点三环结构:问题+影响+建议,不写进度用时
|
|
65
|
+
- 用时单位:XH(小时),估算值注明"约"
|
|
66
|
+
- 文字风格约束:①明日计划用计划态陈述句,动词取「推进/启动/开展/跟进/梳理/筹备/验证/排查/联调」等计划态词,禁用完成态词(完成/搞定/已实现/做完),可带「计划/预计/拟」字样(不必每条都有),句式「动词+对象+目标范围」(如「推进底部导航重构,完成主流程交互调整」),禁祈使句(「去做 xx」)与口语(「把 xx 弄一下/搞搞」);②完成任务用动词开头的完成态陈述句(完成/修复/优化/重构/开发/实现/接入/升级/排查/联调/上线),说清对象与结果(如「完成底部导航重构」);③风险阻塞点三环结构:问题+影响+建议(问题客观陈述、影响说明后果、建议给出可执行对策),不写抱怨与情绪话;④全文通用:不用第一人称「我」、不用口语语气词(啦/啊/吧/哈/哦/呢)、不用 emoji、不用口语形容词(差不多/还行/基本上/搞不定/一堆);用户口述/补充的口语内容一律先润色为规范书面语再写入(「上午改了角标接口」→「完成角标接口改造」),量化数据(文件数/行数/进度/用时)格式不变
|
|
67
|
+
- 报告标题下输出生成时间,精确到时分秒(YYYY-MM-DD HH:MM:SS)
|
|
68
|
+
- 生成报告时,对话中先输出简要「使用提示」(几条速览 + 完整说明引用)再输出报告正文,固定条目:
|
|
69
|
+
- 默认生成今天;「昨天的日报 / 上周三的日报 / 10月20号的日报」指定日期
|
|
70
|
+
- 「补充一下:xxx」追加任务(「明日计划:/风险:」分段词自动归段);贴文字/发图片/给文件「整理成日报」默认直接生成,说「先看看效果」→ 确认后再落盘
|
|
71
|
+
- 默认聚合 git 提交 + 本次会话已提的工作内容;说「只整理这段文字」→ 仅用该文字
|
|
72
|
+
- 「写到桌面 / 不落盘 / 只要 md / 只要截图 / 这次不截图 / 别开浏览器 / 保留之前的」临时控制落盘;「这次按 xxx 模板来」临时换模板
|
|
73
|
+
- 生成完成后在对话中告知本次落盘的文件完整路径清单(目录 + 各文件名,Windows 用反斜杠)
|
|
74
|
+
- 默认配置长期生效:改 skill 文件顶部「默认配置」节(DISK/FORMATS/DIR/OPEN_HTML/TEMPLATE/CONFIRM)
|
|
75
|
+
- 📖 完整使用说明:问「日报怎么用」
|
|
76
|
+
- 默认聚合来源:git 提交 + 本次会话中用户已提及/口述的工作内容全部合并去重;仅当用户明确说"只整理这段文字/只按这段"时,才只用指定文字、忽略其它来源
|
|
77
|
+
- 目录名 = 目标日期(解析出的报告日期),不是生成时刻:整理"昨天的日报"时目录用昨天日期、git 收集范围也是昨天;标题中的 YYYYMMDD 同为该目标日期
|
|
78
|
+
- 纯文字整理模式:用户贴一段文字/发图片/给文件说"整理成日报/只按这段"时跳过 git 收集与会话上下文,内容为主源(图片 Read 视觉提取、md/txt 直接读、docx/pdf 优先 pandoc 转文本);按信号词自动三段归类(用户已分段则按用户分段);无量化数据不写文件数/行数,进度默认完成 100%/计划 0%,用时用户没给就省略;CONFIRM: never 默认直接生成落盘不确认,CONFIRM: text/always 或用户说"先看看效果"时先出整理效果、用户说"可以/确认/落盘"后再落盘,说"改一下 xxx"按意见修改后重新确认,说"不用确认/直接落盘"跳过确认(CONFIRM 配置控制,临时口头要求优先)
|
|
79
|
+
- 模板优先级:用户临时口头指定的模板 > 顶部 TEMPLATE 配置(custom 用「📝 自定义模板」节结构,default 用内置三段式);自定义模板只改段名/段数/条目格式,其余规范(中文顿号序号、生成时间、落盘、HTML 样式)不变
|
|
80
|
+
- 日期优先按自然语言解析(如"昨天的日报"、"上周三的日报");显式参数 `/daily-report [YYYYMMDD] [md|txt|html]` 等价于自然语言,日期缺省为今天
|
|
81
|
+
- 跨系统兼容:执行时先检测当前系统(macOS/Windows),所有路径、命令、浏览器均按系统分支取值(DIR auto→DIR_MAC/DIR_WIN、仓库表对应路径列、Chrome/Edge 路径、open/Start-Process、桌面/回退目录);禁止假设单一系统
|
|
82
|
+
- 落盘行为读取顶部「默认配置」(DISK/FORMATS/DIR/OPEN_HTML/TEMPLATE/CONFIRM);优先级:用户临时口头要求(不落盘/写到桌面/只要 md/只要内容区截图/只要截图/这次不截图/别开浏览器/临时换模板/先看看效果/不用确认等,仅当次生效)> 顶部默认配置;DIR(按系统解析)目录/盘符不存在时回退用户主目录 reports(macOS=~/reports、Windows=%USERPROFILE%\reports);同日期重复生成覆盖原文件,用户说"保留之前的"则时间戳另存
|