dsh-plugin-daily-report 1.0.0 → 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/lib/report.js CHANGED
@@ -1,376 +1,377 @@
1
- /**
2
- * Workbook rendering for collected daily-report data.
3
- */
4
-
5
- import { formatLocalDateTime } from "./collect.js";
6
- import { buildXlsx, styles } from "./xlsx.js";
7
-
8
- function statusLabel(status, en) {
9
- if (status === "error") return en ? "Failed" : "失败";
10
- if (status === "warn") return en ? "Warning" : "有提示";
11
- return en ? "OK" : "正常";
12
- }
13
-
14
- function typeLabel(kind, en) {
15
- if (kind === "git") return "Git";
16
- return en ? "Documents" : "文档";
17
- }
18
-
19
- function commitFileText(commit) {
20
- const items = (commit.visibleFiles || []).map((file) => file.status + " " + file.path);
21
- let text = items.length > 0 ? items.join(";") : "—";
22
- if (commit.filesTruncated) {
23
- text += ";…等 " + commit.fileCount + " 个文件";
24
- }
25
- return text;
26
- }
27
-
28
- export function reportFileName(start, end) {
29
- return "日报汇总_" + start + "_" + end + ".xlsx";
30
- }
31
-
32
- export function summarizeReport(result) {
33
- return {
34
- range: result.range,
35
- stats: result.stats,
36
- sources: result.sources.map((source) => ({
37
- kind: source.kind,
38
- label: source.label,
39
- location: source.location,
40
- branch: source.branch,
41
- status: source.status,
42
- message: source.message,
43
- count: source.count,
44
- })),
45
- warnings: result.warnings.slice(0, 20),
46
- ai: result.ai
47
- ? {
48
- requested: result.ai.requested === true,
49
- ok: result.ai.ok === true,
50
- provider: result.ai.provider || "",
51
- model: result.ai.model || "",
52
- overall: (result.ai.overall || "").slice(0, 3000),
53
- error: result.ai.error || "",
54
- warnings: (result.ai.warnings || []).slice(0, 10),
55
- cachedDays: Number(result.ai.cachedDays) || 0,
56
- analyzedDays: Number(result.ai.analyzedDays) || 0,
57
- }
58
- : null,
59
- generatedAt: result.generatedAt.toISOString(),
60
- };
61
- }
62
-
63
- export function buildReportPreview(result) {
64
- const ai = result.ai && result.ai.ok ? result.ai : null;
65
- const aiDays = new Map((ai && Array.isArray(ai.days) ? ai.days : []).map((day) => [day.date, day]));
66
- const manualByDate = new Map();
67
- for (const item of result.manualItems || []) {
68
- if (!item || typeof item.date !== "string" || typeof item.text !== "string") continue;
69
- if (!manualByDate.has(item.date)) manualByDate.set(item.date, []);
70
- manualByDate.get(item.date).push(item.text);
71
- }
72
- return {
73
- range: result.range,
74
- stats: result.stats,
75
- sources: result.sources,
76
- warnings: result.warnings,
77
- ai: result.ai
78
- ? {
79
- ok: result.ai.ok === true,
80
- provider: result.ai.provider || "",
81
- model: result.ai.model || "",
82
- overall: result.ai.overall || "",
83
- error: result.ai.error || "",
84
- cachedDays: Number(result.ai.cachedDays) || 0,
85
- analyzedDays: Number(result.ai.analyzedDays) || 0,
86
- }
87
- : null,
88
- days: result.days.map((day) => {
89
- const dayAi = aiDays.get(day.date);
90
- return {
91
- date: day.date,
92
- weekday: day.weekday,
93
- dayType: day.dayType,
94
- commitCount: day.commitCount,
95
- docCount: day.docCount,
96
- sources: day.sources || "",
97
- summary: day.summary,
98
- aiSummary: dayAi ? dayAi.summary || "" : "",
99
- aiHighlights: dayAi ? dayAi.highlights || [] : [],
100
- aiCategories: dayAi ? dayAi.categories || [] : [],
101
- manualItems: manualByDate.get(day.date) || [],
102
- };
103
- }),
104
- };
105
- }
106
-
107
- export function applyReportEdits(result, edits) {
108
- const source = edits && typeof edits === "object" ? edits : {};
109
- const next = { ...result };
110
- if (result.ai && result.ai.ok) {
111
- const existing = new Map((result.ai.days || []).map((day) => [day.date, day]));
112
- for (const edit of Array.isArray(source.days) ? source.days : []) {
113
- if (!edit || typeof edit.date !== "string") continue;
114
- const aiSummary = typeof edit.aiSummary === "string" ? edit.aiSummary.trim() : "";
115
- const aiHighlights = Array.isArray(edit.aiHighlights) ? edit.aiHighlights.map((item) => String(item).trim()).filter(Boolean).slice(0, 5) : [];
116
- const aiCategories = Array.isArray(edit.aiCategories) ? edit.aiCategories.map((item) => String(item).trim()).filter(Boolean).slice(0, 5) : [];
117
- if (aiSummary.length === 0 && aiHighlights.length === 0 && aiCategories.length === 0) {
118
- existing.delete(edit.date);
119
- } else {
120
- existing.set(edit.date, { date: edit.date, summary: aiSummary, highlights: aiHighlights, categories: aiCategories });
121
- }
122
- }
123
- next.ai = {
124
- ...result.ai,
125
- overall: typeof source.overall === "string" ? source.overall.trim() : result.ai.overall,
126
- days: result.days.filter((day) => existing.has(day.date)).map((day) => existing.get(day.date)),
127
- warnings: result.ai.warnings || [],
128
- };
129
- }
130
- const manualItems = [];
131
- for (const edit of Array.isArray(source.days) ? source.days : []) {
132
- if (!edit || typeof edit.date !== "string") continue;
133
- for (const text of Array.isArray(edit.manualItems) ? edit.manualItems : []) {
134
- const value = typeof text === "string" ? text.trim() : "";
135
- if (value.length > 0) manualItems.push({ date: edit.date, text: value });
136
- }
137
- }
138
- next.manualItems = manualItems;
139
- return next;
140
- }
141
-
142
- export function buildReportWorkbook(result) {
143
- const en = result.locale === "en";
144
- const { days, commits, docs, sources, warnings, stats, range, generatedAt } = result;
145
- const ai = result.ai && result.ai.ok ? result.ai : null;
146
- const aiDays = new Map((ai && Array.isArray(ai.days) ? ai.days : []).map((day) => [day.date, day]));
147
- const manualByDate = new Map();
148
- for (const item of result.manualItems || []) {
149
- if (!item || typeof item.date !== "string" || typeof item.text !== "string" || item.text.trim().length === 0) continue;
150
- if (!manualByDate.has(item.date)) manualByDate.set(item.date, []);
151
- manualByDate.get(item.date).push(item.text.trim());
152
- }
153
- const hasManual = manualByDate.size > 0;
154
- const manualCount = Array.from(manualByDate.values()).reduce((sum, list) => sum + list.length, 0);
155
-
156
- const summaryHeaders = ["日期", "星期", "日类型", "Git提交", "文档", "涉及仓库/文档", "当日摘要"];
157
- if (hasManual) summaryHeaders.push("手动补充");
158
- if (ai) summaryHeaders.push("AI 分析");
159
- const summaryRows = [summaryHeaders];
160
- const summaryStyles = {};
161
- for (const day of days) {
162
- const row = [
163
- day.date,
164
- day.weekday,
165
- day.dayType,
166
- day.commitCount,
167
- day.docCount,
168
- day.sources || "—",
169
- day.summary,
170
- ];
171
- if (hasManual) row.push((manualByDate.get(day.date) || []).join(";"));
172
- if (ai) row.push((aiDays.get(day.date) && aiDays.get(day.date).summary) || "");
173
- summaryRows.push(row);
174
- if (day.isWeekend) summaryStyles[summaryRows.length - 1] = styles.weekend;
175
- }
176
- const totalRow = [
177
- "合计",
178
- "",
179
- "",
180
- stats.commits,
181
- stats.docs,
182
- stats.okSources + "/" + stats.sources + " 个数据源正常",
183
- "有记录 " + stats.activeDays + " 天,共 " + stats.days + " 天",
184
- ];
185
- if (hasManual) totalRow.push("");
186
- if (ai) totalRow.push(ai.overall ? "见 AI分析 工作表" : "");
187
- summaryRows.push(totalRow);
188
- summaryStyles[summaryRows.length - 1] = styles.total;
189
-
190
- const gitRows = [
191
- ["日期", "仓库", "分支", "提交", "作者", "提交时间", "提交说明", "变更文件数", "变更文件"],
192
- ];
193
- for (const commit of commits) {
194
- gitRows.push([
195
- commit.date,
196
- commit.repo,
197
- commit.branch,
198
- commit.shortHash,
199
- commit.author,
200
- commit.time,
201
- commit.subject,
202
- commit.fileCount,
203
- commitFileText(commit),
204
- ]);
205
- }
206
- if (commits.length === 0) {
207
- gitRows.push(["范围内没有 Git 提交记录"]);
208
- }
209
-
210
- const docRows = [
211
- ["日期", "来源", "文件", "标题", "摘要", "大小(KB)", "修改时间"],
212
- ];
213
- for (const doc of docs) {
214
- docRows.push([
215
- doc.date,
216
- doc.source,
217
- doc.file,
218
- doc.title,
219
- doc.excerpt || "—",
220
- Math.max(1, Math.round(doc.size / 1024)),
221
- doc.mtime,
222
- ]);
223
- }
224
- if (docs.length === 0) {
225
- docRows.push(["范围内没有文档记录"]);
226
- }
227
-
228
- const sourceRows = [
229
- ["类型", "名称", "地址", "分支", "状态", "记录数", "说明"],
230
- ];
231
- for (const source of sources) {
232
- sourceRows.push([
233
- typeLabel(source.kind, en),
234
- source.label,
235
- source.location,
236
- source.branch || "—",
237
- statusLabel(source.status, en),
238
- source.count,
239
- source.message,
240
- ]);
241
- }
242
- if (sources.length === 0) {
243
- sourceRows.push(["—", "没有配置数据源"]);
244
- }
245
-
246
- const notes = [
247
- [{ value: en ? "Daily report notes" : "日报汇总说明", style: styles.title }],
248
- [en ? "Range" : "统计范围", range.start + " ~ " + range.end + "(" + stats.days + (en ? " days" : " 天") + ")"],
249
- [en ? "Generated at" : "生成时间", formatLocalDateTime(generatedAt)],
250
- [
251
- en ? "Data sources" : "数据来源",
252
- stats.sources + (en ? " source(s), " : " 个数据源,") + stats.okSources + (en ? " readable" : " 个读取成功") + (stats.failedSources > 0 ? (en ? ", " : ",") + stats.failedSources + (en ? " failed" : " 个失败") : ""),
253
- ],
254
- [
255
- en ? "Git rule" : "Git 口径",
256
- en
257
- ? "Commits are grouped by author date; the report reads commit subjects and changed file names. Remote repositories are cached as bare partial clones."
258
- : "按提交作者时间归入日期;读取提交说明和变更文件名。远程仓库使用裸仓库缓存,首次拉取后再次生成会复用缓存。",
259
- ],
260
- [
261
- en ? "Document rule" : "文档口径",
262
- en
263
- ? "Supports md, txt, docx, html and similar notes. The date comes from the filename, a date field in the document, otherwise the file modification time."
264
- : "支持 md、txt、docx、html 等笔记文件。日期优先取文件名中的日期,其次取文档内 date/日期 字段,最后使用文件修改时间。",
265
- ],
266
- [
267
- en ? "Summary rule" : "汇总口径",
268
- en
269
- ? "The daily sheet is a per-day digest. Git and document detail sheets keep the original records for checking."
270
- : "【日报汇总】按天给出摘要;【Git明细】和【文档明细】保留原始记录,便于核对。",
271
- ],
272
- [
273
- en ? "Manual items" : "手动补充",
274
- manualCount > 0 ? (en ? manualCount + " item(s)" : manualCount + " 条") : en ? "None" : "无",
275
- ],
276
- [
277
- en ? "AI analysis" : "AI 分析",
278
- result.ai
279
- ? result.ai.ok
280
- ? (en ? "Enabled; model " : "已启用,模型 ") + (result.ai.provider || "") + "/" + (result.ai.model || "")
281
- : (en ? "Not completed: " : "未完成:") + (result.ai.error || "")
282
- : en
283
- ? "Disabled; the report is a direct data-source digest."
284
- : "未启用;日报仅按数据源直接汇总。",
285
- ],
286
- [
287
- en ? "Warnings" : "提示",
288
- warnings.length > 0 ? warnings.join("\n") : en ? "None" : "无",
289
- ],
290
- ];
291
-
292
- const aiSheetRows = result.ai
293
- ? [
294
- [{ value: en ? "AI daily analysis" : "AI 日报分析", style: styles.title }],
295
- [en ? "Status" : "状态", result.ai.ok ? (en ? "Completed" : "已完成") : en ? "Not completed" : "未完成"],
296
- [en ? "Model" : "模型", result.ai.provider || result.ai.model ? (result.ai.provider || "") + "/" + (result.ai.model || "") : "—"],
297
- [en ? "Overall" : "总体摘要", result.ai.ok ? result.ai.overall || "—" : result.ai.error || "—"],
298
- ["", ""],
299
- [en ? "Date" : "日期", en ? "AI summary" : "AI摘要", en ? "Highlights" : "关键事项", en ? "Categories" : "分类"],
300
- ...(result.ai.days || []).map((day) => [
301
- day.date,
302
- day.summary,
303
- (day.highlights || []).join(";"),
304
- (day.categories || []).join("、"),
305
- ]),
306
- ...(result.ai.warnings && result.ai.warnings.length > 0 ? [[en ? "Warnings" : "提示", result.ai.warnings.join("\n")]] : []),
307
- ]
308
- : [];
309
-
310
- const summaryWidths = [12, 10, 10, 10, 10, 26, 90];
311
- if (hasManual) summaryWidths.push(40);
312
- if (ai) summaryWidths.push(60);
313
- const gitWidths = [12, 18, 14, 12, 16, 20, 60, 12, 70];
314
- const docWidths = [12, 18, 50, 30, 70, 10, 20];
315
- const sourceWidths = [10, 22, 54, 16, 10, 10, 64];
316
- const noteWidths = [18, 106];
317
-
318
- const sheets = [
319
- {
320
- name: "日报汇总",
321
- widths: summaryWidths,
322
- rows: summaryRows,
323
- rowStyles: summaryStyles,
324
- headerRows: 1,
325
- freezeColumns: 3,
326
- },
327
- ];
328
- if (result.ai) {
329
- sheets.push({
330
- name: "AI分析",
331
- widths: [14, 90, 50, 26],
332
- rows: aiSheetRows,
333
- rowStyles: { 0: styles.title, 1: styles.note, 2: styles.note, 3: styles.note, 5: styles.header },
334
- headerRows: 0,
335
- freeze: false,
336
- filter: false,
337
- merges: ["A1:D1"],
338
- });
339
- }
340
- sheets.push(
341
- {
342
- name: "Git明细",
343
- widths: gitWidths,
344
- rows: gitRows,
345
- headerRows: 1,
346
- freezeColumns: 3,
347
- filter: commits.length > 0,
348
- },
349
- {
350
- name: "文档明细",
351
- widths: docWidths,
352
- rows: docRows,
353
- headerRows: 1,
354
- freezeColumns: 3,
355
- filter: docs.length > 0,
356
- },
357
- {
358
- name: "数据源",
359
- widths: sourceWidths,
360
- rows: sourceRows,
361
- headerRows: 1,
362
- freezeColumns: 2,
363
- filter: sources.length > 0,
364
- },
365
- {
366
- name: "说明",
367
- widths: noteWidths,
368
- rows: notes,
369
- headerRows: 0,
370
- freeze: false,
371
- filter: false,
372
- },
373
- );
374
-
375
- return buildXlsx(sheets, { title: reportFileName(range.start, range.end).replace(/\.xlsx$/i, "") });
376
- }
1
+ /**
2
+ * Workbook rendering for collected daily-report data.
3
+ */
4
+
5
+ import { formatLocalDateTime } from "./collect.js";
6
+ import { buildXlsx, styles } from "./xlsx.js";
7
+
8
+ function statusLabel(status, en) {
9
+ if (status === "error") return en ? "Failed" : "失败";
10
+ if (status === "warn") return en ? "Warning" : "有提示";
11
+ return en ? "OK" : "正常";
12
+ }
13
+
14
+ function typeLabel(kind, en) {
15
+ if (kind === "git") return "Git";
16
+ return en ? "Documents" : "文档";
17
+ }
18
+
19
+ function commitFileText(commit) {
20
+ const items = (commit.visibleFiles || []).map((file) => file.status + " " + file.path);
21
+ let text = items.length > 0 ? items.join(";") : "—";
22
+ if (commit.filesTruncated) {
23
+ text += ";…等 " + commit.fileCount + " 个文件";
24
+ }
25
+ return text;
26
+ }
27
+
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;
31
+ }
32
+
33
+ export function summarizeReport(result) {
34
+ return {
35
+ range: result.range,
36
+ stats: result.stats,
37
+ sources: result.sources.map((source) => ({
38
+ kind: source.kind,
39
+ label: source.label,
40
+ location: source.location,
41
+ branch: source.branch,
42
+ status: source.status,
43
+ message: source.message,
44
+ count: source.count,
45
+ })),
46
+ warnings: result.warnings.slice(0, 20),
47
+ ai: result.ai
48
+ ? {
49
+ requested: result.ai.requested === true,
50
+ ok: result.ai.ok === true,
51
+ provider: result.ai.provider || "",
52
+ model: result.ai.model || "",
53
+ overall: (result.ai.overall || "").slice(0, 3000),
54
+ error: result.ai.error || "",
55
+ warnings: (result.ai.warnings || []).slice(0, 10),
56
+ cachedDays: Number(result.ai.cachedDays) || 0,
57
+ analyzedDays: Number(result.ai.analyzedDays) || 0,
58
+ }
59
+ : null,
60
+ generatedAt: result.generatedAt.toISOString(),
61
+ };
62
+ }
63
+
64
+ export function buildReportPreview(result) {
65
+ const ai = result.ai && result.ai.ok ? result.ai : null;
66
+ const aiDays = new Map((ai && Array.isArray(ai.days) ? ai.days : []).map((day) => [day.date, day]));
67
+ const manualByDate = new Map();
68
+ for (const item of result.manualItems || []) {
69
+ if (!item || typeof item.date !== "string" || typeof item.text !== "string") continue;
70
+ if (!manualByDate.has(item.date)) manualByDate.set(item.date, []);
71
+ manualByDate.get(item.date).push(item.text);
72
+ }
73
+ return {
74
+ range: result.range,
75
+ stats: result.stats,
76
+ sources: result.sources,
77
+ warnings: result.warnings,
78
+ ai: result.ai
79
+ ? {
80
+ ok: result.ai.ok === true,
81
+ provider: result.ai.provider || "",
82
+ model: result.ai.model || "",
83
+ overall: result.ai.overall || "",
84
+ error: result.ai.error || "",
85
+ cachedDays: Number(result.ai.cachedDays) || 0,
86
+ analyzedDays: Number(result.ai.analyzedDays) || 0,
87
+ }
88
+ : null,
89
+ days: result.days.map((day) => {
90
+ const dayAi = aiDays.get(day.date);
91
+ return {
92
+ date: day.date,
93
+ weekday: day.weekday,
94
+ dayType: day.dayType,
95
+ commitCount: day.commitCount,
96
+ docCount: day.docCount,
97
+ sources: day.sources || "",
98
+ summary: day.summary,
99
+ aiSummary: dayAi ? dayAi.summary || "" : "",
100
+ aiHighlights: dayAi ? dayAi.highlights || [] : [],
101
+ aiCategories: dayAi ? dayAi.categories || [] : [],
102
+ manualItems: manualByDate.get(day.date) || [],
103
+ };
104
+ }),
105
+ };
106
+ }
107
+
108
+ export function applyReportEdits(result, edits) {
109
+ const source = edits && typeof edits === "object" ? edits : {};
110
+ const next = { ...result };
111
+ if (result.ai && result.ai.ok) {
112
+ const existing = new Map((result.ai.days || []).map((day) => [day.date, day]));
113
+ for (const edit of Array.isArray(source.days) ? source.days : []) {
114
+ if (!edit || typeof edit.date !== "string") continue;
115
+ const aiSummary = typeof edit.aiSummary === "string" ? edit.aiSummary.trim() : "";
116
+ const aiHighlights = Array.isArray(edit.aiHighlights) ? edit.aiHighlights.map((item) => String(item).trim()).filter(Boolean).slice(0, 5) : [];
117
+ const aiCategories = Array.isArray(edit.aiCategories) ? edit.aiCategories.map((item) => String(item).trim()).filter(Boolean).slice(0, 5) : [];
118
+ if (aiSummary.length === 0 && aiHighlights.length === 0 && aiCategories.length === 0) {
119
+ existing.delete(edit.date);
120
+ } else {
121
+ existing.set(edit.date, { date: edit.date, summary: aiSummary, highlights: aiHighlights, categories: aiCategories });
122
+ }
123
+ }
124
+ next.ai = {
125
+ ...result.ai,
126
+ overall: typeof source.overall === "string" ? source.overall.trim() : result.ai.overall,
127
+ days: result.days.filter((day) => existing.has(day.date)).map((day) => existing.get(day.date)),
128
+ warnings: result.ai.warnings || [],
129
+ };
130
+ }
131
+ const manualItems = [];
132
+ for (const edit of Array.isArray(source.days) ? source.days : []) {
133
+ if (!edit || typeof edit.date !== "string") continue;
134
+ for (const text of Array.isArray(edit.manualItems) ? edit.manualItems : []) {
135
+ const value = typeof text === "string" ? text.trim() : "";
136
+ if (value.length > 0) manualItems.push({ date: edit.date, text: value });
137
+ }
138
+ }
139
+ next.manualItems = manualItems;
140
+ return next;
141
+ }
142
+
143
+ export function buildReportWorkbook(result) {
144
+ const en = result.locale === "en";
145
+ const { days, commits, docs, sources, warnings, stats, range, generatedAt } = result;
146
+ const ai = result.ai && result.ai.ok ? result.ai : null;
147
+ const aiDays = new Map((ai && Array.isArray(ai.days) ? ai.days : []).map((day) => [day.date, day]));
148
+ const manualByDate = new Map();
149
+ for (const item of result.manualItems || []) {
150
+ if (!item || typeof item.date !== "string" || typeof item.text !== "string" || item.text.trim().length === 0) continue;
151
+ if (!manualByDate.has(item.date)) manualByDate.set(item.date, []);
152
+ manualByDate.get(item.date).push(item.text.trim());
153
+ }
154
+ const hasManual = manualByDate.size > 0;
155
+ const manualCount = Array.from(manualByDate.values()).reduce((sum, list) => sum + list.length, 0);
156
+
157
+ const summaryHeaders = ["日期", "星期", "日类型", "Git提交", "文档", "涉及仓库/文档", "当日摘要"];
158
+ if (hasManual) summaryHeaders.push("手动补充");
159
+ if (ai) summaryHeaders.push("AI 分析");
160
+ const summaryRows = [summaryHeaders];
161
+ const summaryStyles = {};
162
+ for (const day of days) {
163
+ const row = [
164
+ day.date,
165
+ day.weekday,
166
+ day.dayType,
167
+ day.commitCount,
168
+ day.docCount,
169
+ day.sources || "—",
170
+ day.summary,
171
+ ];
172
+ if (hasManual) row.push((manualByDate.get(day.date) || []).join(";"));
173
+ if (ai) row.push((aiDays.get(day.date) && aiDays.get(day.date).summary) || "");
174
+ summaryRows.push(row);
175
+ if (day.isWeekend) summaryStyles[summaryRows.length - 1] = styles.weekend;
176
+ }
177
+ const totalRow = [
178
+ "合计",
179
+ "",
180
+ "",
181
+ stats.commits,
182
+ stats.docs,
183
+ stats.okSources + "/" + stats.sources + " 个数据源正常",
184
+ "有记录 " + stats.activeDays + " 天,共 " + stats.days + " 天",
185
+ ];
186
+ if (hasManual) totalRow.push("");
187
+ if (ai) totalRow.push(ai.overall ? "见 AI分析 工作表" : "");
188
+ summaryRows.push(totalRow);
189
+ summaryStyles[summaryRows.length - 1] = styles.total;
190
+
191
+ const gitRows = [
192
+ ["日期", "仓库", "分支", "提交", "作者", "提交时间", "提交说明", "变更文件数", "变更文件"],
193
+ ];
194
+ for (const commit of commits) {
195
+ gitRows.push([
196
+ commit.date,
197
+ commit.repo,
198
+ commit.branch,
199
+ commit.shortHash,
200
+ commit.author,
201
+ commit.time,
202
+ commit.subject,
203
+ commit.fileCount,
204
+ commitFileText(commit),
205
+ ]);
206
+ }
207
+ if (commits.length === 0) {
208
+ gitRows.push(["范围内没有 Git 提交记录"]);
209
+ }
210
+
211
+ const docRows = [
212
+ ["日期", "来源", "文件", "标题", "摘要", "大小(KB)", "修改时间"],
213
+ ];
214
+ for (const doc of docs) {
215
+ docRows.push([
216
+ doc.date,
217
+ doc.source,
218
+ doc.file,
219
+ doc.title,
220
+ doc.excerpt || "—",
221
+ Math.max(1, Math.round(doc.size / 1024)),
222
+ doc.mtime,
223
+ ]);
224
+ }
225
+ if (docs.length === 0) {
226
+ docRows.push(["范围内没有文档记录"]);
227
+ }
228
+
229
+ const sourceRows = [
230
+ ["类型", "名称", "地址", "分支", "状态", "记录数", "说明"],
231
+ ];
232
+ for (const source of sources) {
233
+ sourceRows.push([
234
+ typeLabel(source.kind, en),
235
+ source.label,
236
+ source.location,
237
+ source.branch || "—",
238
+ statusLabel(source.status, en),
239
+ source.count,
240
+ source.message,
241
+ ]);
242
+ }
243
+ if (sources.length === 0) {
244
+ sourceRows.push(["—", "没有配置数据源"]);
245
+ }
246
+
247
+ const notes = [
248
+ [{ value: en ? "Daily report notes" : "日报汇总说明", style: styles.title }],
249
+ [en ? "Range" : "统计范围", range.start + " ~ " + range.end + "(" + stats.days + (en ? " days" : " 天") + ")"],
250
+ [en ? "Generated at" : "生成时间", formatLocalDateTime(generatedAt)],
251
+ [
252
+ en ? "Data sources" : "数据来源",
253
+ stats.sources + (en ? " source(s), " : " 个数据源,") + stats.okSources + (en ? " readable" : " 个读取成功") + (stats.failedSources > 0 ? (en ? ", " : ",") + stats.failedSources + (en ? " failed" : " 个失败") : ""),
254
+ ],
255
+ [
256
+ en ? "Git rule" : "Git 口径",
257
+ en
258
+ ? "Commits are grouped by author date; the report reads commit subjects and changed file names. Remote repositories are cached as bare partial clones."
259
+ : "按提交作者时间归入日期;读取提交说明和变更文件名。远程仓库使用裸仓库缓存,首次拉取后再次生成会复用缓存。",
260
+ ],
261
+ [
262
+ en ? "Document rule" : "文档口径",
263
+ en
264
+ ? "Supports md, txt, docx, html and similar notes. The date comes from the filename, a date field in the document, otherwise the file modification time."
265
+ : "支持 md、txt、docx、html 等笔记文件。日期优先取文件名中的日期,其次取文档内 date/日期 字段,最后使用文件修改时间。",
266
+ ],
267
+ [
268
+ en ? "Summary rule" : "汇总口径",
269
+ en
270
+ ? "The daily sheet is a per-day digest. Git and document detail sheets keep the original records for checking."
271
+ : "【日报汇总】按天给出摘要;【Git明细】和【文档明细】保留原始记录,便于核对。",
272
+ ],
273
+ [
274
+ en ? "Manual items" : "手动补充",
275
+ manualCount > 0 ? (en ? manualCount + " item(s)" : manualCount + " 条") : en ? "None" : "无",
276
+ ],
277
+ [
278
+ en ? "AI analysis" : "AI 分析",
279
+ result.ai
280
+ ? result.ai.ok
281
+ ? (en ? "Enabled; model " : "已启用,模型 ") + (result.ai.provider || "") + "/" + (result.ai.model || "")
282
+ : (en ? "Not completed: " : "未完成:") + (result.ai.error || "")
283
+ : en
284
+ ? "Disabled; the report is a direct data-source digest."
285
+ : "未启用;日报仅按数据源直接汇总。",
286
+ ],
287
+ [
288
+ en ? "Warnings" : "提示",
289
+ warnings.length > 0 ? warnings.join("\n") : en ? "None" : "无",
290
+ ],
291
+ ];
292
+
293
+ const aiSheetRows = result.ai
294
+ ? [
295
+ [{ value: en ? "AI daily analysis" : "AI 日报分析", style: styles.title }],
296
+ [en ? "Status" : "状态", result.ai.ok ? (en ? "Completed" : "已完成") : en ? "Not completed" : "未完成"],
297
+ [en ? "Model" : "模型", result.ai.provider || result.ai.model ? (result.ai.provider || "") + "/" + (result.ai.model || "") : "—"],
298
+ [en ? "Overall" : "总体摘要", result.ai.ok ? result.ai.overall || "—" : result.ai.error || "—"],
299
+ ["", ""],
300
+ [en ? "Date" : "日期", en ? "AI summary" : "AI摘要", en ? "Highlights" : "关键事项", en ? "Categories" : "分类"],
301
+ ...(result.ai.days || []).map((day) => [
302
+ day.date,
303
+ day.summary,
304
+ (day.highlights || []).join(";"),
305
+ (day.categories || []).join("、"),
306
+ ]),
307
+ ...(result.ai.warnings && result.ai.warnings.length > 0 ? [[en ? "Warnings" : "提示", result.ai.warnings.join("\n")]] : []),
308
+ ]
309
+ : [];
310
+
311
+ const summaryWidths = [12, 10, 10, 10, 10, 26, 90];
312
+ if (hasManual) summaryWidths.push(40);
313
+ if (ai) summaryWidths.push(60);
314
+ const gitWidths = [12, 18, 14, 12, 16, 20, 60, 12, 70];
315
+ const docWidths = [12, 18, 50, 30, 70, 10, 20];
316
+ const sourceWidths = [10, 22, 54, 16, 10, 10, 64];
317
+ const noteWidths = [18, 106];
318
+
319
+ const sheets = [
320
+ {
321
+ name: "日报汇总",
322
+ widths: summaryWidths,
323
+ rows: summaryRows,
324
+ rowStyles: summaryStyles,
325
+ headerRows: 1,
326
+ freezeColumns: 3,
327
+ },
328
+ ];
329
+ if (result.ai) {
330
+ sheets.push({
331
+ name: "AI分析",
332
+ widths: [14, 90, 50, 26],
333
+ rows: aiSheetRows,
334
+ rowStyles: { 0: styles.title, 1: styles.note, 2: styles.note, 3: styles.note, 5: styles.header },
335
+ headerRows: 0,
336
+ freeze: false,
337
+ filter: false,
338
+ merges: ["A1:D1"],
339
+ });
340
+ }
341
+ sheets.push(
342
+ {
343
+ name: "Git明细",
344
+ widths: gitWidths,
345
+ rows: gitRows,
346
+ headerRows: 1,
347
+ freezeColumns: 3,
348
+ filter: commits.length > 0,
349
+ },
350
+ {
351
+ name: "文档明细",
352
+ widths: docWidths,
353
+ rows: docRows,
354
+ headerRows: 1,
355
+ freezeColumns: 3,
356
+ filter: docs.length > 0,
357
+ },
358
+ {
359
+ name: "数据源",
360
+ widths: sourceWidths,
361
+ rows: sourceRows,
362
+ headerRows: 1,
363
+ freezeColumns: 2,
364
+ filter: sources.length > 0,
365
+ },
366
+ {
367
+ name: "说明",
368
+ widths: noteWidths,
369
+ rows: notes,
370
+ headerRows: 0,
371
+ freeze: false,
372
+ filter: false,
373
+ },
374
+ );
375
+
376
+ return buildXlsx(sheets, { title: reportFileName(range.start, range.end).replace(/\.xlsx$/i, "") });
377
+ }