dsh-plugin-daily-report 1.0.1 → 1.0.2
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/README.md +4 -1
- package/client.js +194 -23
- package/index.js +34 -13
- package/lib/collect.js +1 -0
- package/lib/formats.js +190 -0
- package/lib/report.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- **设置持久化**:Git 仓库、文档、AI 分析、仅统计我的提交和日期范围都会保存到 `settings.json`,下次打开页面保持一致;修改后会自动保存,也可以点击右上角「保存设置」立即保存。
|
|
18
18
|
- **生成进度**:生成任务异步执行,页面实时显示当前阶段、百分比和 AI 已接收字数;AI 分析阶段使用不确定进度条,避免长时间无反馈。
|
|
19
19
|
- **XLSX 输出**:工作簿包含【日报汇总】【AI分析】(启用时)【Git明细】【文档明细】【数据源】【说明】工作表,带表头样式、冻结窗格、筛选和列宽。日报汇总冻结日期/星期/日类型三列,Git 明细冻结日期/仓库/分支,左右滚动时保持可见。
|
|
20
|
+
- **多格式导出**:右上角「生成日报」支持下拉选择 XLSX、Markdown、CSV、HTML,默认 XLSX;所选格式会保存到 `settings.json`,预览页的导出按钮会跟随格式变化。
|
|
20
21
|
- **独立页面**:左侧全局菜单新增「日报汇总」入口,点击后在中栏打开独立页面,不依附于任何会话。
|
|
21
22
|
- **两种入口**:左侧「日报汇总」页面;同时注册 `daily_report` Agent 工具,便于在对话中直接生成。
|
|
22
23
|
|
|
@@ -54,7 +55,7 @@ plugin_manager
|
|
|
54
55
|
1. 点击左侧菜单底部的「日报汇总」图标,在中栏打开独立页面。
|
|
55
56
|
2. 添加 Git 仓库:填写远程地址或本地路径,分支留空时使用默认分支;也可以点击「扫描目录」,指定一个包含多个仓库的目录,批量识别其中的仓库和当前分支。
|
|
56
57
|
3. 添加本地文档:填写文件或目录的绝对路径。
|
|
57
|
-
4. 选择日期范围(默认当月),按需勾选「启用 AI
|
|
58
|
+
4. 选择日期范围(默认当月),按需勾选「启用 AI 分析」(默认已勾选),在右上角「生成日报」下拉中选择格式;选择格式会立即生成,直接点击主按钮则按当前格式生成。
|
|
58
59
|
5. 生成完成后先在预览区检查:编辑总体/每日 AI 摘要、对某天重新分析、补充手动工作项,确认后点击「导出 XLSX」下载。
|
|
59
60
|
|
|
60
61
|
Git 仓库、文档、AI 分析开关、作者过滤和日期范围会自动保存在 `$DSH_HOME/daily-report/settings.json`,跨会话与重启有效。
|
|
@@ -70,6 +71,7 @@ Git 仓库、文档、AI 分析开关、作者过滤和日期范围会自动保
|
|
|
70
71
|
| `docs` | 可选,字符串数组;省略时使用已保存配置 |
|
|
71
72
|
| `outputPath` | 可选,输出路径;省略时写入当前会话工作目录 |
|
|
72
73
|
| `ai` | 可选,布尔值;`true` 时调用当前默认模型分析提交 diff 与文档摘要 |
|
|
74
|
+
| `format` | 可选,`xlsx` / `markdown` / `csv` / `html`,默认 `xlsx` |
|
|
73
75
|
| `locale` | `zh` / `en` |
|
|
74
76
|
|
|
75
77
|
## 结构
|
|
@@ -81,6 +83,7 @@ Git 仓库、文档、AI 分析开关、作者过滤和日期范围会自动保
|
|
|
81
83
|
| `lib/collect.js` | Git 日志、作者过滤、文档扫描、按天归集、仓库目录扫描 |
|
|
82
84
|
| `lib/ai.js` | AI 提示词、按天分析、增量缓存、单日重新分析 |
|
|
83
85
|
| `lib/report.js` | XLSX 工作表内容与样式 |
|
|
86
|
+
| `lib/formats.js` | Markdown、CSV、HTML 日报渲染 |
|
|
84
87
|
| `lib/xlsx.js` | 无第三方依赖的 XLSX(ZIP + SpreadsheetML)写入器 |
|
|
85
88
|
| `cordis.patch.yml` | 插入 `daily-report` 插件行 |
|
|
86
89
|
| `locale/*.json` | 插件管理器展示文案 |
|
package/client.js
CHANGED
|
@@ -63,7 +63,12 @@ window.__ModuleLoader__.load({
|
|
|
63
63
|
"progress.hint": "AI 分析需要调用模型,耗时通常比普通汇总更长;请保持页面打开。",
|
|
64
64
|
optional: "可选",
|
|
65
65
|
remove: "移除",
|
|
66
|
-
generate: "
|
|
66
|
+
generate: "生成日报",
|
|
67
|
+
"format.label": "格式",
|
|
68
|
+
"format.xlsx": "XLSX 工作簿",
|
|
69
|
+
"format.markdown": "Markdown 文档",
|
|
70
|
+
"format.csv": "CSV 表格",
|
|
71
|
+
"format.html": "HTML 网页",
|
|
67
72
|
generating: "正在收集并生成…",
|
|
68
73
|
"save.saving": "正在保存设置…",
|
|
69
74
|
"save.savingShort": "正在保存…",
|
|
@@ -85,7 +90,7 @@ window.__ModuleLoader__.load({
|
|
|
85
90
|
"result.ai.failed": "AI 分析未完成",
|
|
86
91
|
"preview.title": "生成结果预览",
|
|
87
92
|
"preview.desc": "先检查并编辑每日 AI 摘要、补充非代码工作,再导出 XLSX。",
|
|
88
|
-
"preview.export": "导出
|
|
93
|
+
"preview.export": "导出",
|
|
89
94
|
"preview.expandAll": "全部展开",
|
|
90
95
|
"preview.collapseAll": "全部收起",
|
|
91
96
|
"preview.exporting": "正在导出…",
|
|
@@ -171,7 +176,12 @@ window.__ModuleLoader__.load({
|
|
|
171
176
|
"progress.hint": "AI analysis calls a model, so it usually takes longer than a plain digest. Keep this page open.",
|
|
172
177
|
optional: "optional",
|
|
173
178
|
remove: "Remove",
|
|
174
|
-
generate: "Generate
|
|
179
|
+
generate: "Generate report",
|
|
180
|
+
"format.label": "Format",
|
|
181
|
+
"format.xlsx": "XLSX workbook",
|
|
182
|
+
"format.markdown": "Markdown document",
|
|
183
|
+
"format.csv": "CSV table",
|
|
184
|
+
"format.html": "HTML page",
|
|
175
185
|
generating: "Collecting and generating…",
|
|
176
186
|
"save.saving": "Saving settings…",
|
|
177
187
|
"save.savingShort": "Saving…",
|
|
@@ -193,7 +203,7 @@ window.__ModuleLoader__.load({
|
|
|
193
203
|
"result.ai.failed": "AI analysis did not complete",
|
|
194
204
|
"preview.title": "Review before export",
|
|
195
205
|
"preview.desc": "Check and edit each day's AI summary, add non-code work, then export the XLSX.",
|
|
196
|
-
"preview.export": "Export
|
|
206
|
+
"preview.export": "Export",
|
|
197
207
|
"preview.expandAll": "Expand all",
|
|
198
208
|
"preview.collapseAll": "Collapse all",
|
|
199
209
|
"preview.exporting": "Exporting…",
|
|
@@ -289,7 +299,7 @@ window.__ModuleLoader__.load({
|
|
|
289
299
|
return { git, docs, mineOnly: source.mineOnly !== false };
|
|
290
300
|
}
|
|
291
301
|
|
|
292
|
-
function cleanSettings(settings, aiEnabled, range) {
|
|
302
|
+
function cleanSettings(settings, aiEnabled, range, format) {
|
|
293
303
|
return {
|
|
294
304
|
git: settings.git
|
|
295
305
|
.map((item) => ({
|
|
@@ -306,6 +316,7 @@ window.__ModuleLoader__.load({
|
|
|
306
316
|
.filter((item) => item.location.length > 0),
|
|
307
317
|
aiEnabled: aiEnabled !== false,
|
|
308
318
|
mineOnly: settings.mineOnly !== false,
|
|
319
|
+
format: FORMAT_OPTIONS.includes(format) ? format : "xlsx",
|
|
309
320
|
range:
|
|
310
321
|
range && typeof range.start === "string" && typeof range.end === "string"
|
|
311
322
|
? { start: range.start, end: range.end }
|
|
@@ -339,6 +350,23 @@ window.__ModuleLoader__.load({
|
|
|
339
350
|
}
|
|
340
351
|
}
|
|
341
352
|
|
|
353
|
+
const FORMAT_OPTIONS = ["xlsx", "markdown", "csv", "html"];
|
|
354
|
+
|
|
355
|
+
function formatLabel(format, t) {
|
|
356
|
+
if (format === "markdown") return t("format.markdown", "Markdown");
|
|
357
|
+
if (format === "csv") return t("format.csv", "CSV");
|
|
358
|
+
if (format === "html") return t("format.html", "HTML");
|
|
359
|
+
return t("format.xlsx", "XLSX");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function formatExtension(format) {
|
|
363
|
+
return format === "markdown" ? "md" : format;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function formatShortLabel(format) {
|
|
367
|
+
return format === "markdown" ? "Markdown" : format.toUpperCase();
|
|
368
|
+
}
|
|
369
|
+
|
|
342
370
|
function useTranslate(ctx) {
|
|
343
371
|
const [, force] = React.useState(0);
|
|
344
372
|
React.useEffect(() => {
|
|
@@ -359,8 +387,20 @@ window.__ModuleLoader__.load({
|
|
|
359
387
|
".dsdr-hero-copy { flex: 1; min-width: 0; }",
|
|
360
388
|
".dsdr-hero-action { display: flex; flex: none; flex-direction: column; align-items: flex-end; gap: 5px; margin-left: auto; }",
|
|
361
389
|
".dsdr-hero-buttons { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; }",
|
|
362
|
-
".dsdr-save-btn { min-height: 38px; }",
|
|
363
|
-
".dsdr-
|
|
390
|
+
".dsdr-save-btn { min-height: 38px; background: #3b4046; border-color: #3b4046; color: #ffffff; font-weight: 600; }",
|
|
391
|
+
".dsdr-save-btn:hover { background: #292d31; border-color: #292d31; }",
|
|
392
|
+
".dsdr-save-btn:disabled { background: #666b70; border-color: #666b70; color: #ffffff; opacity: .8; }",
|
|
393
|
+
".dsdr-generate { min-height: 38px; padding: 9px 16px; font-size: 13px; white-space: nowrap; }",
|
|
394
|
+
".dsdr-btn-with-icon { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }",
|
|
395
|
+
".dsdr-split { position: relative; display: inline-flex; align-items: stretch; }",
|
|
396
|
+
".dsdr-split .dsdr-generate { border-radius: 8px 0 0 8px; }",
|
|
397
|
+
".dsdr-split-toggle { min-height: 38px; padding: 0 9px; border-radius: 0 8px 8px 0; border-left: 1px solid rgba(255, 255, 255, .32); }",
|
|
398
|
+
".dsdr-menu { position: absolute; top: calc(100% + 6px); right: 0; z-index: 30; min-width: 190px; padding: 5px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-overlay); box-shadow: 0 10px 28px rgba(0, 0, 0, .2); }",
|
|
399
|
+
".dsdr-menu-title { padding: 5px 8px 4px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
|
|
400
|
+
".dsdr-menu-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; padding: 7px 8px; border: 0; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 18px; text-align: left; cursor: pointer; }",
|
|
401
|
+
".dsdr-menu-item:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
402
|
+
".dsdr-menu-item.is-active { background: var(--dsw-alias-bg-layer-2); font-weight: 600; }",
|
|
403
|
+
".dsdr-menu-check { width: 14px; color: #146b5a; text-align: center; }",
|
|
364
404
|
".dsdr-save { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
|
|
365
405
|
".dsdr-mark { flex: none; width: 42px; height: 42px; border-radius: 11px; display: grid; place-items: center; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); }",
|
|
366
406
|
".dsdr-title { margin: 0; font-size: 22px; line-height: 30px; font-weight: 600; }",
|
|
@@ -380,9 +420,9 @@ window.__ModuleLoader__.load({
|
|
|
380
420
|
".dsdr-btn:hover { background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-brand-primary); }",
|
|
381
421
|
".dsdr-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
|
|
382
422
|
".dsdr-btn:disabled { opacity: .7; cursor: not-allowed; }",
|
|
383
|
-
".dsdr-btn-primary { background: #
|
|
384
|
-
".dsdr-btn-primary:hover { background: #
|
|
385
|
-
".dsdr-btn-primary:disabled { background: #
|
|
423
|
+
".dsdr-btn-primary { background: #146b5a; border-color: #146b5a; color: #ffffff; font-weight: 600; box-shadow: 0 2px 6px rgba(0, 0, 0, .18); }",
|
|
424
|
+
".dsdr-btn-primary:hover { background: #0f5547; border-color: #0f5547; opacity: 1; }",
|
|
425
|
+
".dsdr-btn-primary:disabled { background: #6b8a82; border-color: #6b8a82; color: #ffffff; opacity: .8; }",
|
|
386
426
|
".dsdr-btn-danger { border-color: transparent; background: transparent; color: var(--dsw-alias-state-error-primary); font-weight: 600; }",
|
|
387
427
|
".dsdr-btn-danger:hover { background: var(--dsw-alias-bg-layer-2); border-color: transparent; }",
|
|
388
428
|
".dsdr-check { display: inline-flex; align-items: center; gap: 7px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 20px; cursor: pointer; white-space: nowrap; }",
|
|
@@ -443,7 +483,7 @@ window.__ModuleLoader__.load({
|
|
|
443
483
|
".dsdr-code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; }",
|
|
444
484
|
".dsdr-footer { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 17px; }",
|
|
445
485
|
".dsdr-spacer { flex: 1; }",
|
|
446
|
-
"@media (max-width: 780px) { .dsdr-page { padding: 20px 16px 40px; } .dsdr-hero-action { flex: 1 0 100%; width: 100%; align-items: stretch; margin-left: 0; } .dsdr-hero-buttons { width: 100%; } .dsdr-hero-buttons .dsdr-btn { flex: 1; } .dsdr-generate { width: 100%; } .dsdr-range { grid-template-columns: 1fr 1fr; } .dsdr-range .dsdr-actions { grid-column: 1 / -1; } .dsdr-stats { grid-template-columns: 1fr 1fr; } .dsdr-scan-row { grid-template-columns: 1fr; } .dsdr-manual-add { grid-template-columns: 1fr; } .dsdr-source, .dsdr-source-docs { grid-template-columns: 1fr; } .dsdr-source .dsdr-icon-btn { justify-self: end; } }",
|
|
486
|
+
"@media (max-width: 780px) { .dsdr-page { padding: 20px 16px 40px; } .dsdr-hero-action { flex: 1 0 100%; width: 100%; align-items: stretch; margin-left: 0; } .dsdr-hero-buttons { width: 100%; } .dsdr-hero-buttons .dsdr-btn { flex: 1; } .dsdr-split { flex: 1; } .dsdr-split .dsdr-generate { flex: 1; } .dsdr-generate { width: 100%; } .dsdr-range { grid-template-columns: 1fr 1fr; } .dsdr-range .dsdr-actions { grid-column: 1 / -1; } .dsdr-stats { grid-template-columns: 1fr 1fr; } .dsdr-scan-row { grid-template-columns: 1fr; } .dsdr-manual-add { grid-template-columns: 1fr; } .dsdr-source, .dsdr-source-docs { grid-template-columns: 1fr; } .dsdr-source .dsdr-icon-btn { justify-self: end; } }",
|
|
447
487
|
].join(NL);
|
|
448
488
|
|
|
449
489
|
function installStyles(ctx) {
|
|
@@ -488,11 +528,73 @@ window.__ModuleLoader__.load({
|
|
|
488
528
|
);
|
|
489
529
|
}
|
|
490
530
|
|
|
531
|
+
function SaveIcon(props) {
|
|
532
|
+
const size = Number(props && props.size) > 0 ? Number(props.size) : 15;
|
|
533
|
+
return h(
|
|
534
|
+
"svg",
|
|
535
|
+
{
|
|
536
|
+
viewBox: "0 0 24 24",
|
|
537
|
+
width: size,
|
|
538
|
+
height: size,
|
|
539
|
+
fill: "none",
|
|
540
|
+
stroke: "currentColor",
|
|
541
|
+
strokeWidth: 1.8,
|
|
542
|
+
strokeLinecap: "round",
|
|
543
|
+
strokeLinejoin: "round",
|
|
544
|
+
"aria-hidden": true,
|
|
545
|
+
},
|
|
546
|
+
h("path", { d: "M5 3.5h11l3.5 3.5v13.5H5z" }),
|
|
547
|
+
h("path", { d: "M8 3.5v5h7v-5M8 20.5v-6h8v6" }),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function GenerateIcon(props) {
|
|
552
|
+
const size = Number(props && props.size) > 0 ? Number(props.size) : 15;
|
|
553
|
+
return h(
|
|
554
|
+
"svg",
|
|
555
|
+
{
|
|
556
|
+
viewBox: "0 0 24 24",
|
|
557
|
+
width: size,
|
|
558
|
+
height: size,
|
|
559
|
+
fill: "none",
|
|
560
|
+
stroke: "currentColor",
|
|
561
|
+
strokeWidth: 1.8,
|
|
562
|
+
strokeLinecap: "round",
|
|
563
|
+
strokeLinejoin: "round",
|
|
564
|
+
"aria-hidden": true,
|
|
565
|
+
},
|
|
566
|
+
h("path", { d: "M6 3.5h8l4 4v13H6z" }),
|
|
567
|
+
h("path", { d: "M14 3.5v4h4M9 12h6M9 15.5h4" }),
|
|
568
|
+
h("path", { d: "M18.5 16.5v4M16.5 18.5h4" }),
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function ChevronIcon(props) {
|
|
573
|
+
const size = Number(props && props.size) > 0 ? Number(props.size) : 12;
|
|
574
|
+
return h(
|
|
575
|
+
"svg",
|
|
576
|
+
{
|
|
577
|
+
viewBox: "0 0 24 24",
|
|
578
|
+
width: size,
|
|
579
|
+
height: size,
|
|
580
|
+
fill: "none",
|
|
581
|
+
stroke: "currentColor",
|
|
582
|
+
strokeWidth: 2,
|
|
583
|
+
strokeLinecap: "round",
|
|
584
|
+
strokeLinejoin: "round",
|
|
585
|
+
"aria-hidden": true,
|
|
586
|
+
},
|
|
587
|
+
h("path", { d: "M6 9l6 6 6-6" }),
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
|
|
491
591
|
function DailyReportView(props) {
|
|
492
592
|
const ctx = props.ctx;
|
|
493
593
|
const t = useTranslate(ctx);
|
|
494
594
|
const [settings, setSettings] = React.useState({ git: [], docs: [] });
|
|
495
595
|
const [range, setRange] = React.useState(currentMonthRange);
|
|
596
|
+
const [exportFormat, setExportFormat] = React.useState("xlsx");
|
|
597
|
+
const [formatMenuOpen, setFormatMenuOpen] = React.useState(false);
|
|
496
598
|
const [aiInfo, setAiInfo] = React.useState({ available: false, provider: "", model: "" });
|
|
497
599
|
const [useAi, setUseAi] = React.useState(true);
|
|
498
600
|
const [loading, setLoading] = React.useState(true);
|
|
@@ -519,6 +621,7 @@ window.__ModuleLoader__.load({
|
|
|
519
621
|
const pollAbortRef = React.useRef(null);
|
|
520
622
|
const saveAbortRef = React.useRef(null);
|
|
521
623
|
const saveTimerRef = React.useRef(null);
|
|
624
|
+
const formatMenuRef = React.useRef(null);
|
|
522
625
|
const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
|
|
523
626
|
const localeIsEnglish = currentLocaleId(ctx).toLowerCase().startsWith("en");
|
|
524
627
|
|
|
@@ -535,6 +638,17 @@ window.__ModuleLoader__.load({
|
|
|
535
638
|
};
|
|
536
639
|
}, []);
|
|
537
640
|
|
|
641
|
+
React.useEffect(() => {
|
|
642
|
+
if (!formatMenuOpen) return;
|
|
643
|
+
const close = (event) => {
|
|
644
|
+
if (formatMenuRef.current && !formatMenuRef.current.contains(event.target)) {
|
|
645
|
+
setFormatMenuOpen(false);
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
document.addEventListener("mousedown", close);
|
|
649
|
+
return () => document.removeEventListener("mousedown", close);
|
|
650
|
+
}, [formatMenuOpen]);
|
|
651
|
+
|
|
538
652
|
React.useEffect(() => {
|
|
539
653
|
let alive = true;
|
|
540
654
|
const query = sessionId.length > 0 ? "?sessionId=" + encodeURIComponent(sessionId) : "";
|
|
@@ -547,6 +661,10 @@ window.__ModuleLoader__.load({
|
|
|
547
661
|
if (!alive) return;
|
|
548
662
|
setSettings(normalizeClientSettings(payload && payload.settings));
|
|
549
663
|
setUseAi(!(payload && payload.settings && payload.settings.aiEnabled === false));
|
|
664
|
+
const savedFormat = payload && payload.settings && payload.settings.format;
|
|
665
|
+
if (typeof savedFormat === "string" && FORMAT_OPTIONS.includes(savedFormat)) {
|
|
666
|
+
setExportFormat(savedFormat);
|
|
667
|
+
}
|
|
550
668
|
const nextAi = payload && payload.ai ? payload.ai : { available: false, provider: "", model: "" };
|
|
551
669
|
setAiInfo({
|
|
552
670
|
available: nextAi.available === true,
|
|
@@ -590,7 +708,7 @@ window.__ModuleLoader__.load({
|
|
|
590
708
|
saveTimerRef.current = null;
|
|
591
709
|
}
|
|
592
710
|
};
|
|
593
|
-
}, [settings, useAi, range, loading]);
|
|
711
|
+
}, [settings, useAi, range, exportFormat, loading]);
|
|
594
712
|
|
|
595
713
|
function updateGit(index, patch) {
|
|
596
714
|
setSettings((previous) => ({
|
|
@@ -637,7 +755,7 @@ window.__ModuleLoader__.load({
|
|
|
637
755
|
const response = await fetch(API + "/state", {
|
|
638
756
|
method: "PUT",
|
|
639
757
|
headers: { "content-type": "application/json" },
|
|
640
|
-
body: JSON.stringify({ settings: cleanSettings(settings, useAi, range) }),
|
|
758
|
+
body: JSON.stringify({ settings: cleanSettings(settings, useAi, range, exportFormat) }),
|
|
641
759
|
signal: controller.signal,
|
|
642
760
|
});
|
|
643
761
|
if (!response.ok) throw new Error("save-failed");
|
|
@@ -821,7 +939,7 @@ window.__ModuleLoader__.load({
|
|
|
821
939
|
const response = await fetch(API + "/job/export", {
|
|
822
940
|
method: "POST",
|
|
823
941
|
headers: { "content-type": "application/json", accept: "application/octet-stream" },
|
|
824
|
-
body: JSON.stringify({ jobId: previewJobId, edits: previewEdits() }),
|
|
942
|
+
body: JSON.stringify({ jobId: previewJobId, edits: previewEdits(), format: exportFormat }),
|
|
825
943
|
});
|
|
826
944
|
if (!response.ok) {
|
|
827
945
|
let payload = null;
|
|
@@ -837,7 +955,7 @@ window.__ModuleLoader__.load({
|
|
|
837
955
|
const filenameHeader = response.headers.get("x-report-filename");
|
|
838
956
|
const filename = filenameHeader
|
|
839
957
|
? decodeURIComponent(filenameHeader)
|
|
840
|
-
: "日报汇总_" + preview.range.start + "_" + preview.range.end + ".
|
|
958
|
+
: "日报汇总_" + preview.range.start + "_" + preview.range.end + "." + formatExtension(exportFormat);
|
|
841
959
|
const blob = await response.blob();
|
|
842
960
|
const url = window.URL.createObjectURL(blob);
|
|
843
961
|
const next = Object.assign({}, summary, { filename, url, size: blob.size });
|
|
@@ -919,9 +1037,12 @@ window.__ModuleLoader__.load({
|
|
|
919
1037
|
});
|
|
920
1038
|
}
|
|
921
1039
|
|
|
922
|
-
async function generate() {
|
|
1040
|
+
async function generate(formatOverride) {
|
|
923
1041
|
if (generating) return;
|
|
924
|
-
const
|
|
1042
|
+
const format = FORMAT_OPTIONS.includes(formatOverride) ? formatOverride : exportFormat;
|
|
1043
|
+
setExportFormat(format);
|
|
1044
|
+
setFormatMenuOpen(false);
|
|
1045
|
+
const cleaned = cleanSettings(settings, useAi, range, format);
|
|
925
1046
|
if (cleaned.git.length === 0 && cleaned.docs.length === 0) {
|
|
926
1047
|
setError(t("error.noSources", "请至少添加一个 Git 仓库或本地文档。"));
|
|
927
1048
|
return;
|
|
@@ -1080,16 +1201,66 @@ window.__ModuleLoader__.load({
|
|
|
1080
1201
|
"button",
|
|
1081
1202
|
{
|
|
1082
1203
|
type: "button",
|
|
1083
|
-
className: "dsdr-btn dsdr-save-btn",
|
|
1204
|
+
className: "dsdr-btn dsdr-save-btn dsdr-btn-with-icon",
|
|
1084
1205
|
onClick: saveSettingsNow,
|
|
1085
1206
|
disabled: saveState === "saving",
|
|
1086
1207
|
},
|
|
1087
|
-
|
|
1208
|
+
h(SaveIcon, { size: 15 }),
|
|
1209
|
+
h("span", null, saveState === "saving" ? t("save.savingShort", "正在保存…") : t("save.button", "保存设置")),
|
|
1088
1210
|
),
|
|
1089
1211
|
h(
|
|
1090
|
-
"
|
|
1091
|
-
{
|
|
1092
|
-
|
|
1212
|
+
"div",
|
|
1213
|
+
{ className: "dsdr-split", ref: formatMenuRef },
|
|
1214
|
+
h(
|
|
1215
|
+
"button",
|
|
1216
|
+
{
|
|
1217
|
+
type: "button",
|
|
1218
|
+
className: "dsdr-btn dsdr-btn-primary dsdr-generate dsdr-btn-with-icon",
|
|
1219
|
+
onClick: () => generate(),
|
|
1220
|
+
disabled: generating,
|
|
1221
|
+
},
|
|
1222
|
+
h(GenerateIcon, { size: 15 }),
|
|
1223
|
+
h("span", null, generating ? t("generating", "正在收集并生成…") : t("generate", "生成日报")),
|
|
1224
|
+
),
|
|
1225
|
+
h(
|
|
1226
|
+
"button",
|
|
1227
|
+
{
|
|
1228
|
+
type: "button",
|
|
1229
|
+
className: "dsdr-btn dsdr-btn-primary dsdr-split-toggle",
|
|
1230
|
+
onClick: () => setFormatMenuOpen((open) => !open),
|
|
1231
|
+
disabled: generating,
|
|
1232
|
+
"aria-haspopup": "menu",
|
|
1233
|
+
"aria-expanded": formatMenuOpen,
|
|
1234
|
+
"aria-label": t("format.label", "格式"),
|
|
1235
|
+
},
|
|
1236
|
+
h(ChevronIcon, { size: 12 }),
|
|
1237
|
+
),
|
|
1238
|
+
formatMenuOpen
|
|
1239
|
+
? h(
|
|
1240
|
+
"div",
|
|
1241
|
+
{ className: "dsdr-menu", role: "menu" },
|
|
1242
|
+
h("div", { className: "dsdr-menu-title" }, t("format.label", "格式")),
|
|
1243
|
+
FORMAT_OPTIONS.map((format) =>
|
|
1244
|
+
h(
|
|
1245
|
+
"button",
|
|
1246
|
+
{
|
|
1247
|
+
type: "button",
|
|
1248
|
+
role: "menuitemradio",
|
|
1249
|
+
"aria-checked": exportFormat === format,
|
|
1250
|
+
className: "dsdr-menu-item" + (exportFormat === format ? " is-active" : ""),
|
|
1251
|
+
key: format,
|
|
1252
|
+
onClick: () => {
|
|
1253
|
+
setExportFormat(format);
|
|
1254
|
+
setFormatMenuOpen(false);
|
|
1255
|
+
generate(format);
|
|
1256
|
+
},
|
|
1257
|
+
},
|
|
1258
|
+
h("span", null, formatLabel(format, t)),
|
|
1259
|
+
h("span", { className: "dsdr-menu-check" }, exportFormat === format ? "✓" : ""),
|
|
1260
|
+
),
|
|
1261
|
+
),
|
|
1262
|
+
)
|
|
1263
|
+
: null,
|
|
1093
1264
|
),
|
|
1094
1265
|
),
|
|
1095
1266
|
saveText ? h("div", { className: "dsdr-save" }, saveText) : null,
|
|
@@ -1150,7 +1321,7 @@ window.__ModuleLoader__.load({
|
|
|
1150
1321
|
h(
|
|
1151
1322
|
"button",
|
|
1152
1323
|
{ type: "button", className: "dsdr-btn dsdr-btn-primary", onClick: exportPreview, disabled: exporting || regenerating.length > 0 },
|
|
1153
|
-
exporting ? t("preview.exporting", "正在导出…") : t("preview.export", "导出
|
|
1324
|
+
exporting ? t("preview.exporting", "正在导出…") : t("preview.export", "导出") + " " + formatShortLabel(exportFormat),
|
|
1154
1325
|
),
|
|
1155
1326
|
),
|
|
1156
1327
|
),
|
package/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
sanitizeSettings,
|
|
19
19
|
} from "./lib/collect.js";
|
|
20
20
|
import { aiInfo, analyzeReport, regenerateDay } from "./lib/ai.js";
|
|
21
|
+
import { buildCsvReport, buildHtmlReport, buildMarkdownReport } from "./lib/formats.js";
|
|
21
22
|
import { buildReportWorkbook, reportFileName, summarizeReport, buildReportPreview, applyReportEdits } from "./lib/report.js";
|
|
22
23
|
|
|
23
24
|
export const inject = ["webServer"];
|
|
@@ -36,12 +37,32 @@ const MAX_RANGE_DAYS = 366;
|
|
|
36
37
|
const JOB_TTL = 60 * 60 * 1000;
|
|
37
38
|
const MAX_JOBS = 20;
|
|
38
39
|
const jobs = new Map();
|
|
40
|
+
const REPORT_FORMATS = new Set(["xlsx", "markdown", "csv", "html"]);
|
|
41
|
+
|
|
42
|
+
function normalizeFormat(value) {
|
|
43
|
+
return typeof value === "string" && REPORT_FORMATS.has(value) ? value : "xlsx";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatContentType(format) {
|
|
47
|
+
if (format === "markdown") return "text/markdown; charset=utf-8";
|
|
48
|
+
if (format === "csv") return "text/csv; charset=utf-8";
|
|
49
|
+
if (format === "html") return "text/html; charset=utf-8";
|
|
50
|
+
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildReportBuffer(result, format) {
|
|
54
|
+
if (format === "markdown") return Buffer.from(buildMarkdownReport(result), "utf8");
|
|
55
|
+
if (format === "csv") return Buffer.from(buildCsvReport(result), "utf8");
|
|
56
|
+
if (format === "html") return Buffer.from(buildHtmlReport(result), "utf8");
|
|
57
|
+
return buildReportWorkbook(result);
|
|
58
|
+
}
|
|
39
59
|
|
|
40
60
|
const DEFAULT_SETTINGS = {
|
|
41
61
|
git: [],
|
|
42
62
|
docs: [],
|
|
43
63
|
aiEnabled: true,
|
|
44
64
|
mineOnly: true,
|
|
65
|
+
format: "xlsx",
|
|
45
66
|
range: null,
|
|
46
67
|
};
|
|
47
68
|
|
|
@@ -80,12 +101,13 @@ function sendError(res, status, code, message, details) {
|
|
|
80
101
|
sendJson(res, status, body);
|
|
81
102
|
}
|
|
82
103
|
|
|
83
|
-
function sendReportFile(res, buffer, filename, summary) {
|
|
104
|
+
function sendReportFile(res, buffer, filename, summary, contentType) {
|
|
84
105
|
const summaryHeader = Buffer.from(JSON.stringify(summary), "utf8").toString("base64");
|
|
85
106
|
const encodedName = encodeURIComponent(filename);
|
|
107
|
+
const extension = filename.includes(".") ? filename.slice(filename.lastIndexOf(".") + 1) : "xlsx";
|
|
86
108
|
res.statusCode = 200;
|
|
87
|
-
res.setHeader("content-type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
88
|
-
res.setHeader("content-disposition", 'attachment; filename="daily-report.
|
|
109
|
+
res.setHeader("content-type", contentType || "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
|
110
|
+
res.setHeader("content-disposition", 'attachment; filename="daily-report.' + extension + '"; filename*=UTF-8\'\'' + encodedName);
|
|
89
111
|
res.setHeader("content-length", String(buffer.length));
|
|
90
112
|
res.setHeader("cache-control", "no-store");
|
|
91
113
|
res.setHeader("x-report-filename", encodedName);
|
|
@@ -320,10 +342,6 @@ function registerRoute(ctx, path, handler, label) {
|
|
|
320
342
|
ctx.effect(() => dispose, label);
|
|
321
343
|
}
|
|
322
344
|
|
|
323
|
-
function outputPathFor(cwd, start, end) {
|
|
324
|
-
return join(cwd, reportFileName(start, end));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
345
|
function ensureAbsolute(cwd, value) {
|
|
328
346
|
if (typeof value !== "string" || value.trim().length === 0) return "";
|
|
329
347
|
return isAbsolute(value) ? resolve(value) : resolve(cwd, value);
|
|
@@ -380,8 +398,9 @@ function registerTool(ctx) {
|
|
|
380
398
|
description: "Local document files or directories. When omitted, the saved plugin configuration is used.",
|
|
381
399
|
items: { type: "string" },
|
|
382
400
|
},
|
|
383
|
-
outputPath: { type: "string", description: "Optional output
|
|
401
|
+
outputPath: { type: "string", description: "Optional output path. Defaults to the session working directory." },
|
|
384
402
|
ai: { type: "boolean", description: "Analyze commit diffs, commit messages and document excerpts with the current default model, then add per-day AI summaries to the workbook." },
|
|
403
|
+
format: { type: "string", enum: ["xlsx", "markdown", "csv", "html"], description: "Output format. Defaults to xlsx." },
|
|
385
404
|
locale: { type: "string", enum: ["zh", "en"], description: "Report language. Defaults to zh." },
|
|
386
405
|
},
|
|
387
406
|
},
|
|
@@ -481,9 +500,9 @@ function registerTool(ctx) {
|
|
|
481
500
|
for (const warning of result.ai.warnings || []) result.warnings.push("AI 分析:" + warning);
|
|
482
501
|
}
|
|
483
502
|
}
|
|
484
|
-
const filename = reportFileName(start, end);
|
|
485
|
-
const target = ensureAbsolute(cwd, input.outputPath) ||
|
|
486
|
-
const buffer =
|
|
503
|
+
const filename = reportFileName(start, end, normalizeFormat(input.format));
|
|
504
|
+
const target = ensureAbsolute(cwd, input.outputPath) || join(cwd, filename);
|
|
505
|
+
const buffer = buildReportBuffer(result, normalizeFormat(input.format));
|
|
487
506
|
await fs.mkdir(dirname(target), { recursive: true });
|
|
488
507
|
await fs.writeFile(target, buffer);
|
|
489
508
|
return {
|
|
@@ -720,8 +739,10 @@ export function apply(ctx) {
|
|
|
720
739
|
if (job.status !== "done" || !job.result) return sendError(res, 409, "job-not-ready", "生成任务尚未完成。");
|
|
721
740
|
try {
|
|
722
741
|
const edited = applyReportEdits(job.result, body.edits || {});
|
|
723
|
-
const
|
|
724
|
-
|
|
742
|
+
const format = normalizeFormat(body.format);
|
|
743
|
+
const buffer = buildReportBuffer(edited, format);
|
|
744
|
+
const filename = reportFileName(edited.range.start, edited.range.end, format);
|
|
745
|
+
return sendReportFile(res, buffer, filename, summarizeReport(edited), formatContentType(format));
|
|
725
746
|
} catch (error) {
|
|
726
747
|
return sendError(res, 400, "export-failed", error.message || "导出日报失败。");
|
|
727
748
|
}
|
package/lib/collect.js
CHANGED
|
@@ -166,6 +166,7 @@ export function sanitizeSettings(raw) {
|
|
|
166
166
|
docs,
|
|
167
167
|
aiEnabled: source.aiEnabled !== false,
|
|
168
168
|
mineOnly: source.mineOnly !== false,
|
|
169
|
+
format: ["xlsx", "markdown", "csv", "html"].includes(source.format) ? source.format : "xlsx",
|
|
169
170
|
range: start && end && start <= end ? { start: range.start, end: range.end } : null,
|
|
170
171
|
};
|
|
171
172
|
}
|
package/lib/formats.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-text and HTML renderings of the same report result used by the XLSX
|
|
3
|
+
* writer. Keeping them here means every export format follows the same AI
|
|
4
|
+
* summaries, manual items and source data.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function manualMap(result) {
|
|
8
|
+
const map = new Map();
|
|
9
|
+
for (const item of result.manualItems || []) {
|
|
10
|
+
if (!item || typeof item.date !== "string" || typeof item.text !== "string") continue;
|
|
11
|
+
if (!map.has(item.date)) map.set(item.date, []);
|
|
12
|
+
map.get(item.date).push(item.text);
|
|
13
|
+
}
|
|
14
|
+
return map;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function aiDayMap(result) {
|
|
18
|
+
const map = new Map();
|
|
19
|
+
for (const day of result.ai && result.ai.ok && Array.isArray(result.ai.days) ? result.ai.days : []) {
|
|
20
|
+
map.set(day.date, day);
|
|
21
|
+
}
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cellText(value) {
|
|
26
|
+
return value === null || value === undefined ? "" : String(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function markdownCell(value) {
|
|
30
|
+
return cellText(value).replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function csvCell(value) {
|
|
34
|
+
return '"' + cellText(value).replace(/"/g, '""') + '"';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function htmlEscape(value) {
|
|
38
|
+
return cellText(value)
|
|
39
|
+
.replace(/&/g, "&")
|
|
40
|
+
.replace(/</g, "<")
|
|
41
|
+
.replace(/>/g, ">")
|
|
42
|
+
.replace(/"/g, """);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function summaryRows(result) {
|
|
46
|
+
const manual = manualMap(result);
|
|
47
|
+
const ai = aiDayMap(result);
|
|
48
|
+
return result.days.map((day) => {
|
|
49
|
+
const dayAi = ai.get(day.date);
|
|
50
|
+
return [
|
|
51
|
+
day.date,
|
|
52
|
+
day.weekday,
|
|
53
|
+
day.dayType,
|
|
54
|
+
day.commitCount,
|
|
55
|
+
day.docCount,
|
|
56
|
+
day.sources || "—",
|
|
57
|
+
day.summary,
|
|
58
|
+
(manual.get(day.date) || []).join(";"),
|
|
59
|
+
dayAi ? dayAi.summary || "" : "",
|
|
60
|
+
];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildMarkdownReport(result) {
|
|
65
|
+
const ai = result.ai && result.ai.ok ? result.ai : null;
|
|
66
|
+
const rows = summaryRows(result);
|
|
67
|
+
const lines = [
|
|
68
|
+
"# 日报汇总 " + result.range.start + " ~ " + result.range.end,
|
|
69
|
+
"",
|
|
70
|
+
"- Git 提交:" + result.stats.commits + " 条",
|
|
71
|
+
"- 文档:" + result.stats.docs + " 份",
|
|
72
|
+
"- 有记录天数:" + result.stats.activeDays + " 天",
|
|
73
|
+
"- 数据源:" + result.stats.okSources + "/" + result.stats.sources + " 个正常",
|
|
74
|
+
"",
|
|
75
|
+
];
|
|
76
|
+
if (ai) {
|
|
77
|
+
lines.push("## 总体 AI 总结", "", ai.overall || "—", "");
|
|
78
|
+
} else if (result.ai && result.ai.error) {
|
|
79
|
+
lines.push("> AI 分析未完成:" + result.ai.error, "");
|
|
80
|
+
}
|
|
81
|
+
lines.push(
|
|
82
|
+
"## 每日汇总",
|
|
83
|
+
"",
|
|
84
|
+
"| 日期 | 星期 | 类型 | Git提交 | 文档 | 涉及仓库/文档 | 当日摘要 | 手动补充 | AI 分析 |",
|
|
85
|
+
"|---|---|---|---:|---:|---|---|---|---|",
|
|
86
|
+
);
|
|
87
|
+
for (const [date, weekday, dayType, commitCount, docCount, sources, summary, manual, aiSummary] of rows) {
|
|
88
|
+
lines.push(
|
|
89
|
+
"| " +
|
|
90
|
+
[date, weekday, dayType, commitCount, docCount, sources, summary, manual, aiSummary]
|
|
91
|
+
.map(markdownCell)
|
|
92
|
+
.join(" | ") +
|
|
93
|
+
" |",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (result.commits.length > 0) {
|
|
97
|
+
lines.push("", "## Git 明细", "", "| 日期 | 仓库 | 分支 | 提交 | 作者 | 说明 |", "|---|---|---|---|---|---|");
|
|
98
|
+
for (const commit of result.commits) {
|
|
99
|
+
lines.push(
|
|
100
|
+
"| " +
|
|
101
|
+
[commit.date, commit.repo, commit.branch, commit.shortHash, commit.author, commit.subject]
|
|
102
|
+
.map(markdownCell)
|
|
103
|
+
.join(" | ") +
|
|
104
|
+
" |",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (result.docs.length > 0) {
|
|
109
|
+
lines.push("", "## 文档明细", "", "| 日期 | 来源 | 文件 | 标题 | 摘要 |", "|---|---|---|---|---|");
|
|
110
|
+
for (const doc of result.docs) {
|
|
111
|
+
lines.push("| " + [doc.date, doc.source, doc.file, doc.title, doc.excerpt].map(markdownCell).join(" | ") + " |");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return lines.join("\n") + "\n";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildCsvReport(result) {
|
|
118
|
+
const header = ["日期", "星期", "日类型", "Git提交", "文档", "涉及仓库/文档", "当日摘要", "手动补充", "AI 分析"];
|
|
119
|
+
const rows = summaryRows(result);
|
|
120
|
+
return "\ufeff" + [header, ...rows].map((row) => row.map(csvCell).join(",")).join("\r\n") + "\r\n";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function buildHtmlReport(result) {
|
|
124
|
+
const ai = result.ai && result.ai.ok ? result.ai : null;
|
|
125
|
+
const rows = summaryRows(result);
|
|
126
|
+
const tableRows = rows
|
|
127
|
+
.map(
|
|
128
|
+
(row) =>
|
|
129
|
+
"<tr>" +
|
|
130
|
+
row
|
|
131
|
+
.map((value, index) => {
|
|
132
|
+
const cls = index >= 3 && index <= 4 ? ' class="num"' : "";
|
|
133
|
+
return "<td" + cls + ">" + htmlEscape(value) + "</td>";
|
|
134
|
+
})
|
|
135
|
+
.join("") +
|
|
136
|
+
"</tr>",
|
|
137
|
+
)
|
|
138
|
+
.join("\n");
|
|
139
|
+
const gitRows = result.commits
|
|
140
|
+
.map(
|
|
141
|
+
(commit) =>
|
|
142
|
+
"<tr>" +
|
|
143
|
+
[commit.date, commit.repo, commit.branch, commit.shortHash, commit.author, commit.subject, commit.fileCount]
|
|
144
|
+
.map((value) => "<td>" + htmlEscape(value) + "</td>")
|
|
145
|
+
.join("") +
|
|
146
|
+
"</tr>",
|
|
147
|
+
)
|
|
148
|
+
.join("\n");
|
|
149
|
+
return `<!doctype html>
|
|
150
|
+
<html lang="zh-CN">
|
|
151
|
+
<head>
|
|
152
|
+
<meta charset="utf-8">
|
|
153
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
154
|
+
<title>日报汇总 ${htmlEscape(result.range.start)} ~ ${htmlEscape(result.range.end)}</title>
|
|
155
|
+
<style>
|
|
156
|
+
:root { color-scheme: light dark; }
|
|
157
|
+
body { margin: 0; padding: 32px; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; background: #f5f7fa; color: #1f2933; }
|
|
158
|
+
main { max-width: 1400px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 28px 32px; box-shadow: 0 2px 16px rgba(0,0,0,.08); }
|
|
159
|
+
h1 { margin: 0 0 8px; font-size: 26px; }
|
|
160
|
+
.meta { color: #617080; font-size: 13px; margin-bottom: 18px; }
|
|
161
|
+
.stats { display: flex; flex-wrap: wrap; gap: 10px; margin: 16px 0 22px; }
|
|
162
|
+
.stat { border: 1px solid #d9e1ea; border-radius: 8px; padding: 8px 12px; min-width: 110px; }
|
|
163
|
+
.stat b { display: block; font-size: 20px; }
|
|
164
|
+
.stat span { color: #617080; font-size: 12px; }
|
|
165
|
+
h2 { margin: 26px 0 10px; font-size: 18px; }
|
|
166
|
+
p { line-height: 1.75; margin: 0 0 12px; white-space: pre-wrap; }
|
|
167
|
+
table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
168
|
+
th, td { border: 1px solid #d9e1ea; padding: 6px 8px; text-align: left; vertical-align: top; }
|
|
169
|
+
th { background: #1f4e79; color: #fff; font-weight: 600; }
|
|
170
|
+
tr:nth-child(even) td { background: #f7f9fc; }
|
|
171
|
+
td.num { text-align: center; white-space: nowrap; }
|
|
172
|
+
.note { color: #617080; font-size: 12px; }
|
|
173
|
+
@media print { body { padding: 0; background: #fff; } main { box-shadow: none; border-radius: 0; } }
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<main>
|
|
178
|
+
<h1>日报汇总 ${htmlEscape(result.range.start)} ~ ${htmlEscape(result.range.end)}</h1>
|
|
179
|
+
<div class="meta">Git 提交 ${result.stats.commits} 条 · 文档 ${result.stats.docs} 份 · 有记录 ${result.stats.activeDays} 天 · 数据源 ${result.stats.okSources}/${result.stats.sources} 个正常</div>
|
|
180
|
+
${ai ? "<h2>总体 AI 总结</h2><p>" + htmlEscape(ai.overall || "—") + "</p>" : result.ai && result.ai.error ? '<p class="note">AI 分析未完成:' + htmlEscape(result.ai.error) + "</p>" : ""}
|
|
181
|
+
<h2>每日汇总</h2>
|
|
182
|
+
<table>
|
|
183
|
+
<thead><tr><th>日期</th><th>星期</th><th>类型</th><th>Git提交</th><th>文档</th><th>涉及仓库/文档</th><th>当日摘要</th><th>手动补充</th><th>AI 分析</th></tr></thead>
|
|
184
|
+
<tbody>${tableRows}</tbody>
|
|
185
|
+
</table>
|
|
186
|
+
${gitRows ? "<h2>Git 明细</h2><table><thead><tr><th>日期</th><th>仓库</th><th>分支</th><th>提交</th><th>作者</th><th>说明</th><th>变更文件数</th></tr></thead><tbody>" + gitRows + "</tbody></table>" : ""}
|
|
187
|
+
</main>
|
|
188
|
+
</body>
|
|
189
|
+
</html>`;
|
|
190
|
+
}
|
package/lib/report.js
CHANGED
|
@@ -25,8 +25,9 @@ function commitFileText(commit) {
|
|
|
25
25
|
return text;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function reportFileName(start, end) {
|
|
29
|
-
|
|
28
|
+
export function reportFileName(start, end, format = "xlsx") {
|
|
29
|
+
const extension = format === "markdown" ? "md" : format === "html" || format === "csv" ? format : "xlsx";
|
|
30
|
+
return "日报汇总_" + start + "_" + end + "." + extension;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function summarizeReport(result) {
|