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/client.js CHANGED
@@ -1,1716 +1,1851 @@
1
- window.__ModuleLoader__.load({
2
- id: "dsh-plugin-daily-report",
3
- factory(require) {
4
- const React = require("react");
5
- const h = React.createElement;
6
-
7
- const NS = "daily-report";
8
- const API = "/daily-report";
9
- const NL = String.fromCharCode(10);
10
-
11
- const zh = {
12
- "panel.title": "日报汇总",
13
- "page.title": "日报汇总",
14
- "page.desc": "按天汇总 Git 提交与本地文档,导出可直接查看的 XLSX 日报。",
15
- loading: "正在读取配置…",
16
- "range.title": "汇总时间范围",
17
- "range.desc": "默认当月月初到月底;可改为任意日期范围。",
18
- "range.start": "开始日期",
19
- "range.end": "结束日期",
20
- "range.thisMonth": "本月",
21
- "range.lastMonth": "上月",
22
- "range.last7": "最近 7 天",
23
- "range.days": "天",
24
- "range.total": "共",
25
- "git.title": "Git 仓库",
26
- "git.desc": "支持远程地址或本地仓库;多个仓库、多个分支会合并到同一份日报。",
27
- "git.add": "添加仓库",
28
- "git.location": "仓库地址或本地路径",
29
- "git.branch": "分支",
30
- "git.label": "显示名称(可选)",
31
- "git.placeholder": "https://…/repo.git 或 D:\\code\\repo",
32
- "git.empty": "还没有 Git 仓库。",
33
- "git.mine": "仅统计我的提交",
34
- "git.mineHint": "按各仓库 git config user.email / user.name 过滤",
35
- "scan.open": "扫描目录",
36
- "scan.title": "从目录批量添加",
37
- "scan.desc": "扫描目录下最多 6 层的 Git 仓库,已存在的会自动跳过。",
38
- "scan.placeholder": "要扫描的目录,例如 E:\\sunline\\SBG",
39
- "scan.scan": "扫描并添加",
40
- "scan.scanning": "正在扫描…",
41
- "scan.pick": "选择目录",
42
- "scan.found": "发现",
43
- "scan.added": "新增",
44
- "scan.empty": "该目录下没有发现 Git 仓库。",
45
- "scan.noPicker": "请手动输入目录路径。",
46
- "scan.failed": "扫描目录失败",
47
- "docs.title": "本地文档",
48
- "docs.desc": "文件或目录;目录会递归扫描 md、txt、docx、html 等笔记文件。",
49
- "docs.add": "添加文档/目录",
50
- "docs.location": "文件或目录路径",
51
- "docs.label": "显示名称(可选)",
52
- "docs.placeholder": "C:\\notes 或 ./docs",
53
- "docs.empty": "还没有本地文档。",
54
- "docs.useCwd": "使用当前目录",
55
- "cwd.title": "工作目录",
56
- "cwd.desc": "相对路径以此目录为基准;可填写绝对路径。",
57
- "cwd.placeholder": "C:\\work 或 /home/me/work",
58
- "ai.title": "AI 分析",
59
- "ai.desc": "使用当前默认模型分析提交说明、代码变更和文档摘要,生成按天简要总结。",
60
- "ai.enable": "启用 AI 分析",
61
- "ai.model": "当前模型:",
62
- "ai.unavailable": "当前没有可用的默认模型。",
63
- "ai.cost": "会增加生成时间和模型 token 消耗。",
64
- "progress.start": "正在创建生成任务…",
65
- "progress.done": "生成完成,正在下载…",
66
- "progress.preview": "生成完成,请预览并编辑后导出 XLSX。",
67
- "progress.hint": "AI 分析需要调用模型,耗时通常比普通汇总更长;请保持页面打开。",
68
- optional: "可选",
69
- remove: "移除",
70
- generate: "生成 XLSX 日报",
71
- generating: "正在收集并生成…",
72
- "save.saving": "正在保存设置…",
73
- "save.savingShort": "正在保存…",
74
- "save.button": "保存设置",
75
- "save.saved": "设置已保存",
76
- "save.failed": "设置保存失败",
77
- "result.title": "生成结果",
78
- "result.download": "下载 XLSX",
79
- "result.stats.commits": "Git 提交",
80
- "result.stats.docs": "文档",
81
- "result.stats.activeDays": "有记录天数",
82
- "result.stats.sources": "数据源",
83
- "result.unit.commits": "次提交",
84
- "result.unit.docs": "份文档",
85
- "result.sources": "数据源状态",
86
- "result.warnings": "提示",
87
- "result.ai.title": "AI 分析",
88
- "result.ai.hint": "逐日 AI 摘要已写入 XLSX 的「AI分析」工作表。",
89
- "result.ai.failed": "AI 分析未完成",
90
- "preview.title": "生成结果预览",
91
- "preview.desc": "先检查并编辑每日 AI 摘要、补充非代码工作,再导出 XLSX。",
92
- "preview.export": "导出 XLSX",
93
- "preview.expandAll": "全部展开",
94
- "preview.collapseAll": "全部收起",
95
- "preview.exporting": "正在导出…",
96
- "preview.regenerateDay": "重新分析这天",
97
- "preview.regenerating": "分析中…",
98
- "preview.overall": "总体 AI 总结",
99
- "preview.aiSummary": "AI 摘要(可编辑)",
100
- "preview.raw": "原始汇总",
101
- "preview.manual": "手动补充",
102
- "preview.addManual": "添加手动工作项",
103
- "preview.manualPlaceholder": "例如:参加需求评审、处理线上问题",
104
- "preview.noDays": "没有可预览的日报内容。",
105
- "preview.aiFailed": "AI 分析未完成,仍可补充手动工作项后导出。",
106
- "result.ready": "已生成",
107
- "status.ok": "正常",
108
- "status.warn": "有提示",
109
- "status.error": "失败",
110
- "error.noSources": "请至少添加一个 Git 仓库或本地文档。",
111
- "error.invalidRange": "日期范围无效,请检查开始和结束日期。",
112
- "error.rangeTooLong": "时间范围最长支持 366 天。",
113
- "error.generate": "生成失败",
114
- "error.network": "无法连接本地服务,请确认插件已启用。",
115
- "error.save": "保存失败",
116
- "error.allSourcesFailed": "所有数据源都读取失败。",
117
- "footer.tip": "Git 使用系统 git 与你的凭据配置;远程仓库会缓存在 DSH 数据目录。",
118
- "page.footer": "数据仅在本机处理;生成的 XLSX 包含日报汇总、Git明细、文档明细、数据源与说明。",
119
- "misc.unknown": "未知错误",
120
- "misc.retry": "重试",
121
- };
122
-
123
- const en = {
124
- "panel.title": "Daily report",
125
- "page.title": "Daily report",
126
- "page.desc": "Aggregate Git commits and local notes by day, then export an XLSX report.",
127
- loading: "Loading configuration…",
128
- "range.title": "Date range",
129
- "range.desc": "Defaults to the current month; change it to any range.",
130
- "range.start": "Start date",
131
- "range.end": "End date",
132
- "range.thisMonth": "This month",
133
- "range.lastMonth": "Last month",
134
- "range.last7": "Last 7 days",
135
- "range.days": "days",
136
- "range.total": "Total",
137
- "git.title": "Git repositories",
138
- "git.desc": "Remote URLs or local repositories; multiple repositories and branches are merged into one report.",
139
- "git.add": "Add repository",
140
- "git.location": "Repository URL or local path",
141
- "git.branch": "Branch",
142
- "git.label": "Display name (optional)",
143
- "git.placeholder": "https://…/repo.git or D:\\code\\repo",
144
- "git.empty": "No repositories yet.",
145
- "git.mine": "Only my commits",
146
- "git.mineHint": "Filter by each repository's git config user.email / user.name",
147
- "scan.open": "Scan directory",
148
- "scan.title": "Add repositories from a directory",
149
- "scan.desc": "Scans up to 6 levels deep for Git repositories and skips ones already added.",
150
- "scan.placeholder": "Directory to scan, e.g. E:\\sunline\\SBG",
151
- "scan.scan": "Scan and add",
152
- "scan.scanning": "Scanning…",
153
- "scan.pick": "Choose directory",
154
- "scan.found": "Found",
155
- "scan.added": "Added",
156
- "scan.empty": "No Git repositories found in that directory.",
157
- "scan.noPicker": "Enter the directory path manually.",
158
- "scan.failed": "Could not scan the directory",
159
- "docs.title": "Local documents",
160
- "docs.desc": "A file or directory; directories are scanned recursively for md, txt, docx, html and similar notes.",
161
- "docs.add": "Add document/directory",
162
- "docs.location": "File or directory path",
163
- "docs.label": "Display name (optional)",
164
- "docs.placeholder": "C:\\notes or ./docs",
165
- "docs.empty": "No documents yet.",
166
- "docs.useCwd": "Use current directory",
167
- "cwd.title": "Working directory",
168
- "cwd.desc": "Relative paths resolve from this directory; an absolute path is allowed.",
169
- "cwd.placeholder": "C:\\work or /home/me/work",
170
- "ai.title": "AI analysis",
171
- "ai.desc": "Use the current default model to analyze commit messages, code changes and document excerpts into concise per-day summaries.",
172
- "ai.enable": "Enable AI analysis",
173
- "ai.model": "Current model: ",
174
- "ai.unavailable": "No default model is available.",
175
- "ai.cost": "This adds generation time and model token usage.",
176
- "progress.start": "Creating the report job…",
177
- "progress.done": "Done. Preparing the download…",
178
- "progress.preview": "Generated. Review, edit, then export the XLSX.",
179
- "progress.hint": "AI analysis calls a model, so it usually takes longer than a plain digest. Keep this page open.",
180
- optional: "optional",
181
- remove: "Remove",
182
- generate: "Generate XLSX report",
183
- generating: "Collecting and generating…",
184
- "save.saving": "Saving settings…",
185
- "save.savingShort": "Saving…",
186
- "save.button": "Save settings",
187
- "save.saved": "Settings saved",
188
- "save.failed": "Could not save settings",
189
- "result.title": "Result",
190
- "result.download": "Download XLSX",
191
- "result.stats.commits": "Git commits",
192
- "result.stats.docs": "Documents",
193
- "result.stats.activeDays": "Days with data",
194
- "result.stats.sources": "Data sources",
195
- "result.unit.commits": "commits",
196
- "result.unit.docs": "documents",
197
- "result.sources": "Source status",
198
- "result.warnings": "Warnings",
199
- "result.ai.title": "AI analysis",
200
- "result.ai.hint": "Per-day AI summaries are included in the AI analysis worksheet.",
201
- "result.ai.failed": "AI analysis did not complete",
202
- "preview.title": "Review before export",
203
- "preview.desc": "Check and edit each day's AI summary, add non-code work, then export the XLSX.",
204
- "preview.export": "Export XLSX",
205
- "preview.expandAll": "Expand all",
206
- "preview.collapseAll": "Collapse all",
207
- "preview.exporting": "Exporting…",
208
- "preview.regenerateDay": "Re-analyze this day",
209
- "preview.regenerating": "Analyzing…",
210
- "preview.overall": "Overall AI summary",
211
- "preview.aiSummary": "AI summary (editable)",
212
- "preview.raw": "Raw digest",
213
- "preview.manual": "Manual items",
214
- "preview.addManual": "Add manual work item",
215
- "preview.manualPlaceholder": "e.g. attended a review, handled an incident",
216
- "preview.noDays": "There is nothing to preview.",
217
- "preview.aiFailed": "AI analysis did not complete; you can still add manual items and export.",
218
- "result.ready": "Generated",
219
- "status.ok": "OK",
220
- "status.warn": "Warning",
221
- "status.error": "Failed",
222
- "error.noSources": "Add at least one Git repository or local document.",
223
- "error.invalidRange": "The date range is invalid. Check the start and end dates.",
224
- "error.rangeTooLong": "The range can cover at most 366 days.",
225
- "error.generate": "Could not generate the report",
226
- "error.network": "Could not reach the local plugin service. Make sure the plugin is enabled.",
227
- "error.save": "Could not save",
228
- "error.allSourcesFailed": "All data sources failed.",
229
- "footer.tip": "Git uses the system git and your existing credentials; remote repositories are cached in the DSH data directory.",
230
- "page.footer": "All processing happens locally; the XLSX contains the daily summary, Git details, document details, sources and notes.",
231
- "misc.unknown": "Unknown error",
232
- "misc.retry": "Retry",
233
- };
234
-
235
- function uid() {
236
- return "s" + Math.random().toString(36).slice(2, 9) + Date.now().toString(36);
237
- }
238
-
239
- function pad2(value) {
240
- return String(value).padStart(2, "0");
241
- }
242
-
243
- function formatDate(date) {
244
- return date.getFullYear() + "-" + pad2(date.getMonth() + 1) + "-" + pad2(date.getDate());
245
- }
246
-
247
- function currentMonthRange() {
248
- const now = new Date();
249
- return {
250
- start: formatDate(new Date(now.getFullYear(), now.getMonth(), 1)),
251
- end: formatDate(new Date(now.getFullYear(), now.getMonth() + 1, 0)),
252
- };
253
- }
254
-
255
- function lastMonthRange() {
256
- const now = new Date();
257
- return {
258
- start: formatDate(new Date(now.getFullYear(), now.getMonth() - 1, 1)),
259
- end: formatDate(new Date(now.getFullYear(), now.getMonth(), 0)),
260
- };
261
- }
262
-
263
- function last7Range() {
264
- const now = new Date();
265
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
266
- return { start: formatDate(start), end: formatDate(now) };
267
- }
268
-
269
- function daysBetween(start, end) {
270
- if (!start || !end) return 0;
271
- const first = new Date(start + "T00:00:00");
272
- const last = new Date(end + "T00:00:00");
273
- if (Number.isNaN(first.getTime()) || Number.isNaN(last.getTime())) return 0;
274
- return Math.round((last.getTime() - first.getTime()) / 86400000) + 1;
275
- }
276
-
277
- function normalizeClientSettings(raw) {
278
- const source = raw && typeof raw === "object" ? raw : {};
279
- const git = Array.isArray(source.git)
280
- ? source.git.map((item) => ({
281
- _key: uid(),
282
- location: typeof item.location === "string" ? item.location : "",
283
- branch: typeof item.branch === "string" ? item.branch : "",
284
- label: typeof item.label === "string" ? item.label : "",
285
- }))
286
- : [];
287
- const docs = Array.isArray(source.docs)
288
- ? source.docs.map((item) => {
289
- const value = typeof item === "string" ? { location: item, label: "" } : item || {};
290
- return {
291
- _key: uid(),
292
- location: typeof value.location === "string" ? value.location : "",
293
- label: typeof value.label === "string" ? value.label : "",
294
- };
295
- })
296
- : [];
297
- return { git, docs, mineOnly: source.mineOnly !== false };
298
- }
299
-
300
- function cleanSettings(settings, aiEnabled) {
301
- return {
302
- git: settings.git
303
- .map((item) => ({
304
- location: item.location.trim(),
305
- branch: item.branch.trim(),
306
- label: item.label.trim(),
307
- }))
308
- .filter((item) => item.location.length > 0),
309
- docs: settings.docs
310
- .map((item) => ({
311
- location: item.location.trim(),
312
- label: item.label.trim(),
313
- }))
314
- .filter((item) => item.location.length > 0),
315
- aiEnabled: aiEnabled !== false,
316
- mineOnly: settings.mineOnly !== false,
317
- };
318
- }
319
-
320
- function decodeBase64Json(value) {
321
- const binary = window.atob(value);
322
- const bytes = new Uint8Array(binary.length);
323
- for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
324
- return JSON.parse(new TextDecoder().decode(bytes));
325
- }
326
-
327
- function triggerDownload(url, filename) {
328
- const link = document.createElement("a");
329
- link.href = url;
330
- link.download = filename;
331
- link.style.display = "none";
332
- document.body.appendChild(link);
333
- link.click();
334
- link.remove();
335
- }
336
-
337
- function currentLocaleId(ctx) {
338
- try {
339
- const snapshot = ctx.locale.getLocale();
340
- return snapshot && typeof snapshot.id === "string" ? snapshot.id : "zh";
341
- } catch {
342
- return "zh";
343
- }
344
- }
345
-
346
- function useTranslate(ctx) {
347
- const [, force] = React.useState(0);
348
- React.useEffect(() => {
349
- return ctx.locale.subscribe(() => force((value) => value + 1));
350
- }, [ctx]);
351
- const bound = ctx.locale.bind(NS);
352
- return function translate(key, fallback) {
353
- const value = bound(key);
354
- if (value === key && typeof fallback === "string") return fallback;
355
- return value;
356
- };
357
- }
358
-
359
- const CSS = [
360
- ".dsdr-root { height: 100%; overflow: auto; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); }",
361
- ".dsdr-page { box-sizing: border-box; max-width: 1080px; margin: 0 auto; padding: 28px 28px 56px; display: flex; flex-direction: column; gap: 16px; }",
362
- ".dsdr-hero { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 14px; }",
363
- ".dsdr-hero-copy { flex: 1; min-width: 0; }",
364
- ".dsdr-hero-action { display: flex; flex: none; flex-direction: column; align-items: flex-end; gap: 5px; margin-left: auto; }",
365
- ".dsdr-hero-buttons { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; }",
366
- ".dsdr-save-btn { min-height: 38px; }",
367
- ".dsdr-generate { min-height: 38px; padding: 9px 18px; font-size: 13px; white-space: nowrap; }",
368
- ".dsdr-save { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
369
- ".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); }",
370
- ".dsdr-title { margin: 0; font-size: 22px; line-height: 30px; font-weight: 600; }",
371
- ".dsdr-sub { margin: 2px 0 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; }",
372
- ".dsdr-card { border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); padding: 16px 18px; }",
373
- ".dsdr-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }",
374
- ".dsdr-card-title { font-size: 14px; line-height: 22px; font-weight: 600; }",
375
- ".dsdr-card-desc { margin-top: 2px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
376
- ".dsdr-range { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: 10px; align-items: end; }",
377
- ".dsdr-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }",
378
- ".dsdr-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
379
- ".dsdr-input { box-sizing: border-box; width: 100%; height: 32px; padding: 4px 9px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; }",
380
- ".dsdr-input:focus { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
381
- ".dsdr-input::placeholder { color: var(--dsw-alias-label-secondary); opacity: .75; }",
382
- ".dsdr-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }",
383
- ".dsdr-btn { box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; font-weight: 500; line-height: 20px; padding: 5px 11px; cursor: pointer; }",
384
- ".dsdr-btn:hover { background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-brand-primary); }",
385
- ".dsdr-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
386
- ".dsdr-btn:disabled { opacity: .7; cursor: not-allowed; }",
387
- ".dsdr-btn-primary { background: #15557f; border-color: #15557f; color: #ffffff; font-weight: 600; box-shadow: 0 2px 6px rgba(0, 0, 0, .18); }",
388
- ".dsdr-btn-primary:hover { background: #103f5f; border-color: #103f5f; opacity: 1; }",
389
- ".dsdr-btn-primary:disabled { background: #5e7c8f; border-color: #5e7c8f; color: #ffffff; opacity: .8; }",
390
- ".dsdr-btn-danger { border-color: transparent; background: transparent; color: var(--dsw-alias-state-error-primary); font-weight: 600; }",
391
- ".dsdr-btn-danger:hover { background: var(--dsw-alias-bg-layer-2); border-color: transparent; }",
392
- ".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; }",
393
- ".dsdr-check input { width: 15px; height: 15px; margin: 0; accent-color: #15557f; cursor: pointer; }",
394
- ".dsdr-check.is-disabled { color: var(--dsw-alias-label-secondary); cursor: not-allowed; opacity: .7; }",
395
- ".dsdr-check.is-disabled input { cursor: not-allowed; }",
396
- ".dsdr-progress-card { display: flex; flex-direction: column; gap: 7px; }",
397
- ".dsdr-progress-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); }",
398
- ".dsdr-progress-value { flex: none; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }",
399
- ".dsdr-progress-track { height: 6px; overflow: hidden; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); }",
400
- ".dsdr-progress-fill { height: 100%; border-radius: 999px; background: #15557f; transition: width .3s ease; }",
401
- ".dsdr-progress-track.is-indeterminate .dsdr-progress-fill { width: 35% !important; animation: dsdr-progress 1.2s ease-in-out infinite; }",
402
- "@keyframes dsdr-progress { 0% { transform: translateX(-110%); } 100% { transform: translateX(320%); } }",
403
- ".dsdr-ai-result { margin-top: 12px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }",
404
- ".dsdr-preview-overall { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }",
405
- ".dsdr-textarea { box-sizing: border-box; width: 100%; min-height: 64px; resize: vertical; padding: 7px 9px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 19px; }",
406
- ".dsdr-textarea:focus { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
407
- ".dsdr-manual-add { display: grid; grid-template-columns: 145px minmax(0, 1fr) auto; gap: 8px; align-items: center; margin: 10px 0 14px; }",
408
- ".dsdr-preview-list { display: flex; flex-direction: column; gap: 10px; }",
409
- ".dsdr-preview-day { display: flex; flex-direction: column; gap: 7px; padding: 11px 12px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }",
410
- ".dsdr-preview-day-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }",
411
- ".dsdr-preview-toggle { display: flex; flex: 1; align-items: center; flex-wrap: wrap; gap: 4px 6px; min-width: 0; padding: 0; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; text-align: left; cursor: pointer; }",
412
- ".dsdr-preview-toggle:hover .dsdr-preview-date { color: var(--dsw-alias-brand-primary); }",
413
- ".dsdr-preview-chevron { flex: none; width: 13px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 20px; }",
414
- ".dsdr-preview-badge { display: inline-flex; align-items: center; padding: 1px 6px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 999px; color: var(--dsw-alias-label-secondary); font-size: 10px; line-height: 16px; }",
415
- ".dsdr-preview-body { display: flex; flex-direction: column; gap: 7px; margin-top: 2px; padding-top: 8px; border-top: 1px dashed var(--dsw-alias-border-l1); }",
416
- ".dsdr-preview-date { font-size: 13px; line-height: 20px; font-weight: 600; }",
417
- ".dsdr-preview-raw { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 19px; }",
418
- ".dsdr-manual-list { display: flex; flex-wrap: wrap; gap: 6px; }",
419
- ".dsdr-manual-chip { display: inline-flex; align-items: center; gap: 5px; max-width: 100%; padding: 2px 6px 2px 9px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 999px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-size: 11px; line-height: 18px; overflow-wrap: anywhere; }",
420
- ".dsdr-manual-remove { border: 0; background: transparent; color: var(--dsw-alias-state-error-primary); font: inherit; font-size: 14px; line-height: 16px; padding: 0 1px; cursor: pointer; }",
421
- ".dsdr-ai-text { margin-top: 5px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 20px; white-space: pre-wrap; }",
422
- ".dsdr-filter-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin: 0 0 10px; }",
423
- ".dsdr-scan { display: flex; flex-direction: column; gap: 7px; margin: 0 0 12px; padding: 12px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }",
424
- ".dsdr-scan-title { font-size: 13px; line-height: 20px; font-weight: 600; }",
425
- ".dsdr-scan-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; }",
426
- ".dsdr-source { display: grid; grid-template-columns: minmax(0, 1.5fr) 150px 34px; gap: 8px; align-items: end; margin-top: 8px; }", ".dsdr-source-docs { grid-template-columns: minmax(0, 1fr) minmax(120px, .35fr) 34px; }",
427
- ".dsdr-icon-btn { width: 32px; height: 32px; padding: 0; display: grid; place-items: center; line-height: 1; font-size: 18px; }",
428
- ".dsdr-hint { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
429
- ".dsdr-error { border: 1px solid var(--dsw-alias-state-error-primary); border-radius: 9px; color: var(--dsw-alias-state-error-primary); background: transparent; padding: 9px 12px; font-size: 12px; line-height: 18px; white-space: pre-wrap; }",
430
- ".dsdr-notice { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
431
- ".dsdr-ok { color: var(--dsw-alias-state-success-primary); }",
432
- ".dsdr-warn { color: var(--dsw-alias-state-warn-primary); }",
433
- ".dsdr-bad { color: var(--dsw-alias-state-error-primary); }",
434
- ".dsdr-empty { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; padding: 10px 0 2px; }",
435
- ".dsdr-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }",
436
- ".dsdr-stat { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 10px 12px; background: var(--dsw-alias-bg-layer-2); }",
437
- ".dsdr-stat-value { font-size: 20px; line-height: 26px; font-weight: 600; }",
438
- ".dsdr-stat-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
439
- ".dsdr-list { display: flex; flex-direction: column; gap: 7px; margin-top: 10px; }",
440
- ".dsdr-row { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; line-height: 18px; }",
441
- ".dsdr-dot { flex: none; width: 7px; height: 7px; margin-top: 6px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }",
442
- ".dsdr-dot.ok { background: var(--dsw-alias-state-success-primary); }",
443
- ".dsdr-dot.warn { background: var(--dsw-alias-state-warn-primary); }",
444
- ".dsdr-dot.error { background: var(--dsw-alias-state-error-primary); }",
445
- ".dsdr-source-name { font-weight: 500; }",
446
- ".dsdr-source-meta { color: var(--dsw-alias-label-secondary); }",
447
- ".dsdr-code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; }",
448
- ".dsdr-footer { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 17px; }",
449
- ".dsdr-spacer { flex: 1; }",
450
- "@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; } }",
451
- ].join(NL);
452
-
453
- function installStyles(ctx) {
454
- const style = document.createElement("style");
455
- style.dataset.plugin = "dsh-plugin-daily-report";
456
- style.textContent = CSS;
457
- document.head.appendChild(style);
458
- ctx.effect(() => () => style.remove(), "daily-report: styles");
459
- }
460
-
461
- function Icon() {
462
- return h(
463
- "svg",
464
- { viewBox: "0 0 64 64", width: 26, height: 26, "aria-hidden": true },
465
- h("rect", { width: 64, height: 64, rx: 14, fill: "#1f4e79" }),
466
- h("path", { d: "M17 19h30a3 3 0 0 1 3 3v26a3 3 0 0 1-3 3H17a3 3 0 0 1-3-3V22a3 3 0 0 1 3-3z", fill: "#f7fbff" }),
467
- h("path", { d: "M14 27h36v4H14z", fill: "#8db9e2" }),
468
- h("path", { d: "M22 14v8M42 14v8", stroke: "#bfe0ff", strokeWidth: 4, strokeLinecap: "round" }),
469
- h("path", { d: "M22 38h7v7h-7zM33 38h7v7h-7zM22 48h7v3h-7z", fill: "#1f7a5c" }),
470
- h("path", { d: "M33 48h9", stroke: "#e0a526", strokeWidth: 3, strokeLinecap: "round" }),
471
- );
472
- }
473
-
474
- function PanelIcon(props) {
475
- const size = Number(props && props.size) > 0 ? Number(props.size) : 20;
476
- return h(
477
- "svg",
478
- {
479
- viewBox: "0 0 24 24",
480
- width: size,
481
- height: size,
482
- fill: "none",
483
- stroke: "currentColor",
484
- strokeWidth: 1.6,
485
- strokeLinecap: "round",
486
- strokeLinejoin: "round",
487
- "aria-hidden": true,
488
- },
489
- h("rect", { x: 3.5, y: 4.5, width: 17, height: 15, rx: 2.5 }),
490
- h("path", { d: "M8 3v3M16 3v3M3.5 9h17" }),
491
- h("path", { d: "M8 13h3M8 16.5h3M14 13h2.5M14 16.5h2.5" }),
492
- );
493
- }
494
-
495
- function DailyReportView(props) {
496
- const ctx = props.ctx;
497
- const t = useTranslate(ctx);
498
- const [settings, setSettings] = React.useState({ git: [], docs: [] });
499
- const [range, setRange] = React.useState(currentMonthRange);
500
- const [cwd, setCwd] = React.useState("");
501
- const [aiInfo, setAiInfo] = React.useState({ available: false, provider: "", model: "" });
502
- const [useAi, setUseAi] = React.useState(true);
503
- const [loading, setLoading] = React.useState(true);
504
- const [loadError, setLoadError] = React.useState("");
505
- const [saveState, setSaveState] = React.useState("idle");
506
- const [error, setError] = React.useState("");
507
- const [generating, setGenerating] = React.useState(false);
508
- const [progress, setProgress] = React.useState(null);
509
- const [result, setResult] = React.useState(null);
510
- const [showScan, setShowScan] = React.useState(false);
511
- const [scanDir, setScanDir] = React.useState("");
512
- const [scanning, setScanning] = React.useState(false);
513
- const [scanMessage, setScanMessage] = React.useState("");
514
- const [scanError, setScanError] = React.useState("");
515
- const [preview, setPreview] = React.useState(null);
516
- const [previewJobId, setPreviewJobId] = React.useState("");
517
- const [exporting, setExporting] = React.useState(false);
518
- const [regenerating, setRegenerating] = React.useState("");
519
- const [manualDate, setManualDate] = React.useState("");
520
- const [manualText, setManualText] = React.useState("");
521
- const [expandedPreviewDays, setExpandedPreviewDays] = React.useState({});
522
- const firstSave = React.useRef(true);
523
- const resultRef = React.useRef(null);
524
- const pollAbortRef = React.useRef(null);
525
- const saveAbortRef = React.useRef(null);
526
- const saveTimerRef = React.useRef(null);
527
- const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
528
- const localeIsEnglish = currentLocaleId(ctx).toLowerCase().startsWith("en");
529
-
530
- React.useEffect(() => {
531
- resultRef.current = result;
532
- }, [result]);
533
-
534
- React.useEffect(() => {
535
- return () => {
536
- if (pollAbortRef.current) pollAbortRef.current.abort();
537
- if (saveAbortRef.current) saveAbortRef.current.abort();
538
- if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
539
- if (resultRef.current && resultRef.current.url) window.URL.revokeObjectURL(resultRef.current.url);
540
- };
541
- }, []);
542
-
543
- React.useEffect(() => {
544
- let alive = true;
545
- const query = sessionId.length > 0 ? "?sessionId=" + encodeURIComponent(sessionId) : "";
546
- fetch(API + "/state" + query, { headers: { accept: "application/json" } })
547
- .then((response) => {
548
- if (!response.ok) throw new Error("state-failed");
549
- return response.json();
550
- })
551
- .then((payload) => {
552
- if (!alive) return;
553
- setSettings(normalizeClientSettings(payload && payload.settings));
554
- setUseAi(!(payload && payload.settings && payload.settings.aiEnabled === false));
555
- setCwd(payload && typeof payload.cwd === "string" ? payload.cwd : "");
556
- const nextAi = payload && payload.ai ? payload.ai : { available: false, provider: "", model: "" };
557
- setAiInfo({
558
- available: nextAi.available === true,
559
- provider: typeof nextAi.provider === "string" ? nextAi.provider : "",
560
- model: typeof nextAi.model === "string" ? nextAi.model : "",
561
- });
562
- if (payload && payload.defaults && payload.defaults.start && payload.defaults.end) {
563
- setRange({ start: payload.defaults.start, end: payload.defaults.end });
564
- }
565
- setLoadError(payload && payload.warning ? payload.warning : "");
566
- })
567
- .catch(() => {
568
- if (!alive) return;
569
- setLoadError(t("error.network", "无法连接本地服务,请确认插件已启用。"));
570
- })
571
- .finally(() => {
572
- if (alive) setLoading(false);
573
- });
574
- return () => {
575
- alive = false;
576
- };
577
- }, [sessionId]);
578
-
579
- React.useEffect(() => {
580
- if (loading) return;
581
- if (firstSave.current) {
582
- firstSave.current = false;
583
- return;
584
- }
585
- if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
586
- saveTimerRef.current = window.setTimeout(() => {
587
- saveTimerRef.current = null;
588
- persistSettings();
589
- }, 650);
590
- return () => {
591
- if (saveTimerRef.current) {
592
- window.clearTimeout(saveTimerRef.current);
593
- saveTimerRef.current = null;
594
- }
595
- };
596
- }, [settings, useAi, loading]);
597
-
598
- function updateGit(index, patch) {
599
- setSettings((previous) => ({
600
- ...previous,
601
- git: previous.git.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)),
602
- }));
603
- }
604
-
605
- function updateDoc(index, patch) {
606
- setSettings((previous) => ({
607
- ...previous,
608
- docs: previous.docs.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)),
609
- }));
610
- }
611
-
612
- function addGit() {
613
- setSettings((previous) => ({
614
- ...previous,
615
- git: [...previous.git, { _key: uid(), location: "", branch: "", label: "" }],
616
- }));
617
- }
618
-
619
- function addDoc() {
620
- setSettings((previous) => ({
621
- ...previous,
622
- docs: [...previous.docs, { _key: uid(), location: "", label: "" }],
623
- }));
624
- }
625
-
626
- function removeGit(index) {
627
- setSettings((previous) => ({ ...previous, git: previous.git.filter((_, itemIndex) => itemIndex !== index) }));
628
- }
629
-
630
- function removeDoc(index) {
631
- setSettings((previous) => ({ ...previous, docs: previous.docs.filter((_, itemIndex) => itemIndex !== index) }));
632
- }
633
-
634
- function useCurrentDirectory() {
635
- const value = cwd || ".";
636
- setSettings((previous) => ({
637
- ...previous,
638
- docs: [...previous.docs, { _key: uid(), location: value, label: "" }],
639
- }));
640
- }
641
-
642
- async function persistSettings() {
643
- if (saveAbortRef.current) saveAbortRef.current.abort();
644
- const controller = new AbortController();
645
- saveAbortRef.current = controller;
646
- setSaveState("saving");
647
- try {
648
- const response = await fetch(API + "/state", {
649
- method: "PUT",
650
- headers: { "content-type": "application/json" },
651
- body: JSON.stringify({ settings: cleanSettings(settings, useAi) }),
652
- signal: controller.signal,
653
- });
654
- if (!response.ok) throw new Error("save-failed");
655
- setSaveState("saved");
656
- } catch (err) {
657
- if (err && err.name === "AbortError") return;
658
- setSaveState("failed");
659
- } finally {
660
- if (saveAbortRef.current === controller) saveAbortRef.current = null;
661
- }
662
- }
663
-
664
- function saveSettingsNow() {
665
- if (saveTimerRef.current) {
666
- window.clearTimeout(saveTimerRef.current);
667
- saveTimerRef.current = null;
668
- }
669
- persistSettings();
670
- }
671
-
672
- function toggleScan() {
673
- const next = !showScan;
674
- setShowScan(next);
675
- if (next && scanDir.trim().length === 0) setScanDir(cwd || "");
676
- setScanError("");
677
- setScanMessage("");
678
- }
679
-
680
- async function pickScanDirectory() {
681
- const service = ctx.get("uiWorkspace");
682
- if (!service || typeof service.pickDirectory !== "function") {
683
- setScanError(t("scan.noPicker", "请手动输入目录路径。"));
684
- return;
685
- }
686
- try {
687
- const selected = await service.pickDirectory();
688
- if (typeof selected === "string" && selected.trim().length > 0) {
689
- setScanDir(selected);
690
- setScanError("");
691
- }
692
- } catch (err) {
693
- setScanError(err && err.message ? err.message : t("scan.failed", "扫描目录失败"));
694
- }
695
- }
696
-
697
- async function scanAndAdd() {
698
- const directory = scanDir.trim();
699
- if (directory.length === 0) {
700
- setScanError(t("scan.placeholder", "要扫描的目录,例如 E:\\sunline\\SBG"));
701
- return;
702
- }
703
- if (scanning) return;
704
- setScanning(true);
705
- setScanError("");
706
- setScanMessage("");
707
- try {
708
- const response = await fetch(API + "/scan", {
709
- method: "POST",
710
- headers: { "content-type": "application/json", accept: "application/json" },
711
- body: JSON.stringify({ directory, cwd: cwd.trim() }),
712
- });
713
- if (!response.ok) {
714
- let payload = null;
715
- try {
716
- payload = await response.json();
717
- } catch {
718
- payload = null;
719
- }
720
- throw new Error(errorMessageFromPayload(payload));
721
- }
722
- const payload = await response.json();
723
- const repos = Array.isArray(payload.repos) ? payload.repos : [];
724
- const existing = new Set(settings.git.map((item) => item.location.toLowerCase() + "\u0000" + item.branch));
725
- const additions = [];
726
- for (const repo of repos) {
727
- if (!repo || typeof repo.location !== "string" || repo.location.length === 0) continue;
728
- const key = repo.location.toLowerCase() + "\u0000" + (repo.branch || "");
729
- if (existing.has(key)) continue;
730
- existing.add(key);
731
- additions.push({
732
- _key: uid(),
733
- location: repo.location,
734
- branch: typeof repo.branch === "string" ? repo.branch : "",
735
- label: typeof repo.label === "string" ? repo.label : "",
736
- });
737
- }
738
- if (additions.length > 0) {
739
- setSettings((previous) => ({ ...previous, git: [...previous.git, ...additions] }));
740
- }
741
- const warnings = Array.isArray(payload.warnings) ? payload.warnings : [];
742
- setScanMessage(
743
- repos.length === 0
744
- ? t("scan.empty", "该目录下没有发现 Git 仓库。")
745
- : t("scan.found", "发现") + " " + repos.length + " · " + t("scan.added", "新增") + " " + additions.length + (warnings.length > 0 ? " · " + warnings.join(" ") : ""),
746
- );
747
- } catch (err) {
748
- setScanError(err && err.message ? err.message : t("scan.failed", "扫描目录失败"));
749
- } finally {
750
- setScanning(false);
751
- }
752
- }
753
-
754
- function updatePreviewDay(date, patch) {
755
- setPreview((previous) => {
756
- if (!previous) return previous;
757
- return {
758
- ...previous,
759
- days: previous.days.map((day) => (day.date === date ? { ...day, ...patch } : day)),
760
- };
761
- });
762
- }
763
-
764
- function addManualItem() {
765
- const text = manualText.trim();
766
- const date = manualDate;
767
- if (text.length === 0 || date.length === 0 || !preview) return;
768
- setPreview((previous) => {
769
- if (!previous) return previous;
770
- return {
771
- ...previous,
772
- days: previous.days.map((day) =>
773
- day.date === date ? { ...day, manualItems: [...(day.manualItems || []), text] } : day,
774
- ),
775
- };
776
- });
777
- setExpandedPreviewDays((previous) => ({ ...previous, [date]: true }));
778
- setManualText("");
779
- }
780
-
781
- function removeManualItem(date, index) {
782
- setPreview((previous) => {
783
- if (!previous) return previous;
784
- return {
785
- ...previous,
786
- days: previous.days.map((day) =>
787
- day.date === date
788
- ? { ...day, manualItems: (day.manualItems || []).filter((_, itemIndex) => itemIndex !== index) }
789
- : day,
790
- ),
791
- };
792
- });
793
- }
794
-
795
- function togglePreviewDay(date) {
796
- setExpandedPreviewDays((previous) => ({ ...previous, [date]: !previous[date] }));
797
- }
798
-
799
- function setAllPreviewExpanded(expand) {
800
- if (!preview) return;
801
- if (!expand) {
802
- setExpandedPreviewDays({});
803
- return;
804
- }
805
- const next = {};
806
- for (const day of preview.days) {
807
- if (day.commitCount > 0 || day.docCount > 0 || (day.manualItems || []).length > 0 || (day.aiSummary || "").length > 0) {
808
- next[day.date] = true;
809
- }
810
- }
811
- setExpandedPreviewDays(next);
812
- }
813
-
814
- function previewEdits() {
815
- if (!preview) return { overall: "", days: [] };
816
- return {
817
- overall: preview.ai ? preview.ai.overall || "" : "",
818
- days: preview.days.map((day) => ({
819
- date: day.date,
820
- aiSummary: day.aiSummary || "",
821
- aiHighlights: day.aiHighlights || [],
822
- aiCategories: day.aiCategories || [],
823
- manualItems: day.manualItems || [],
824
- })),
825
- };
826
- }
827
-
828
- async function exportPreview() {
829
- if (!preview || previewJobId.length === 0 || exporting) return;
830
- setExporting(true);
831
- setError("");
832
- try {
833
- const response = await fetch(API + "/job/export", {
834
- method: "POST",
835
- headers: { "content-type": "application/json", accept: "application/octet-stream" },
836
- body: JSON.stringify({ jobId: previewJobId, edits: previewEdits() }),
837
- });
838
- if (!response.ok) {
839
- let payload = null;
840
- try {
841
- payload = await response.json();
842
- } catch {
843
- payload = null;
844
- }
845
- throw new Error(errorMessageFromPayload(payload));
846
- }
847
- const summaryHeader = response.headers.get("x-report-summary");
848
- const summary = summaryHeader ? decodeBase64Json(summaryHeader) : {};
849
- const filenameHeader = response.headers.get("x-report-filename");
850
- const filename = filenameHeader
851
- ? decodeURIComponent(filenameHeader)
852
- : "日报汇总_" + preview.range.start + "_" + preview.range.end + ".xlsx";
853
- const blob = await response.blob();
854
- const url = window.URL.createObjectURL(blob);
855
- const next = Object.assign({}, summary, { filename, url, size: blob.size });
856
- if (resultRef.current && resultRef.current.url) window.URL.revokeObjectURL(resultRef.current.url);
857
- setResult(next);
858
- triggerDownload(url, filename);
859
- } catch (err) {
860
- setError(err && err.message ? err.message : t("error.generate", "导出失败"));
861
- } finally {
862
- setExporting(false);
863
- }
864
- }
865
-
866
- async function regeneratePreviewDay(date) {
867
- if (!preview || !preview.ai || !preview.ai.ok || regenerating.length > 0) return;
868
- const day = preview.days.find((item) => item.date === date);
869
- if (!day) return;
870
- setRegenerating(date);
871
- setError("");
872
- try {
873
- const response = await fetch(API + "/job/regenerate-day", {
874
- method: "POST",
875
- headers: { "content-type": "application/json", accept: "application/json" },
876
- body: JSON.stringify({ jobId: previewJobId, date, manualItems: day.manualItems || [] }),
877
- });
878
- if (!response.ok) {
879
- let payload = null;
880
- try {
881
- payload = await response.json();
882
- } catch {
883
- payload = null;
884
- }
885
- throw new Error(errorMessageFromPayload(payload));
886
- }
887
- const payload = await response.json();
888
- if (!payload.ok || !payload.day) throw new Error(payload.error || t("error.generate", "重新分析失败"));
889
- updatePreviewDay(date, {
890
- aiSummary: payload.day.summary || "",
891
- aiHighlights: payload.day.highlights || [],
892
- aiCategories: payload.day.categories || [],
893
- });
894
- } catch (err) {
895
- setError(err && err.message ? err.message : t("error.generate", "重新分析失败"));
896
- } finally {
897
- setRegenerating("");
898
- }
899
- }
900
-
901
- function errorMessageFromPayload(payload) {
902
- const code = payload && payload.error ? payload.error.code : "";
903
- const serverMessage = payload && payload.error ? payload.error.message : "";
904
- const messages = {
905
- "no-sources": t("error.noSources", "请至少添加一个 Git 仓库或本地文档。"),
906
- "invalid-range": t("error.invalidRange", "日期范围无效,请检查开始和结束日期。"),
907
- "range-too-long": t("error.rangeTooLong", "时间范围最长支持 366 天。"),
908
- "all-sources-failed": t("error.allSourcesFailed", "所有数据源都读取失败。"),
909
- "generate-failed": t("error.generate", "生成失败"),
910
- };
911
- return messages[code] || serverMessage || t("error.generate", "生成失败");
912
- }
913
-
914
- function wait(ms, signal) {
915
- return new Promise((resolve, reject) => {
916
- const timer = window.setTimeout(() => {
917
- signal.removeEventListener("abort", onAbort);
918
- resolve();
919
- }, ms);
920
- const onAbort = () => {
921
- window.clearTimeout(timer);
922
- const error = new Error("aborted");
923
- error.name = "AbortError";
924
- reject(error);
925
- };
926
- if (signal.aborted) {
927
- onAbort();
928
- return;
929
- }
930
- signal.addEventListener("abort", onAbort, { once: true });
931
- });
932
- }
933
-
934
- async function generate() {
935
- if (generating) return;
936
- const cleaned = cleanSettings(settings, useAi);
937
- if (cleaned.git.length === 0 && cleaned.docs.length === 0) {
938
- setError(t("error.noSources", "请至少添加一个 Git 仓库或本地文档。"));
939
- return;
940
- }
941
- const totalDays = daysBetween(range.start, range.end);
942
- if (!range.start || !range.end || totalDays <= 0) {
943
- setError(t("error.invalidRange", "日期范围无效,请检查开始和结束日期。"));
944
- return;
945
- }
946
- if (totalDays > 366) {
947
- setError(t("error.rangeTooLong", "时间范围最长支持 366 天。"));
948
- return;
949
- }
950
- setError("");
951
- setPreview(null);
952
- setPreviewJobId("");
953
- if (resultRef.current && resultRef.current.url) window.URL.revokeObjectURL(resultRef.current.url);
954
- setResult(null);
955
- setGenerating(true);
956
- setProgress({ percent: 1, indeterminate: false, message: t("progress.start", "正在创建生成任务…") });
957
- if (pollAbortRef.current) pollAbortRef.current.abort();
958
- const controller = new AbortController();
959
- pollAbortRef.current = controller;
960
- try {
961
- const startResponse = await fetch(API + "/generate/start", {
962
- method: "POST",
963
- headers: { "content-type": "application/json", accept: "application/json" },
964
- body: JSON.stringify({
965
- settings: cleaned,
966
- start: range.start,
967
- end: range.end,
968
- sessionId,
969
- cwd: cwd.trim(),
970
- ai: useAi && aiInfo.available,
971
- locale: currentLocaleId(ctx).toLowerCase().startsWith("en") ? "en" : "zh",
972
- }),
973
- signal: controller.signal,
974
- });
975
- if (!startResponse.ok) {
976
- let payload = null;
977
- try {
978
- payload = await startResponse.json();
979
- } catch {
980
- payload = null;
981
- }
982
- throw new Error(errorMessageFromPayload(payload));
983
- }
984
- const started = await startResponse.json();
985
- const jobId = started && typeof started.jobId === "string" ? started.jobId : "";
986
- if (jobId.length === 0) throw new Error(t("error.generate", "生成失败"));
987
- while (true) {
988
- if (controller.signal.aborted) {
989
- const aborted = new Error("aborted");
990
- aborted.name = "AbortError";
991
- throw aborted;
992
- }
993
- const statusResponse = await fetch(API + "/job?jobId=" + encodeURIComponent(jobId), {
994
- headers: { accept: "application/json" },
995
- signal: controller.signal,
996
- });
997
- if (!statusResponse.ok) {
998
- let payload = null;
999
- try {
1000
- payload = await statusResponse.json();
1001
- } catch {
1002
- payload = null;
1003
- }
1004
- throw new Error(errorMessageFromPayload(payload));
1005
- }
1006
- const job = await statusResponse.json();
1007
- if (job.progress) {
1008
- setProgress({
1009
- percent: Number(job.progress.percent) || 0,
1010
- indeterminate: job.progress.indeterminate === true,
1011
- message: job.progress.message || t("generating", "正在收集并生成…"),
1012
- });
1013
- }
1014
- if (job.status === "done") break;
1015
- if (job.status === "failed") throw new Error(errorMessageFromPayload({ error: job.error }));
1016
- if (job.status === "cancelled") {
1017
- const aborted = new Error("aborted");
1018
- aborted.name = "AbortError";
1019
- throw aborted;
1020
- }
1021
- await wait(650, controller.signal);
1022
- }
1023
- const previewResponse = await fetch(API + "/job/preview?jobId=" + encodeURIComponent(jobId), {
1024
- headers: { accept: "application/json" },
1025
- signal: controller.signal,
1026
- });
1027
- if (!previewResponse.ok) {
1028
- let payload = null;
1029
- try {
1030
- payload = await previewResponse.json();
1031
- } catch {
1032
- payload = null;
1033
- }
1034
- throw new Error(errorMessageFromPayload(payload));
1035
- }
1036
- const previewData = await previewResponse.json();
1037
- setPreview(previewData);
1038
- setPreviewJobId(jobId);
1039
- setExpandedPreviewDays({});
1040
- const firstDay = (previewData.days || []).find((day) => day.commitCount > 0 || day.docCount > 0 || (day.manualItems || []).length > 0) || (previewData.days || [])[0];
1041
- setManualDate(firstDay ? firstDay.date : range.start);
1042
- setProgress({ percent: 100, indeterminate: false, message: t("progress.preview", "生成完成,请预览并编辑后导出 XLSX。") });
1043
- } catch (err) {
1044
- if (err && err.name === "AbortError") return;
1045
- setError(err && err.message ? err.message : t("error.generate", "生成失败"));
1046
- } finally {
1047
- if (pollAbortRef.current === controller) pollAbortRef.current = null;
1048
- setGenerating(false);
1049
- }
1050
- }
1051
-
1052
- const rangeDays = daysBetween(range.start, range.end);
1053
- const previewDays = preview
1054
- ? preview.days.filter((day) => day.commitCount > 0 || day.docCount > 0 || (day.manualItems || []).length > 0 || (day.aiSummary || "").length > 0)
1055
- : [];
1056
- const allPreviewExpanded = previewDays.length > 0 && previewDays.every((day) => expandedPreviewDays[day.date] === true);
1057
- const saveText =
1058
- saveState === "saving"
1059
- ? t("save.saving", "正在保存设置…")
1060
- : saveState === "saved"
1061
- ? t("save.saved", "设置已保存")
1062
- : saveState === "failed"
1063
- ? t("save.failed", "设置保存失败")
1064
- : "";
1065
-
1066
- if (loading) {
1067
- return h("div", { className: "dsdr-root" }, h("div", { className: "dsdr-page" }, h("div", { className: "dsdr-card dsdr-hint" }, t("loading", "正在读取配置…"))));
1068
- }
1069
-
1070
- return h(
1071
- "div",
1072
- { className: "dsdr-root" },
1073
- h(
1074
- "div",
1075
- { className: "dsdr-page" },
1076
- h(
1077
- "div",
1078
- { className: "dsdr-hero" },
1079
- h("div", { className: "dsdr-mark" }, h(Icon)),
1080
- h(
1081
- "div",
1082
- { className: "dsdr-hero-copy" },
1083
- h("h1", { className: "dsdr-title" }, t("page.title", "日报汇总")),
1084
- h("p", { className: "dsdr-sub" }, t("page.desc", "按天汇总 Git 提交与本地文档,导出可直接查看的 XLSX 日报。")),
1085
- ),
1086
- h(
1087
- "div",
1088
- { className: "dsdr-hero-action" },
1089
- h(
1090
- "div",
1091
- { className: "dsdr-hero-buttons" },
1092
- h(
1093
- "button",
1094
- {
1095
- type: "button",
1096
- className: "dsdr-btn dsdr-save-btn",
1097
- onClick: saveSettingsNow,
1098
- disabled: saveState === "saving",
1099
- },
1100
- saveState === "saving" ? t("save.savingShort", "正在保存…") : t("save.button", "保存设置"),
1101
- ),
1102
- h(
1103
- "button",
1104
- { type: "button", className: "dsdr-btn dsdr-btn-primary dsdr-generate", onClick: generate, disabled: generating },
1105
- generating ? t("generating", "正在收集并生成…") : t("generate", "生成 XLSX 日报"),
1106
- ),
1107
- ),
1108
- saveText ? h("div", { className: "dsdr-save" }, saveText) : null,
1109
- ),
1110
- ),
1111
-
1112
- generating
1113
- ? h(
1114
- "section",
1115
- { className: "dsdr-card dsdr-progress-card", role: "status", "aria-live": "polite" },
1116
- h(
1117
- "div",
1118
- { className: "dsdr-progress-head" },
1119
- h("span", null, (progress && progress.message) || t("generating", "正在收集并生成…")),
1120
- h("span", { className: "dsdr-progress-value" }, progress && !progress.indeterminate ? Math.round(Number(progress.percent) || 0) + "%" : "…"),
1121
- ),
1122
- h(
1123
- "div",
1124
- { className: "dsdr-progress-track" + (progress && progress.indeterminate ? " is-indeterminate" : "") },
1125
- h("div", {
1126
- className: "dsdr-progress-fill",
1127
- style: { width: progress && progress.indeterminate ? "35%" : Math.max(2, Math.min(100, Number(progress && progress.percent) || 0)) + "%" },
1128
- }),
1129
- ),
1130
- h("div", { className: "dsdr-hint" }, t("progress.hint", "AI 分析需要调用模型,耗时通常比普通汇总更长;请保持页面打开。")),
1131
- )
1132
- : null,
1133
-
1134
- loadError ? h("div", { className: "dsdr-error" }, loadError) : null,
1135
- error ? h("div", { className: "dsdr-error", role: "alert" }, error) : null,
1136
-
1137
- preview
1138
- ? h(
1139
- "section",
1140
- { className: "dsdr-card dsdr-preview-card" },
1141
- h(
1142
- "div",
1143
- { className: "dsdr-card-head" },
1144
- h(
1145
- "div",
1146
- null,
1147
- h("div", { className: "dsdr-card-title" }, t("preview.title", "生成结果预览")),
1148
- h("div", { className: "dsdr-card-desc" }, t("preview.desc", "先检查并编辑每日 AI 摘要、补充非代码工作,再导出 XLSX。")),
1149
- ),
1150
- h(
1151
- "div",
1152
- { className: "dsdr-actions" },
1153
- h(
1154
- "button",
1155
- {
1156
- type: "button",
1157
- className: "dsdr-btn",
1158
- onClick: () => setAllPreviewExpanded(!allPreviewExpanded),
1159
- disabled: previewDays.length === 0 || exporting,
1160
- },
1161
- allPreviewExpanded ? t("preview.collapseAll", "全部收起") : t("preview.expandAll", "全部展开"),
1162
- ),
1163
- h(
1164
- "button",
1165
- { type: "button", className: "dsdr-btn dsdr-btn-primary", onClick: exportPreview, disabled: exporting || regenerating.length > 0 },
1166
- exporting ? t("preview.exporting", "正在导出…") : t("preview.export", "导出 XLSX"),
1167
- ),
1168
- ),
1169
- ),
1170
- preview.ai && preview.ai.ok
1171
- ? h(
1172
- "div",
1173
- { className: "dsdr-preview-overall" },
1174
- h("label", { className: "dsdr-label", htmlFor: "dsdr-preview-overall" }, t("preview.overall", "总体 AI 总结")),
1175
- h("textarea", {
1176
- id: "dsdr-preview-overall",
1177
- className: "dsdr-textarea",
1178
- value: preview.ai.overall || "",
1179
- onChange: (event) => setPreview((previous) => (previous ? { ...previous, ai: { ...previous.ai, overall: event.target.value } } : previous)),
1180
- }),
1181
- )
1182
- : preview.ai
1183
- ? h("div", { className: "dsdr-error" }, preview.ai.error || t("preview.aiFailed", "AI 分析未完成,仍可补充手动工作项后导出。"))
1184
- : null,
1185
- h(
1186
- "div",
1187
- { className: "dsdr-manual-add" },
1188
- h("input", {
1189
- type: "date",
1190
- className: "dsdr-input",
1191
- value: manualDate,
1192
- min: preview.range.start,
1193
- max: preview.range.end,
1194
- onChange: (event) => setManualDate(event.target.value),
1195
- }),
1196
- h("input", {
1197
- className: "dsdr-input",
1198
- value: manualText,
1199
- placeholder: t("preview.manualPlaceholder", "例如:参加需求评审、处理线上问题"),
1200
- onChange: (event) => setManualText(event.target.value),
1201
- onKeyDown: (event) => {
1202
- if (event.key === "Enter") addManualItem();
1203
- },
1204
- }),
1205
- h("button", { type: "button", className: "dsdr-btn", onClick: addManualItem, disabled: manualText.trim().length === 0 }, t("preview.addManual", "添加手动工作项")),
1206
- ),
1207
- previewDays.length === 0
1208
- ? h("div", { className: "dsdr-empty" }, t("preview.noDays", "没有可预览的日报内容。"))
1209
- : h(
1210
- "div",
1211
- { className: "dsdr-preview-list" },
1212
- previewDays.map((day) => {
1213
- const expanded = expandedPreviewDays[day.date] === true;
1214
- return h(
1215
- "div",
1216
- { className: "dsdr-preview-day" + (expanded ? " is-expanded" : ""), key: day.date },
1217
- h(
1218
- "div",
1219
- { className: "dsdr-preview-day-head" },
1220
- h(
1221
- "button",
1222
- {
1223
- type: "button",
1224
- className: "dsdr-preview-toggle",
1225
- "aria-expanded": expanded,
1226
- onClick: () => togglePreviewDay(day.date),
1227
- },
1228
- h("span", { className: "dsdr-preview-chevron" }, expanded ? "▾" : "▸"),
1229
- h("span", { className: "dsdr-preview-date" }, day.date),
1230
- h("span", { className: "dsdr-hint" }, " · " + day.weekday + " · " + day.commitCount + " commits · " + day.docCount + " docs"),
1231
- day.manualItems && day.manualItems.length > 0
1232
- ? h("span", { className: "dsdr-preview-badge" }, day.manualItems.length + " " + t("preview.manual", "手动补充"))
1233
- : null,
1234
- ),
1235
- preview.ai && preview.ai.ok
1236
- ? h(
1237
- "button",
1238
- {
1239
- type: "button",
1240
- className: "dsdr-btn",
1241
- onClick: () => regeneratePreviewDay(day.date),
1242
- disabled: regenerating.length > 0 || exporting,
1243
- },
1244
- regenerating === day.date ? t("preview.regenerating", "分析中…") : t("preview.regenerateDay", "重新分析这天"),
1245
- )
1246
- : null,
1247
- ),
1248
- expanded
1249
- ? h(
1250
- "div",
1251
- { className: "dsdr-preview-body" },
1252
- h("div", { className: "dsdr-preview-raw" }, t("preview.raw", "原始汇总") + ":" + day.summary),
1253
- preview.ai && preview.ai.ok
1254
- ? h("textarea", {
1255
- className: "dsdr-textarea",
1256
- value: day.aiSummary || "",
1257
- placeholder: t("preview.aiSummary", "AI 摘要(可编辑)"),
1258
- onChange: (event) => updatePreviewDay(day.date, { aiSummary: event.target.value }),
1259
- })
1260
- : null,
1261
- day.aiHighlights && day.aiHighlights.length > 0
1262
- ? h("div", { className: "dsdr-hint" }, "• " + day.aiHighlights.join(";"))
1263
- : null,
1264
- day.manualItems && day.manualItems.length > 0
1265
- ? h(
1266
- "div",
1267
- { className: "dsdr-manual-list" },
1268
- day.manualItems.map((text, index) =>
1269
- h(
1270
- "span",
1271
- { className: "dsdr-manual-chip", key: String(index) },
1272
- text,
1273
- h(
1274
- "button",
1275
- {
1276
- type: "button",
1277
- className: "dsdr-manual-remove",
1278
- "aria-label": t("remove", "移除"),
1279
- onClick: () => removeManualItem(day.date, index),
1280
- },
1281
- "×",
1282
- ),
1283
- ),
1284
- ),
1285
- )
1286
- : null,
1287
- )
1288
- : null,
1289
- );
1290
- }),
1291
- ),
1292
- )
1293
- : null,
1294
-
1295
- h(
1296
- "section",
1297
- { className: "dsdr-card" },
1298
- h(
1299
- "div",
1300
- { className: "dsdr-card-head" },
1301
- h(
1302
- "div",
1303
- null,
1304
- h("div", { className: "dsdr-card-title" }, t("range.title", "汇总时间范围")),
1305
- h("div", { className: "dsdr-card-desc" }, t("range.desc", "默认当月月初到月底;可改为任意日期范围。")),
1306
- ),
1307
- h("div", { className: "dsdr-hint" }, rangeDays > 0 ? t("range.total", "共") + " " + rangeDays + " " + t("range.days", "天") : ""),
1308
- ),
1309
- h(
1310
- "div",
1311
- { className: "dsdr-range" },
1312
- h(
1313
- "div",
1314
- { className: "dsdr-field" },
1315
- h("label", { className: "dsdr-label", htmlFor: "dsdr-start" }, t("range.start", "开始日期")),
1316
- h("input", {
1317
- id: "dsdr-start",
1318
- className: "dsdr-input",
1319
- type: "date",
1320
- value: range.start,
1321
- onChange: (event) => setRange((previous) => ({ ...previous, start: event.target.value })),
1322
- }),
1323
- ),
1324
- h(
1325
- "div",
1326
- { className: "dsdr-field" },
1327
- h("label", { className: "dsdr-label", htmlFor: "dsdr-end" }, t("range.end", "结束日期")),
1328
- h("input", {
1329
- id: "dsdr-end",
1330
- className: "dsdr-input",
1331
- type: "date",
1332
- value: range.end,
1333
- onChange: (event) => setRange((previous) => ({ ...previous, end: event.target.value })),
1334
- }),
1335
- ),
1336
- h(
1337
- "div",
1338
- { className: "dsdr-actions" },
1339
- h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(currentMonthRange()) }, t("range.thisMonth", "本月")),
1340
- h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(lastMonthRange()) }, t("range.lastMonth", "上月")),
1341
- h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(last7Range()) }, t("range.last7", "最近 7 天")),
1342
- ),
1343
- ),
1344
- ),
1345
-
1346
- h(
1347
- "section",
1348
- { className: "dsdr-card" },
1349
- h(
1350
- "div",
1351
- { className: "dsdr-card-head" },
1352
- h(
1353
- "div",
1354
- null,
1355
- h("div", { className: "dsdr-card-title" }, t("cwd.title", "工作目录")),
1356
- h("div", { className: "dsdr-card-desc" }, t("cwd.desc", "相对路径以此目录为基准;可填写绝对路径。")),
1357
- ),
1358
- ),
1359
- h("input", {
1360
- id: "dsdr-cwd",
1361
- className: "dsdr-input",
1362
- value: cwd,
1363
- placeholder: t("cwd.placeholder", "C:\\work 或 /home/me/work"),
1364
- onChange: (event) => setCwd(event.target.value),
1365
- }),
1366
- ),
1367
-
1368
- h(
1369
- "section",
1370
- { className: "dsdr-card" },
1371
- h(
1372
- "div",
1373
- { className: "dsdr-card-head" },
1374
- h(
1375
- "div",
1376
- null,
1377
- h("div", { className: "dsdr-card-title" }, t("ai.title", "AI 分析")),
1378
- h("div", { className: "dsdr-card-desc" }, t("ai.desc", "使用当前默认模型分析提交说明、代码变更和文档摘要,生成按天简要总结。")),
1379
- ),
1380
- h(
1381
- "label",
1382
- { className: "dsdr-check" + (aiInfo.available ? "" : " is-disabled") },
1383
- h("input", {
1384
- type: "checkbox",
1385
- checked: useAi,
1386
- disabled: !aiInfo.available,
1387
- onChange: (event) => setUseAi(event.target.checked),
1388
- }),
1389
- h("span", null, t("ai.enable", "启用 AI 分析")),
1390
- ),
1391
- ),
1392
- h(
1393
- "div",
1394
- { className: "dsdr-hint" },
1395
- aiInfo.available
1396
- ? t("ai.model", "当前模型:") + aiInfo.provider + "/" + aiInfo.model + " · " + t("ai.cost", "会增加生成时间和模型 token 消耗。")
1397
- : t("ai.unavailable", "当前没有可用的默认模型。"),
1398
- ),
1399
- ),
1400
-
1401
- h(
1402
- "section",
1403
- { className: "dsdr-card" },
1404
- h(
1405
- "div",
1406
- { className: "dsdr-card-head" },
1407
- h(
1408
- "div",
1409
- null,
1410
- h("div", { className: "dsdr-card-title" }, t("git.title", "Git 仓库")),
1411
- h("div", { className: "dsdr-card-desc" }, t("git.desc", "支持远程地址或本地仓库;多个仓库、多个分支会合并到同一份日报。")),
1412
- ),
1413
- h(
1414
- "div",
1415
- { className: "dsdr-actions" },
1416
- h("button", { type: "button", className: "dsdr-btn", onClick: toggleScan }, t("scan.open", "扫描目录")),
1417
- h("button", { type: "button", className: "dsdr-btn", onClick: addGit }, "+ " + t("git.add", "添加仓库")),
1418
- ),
1419
- ),
1420
- h(
1421
- "div",
1422
- { className: "dsdr-filter-row" },
1423
- h(
1424
- "label",
1425
- { className: "dsdr-check" },
1426
- h("input", {
1427
- type: "checkbox",
1428
- checked: settings.mineOnly !== false,
1429
- onChange: (event) => setSettings((previous) => ({ ...previous, mineOnly: event.target.checked })),
1430
- }),
1431
- h("span", null, t("git.mine", "仅统计我的提交")),
1432
- ),
1433
- h("span", { className: "dsdr-hint" }, t("git.mineHint", "按各仓库 git config user.email / user.name 过滤")),
1434
- ),
1435
- showScan
1436
- ? h(
1437
- "div",
1438
- { className: "dsdr-scan" },
1439
- h("div", { className: "dsdr-scan-title" }, t("scan.title", "从目录批量添加")),
1440
- h("div", { className: "dsdr-card-desc" }, t("scan.desc", "扫描目录下最多 6 层的 Git 仓库,已存在的会自动跳过。")),
1441
- h(
1442
- "div",
1443
- { className: "dsdr-scan-row" },
1444
- h(
1445
- "div",
1446
- { className: "dsdr-field" },
1447
- h("label", { className: "dsdr-label", htmlFor: "dsdr-scan-dir" }, t("scan.placeholder", "要扫描的目录,例如 E:\\sunline\\SBG")),
1448
- h("input", {
1449
- id: "dsdr-scan-dir",
1450
- className: "dsdr-input",
1451
- value: scanDir,
1452
- placeholder: t("scan.placeholder", "要扫描的目录,例如 E:\\sunline\\SBG"),
1453
- onChange: (event) => setScanDir(event.target.value),
1454
- }),
1455
- ),
1456
- h(
1457
- "div",
1458
- { className: "dsdr-actions" },
1459
- h(
1460
- "button",
1461
- { type: "button", className: "dsdr-btn dsdr-btn-primary", onClick: scanAndAdd, disabled: scanning },
1462
- scanning ? t("scan.scanning", "正在扫描…") : t("scan.scan", "扫描并添加"),
1463
- ),
1464
- h("button", { type: "button", className: "dsdr-btn", onClick: pickScanDirectory, disabled: scanning }, t("scan.pick", "选择目录")),
1465
- ),
1466
- ),
1467
- scanError ? h("div", { className: "dsdr-error" }, scanError) : null,
1468
- scanMessage ? h("div", { className: "dsdr-hint dsdr-ok" }, scanMessage) : null,
1469
- )
1470
- : null,
1471
- settings.git.length === 0
1472
- ? h("div", { className: "dsdr-empty" }, t("git.empty", "还没有 Git 仓库。"))
1473
- : settings.git.map((item, index) =>
1474
- h(
1475
- "div",
1476
- { className: "dsdr-source", key: item._key },
1477
- h(
1478
- "div",
1479
- { className: "dsdr-field" },
1480
- h("label", { className: "dsdr-label" }, t("git.location", "仓库地址或本地路径")),
1481
- h("input", {
1482
- className: "dsdr-input",
1483
- value: item.location,
1484
- placeholder: t("git.placeholder", "https://…/repo.git 或 D:\\code\\repo"),
1485
- onChange: (event) => updateGit(index, { location: event.target.value }),
1486
- }),
1487
- ),
1488
- h(
1489
- "div",
1490
- { className: "dsdr-field" },
1491
- h("label", { className: "dsdr-label" }, t("git.branch", "分支")),
1492
- h("input", {
1493
- className: "dsdr-input",
1494
- value: item.branch,
1495
- placeholder: "main",
1496
- onChange: (event) => updateGit(index, { branch: event.target.value }),
1497
- }),
1498
- ),
1499
- h(
1500
- "button",
1501
- {
1502
- type: "button",
1503
- className: "dsdr-btn dsdr-btn-danger dsdr-icon-btn",
1504
- title: t("remove", "移除"),
1505
- "aria-label": t("remove", "移除"),
1506
- onClick: () => removeGit(index),
1507
- },
1508
- "×",
1509
- ),
1510
- ),
1511
- ),
1512
- ),
1513
-
1514
- h(
1515
- "section",
1516
- { className: "dsdr-card" },
1517
- h(
1518
- "div",
1519
- { className: "dsdr-card-head" },
1520
- h(
1521
- "div",
1522
- null,
1523
- h("div", { className: "dsdr-card-title" }, t("docs.title", "本地文档")),
1524
- h("div", { className: "dsdr-card-desc" }, t("docs.desc", "文件或目录;目录会递归扫描 md、txt、docx、html 等笔记文件。")),
1525
- ),
1526
- h(
1527
- "div",
1528
- { className: "dsdr-actions" },
1529
- h("button", { type: "button", className: "dsdr-btn", onClick: useCurrentDirectory }, t("docs.useCwd", "使用当前目录")),
1530
- h("button", { type: "button", className: "dsdr-btn", onClick: addDoc }, "+ " + t("docs.add", "添加文档/目录")),
1531
- ),
1532
- ),
1533
- settings.docs.length === 0
1534
- ? h("div", { className: "dsdr-empty" }, t("docs.empty", "还没有本地文档。"))
1535
- : settings.docs.map((item, index) =>
1536
- h(
1537
- "div",
1538
- { className: "dsdr-source dsdr-source-docs", key: item._key },
1539
- h(
1540
- "div",
1541
- { className: "dsdr-field" },
1542
- h("label", { className: "dsdr-label" }, t("docs.location", "文件或目录路径")),
1543
- h("input", {
1544
- className: "dsdr-input",
1545
- value: item.location,
1546
- placeholder: t("docs.placeholder", "C:\\notes 或 ./docs"),
1547
- onChange: (event) => updateDoc(index, { location: event.target.value }),
1548
- }),
1549
- ),
1550
- h(
1551
- "div",
1552
- { className: "dsdr-field" },
1553
- h("label", { className: "dsdr-label" }, t("docs.label", "显示名称(可选)")),
1554
- h("input", {
1555
- className: "dsdr-input",
1556
- value: item.label,
1557
- placeholder: t("optional", "可选"),
1558
- onChange: (event) => updateDoc(index, { label: event.target.value }),
1559
- }),
1560
- ),
1561
- h(
1562
- "button",
1563
- {
1564
- type: "button",
1565
- className: "dsdr-btn dsdr-btn-danger dsdr-icon-btn",
1566
- title: t("remove", "移除"),
1567
- "aria-label": t("remove", "移除"),
1568
- onClick: () => removeDoc(index),
1569
- },
1570
- "×",
1571
- ),
1572
- ),
1573
- ),
1574
- ),
1575
-
1576
- h("div", { className: "dsdr-footer" }, t("footer.tip", "Git 使用系统 git 与你的凭据配置;远程仓库会缓存在 DSH 数据目录。")),
1577
-
1578
- result
1579
- ? h(
1580
- "section",
1581
- { className: "dsdr-card" },
1582
- h(
1583
- "div",
1584
- { className: "dsdr-card-head" },
1585
- h(
1586
- "div",
1587
- null,
1588
- h("div", { className: "dsdr-card-title" }, t("result.title", "生成结果")),
1589
- h("div", { className: "dsdr-card-desc" }, result.filename || ""),
1590
- ),
1591
- h(
1592
- "button",
1593
- {
1594
- type: "button",
1595
- className: "dsdr-btn dsdr-btn-primary",
1596
- onClick: () => triggerDownload(result.url, result.filename),
1597
- },
1598
- t("result.download", "下载 XLSX"),
1599
- ),
1600
- ),
1601
- h(
1602
- "div",
1603
- { className: "dsdr-stats" },
1604
- h(
1605
- "div",
1606
- { className: "dsdr-stat" },
1607
- h("div", { className: "dsdr-stat-value" }, String((result.stats && result.stats.commits) || 0)),
1608
- h("div", { className: "dsdr-stat-label" }, t("result.stats.commits", "Git 提交")),
1609
- ),
1610
- h(
1611
- "div",
1612
- { className: "dsdr-stat" },
1613
- h("div", { className: "dsdr-stat-value" }, String((result.stats && result.stats.docs) || 0)),
1614
- h("div", { className: "dsdr-stat-label" }, t("result.stats.docs", "文档")),
1615
- ),
1616
- h(
1617
- "div",
1618
- { className: "dsdr-stat" },
1619
- h("div", { className: "dsdr-stat-value" }, String((result.stats && result.stats.activeDays) || 0)),
1620
- h("div", { className: "dsdr-stat-label" }, t("result.stats.activeDays", "有记录天数")),
1621
- ),
1622
- h(
1623
- "div",
1624
- { className: "dsdr-stat" },
1625
- h("div", { className: "dsdr-stat-value" }, String((result.stats && result.stats.sources) || 0)),
1626
- h("div", { className: "dsdr-stat-label" }, t("result.stats.sources", "数据源")),
1627
- ),
1628
- ),
1629
- result.ai
1630
- ? h(
1631
- "div",
1632
- { className: "dsdr-ai-result" },
1633
- h(
1634
- "div",
1635
- { className: "dsdr-hint" },
1636
- t("result.ai.title", "AI 分析") +
1637
- (result.ai.provider ? " · " + result.ai.provider + "/" + result.ai.model : "") +
1638
- (result.ai.cachedDays > 0 ? " · " + (localeIsEnglish ? "cache " : "缓存 ") + result.ai.cachedDays + (localeIsEnglish ? " day(s)" : " 天") : ""),
1639
- ),
1640
- result.ai.ok
1641
- ? h("div", { className: "dsdr-ai-text" }, result.ai.overall || t("result.ai.hint", "逐日 AI 摘要已写入 XLSX 的「AI分析」工作表。"))
1642
- : h("div", { className: "dsdr-ai-text dsdr-bad" }, t("result.ai.failed", "AI 分析未完成") + (result.ai.error ? ":" + result.ai.error : "")),
1643
- )
1644
- : null,
1645
- result.sources && result.sources.length > 0
1646
- ? h(
1647
- "div",
1648
- { className: "dsdr-list" },
1649
- h("div", { className: "dsdr-hint" }, t("result.sources", "数据源状态")),
1650
- result.sources.map((source, index) =>
1651
- h(
1652
- "div",
1653
- { className: "dsdr-row", key: String(index) },
1654
- h("span", { className: "dsdr-dot " + (source.status || "ok") }),
1655
- h(
1656
- "div",
1657
- null,
1658
- h("span", { className: "dsdr-source-name" }, source.label || source.location || "—"),
1659
- source.branch ? h("span", { className: "dsdr-source-meta" }, " · " + source.branch) : null,
1660
- h("span", { className: "dsdr-source-meta" }, " · " + (source.count || 0) + " " + (source.kind === "git" ? t("result.unit.commits", "次提交") : t("result.unit.docs", "份文档"))),
1661
- h("div", { className: "dsdr-source-meta" }, source.message || ""),
1662
- ),
1663
- ),
1664
- ),
1665
- )
1666
- : null,
1667
- result.warnings && result.warnings.length > 0
1668
- ? h(
1669
- "div",
1670
- { className: "dsdr-list" },
1671
- h("div", { className: "dsdr-hint dsdr-warn" }, t("result.warnings", "提示")),
1672
- result.warnings.map((warning, index) => h("div", { className: "dsdr-row", key: String(index) }, "• " + warning)),
1673
- )
1674
- : null,
1675
- )
1676
- : null,
1677
-
1678
- h("div", { className: "dsdr-footer" }, t("page.footer", "数据仅在本机处理;生成的 XLSX 包含日报汇总、Git明细、文档明细、数据源与说明。")),
1679
- ),
1680
- );
1681
- }
1682
-
1683
- function apply(ctx) {
1684
- installStyles(ctx);
1685
- ctx.effect(() => ctx.locale.register(NS, { zh: zh, en: en }), "daily-report: locale");
1686
- const t = ctx.locale.bind(NS);
1687
-
1688
- ctx.slots.inject("main", () =>
1689
- ctx.slots.register(
1690
- {
1691
- name: "main",
1692
- key: "daily-report",
1693
- },
1694
- (props) => h(DailyReportView, { ...props, ctx }),
1695
- ),
1696
- );
1697
-
1698
- ctx.slots.inject("sidebar.panellist", () =>
1699
- ctx.slots.register(
1700
- {
1701
- name: "sidebar.panellist",
1702
- id: "daily-report",
1703
- order: 20,
1704
- label: () => t("panel.title"),
1705
- },
1706
- (props) => h(PanelIcon, props),
1707
- ),
1708
- );
1709
- }
1710
-
1711
- return { inject: ["slots", "locale"], apply: apply };
1712
- },
1713
- });
1714
-
1715
-
1716
-
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-plugin-daily-report",
3
+ factory(require) {
4
+ const React = require("react");
5
+ const h = React.createElement;
6
+
7
+ const NS = "daily-report";
8
+ const API = "/daily-report";
9
+ const NL = String.fromCharCode(10);
10
+
11
+ const zh = {
12
+ "panel.title": "日报汇总",
13
+ "page.title": "日报汇总",
14
+ "page.desc": "按天汇总 Git 提交与本地文档,导出可直接查看的 XLSX 日报。",
15
+ loading: "正在读取配置…",
16
+ "range.title": "汇总时间范围",
17
+ "range.desc": "默认当月月初到月底;可改为任意日期范围。",
18
+ "range.start": "开始日期",
19
+ "range.end": "结束日期",
20
+ "range.thisMonth": "本月",
21
+ "range.lastMonth": "上月",
22
+ "range.last7": "最近 7 天",
23
+ "range.days": "天",
24
+ "range.total": "共",
25
+ "git.title": "Git 仓库",
26
+ "git.desc": "支持远程地址或本地仓库;多个仓库、多个分支会合并到同一份日报。",
27
+ "git.add": "添加仓库",
28
+ "git.location": "仓库地址或本地路径",
29
+ "git.branch": "分支",
30
+ "git.label": "显示名称(可选)",
31
+ "git.placeholder": "https://…/repo.git 或 D:\\code\\repo",
32
+ "git.empty": "还没有 Git 仓库。",
33
+ "git.mine": "仅统计我的提交",
34
+ "git.mineHint": "按各仓库 git config user.email / user.name 过滤",
35
+ "scan.open": "扫描目录",
36
+ "scan.title": "从目录批量添加",
37
+ "scan.desc": "扫描目录下最多 6 层的 Git 仓库,已存在的会自动跳过。",
38
+ "scan.placeholder": "要扫描的目录,例如 E:\\sunline\\SBG",
39
+ "scan.scan": "扫描并添加",
40
+ "scan.scanning": "正在扫描…",
41
+ "scan.pick": "选择目录",
42
+ "scan.found": "发现",
43
+ "scan.added": "新增",
44
+ "scan.empty": "该目录下没有发现 Git 仓库。",
45
+ "scan.noPicker": "请手动输入目录路径。",
46
+ "scan.failed": "扫描目录失败",
47
+ "docs.title": "本地文档",
48
+ "docs.desc": "文件或目录;目录会递归扫描 md、txt、docx、html 等笔记文件。",
49
+ "docs.add": "添加文档/目录",
50
+ "docs.location": "文件或目录路径",
51
+ "docs.label": "显示名称(可选)",
52
+ "docs.placeholder": "C:\\notes 或 ./docs",
53
+ "docs.empty": "还没有本地文档。",
54
+ "ai.title": "AI 分析",
55
+ "ai.desc": "使用当前默认模型分析提交说明、代码变更和文档摘要,生成按天简要总结。",
56
+ "ai.enable": "启用 AI 分析",
57
+ "ai.model": "当前模型:",
58
+ "ai.unavailable": "当前没有可用的默认模型。",
59
+ "ai.cost": "会增加生成时间和模型 token 消耗。",
60
+ "progress.start": "正在创建生成任务…",
61
+ "progress.done": "生成完成,正在下载…",
62
+ "progress.preview": "生成完成,请预览并编辑后导出 XLSX。",
63
+ "progress.hint": "AI 分析需要调用模型,耗时通常比普通汇总更长;请保持页面打开。",
64
+ optional: "可选",
65
+ remove: "移除",
66
+ generate: "生成日报",
67
+ "format.label": "格式",
68
+ "format.xlsx": "XLSX 工作簿",
69
+ "format.markdown": "Markdown 文档",
70
+ "format.csv": "CSV 表格",
71
+ "format.html": "HTML 网页",
72
+ generating: "正在收集并生成…",
73
+ "save.saving": "正在保存设置…",
74
+ "save.savingShort": "正在保存…",
75
+ "save.button": "保存设置",
76
+ "save.saved": "设置已保存",
77
+ "save.failed": "设置保存失败",
78
+ "result.title": "生成结果",
79
+ "result.download": "下载 XLSX",
80
+ "result.stats.commits": "Git 提交",
81
+ "result.stats.docs": "文档",
82
+ "result.stats.activeDays": "有记录天数",
83
+ "result.stats.sources": "数据源",
84
+ "result.unit.commits": "次提交",
85
+ "result.unit.docs": "份文档",
86
+ "result.sources": "数据源状态",
87
+ "result.warnings": "提示",
88
+ "result.ai.title": "AI 分析",
89
+ "result.ai.hint": "逐日 AI 摘要已写入 XLSX 的「AI分析」工作表。",
90
+ "result.ai.failed": "AI 分析未完成",
91
+ "preview.title": "生成结果预览",
92
+ "preview.desc": "先检查并编辑每日 AI 摘要、补充非代码工作,再导出 XLSX。",
93
+ "preview.export": "导出",
94
+ "preview.expandAll": "全部展开",
95
+ "preview.collapseAll": "全部收起",
96
+ "preview.exporting": "正在导出…",
97
+ "preview.regenerateDay": "重新分析这天",
98
+ "preview.regenerating": "分析中…",
99
+ "preview.overall": "总体 AI 总结",
100
+ "preview.aiSummary": "AI 摘要(可编辑)",
101
+ "preview.raw": "原始汇总",
102
+ "preview.manual": "手动补充",
103
+ "preview.addManual": "添加手动工作项",
104
+ "preview.manualPlaceholder": "例如:参加需求评审、处理线上问题",
105
+ "preview.noDays": "没有可预览的日报内容。",
106
+ "preview.aiFailed": "AI 分析未完成,仍可补充手动工作项后导出。",
107
+ "result.ready": "已生成",
108
+ "status.ok": "正常",
109
+ "status.warn": "有提示",
110
+ "status.error": "失败",
111
+ "error.noSources": "请至少添加一个 Git 仓库或本地文档。",
112
+ "error.invalidRange": "日期范围无效,请检查开始和结束日期。",
113
+ "error.rangeTooLong": "时间范围最长支持 366 天。",
114
+ "error.generate": "生成失败",
115
+ "error.network": "无法连接本地服务,请确认插件已启用。",
116
+ "error.save": "保存失败",
117
+ "error.allSourcesFailed": "所有数据源都读取失败。",
118
+ "footer.tip": "Git 使用系统 git 与你的凭据配置;远程仓库会缓存在 DSH 数据目录。",
119
+ "page.footer": "数据仅在本机处理;生成的 XLSX 包含日报汇总、Git明细、文档明细、数据源与说明。",
120
+ "misc.unknown": "未知错误",
121
+ "misc.retry": "重试",
122
+ };
123
+
124
+ const en = {
125
+ "panel.title": "Daily report",
126
+ "page.title": "Daily report",
127
+ "page.desc": "Aggregate Git commits and local notes by day, then export an XLSX report.",
128
+ loading: "Loading configuration…",
129
+ "range.title": "Date range",
130
+ "range.desc": "Defaults to the current month; change it to any range.",
131
+ "range.start": "Start date",
132
+ "range.end": "End date",
133
+ "range.thisMonth": "This month",
134
+ "range.lastMonth": "Last month",
135
+ "range.last7": "Last 7 days",
136
+ "range.days": "days",
137
+ "range.total": "Total",
138
+ "git.title": "Git repositories",
139
+ "git.desc": "Remote URLs or local repositories; multiple repositories and branches are merged into one report.",
140
+ "git.add": "Add repository",
141
+ "git.location": "Repository URL or local path",
142
+ "git.branch": "Branch",
143
+ "git.label": "Display name (optional)",
144
+ "git.placeholder": "https://…/repo.git or D:\\code\\repo",
145
+ "git.empty": "No repositories yet.",
146
+ "git.mine": "Only my commits",
147
+ "git.mineHint": "Filter by each repository's git config user.email / user.name",
148
+ "scan.open": "Scan directory",
149
+ "scan.title": "Add repositories from a directory",
150
+ "scan.desc": "Scans up to 6 levels deep for Git repositories and skips ones already added.",
151
+ "scan.placeholder": "Directory to scan, e.g. E:\\sunline\\SBG",
152
+ "scan.scan": "Scan and add",
153
+ "scan.scanning": "Scanning…",
154
+ "scan.pick": "Choose directory",
155
+ "scan.found": "Found",
156
+ "scan.added": "Added",
157
+ "scan.empty": "No Git repositories found in that directory.",
158
+ "scan.noPicker": "Enter the directory path manually.",
159
+ "scan.failed": "Could not scan the directory",
160
+ "docs.title": "Local documents",
161
+ "docs.desc": "A file or directory; directories are scanned recursively for md, txt, docx, html and similar notes.",
162
+ "docs.add": "Add document/directory",
163
+ "docs.location": "File or directory path",
164
+ "docs.label": "Display name (optional)",
165
+ "docs.placeholder": "C:\\notes or ./docs",
166
+ "docs.empty": "No documents yet.",
167
+ "ai.title": "AI analysis",
168
+ "ai.desc": "Use the current default model to analyze commit messages, code changes and document excerpts into concise per-day summaries.",
169
+ "ai.enable": "Enable AI analysis",
170
+ "ai.model": "Current model: ",
171
+ "ai.unavailable": "No default model is available.",
172
+ "ai.cost": "This adds generation time and model token usage.",
173
+ "progress.start": "Creating the report job…",
174
+ "progress.done": "Done. Preparing the download…",
175
+ "progress.preview": "Generated. Review, edit, then export the XLSX.",
176
+ "progress.hint": "AI analysis calls a model, so it usually takes longer than a plain digest. Keep this page open.",
177
+ optional: "optional",
178
+ remove: "Remove",
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",
185
+ generating: "Collecting and generating…",
186
+ "save.saving": "Saving settings…",
187
+ "save.savingShort": "Saving…",
188
+ "save.button": "Save settings",
189
+ "save.saved": "Settings saved",
190
+ "save.failed": "Could not save settings",
191
+ "result.title": "Result",
192
+ "result.download": "Download XLSX",
193
+ "result.stats.commits": "Git commits",
194
+ "result.stats.docs": "Documents",
195
+ "result.stats.activeDays": "Days with data",
196
+ "result.stats.sources": "Data sources",
197
+ "result.unit.commits": "commits",
198
+ "result.unit.docs": "documents",
199
+ "result.sources": "Source status",
200
+ "result.warnings": "Warnings",
201
+ "result.ai.title": "AI analysis",
202
+ "result.ai.hint": "Per-day AI summaries are included in the AI analysis worksheet.",
203
+ "result.ai.failed": "AI analysis did not complete",
204
+ "preview.title": "Review before export",
205
+ "preview.desc": "Check and edit each day's AI summary, add non-code work, then export the XLSX.",
206
+ "preview.export": "Export",
207
+ "preview.expandAll": "Expand all",
208
+ "preview.collapseAll": "Collapse all",
209
+ "preview.exporting": "Exporting…",
210
+ "preview.regenerateDay": "Re-analyze this day",
211
+ "preview.regenerating": "Analyzing…",
212
+ "preview.overall": "Overall AI summary",
213
+ "preview.aiSummary": "AI summary (editable)",
214
+ "preview.raw": "Raw digest",
215
+ "preview.manual": "Manual items",
216
+ "preview.addManual": "Add manual work item",
217
+ "preview.manualPlaceholder": "e.g. attended a review, handled an incident",
218
+ "preview.noDays": "There is nothing to preview.",
219
+ "preview.aiFailed": "AI analysis did not complete; you can still add manual items and export.",
220
+ "result.ready": "Generated",
221
+ "status.ok": "OK",
222
+ "status.warn": "Warning",
223
+ "status.error": "Failed",
224
+ "error.noSources": "Add at least one Git repository or local document.",
225
+ "error.invalidRange": "The date range is invalid. Check the start and end dates.",
226
+ "error.rangeTooLong": "The range can cover at most 366 days.",
227
+ "error.generate": "Could not generate the report",
228
+ "error.network": "Could not reach the local plugin service. Make sure the plugin is enabled.",
229
+ "error.save": "Could not save",
230
+ "error.allSourcesFailed": "All data sources failed.",
231
+ "footer.tip": "Git uses the system git and your existing credentials; remote repositories are cached in the DSH data directory.",
232
+ "page.footer": "All processing happens locally; the XLSX contains the daily summary, Git details, document details, sources and notes.",
233
+ "misc.unknown": "Unknown error",
234
+ "misc.retry": "Retry",
235
+ };
236
+
237
+ function uid() {
238
+ return "s" + Math.random().toString(36).slice(2, 9) + Date.now().toString(36);
239
+ }
240
+
241
+ function pad2(value) {
242
+ return String(value).padStart(2, "0");
243
+ }
244
+
245
+ function formatDate(date) {
246
+ return date.getFullYear() + "-" + pad2(date.getMonth() + 1) + "-" + pad2(date.getDate());
247
+ }
248
+
249
+ function currentMonthRange() {
250
+ const now = new Date();
251
+ return {
252
+ start: formatDate(new Date(now.getFullYear(), now.getMonth(), 1)),
253
+ end: formatDate(new Date(now.getFullYear(), now.getMonth() + 1, 0)),
254
+ };
255
+ }
256
+
257
+ function lastMonthRange() {
258
+ const now = new Date();
259
+ return {
260
+ start: formatDate(new Date(now.getFullYear(), now.getMonth() - 1, 1)),
261
+ end: formatDate(new Date(now.getFullYear(), now.getMonth(), 0)),
262
+ };
263
+ }
264
+
265
+ function last7Range() {
266
+ const now = new Date();
267
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 6);
268
+ return { start: formatDate(start), end: formatDate(now) };
269
+ }
270
+
271
+ function daysBetween(start, end) {
272
+ if (!start || !end) return 0;
273
+ const first = new Date(start + "T00:00:00");
274
+ const last = new Date(end + "T00:00:00");
275
+ if (Number.isNaN(first.getTime()) || Number.isNaN(last.getTime())) return 0;
276
+ return Math.round((last.getTime() - first.getTime()) / 86400000) + 1;
277
+ }
278
+
279
+ function normalizeClientSettings(raw) {
280
+ const source = raw && typeof raw === "object" ? raw : {};
281
+ const git = Array.isArray(source.git)
282
+ ? source.git.map((item) => ({
283
+ _key: uid(),
284
+ location: typeof item.location === "string" ? item.location : "",
285
+ branch: typeof item.branch === "string" ? item.branch : "",
286
+ label: typeof item.label === "string" ? item.label : "",
287
+ }))
288
+ : [];
289
+ const docs = Array.isArray(source.docs)
290
+ ? source.docs.map((item) => {
291
+ const value = typeof item === "string" ? { location: item, label: "" } : item || {};
292
+ return {
293
+ _key: uid(),
294
+ location: typeof value.location === "string" ? value.location : "",
295
+ label: typeof value.label === "string" ? value.label : "",
296
+ };
297
+ })
298
+ : [];
299
+ return { git, docs, mineOnly: source.mineOnly !== false };
300
+ }
301
+
302
+ function cleanSettings(settings, aiEnabled, range, format) {
303
+ return {
304
+ git: settings.git
305
+ .map((item) => ({
306
+ location: item.location.trim(),
307
+ branch: item.branch.trim(),
308
+ label: item.label.trim(),
309
+ }))
310
+ .filter((item) => item.location.length > 0),
311
+ docs: settings.docs
312
+ .map((item) => ({
313
+ location: item.location.trim(),
314
+ label: item.label.trim(),
315
+ }))
316
+ .filter((item) => item.location.length > 0),
317
+ aiEnabled: aiEnabled !== false,
318
+ mineOnly: settings.mineOnly !== false,
319
+ format: FORMAT_OPTIONS.includes(format) ? format : "xlsx",
320
+ range:
321
+ range && typeof range.start === "string" && typeof range.end === "string"
322
+ ? { start: range.start, end: range.end }
323
+ : null,
324
+ };
325
+ }
326
+
327
+ function decodeBase64Json(value) {
328
+ const binary = window.atob(value);
329
+ const bytes = new Uint8Array(binary.length);
330
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
331
+ return JSON.parse(new TextDecoder().decode(bytes));
332
+ }
333
+
334
+ function triggerDownload(url, filename) {
335
+ const link = document.createElement("a");
336
+ link.href = url;
337
+ link.download = filename;
338
+ link.style.display = "none";
339
+ document.body.appendChild(link);
340
+ link.click();
341
+ link.remove();
342
+ }
343
+
344
+ function currentLocaleId(ctx) {
345
+ try {
346
+ const snapshot = ctx.locale.getLocale();
347
+ return snapshot && typeof snapshot.id === "string" ? snapshot.id : "zh";
348
+ } catch {
349
+ return "zh";
350
+ }
351
+ }
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
+
370
+ function useTranslate(ctx) {
371
+ const [, force] = React.useState(0);
372
+ React.useEffect(() => {
373
+ return ctx.locale.subscribe(() => force((value) => value + 1));
374
+ }, [ctx]);
375
+ const bound = ctx.locale.bind(NS);
376
+ return function translate(key, fallback) {
377
+ const value = bound(key);
378
+ if (value === key && typeof fallback === "string") return fallback;
379
+ return value;
380
+ };
381
+ }
382
+
383
+ const CSS = [
384
+ ".dsdr-root { height: 100%; overflow: auto; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); }",
385
+ ".dsdr-page { box-sizing: border-box; max-width: 1080px; margin: 0 auto; padding: 28px 28px 56px; display: flex; flex-direction: column; gap: 16px; }",
386
+ ".dsdr-hero { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 14px; }",
387
+ ".dsdr-hero-copy { flex: 1; min-width: 0; }",
388
+ ".dsdr-hero-action { display: flex; flex: none; flex-direction: column; align-items: flex-end; gap: 5px; margin-left: auto; }",
389
+ ".dsdr-hero-buttons { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; }",
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; }",
404
+ ".dsdr-save { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
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); }",
406
+ ".dsdr-title { margin: 0; font-size: 22px; line-height: 30px; font-weight: 600; }",
407
+ ".dsdr-sub { margin: 2px 0 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; }",
408
+ ".dsdr-card { border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); padding: 16px 18px; }",
409
+ ".dsdr-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }",
410
+ ".dsdr-card-title { font-size: 14px; line-height: 22px; font-weight: 600; }",
411
+ ".dsdr-card-desc { margin-top: 2px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
412
+ ".dsdr-range { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: 10px; align-items: end; }",
413
+ ".dsdr-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }",
414
+ ".dsdr-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
415
+ ".dsdr-input { box-sizing: border-box; width: 100%; height: 32px; padding: 4px 9px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; }",
416
+ ".dsdr-input:focus { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
417
+ ".dsdr-input::placeholder { color: var(--dsw-alias-label-secondary); opacity: .75; }",
418
+ ".dsdr-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }",
419
+ ".dsdr-btn { box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; font-weight: 500; line-height: 20px; padding: 5px 11px; cursor: pointer; }",
420
+ ".dsdr-btn:hover { background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-brand-primary); }",
421
+ ".dsdr-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
422
+ ".dsdr-btn:disabled { opacity: .7; cursor: not-allowed; }",
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; }",
426
+ ".dsdr-btn-danger { border-color: transparent; background: transparent; color: var(--dsw-alias-state-error-primary); font-weight: 600; }",
427
+ ".dsdr-btn-danger:hover { background: var(--dsw-alias-bg-layer-2); border-color: transparent; }",
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; }",
429
+ ".dsdr-check input { width: 15px; height: 15px; margin: 0; accent-color: #15557f; cursor: pointer; }",
430
+ ".dsdr-check.is-disabled { color: var(--dsw-alias-label-secondary); cursor: not-allowed; opacity: .7; }",
431
+ ".dsdr-check.is-disabled input { cursor: not-allowed; }",
432
+ ".dsdr-progress-card { display: flex; flex-direction: column; gap: 7px; }",
433
+ ".dsdr-progress-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-primary); }",
434
+ ".dsdr-progress-value { flex: none; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }",
435
+ ".dsdr-progress-track { height: 6px; overflow: hidden; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); }",
436
+ ".dsdr-progress-fill { height: 100%; border-radius: 999px; background: #15557f; transition: width .3s ease; }",
437
+ ".dsdr-progress-track.is-indeterminate .dsdr-progress-fill { width: 35% !important; animation: dsdr-progress 1.2s ease-in-out infinite; }",
438
+ "@keyframes dsdr-progress { 0% { transform: translateX(-110%); } 100% { transform: translateX(320%); } }",
439
+ ".dsdr-ai-result { margin-top: 12px; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }",
440
+ ".dsdr-preview-overall { display: flex; flex-direction: column; gap: 5px; margin-bottom: 10px; }",
441
+ ".dsdr-textarea { box-sizing: border-box; width: 100%; min-height: 64px; resize: vertical; padding: 7px 9px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; line-height: 19px; }",
442
+ ".dsdr-textarea:focus { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
443
+ ".dsdr-manual-add { display: grid; grid-template-columns: 145px minmax(0, 1fr) auto; gap: 8px; align-items: center; margin: 10px 0 14px; }",
444
+ ".dsdr-preview-list { display: flex; flex-direction: column; gap: 10px; }",
445
+ ".dsdr-preview-day { display: flex; flex-direction: column; gap: 7px; padding: 11px 12px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }",
446
+ ".dsdr-preview-day-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }",
447
+ ".dsdr-preview-toggle { display: flex; flex: 1; align-items: center; flex-wrap: wrap; gap: 4px 6px; min-width: 0; padding: 0; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; text-align: left; cursor: pointer; }",
448
+ ".dsdr-preview-toggle:hover .dsdr-preview-date { color: var(--dsw-alias-brand-primary); }",
449
+ ".dsdr-preview-chevron { flex: none; width: 13px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 20px; }",
450
+ ".dsdr-preview-badge { display: inline-flex; align-items: center; padding: 1px 6px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 999px; color: var(--dsw-alias-label-secondary); font-size: 10px; line-height: 16px; }",
451
+ ".dsdr-preview-body { display: flex; flex-direction: column; gap: 7px; margin-top: 2px; padding-top: 8px; border-top: 1px dashed var(--dsw-alias-border-l1); }",
452
+ ".dsdr-preview-date { font-size: 13px; line-height: 20px; font-weight: 600; }",
453
+ ".dsdr-preview-raw { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 19px; }",
454
+ ".dsdr-manual-list { display: flex; flex-wrap: wrap; gap: 6px; }",
455
+ ".dsdr-manual-chip { display: inline-flex; align-items: center; gap: 5px; max-width: 100%; padding: 2px 6px 2px 9px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 999px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-size: 11px; line-height: 18px; overflow-wrap: anywhere; }",
456
+ ".dsdr-manual-remove { border: 0; background: transparent; color: var(--dsw-alias-state-error-primary); font: inherit; font-size: 14px; line-height: 16px; padding: 0 1px; cursor: pointer; }",
457
+ ".dsdr-ai-text { margin-top: 5px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 20px; white-space: pre-wrap; }",
458
+ ".dsdr-filter-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin: 0 0 10px; }",
459
+ ".dsdr-scan { display: flex; flex-direction: column; gap: 7px; margin: 0 0 12px; padding: 12px; border: 1px dashed var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }",
460
+ ".dsdr-scan-title { font-size: 13px; line-height: 20px; font-weight: 600; }",
461
+ ".dsdr-scan-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; }",
462
+ ".dsdr-source { display: grid; grid-template-columns: minmax(0, 1.5fr) 150px 34px; gap: 8px; align-items: end; margin-top: 8px; }", ".dsdr-source-docs { grid-template-columns: minmax(0, 1fr) minmax(120px, .35fr) 34px; }",
463
+ ".dsdr-icon-btn { width: 32px; height: 32px; padding: 0; display: grid; place-items: center; line-height: 1; font-size: 18px; }",
464
+ ".dsdr-hint { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
465
+ ".dsdr-error { border: 1px solid var(--dsw-alias-state-error-primary); border-radius: 9px; color: var(--dsw-alias-state-error-primary); background: transparent; padding: 9px 12px; font-size: 12px; line-height: 18px; white-space: pre-wrap; }",
466
+ ".dsdr-notice { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
467
+ ".dsdr-ok { color: var(--dsw-alias-state-success-primary); }",
468
+ ".dsdr-warn { color: var(--dsw-alias-state-warn-primary); }",
469
+ ".dsdr-bad { color: var(--dsw-alias-state-error-primary); }",
470
+ ".dsdr-empty { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; padding: 10px 0 2px; }",
471
+ ".dsdr-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }",
472
+ ".dsdr-stat { border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 10px 12px; background: var(--dsw-alias-bg-layer-2); }",
473
+ ".dsdr-stat-value { font-size: 20px; line-height: 26px; font-weight: 600; }",
474
+ ".dsdr-stat-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
475
+ ".dsdr-list { display: flex; flex-direction: column; gap: 7px; margin-top: 10px; }",
476
+ ".dsdr-row { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; line-height: 18px; }",
477
+ ".dsdr-dot { flex: none; width: 7px; height: 7px; margin-top: 6px; border-radius: 50%; background: var(--dsw-alias-state-idle-primary); }",
478
+ ".dsdr-dot.ok { background: var(--dsw-alias-state-success-primary); }",
479
+ ".dsdr-dot.warn { background: var(--dsw-alias-state-warn-primary); }",
480
+ ".dsdr-dot.error { background: var(--dsw-alias-state-error-primary); }",
481
+ ".dsdr-source-name { font-weight: 500; }",
482
+ ".dsdr-source-meta { color: var(--dsw-alias-label-secondary); }",
483
+ ".dsdr-code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; }",
484
+ ".dsdr-footer { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 17px; }",
485
+ ".dsdr-spacer { flex: 1; }",
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; } }",
487
+ ].join(NL);
488
+
489
+ function installStyles(ctx) {
490
+ const style = document.createElement("style");
491
+ style.dataset.plugin = "dsh-plugin-daily-report";
492
+ style.textContent = CSS;
493
+ document.head.appendChild(style);
494
+ ctx.effect(() => () => style.remove(), "daily-report: styles");
495
+ }
496
+
497
+ function Icon() {
498
+ return h(
499
+ "svg",
500
+ { viewBox: "0 0 64 64", width: 26, height: 26, "aria-hidden": true },
501
+ h("rect", { width: 64, height: 64, rx: 14, fill: "#1f4e79" }),
502
+ h("path", { d: "M17 19h30a3 3 0 0 1 3 3v26a3 3 0 0 1-3 3H17a3 3 0 0 1-3-3V22a3 3 0 0 1 3-3z", fill: "#f7fbff" }),
503
+ h("path", { d: "M14 27h36v4H14z", fill: "#8db9e2" }),
504
+ h("path", { d: "M22 14v8M42 14v8", stroke: "#bfe0ff", strokeWidth: 4, strokeLinecap: "round" }),
505
+ h("path", { d: "M22 38h7v7h-7zM33 38h7v7h-7zM22 48h7v3h-7z", fill: "#1f7a5c" }),
506
+ h("path", { d: "M33 48h9", stroke: "#e0a526", strokeWidth: 3, strokeLinecap: "round" }),
507
+ );
508
+ }
509
+
510
+ function PanelIcon(props) {
511
+ const size = Number(props && props.size) > 0 ? Number(props.size) : 20;
512
+ return h(
513
+ "svg",
514
+ {
515
+ viewBox: "0 0 24 24",
516
+ width: size,
517
+ height: size,
518
+ fill: "none",
519
+ stroke: "currentColor",
520
+ strokeWidth: 1.6,
521
+ strokeLinecap: "round",
522
+ strokeLinejoin: "round",
523
+ "aria-hidden": true,
524
+ },
525
+ h("rect", { x: 3.5, y: 4.5, width: 17, height: 15, rx: 2.5 }),
526
+ h("path", { d: "M8 3v3M16 3v3M3.5 9h17" }),
527
+ h("path", { d: "M8 13h3M8 16.5h3M14 13h2.5M14 16.5h2.5" }),
528
+ );
529
+ }
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
+
591
+ function DailyReportView(props) {
592
+ const ctx = props.ctx;
593
+ const t = useTranslate(ctx);
594
+ const [settings, setSettings] = React.useState({ git: [], docs: [] });
595
+ const [range, setRange] = React.useState(currentMonthRange);
596
+ const [exportFormat, setExportFormat] = React.useState("xlsx");
597
+ const [formatMenuOpen, setFormatMenuOpen] = React.useState(false);
598
+ const [aiInfo, setAiInfo] = React.useState({ available: false, provider: "", model: "" });
599
+ const [useAi, setUseAi] = React.useState(true);
600
+ const [loading, setLoading] = React.useState(true);
601
+ const [loadError, setLoadError] = React.useState("");
602
+ const [saveState, setSaveState] = React.useState("idle");
603
+ const [error, setError] = React.useState("");
604
+ const [generating, setGenerating] = React.useState(false);
605
+ const [progress, setProgress] = React.useState(null);
606
+ const [result, setResult] = React.useState(null);
607
+ const [showScan, setShowScan] = React.useState(false);
608
+ const [scanDir, setScanDir] = React.useState("");
609
+ const [scanning, setScanning] = React.useState(false);
610
+ const [scanMessage, setScanMessage] = React.useState("");
611
+ const [scanError, setScanError] = React.useState("");
612
+ const [preview, setPreview] = React.useState(null);
613
+ const [previewJobId, setPreviewJobId] = React.useState("");
614
+ const [exporting, setExporting] = React.useState(false);
615
+ const [regenerating, setRegenerating] = React.useState("");
616
+ const [manualDate, setManualDate] = React.useState("");
617
+ const [manualText, setManualText] = React.useState("");
618
+ const [expandedPreviewDays, setExpandedPreviewDays] = React.useState({});
619
+ const firstSave = React.useRef(true);
620
+ const resultRef = React.useRef(null);
621
+ const pollAbortRef = React.useRef(null);
622
+ const saveAbortRef = React.useRef(null);
623
+ const saveTimerRef = React.useRef(null);
624
+ const formatMenuRef = React.useRef(null);
625
+ const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
626
+ const localeIsEnglish = currentLocaleId(ctx).toLowerCase().startsWith("en");
627
+
628
+ React.useEffect(() => {
629
+ resultRef.current = result;
630
+ }, [result]);
631
+
632
+ React.useEffect(() => {
633
+ return () => {
634
+ if (pollAbortRef.current) pollAbortRef.current.abort();
635
+ if (saveAbortRef.current) saveAbortRef.current.abort();
636
+ if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
637
+ if (resultRef.current && resultRef.current.url) window.URL.revokeObjectURL(resultRef.current.url);
638
+ };
639
+ }, []);
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
+
652
+ React.useEffect(() => {
653
+ let alive = true;
654
+ const query = sessionId.length > 0 ? "?sessionId=" + encodeURIComponent(sessionId) : "";
655
+ fetch(API + "/state" + query, { headers: { accept: "application/json" } })
656
+ .then((response) => {
657
+ if (!response.ok) throw new Error("state-failed");
658
+ return response.json();
659
+ })
660
+ .then((payload) => {
661
+ if (!alive) return;
662
+ setSettings(normalizeClientSettings(payload && payload.settings));
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
+ }
668
+ const nextAi = payload && payload.ai ? payload.ai : { available: false, provider: "", model: "" };
669
+ setAiInfo({
670
+ available: nextAi.available === true,
671
+ provider: typeof nextAi.provider === "string" ? nextAi.provider : "",
672
+ model: typeof nextAi.model === "string" ? nextAi.model : "",
673
+ });
674
+ const savedRange = payload && payload.settings && payload.settings.range;
675
+ if (savedRange && typeof savedRange.start === "string" && typeof savedRange.end === "string") {
676
+ setRange({ start: savedRange.start, end: savedRange.end });
677
+ } else if (payload && payload.defaults && payload.defaults.start && payload.defaults.end) {
678
+ setRange({ start: payload.defaults.start, end: payload.defaults.end });
679
+ }
680
+ setLoadError(payload && payload.warning ? payload.warning : "");
681
+ })
682
+ .catch(() => {
683
+ if (!alive) return;
684
+ setLoadError(t("error.network", "无法连接本地服务,请确认插件已启用。"));
685
+ })
686
+ .finally(() => {
687
+ if (alive) setLoading(false);
688
+ });
689
+ return () => {
690
+ alive = false;
691
+ };
692
+ }, [sessionId]);
693
+
694
+ React.useEffect(() => {
695
+ if (loading) return;
696
+ if (firstSave.current) {
697
+ firstSave.current = false;
698
+ return;
699
+ }
700
+ if (saveTimerRef.current) window.clearTimeout(saveTimerRef.current);
701
+ saveTimerRef.current = window.setTimeout(() => {
702
+ saveTimerRef.current = null;
703
+ persistSettings();
704
+ }, 650);
705
+ return () => {
706
+ if (saveTimerRef.current) {
707
+ window.clearTimeout(saveTimerRef.current);
708
+ saveTimerRef.current = null;
709
+ }
710
+ };
711
+ }, [settings, useAi, range, exportFormat, loading]);
712
+
713
+ function updateGit(index, patch) {
714
+ setSettings((previous) => ({
715
+ ...previous,
716
+ git: previous.git.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)),
717
+ }));
718
+ }
719
+
720
+ function updateDoc(index, patch) {
721
+ setSettings((previous) => ({
722
+ ...previous,
723
+ docs: previous.docs.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)),
724
+ }));
725
+ }
726
+
727
+ function addGit() {
728
+ setSettings((previous) => ({
729
+ ...previous,
730
+ git: [...previous.git, { _key: uid(), location: "", branch: "", label: "" }],
731
+ }));
732
+ }
733
+
734
+ function addDoc() {
735
+ setSettings((previous) => ({
736
+ ...previous,
737
+ docs: [...previous.docs, { _key: uid(), location: "", label: "" }],
738
+ }));
739
+ }
740
+
741
+ function removeGit(index) {
742
+ setSettings((previous) => ({ ...previous, git: previous.git.filter((_, itemIndex) => itemIndex !== index) }));
743
+ }
744
+
745
+ function removeDoc(index) {
746
+ setSettings((previous) => ({ ...previous, docs: previous.docs.filter((_, itemIndex) => itemIndex !== index) }));
747
+ }
748
+
749
+ async function persistSettings() {
750
+ if (saveAbortRef.current) saveAbortRef.current.abort();
751
+ const controller = new AbortController();
752
+ saveAbortRef.current = controller;
753
+ setSaveState("saving");
754
+ try {
755
+ const response = await fetch(API + "/state", {
756
+ method: "PUT",
757
+ headers: { "content-type": "application/json" },
758
+ body: JSON.stringify({ settings: cleanSettings(settings, useAi, range, exportFormat) }),
759
+ signal: controller.signal,
760
+ });
761
+ if (!response.ok) throw new Error("save-failed");
762
+ setSaveState("saved");
763
+ } catch (err) {
764
+ if (err && err.name === "AbortError") return;
765
+ setSaveState("failed");
766
+ } finally {
767
+ if (saveAbortRef.current === controller) saveAbortRef.current = null;
768
+ }
769
+ }
770
+
771
+ function saveSettingsNow() {
772
+ if (saveTimerRef.current) {
773
+ window.clearTimeout(saveTimerRef.current);
774
+ saveTimerRef.current = null;
775
+ }
776
+ persistSettings();
777
+ }
778
+
779
+ function toggleScan() {
780
+ const next = !showScan;
781
+ setShowScan(next);
782
+ setScanError("");
783
+ setScanMessage("");
784
+ }
785
+
786
+ async function pickScanDirectory() {
787
+ const service = ctx.get("uiWorkspace");
788
+ if (!service || typeof service.pickDirectory !== "function") {
789
+ setScanError(t("scan.noPicker", "请手动输入目录路径。"));
790
+ return;
791
+ }
792
+ try {
793
+ const selected = await service.pickDirectory();
794
+ if (typeof selected === "string" && selected.trim().length > 0) {
795
+ setScanDir(selected);
796
+ setScanError("");
797
+ }
798
+ } catch (err) {
799
+ setScanError(err && err.message ? err.message : t("scan.failed", "扫描目录失败"));
800
+ }
801
+ }
802
+
803
+ async function scanAndAdd() {
804
+ const directory = scanDir.trim();
805
+ if (directory.length === 0) {
806
+ setScanError(t("scan.placeholder", "要扫描的目录,例如 E:\\sunline\\SBG"));
807
+ return;
808
+ }
809
+ if (scanning) return;
810
+ setScanning(true);
811
+ setScanError("");
812
+ setScanMessage("");
813
+ try {
814
+ const response = await fetch(API + "/scan", {
815
+ method: "POST",
816
+ headers: { "content-type": "application/json", accept: "application/json" },
817
+ body: JSON.stringify({ directory }),
818
+ });
819
+ if (!response.ok) {
820
+ let payload = null;
821
+ try {
822
+ payload = await response.json();
823
+ } catch {
824
+ payload = null;
825
+ }
826
+ throw new Error(errorMessageFromPayload(payload));
827
+ }
828
+ const payload = await response.json();
829
+ const repos = Array.isArray(payload.repos) ? payload.repos : [];
830
+ const existing = new Set(settings.git.map((item) => item.location.toLowerCase() + "\u0000" + item.branch));
831
+ const additions = [];
832
+ for (const repo of repos) {
833
+ if (!repo || typeof repo.location !== "string" || repo.location.length === 0) continue;
834
+ const key = repo.location.toLowerCase() + "\u0000" + (repo.branch || "");
835
+ if (existing.has(key)) continue;
836
+ existing.add(key);
837
+ additions.push({
838
+ _key: uid(),
839
+ location: repo.location,
840
+ branch: typeof repo.branch === "string" ? repo.branch : "",
841
+ label: typeof repo.label === "string" ? repo.label : "",
842
+ });
843
+ }
844
+ if (additions.length > 0) {
845
+ setSettings((previous) => ({ ...previous, git: [...previous.git, ...additions] }));
846
+ }
847
+ const warnings = Array.isArray(payload.warnings) ? payload.warnings : [];
848
+ setScanMessage(
849
+ repos.length === 0
850
+ ? t("scan.empty", "该目录下没有发现 Git 仓库。")
851
+ : t("scan.found", "发现") + " " + repos.length + " · " + t("scan.added", "新增") + " " + additions.length + (warnings.length > 0 ? " · " + warnings.join(" ") : ""),
852
+ );
853
+ } catch (err) {
854
+ setScanError(err && err.message ? err.message : t("scan.failed", "扫描目录失败"));
855
+ } finally {
856
+ setScanning(false);
857
+ }
858
+ }
859
+
860
+ function updatePreviewDay(date, patch) {
861
+ setPreview((previous) => {
862
+ if (!previous) return previous;
863
+ return {
864
+ ...previous,
865
+ days: previous.days.map((day) => (day.date === date ? { ...day, ...patch } : day)),
866
+ };
867
+ });
868
+ }
869
+
870
+ function addManualItem() {
871
+ const text = manualText.trim();
872
+ const date = manualDate;
873
+ if (text.length === 0 || date.length === 0 || !preview) return;
874
+ setPreview((previous) => {
875
+ if (!previous) return previous;
876
+ return {
877
+ ...previous,
878
+ days: previous.days.map((day) =>
879
+ day.date === date ? { ...day, manualItems: [...(day.manualItems || []), text] } : day,
880
+ ),
881
+ };
882
+ });
883
+ setExpandedPreviewDays((previous) => ({ ...previous, [date]: true }));
884
+ setManualText("");
885
+ }
886
+
887
+ function removeManualItem(date, index) {
888
+ setPreview((previous) => {
889
+ if (!previous) return previous;
890
+ return {
891
+ ...previous,
892
+ days: previous.days.map((day) =>
893
+ day.date === date
894
+ ? { ...day, manualItems: (day.manualItems || []).filter((_, itemIndex) => itemIndex !== index) }
895
+ : day,
896
+ ),
897
+ };
898
+ });
899
+ }
900
+
901
+ function togglePreviewDay(date) {
902
+ setExpandedPreviewDays((previous) => ({ ...previous, [date]: !previous[date] }));
903
+ }
904
+
905
+ function setAllPreviewExpanded(expand) {
906
+ if (!preview) return;
907
+ if (!expand) {
908
+ setExpandedPreviewDays({});
909
+ return;
910
+ }
911
+ const next = {};
912
+ for (const day of preview.days) {
913
+ if (day.commitCount > 0 || day.docCount > 0 || (day.manualItems || []).length > 0 || (day.aiSummary || "").length > 0) {
914
+ next[day.date] = true;
915
+ }
916
+ }
917
+ setExpandedPreviewDays(next);
918
+ }
919
+
920
+ function previewEdits() {
921
+ if (!preview) return { overall: "", days: [] };
922
+ return {
923
+ overall: preview.ai ? preview.ai.overall || "" : "",
924
+ days: preview.days.map((day) => ({
925
+ date: day.date,
926
+ aiSummary: day.aiSummary || "",
927
+ aiHighlights: day.aiHighlights || [],
928
+ aiCategories: day.aiCategories || [],
929
+ manualItems: day.manualItems || [],
930
+ })),
931
+ };
932
+ }
933
+
934
+ async function exportPreview() {
935
+ if (!preview || previewJobId.length === 0 || exporting) return;
936
+ setExporting(true);
937
+ setError("");
938
+ try {
939
+ const response = await fetch(API + "/job/export", {
940
+ method: "POST",
941
+ headers: { "content-type": "application/json", accept: "application/octet-stream" },
942
+ body: JSON.stringify({ jobId: previewJobId, edits: previewEdits(), format: exportFormat }),
943
+ });
944
+ if (!response.ok) {
945
+ let payload = null;
946
+ try {
947
+ payload = await response.json();
948
+ } catch {
949
+ payload = null;
950
+ }
951
+ throw new Error(errorMessageFromPayload(payload));
952
+ }
953
+ const summaryHeader = response.headers.get("x-report-summary");
954
+ const summary = summaryHeader ? decodeBase64Json(summaryHeader) : {};
955
+ const filenameHeader = response.headers.get("x-report-filename");
956
+ const filename = filenameHeader
957
+ ? decodeURIComponent(filenameHeader)
958
+ : "日报汇总_" + preview.range.start + "_" + preview.range.end + "." + formatExtension(exportFormat);
959
+ const blob = await response.blob();
960
+ const url = window.URL.createObjectURL(blob);
961
+ const next = Object.assign({}, summary, { filename, url, size: blob.size });
962
+ if (resultRef.current && resultRef.current.url) window.URL.revokeObjectURL(resultRef.current.url);
963
+ setResult(next);
964
+ triggerDownload(url, filename);
965
+ } catch (err) {
966
+ setError(err && err.message ? err.message : t("error.generate", "导出失败"));
967
+ } finally {
968
+ setExporting(false);
969
+ }
970
+ }
971
+
972
+ async function regeneratePreviewDay(date) {
973
+ if (!preview || !preview.ai || !preview.ai.ok || regenerating.length > 0) return;
974
+ const day = preview.days.find((item) => item.date === date);
975
+ if (!day) return;
976
+ setRegenerating(date);
977
+ setError("");
978
+ try {
979
+ const response = await fetch(API + "/job/regenerate-day", {
980
+ method: "POST",
981
+ headers: { "content-type": "application/json", accept: "application/json" },
982
+ body: JSON.stringify({ jobId: previewJobId, date, manualItems: day.manualItems || [] }),
983
+ });
984
+ if (!response.ok) {
985
+ let payload = null;
986
+ try {
987
+ payload = await response.json();
988
+ } catch {
989
+ payload = null;
990
+ }
991
+ throw new Error(errorMessageFromPayload(payload));
992
+ }
993
+ const payload = await response.json();
994
+ if (!payload.ok || !payload.day) throw new Error(payload.error || t("error.generate", "重新分析失败"));
995
+ updatePreviewDay(date, {
996
+ aiSummary: payload.day.summary || "",
997
+ aiHighlights: payload.day.highlights || [],
998
+ aiCategories: payload.day.categories || [],
999
+ });
1000
+ } catch (err) {
1001
+ setError(err && err.message ? err.message : t("error.generate", "重新分析失败"));
1002
+ } finally {
1003
+ setRegenerating("");
1004
+ }
1005
+ }
1006
+
1007
+ function errorMessageFromPayload(payload) {
1008
+ const code = payload && payload.error ? payload.error.code : "";
1009
+ const serverMessage = payload && payload.error ? payload.error.message : "";
1010
+ const messages = {
1011
+ "no-sources": t("error.noSources", "请至少添加一个 Git 仓库或本地文档。"),
1012
+ "invalid-range": t("error.invalidRange", "日期范围无效,请检查开始和结束日期。"),
1013
+ "range-too-long": t("error.rangeTooLong", "时间范围最长支持 366 天。"),
1014
+ "all-sources-failed": t("error.allSourcesFailed", "所有数据源都读取失败。"),
1015
+ "generate-failed": t("error.generate", "生成失败"),
1016
+ };
1017
+ return messages[code] || serverMessage || t("error.generate", "生成失败");
1018
+ }
1019
+
1020
+ function wait(ms, signal) {
1021
+ return new Promise((resolve, reject) => {
1022
+ const timer = window.setTimeout(() => {
1023
+ signal.removeEventListener("abort", onAbort);
1024
+ resolve();
1025
+ }, ms);
1026
+ const onAbort = () => {
1027
+ window.clearTimeout(timer);
1028
+ const error = new Error("aborted");
1029
+ error.name = "AbortError";
1030
+ reject(error);
1031
+ };
1032
+ if (signal.aborted) {
1033
+ onAbort();
1034
+ return;
1035
+ }
1036
+ signal.addEventListener("abort", onAbort, { once: true });
1037
+ });
1038
+ }
1039
+
1040
+ async function generate(formatOverride) {
1041
+ if (generating) return;
1042
+ const format = FORMAT_OPTIONS.includes(formatOverride) ? formatOverride : exportFormat;
1043
+ setExportFormat(format);
1044
+ setFormatMenuOpen(false);
1045
+ const cleaned = cleanSettings(settings, useAi, range, format);
1046
+ if (cleaned.git.length === 0 && cleaned.docs.length === 0) {
1047
+ setError(t("error.noSources", "请至少添加一个 Git 仓库或本地文档。"));
1048
+ return;
1049
+ }
1050
+ const totalDays = daysBetween(range.start, range.end);
1051
+ if (!range.start || !range.end || totalDays <= 0) {
1052
+ setError(t("error.invalidRange", "日期范围无效,请检查开始和结束日期。"));
1053
+ return;
1054
+ }
1055
+ if (totalDays > 366) {
1056
+ setError(t("error.rangeTooLong", "时间范围最长支持 366 天。"));
1057
+ return;
1058
+ }
1059
+ setError("");
1060
+ setPreview(null);
1061
+ setPreviewJobId("");
1062
+ if (resultRef.current && resultRef.current.url) window.URL.revokeObjectURL(resultRef.current.url);
1063
+ setResult(null);
1064
+ setGenerating(true);
1065
+ setProgress({ percent: 1, indeterminate: false, message: t("progress.start", "正在创建生成任务…") });
1066
+ if (pollAbortRef.current) pollAbortRef.current.abort();
1067
+ const controller = new AbortController();
1068
+ pollAbortRef.current = controller;
1069
+ try {
1070
+ const startResponse = await fetch(API + "/generate/start", {
1071
+ method: "POST",
1072
+ headers: { "content-type": "application/json", accept: "application/json" },
1073
+ body: JSON.stringify({
1074
+ settings: cleaned,
1075
+ start: range.start,
1076
+ end: range.end,
1077
+ sessionId,
1078
+ ai: useAi && aiInfo.available,
1079
+ locale: currentLocaleId(ctx).toLowerCase().startsWith("en") ? "en" : "zh",
1080
+ }),
1081
+ signal: controller.signal,
1082
+ });
1083
+ if (!startResponse.ok) {
1084
+ let payload = null;
1085
+ try {
1086
+ payload = await startResponse.json();
1087
+ } catch {
1088
+ payload = null;
1089
+ }
1090
+ throw new Error(errorMessageFromPayload(payload));
1091
+ }
1092
+ const started = await startResponse.json();
1093
+ const jobId = started && typeof started.jobId === "string" ? started.jobId : "";
1094
+ if (jobId.length === 0) throw new Error(t("error.generate", "生成失败"));
1095
+ while (true) {
1096
+ if (controller.signal.aborted) {
1097
+ const aborted = new Error("aborted");
1098
+ aborted.name = "AbortError";
1099
+ throw aborted;
1100
+ }
1101
+ const statusResponse = await fetch(API + "/job?jobId=" + encodeURIComponent(jobId), {
1102
+ headers: { accept: "application/json" },
1103
+ signal: controller.signal,
1104
+ });
1105
+ if (!statusResponse.ok) {
1106
+ let payload = null;
1107
+ try {
1108
+ payload = await statusResponse.json();
1109
+ } catch {
1110
+ payload = null;
1111
+ }
1112
+ throw new Error(errorMessageFromPayload(payload));
1113
+ }
1114
+ const job = await statusResponse.json();
1115
+ if (job.progress) {
1116
+ setProgress({
1117
+ percent: Number(job.progress.percent) || 0,
1118
+ indeterminate: job.progress.indeterminate === true,
1119
+ message: job.progress.message || t("generating", "正在收集并生成…"),
1120
+ });
1121
+ }
1122
+ if (job.status === "done") break;
1123
+ if (job.status === "failed") throw new Error(errorMessageFromPayload({ error: job.error }));
1124
+ if (job.status === "cancelled") {
1125
+ const aborted = new Error("aborted");
1126
+ aborted.name = "AbortError";
1127
+ throw aborted;
1128
+ }
1129
+ await wait(650, controller.signal);
1130
+ }
1131
+ const previewResponse = await fetch(API + "/job/preview?jobId=" + encodeURIComponent(jobId), {
1132
+ headers: { accept: "application/json" },
1133
+ signal: controller.signal,
1134
+ });
1135
+ if (!previewResponse.ok) {
1136
+ let payload = null;
1137
+ try {
1138
+ payload = await previewResponse.json();
1139
+ } catch {
1140
+ payload = null;
1141
+ }
1142
+ throw new Error(errorMessageFromPayload(payload));
1143
+ }
1144
+ const previewData = await previewResponse.json();
1145
+ setPreview(previewData);
1146
+ setPreviewJobId(jobId);
1147
+ setExpandedPreviewDays({});
1148
+ const firstDay = (previewData.days || []).find((day) => day.commitCount > 0 || day.docCount > 0 || (day.manualItems || []).length > 0) || (previewData.days || [])[0];
1149
+ setManualDate(firstDay ? firstDay.date : range.start);
1150
+ setProgress({ percent: 100, indeterminate: false, message: t("progress.preview", "生成完成,请预览并编辑后导出 XLSX。") });
1151
+ } catch (err) {
1152
+ if (err && err.name === "AbortError") return;
1153
+ setError(err && err.message ? err.message : t("error.generate", "生成失败"));
1154
+ } finally {
1155
+ if (pollAbortRef.current === controller) pollAbortRef.current = null;
1156
+ setGenerating(false);
1157
+ }
1158
+ }
1159
+
1160
+ const rangeDays = daysBetween(range.start, range.end);
1161
+ const previewDays = preview
1162
+ ? preview.days.filter((day) => day.commitCount > 0 || day.docCount > 0 || (day.manualItems || []).length > 0 || (day.aiSummary || "").length > 0)
1163
+ : [];
1164
+ const allPreviewExpanded = previewDays.length > 0 && previewDays.every((day) => expandedPreviewDays[day.date] === true);
1165
+ const saveText =
1166
+ saveState === "saving"
1167
+ ? t("save.saving", "正在保存设置…")
1168
+ : saveState === "saved"
1169
+ ? t("save.saved", "设置已保存")
1170
+ : saveState === "failed"
1171
+ ? t("save.failed", "设置保存失败")
1172
+ : "";
1173
+
1174
+ if (loading) {
1175
+ return h("div", { className: "dsdr-root" }, h("div", { className: "dsdr-page" }, h("div", { className: "dsdr-card dsdr-hint" }, t("loading", "正在读取配置…"))));
1176
+ }
1177
+
1178
+ return h(
1179
+ "div",
1180
+ { className: "dsdr-root" },
1181
+ h(
1182
+ "div",
1183
+ { className: "dsdr-page" },
1184
+ h(
1185
+ "div",
1186
+ { className: "dsdr-hero" },
1187
+ h("div", { className: "dsdr-mark" }, h(Icon)),
1188
+ h(
1189
+ "div",
1190
+ { className: "dsdr-hero-copy" },
1191
+ h("h1", { className: "dsdr-title" }, t("page.title", "日报汇总")),
1192
+ h("p", { className: "dsdr-sub" }, t("page.desc", "按天汇总 Git 提交与本地文档,导出可直接查看的 XLSX 日报。")),
1193
+ ),
1194
+ h(
1195
+ "div",
1196
+ { className: "dsdr-hero-action" },
1197
+ h(
1198
+ "div",
1199
+ { className: "dsdr-hero-buttons" },
1200
+ h(
1201
+ "button",
1202
+ {
1203
+ type: "button",
1204
+ className: "dsdr-btn dsdr-save-btn dsdr-btn-with-icon",
1205
+ onClick: saveSettingsNow,
1206
+ disabled: saveState === "saving",
1207
+ },
1208
+ h(SaveIcon, { size: 15 }),
1209
+ h("span", null, saveState === "saving" ? t("save.savingShort", "正在保存…") : t("save.button", "保存设置")),
1210
+ ),
1211
+ h(
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,
1264
+ ),
1265
+ ),
1266
+ saveText ? h("div", { className: "dsdr-save" }, saveText) : null,
1267
+ ),
1268
+ ),
1269
+
1270
+ generating
1271
+ ? h(
1272
+ "section",
1273
+ { className: "dsdr-card dsdr-progress-card", role: "status", "aria-live": "polite" },
1274
+ h(
1275
+ "div",
1276
+ { className: "dsdr-progress-head" },
1277
+ h("span", null, (progress && progress.message) || t("generating", "正在收集并生成…")),
1278
+ h("span", { className: "dsdr-progress-value" }, progress && !progress.indeterminate ? Math.round(Number(progress.percent) || 0) + "%" : "…"),
1279
+ ),
1280
+ h(
1281
+ "div",
1282
+ { className: "dsdr-progress-track" + (progress && progress.indeterminate ? " is-indeterminate" : "") },
1283
+ h("div", {
1284
+ className: "dsdr-progress-fill",
1285
+ style: { width: progress && progress.indeterminate ? "35%" : Math.max(2, Math.min(100, Number(progress && progress.percent) || 0)) + "%" },
1286
+ }),
1287
+ ),
1288
+ h("div", { className: "dsdr-hint" }, t("progress.hint", "AI 分析需要调用模型,耗时通常比普通汇总更长;请保持页面打开。")),
1289
+ )
1290
+ : null,
1291
+
1292
+ loadError ? h("div", { className: "dsdr-error" }, loadError) : null,
1293
+ error ? h("div", { className: "dsdr-error", role: "alert" }, error) : null,
1294
+
1295
+ preview
1296
+ ? h(
1297
+ "section",
1298
+ { className: "dsdr-card dsdr-preview-card" },
1299
+ h(
1300
+ "div",
1301
+ { className: "dsdr-card-head" },
1302
+ h(
1303
+ "div",
1304
+ null,
1305
+ h("div", { className: "dsdr-card-title" }, t("preview.title", "生成结果预览")),
1306
+ h("div", { className: "dsdr-card-desc" }, t("preview.desc", "先检查并编辑每日 AI 摘要、补充非代码工作,再导出 XLSX。")),
1307
+ ),
1308
+ h(
1309
+ "div",
1310
+ { className: "dsdr-actions" },
1311
+ h(
1312
+ "button",
1313
+ {
1314
+ type: "button",
1315
+ className: "dsdr-btn",
1316
+ onClick: () => setAllPreviewExpanded(!allPreviewExpanded),
1317
+ disabled: previewDays.length === 0 || exporting,
1318
+ },
1319
+ allPreviewExpanded ? t("preview.collapseAll", "全部收起") : t("preview.expandAll", "全部展开"),
1320
+ ),
1321
+ h(
1322
+ "button",
1323
+ { type: "button", className: "dsdr-btn dsdr-btn-primary", onClick: exportPreview, disabled: exporting || regenerating.length > 0 },
1324
+ exporting ? t("preview.exporting", "正在导出…") : t("preview.export", "导出") + " " + formatShortLabel(exportFormat),
1325
+ ),
1326
+ ),
1327
+ ),
1328
+ preview.ai && preview.ai.ok
1329
+ ? h(
1330
+ "div",
1331
+ { className: "dsdr-preview-overall" },
1332
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-preview-overall" }, t("preview.overall", "总体 AI 总结")),
1333
+ h("textarea", {
1334
+ id: "dsdr-preview-overall",
1335
+ className: "dsdr-textarea",
1336
+ value: preview.ai.overall || "",
1337
+ onChange: (event) => setPreview((previous) => (previous ? { ...previous, ai: { ...previous.ai, overall: event.target.value } } : previous)),
1338
+ }),
1339
+ )
1340
+ : preview.ai
1341
+ ? h("div", { className: "dsdr-error" }, preview.ai.error || t("preview.aiFailed", "AI 分析未完成,仍可补充手动工作项后导出。"))
1342
+ : null,
1343
+ h(
1344
+ "div",
1345
+ { className: "dsdr-manual-add" },
1346
+ h("input", {
1347
+ type: "date",
1348
+ className: "dsdr-input",
1349
+ value: manualDate,
1350
+ min: preview.range.start,
1351
+ max: preview.range.end,
1352
+ onChange: (event) => setManualDate(event.target.value),
1353
+ }),
1354
+ h("input", {
1355
+ className: "dsdr-input",
1356
+ value: manualText,
1357
+ placeholder: t("preview.manualPlaceholder", "例如:参加需求评审、处理线上问题"),
1358
+ onChange: (event) => setManualText(event.target.value),
1359
+ onKeyDown: (event) => {
1360
+ if (event.key === "Enter") addManualItem();
1361
+ },
1362
+ }),
1363
+ h("button", { type: "button", className: "dsdr-btn", onClick: addManualItem, disabled: manualText.trim().length === 0 }, t("preview.addManual", "添加手动工作项")),
1364
+ ),
1365
+ previewDays.length === 0
1366
+ ? h("div", { className: "dsdr-empty" }, t("preview.noDays", "没有可预览的日报内容。"))
1367
+ : h(
1368
+ "div",
1369
+ { className: "dsdr-preview-list" },
1370
+ previewDays.map((day) => {
1371
+ const expanded = expandedPreviewDays[day.date] === true;
1372
+ return h(
1373
+ "div",
1374
+ { className: "dsdr-preview-day" + (expanded ? " is-expanded" : ""), key: day.date },
1375
+ h(
1376
+ "div",
1377
+ { className: "dsdr-preview-day-head" },
1378
+ h(
1379
+ "button",
1380
+ {
1381
+ type: "button",
1382
+ className: "dsdr-preview-toggle",
1383
+ "aria-expanded": expanded,
1384
+ onClick: () => togglePreviewDay(day.date),
1385
+ },
1386
+ h("span", { className: "dsdr-preview-chevron" }, expanded ? "▾" : "▸"),
1387
+ h("span", { className: "dsdr-preview-date" }, day.date),
1388
+ h("span", { className: "dsdr-hint" }, " · " + day.weekday + " · " + day.commitCount + " commits · " + day.docCount + " docs"),
1389
+ day.manualItems && day.manualItems.length > 0
1390
+ ? h("span", { className: "dsdr-preview-badge" }, day.manualItems.length + " " + t("preview.manual", "手动补充"))
1391
+ : null,
1392
+ ),
1393
+ preview.ai && preview.ai.ok
1394
+ ? h(
1395
+ "button",
1396
+ {
1397
+ type: "button",
1398
+ className: "dsdr-btn",
1399
+ onClick: () => regeneratePreviewDay(day.date),
1400
+ disabled: regenerating.length > 0 || exporting,
1401
+ },
1402
+ regenerating === day.date ? t("preview.regenerating", "分析中…") : t("preview.regenerateDay", "重新分析这天"),
1403
+ )
1404
+ : null,
1405
+ ),
1406
+ expanded
1407
+ ? h(
1408
+ "div",
1409
+ { className: "dsdr-preview-body" },
1410
+ h("div", { className: "dsdr-preview-raw" }, t("preview.raw", "原始汇总") + ":" + day.summary),
1411
+ preview.ai && preview.ai.ok
1412
+ ? h("textarea", {
1413
+ className: "dsdr-textarea",
1414
+ value: day.aiSummary || "",
1415
+ placeholder: t("preview.aiSummary", "AI 摘要(可编辑)"),
1416
+ onChange: (event) => updatePreviewDay(day.date, { aiSummary: event.target.value }),
1417
+ })
1418
+ : null,
1419
+ day.aiHighlights && day.aiHighlights.length > 0
1420
+ ? h("div", { className: "dsdr-hint" }, "• " + day.aiHighlights.join(";"))
1421
+ : null,
1422
+ day.manualItems && day.manualItems.length > 0
1423
+ ? h(
1424
+ "div",
1425
+ { className: "dsdr-manual-list" },
1426
+ day.manualItems.map((text, index) =>
1427
+ h(
1428
+ "span",
1429
+ { className: "dsdr-manual-chip", key: String(index) },
1430
+ text,
1431
+ h(
1432
+ "button",
1433
+ {
1434
+ type: "button",
1435
+ className: "dsdr-manual-remove",
1436
+ "aria-label": t("remove", "移除"),
1437
+ onClick: () => removeManualItem(day.date, index),
1438
+ },
1439
+ "×",
1440
+ ),
1441
+ ),
1442
+ ),
1443
+ )
1444
+ : null,
1445
+ )
1446
+ : null,
1447
+ );
1448
+ }),
1449
+ ),
1450
+ )
1451
+ : null,
1452
+
1453
+ h(
1454
+ "section",
1455
+ { className: "dsdr-card" },
1456
+ h(
1457
+ "div",
1458
+ { className: "dsdr-card-head" },
1459
+ h(
1460
+ "div",
1461
+ null,
1462
+ h("div", { className: "dsdr-card-title" }, t("range.title", "汇总时间范围")),
1463
+ h("div", { className: "dsdr-card-desc" }, t("range.desc", "默认当月月初到月底;可改为任意日期范围。")),
1464
+ ),
1465
+ h("div", { className: "dsdr-hint" }, rangeDays > 0 ? t("range.total", "共") + " " + rangeDays + " " + t("range.days", "天") : ""),
1466
+ ),
1467
+ h(
1468
+ "div",
1469
+ { className: "dsdr-range" },
1470
+ h(
1471
+ "div",
1472
+ { className: "dsdr-field" },
1473
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-start" }, t("range.start", "开始日期")),
1474
+ h("input", {
1475
+ id: "dsdr-start",
1476
+ className: "dsdr-input",
1477
+ type: "date",
1478
+ value: range.start,
1479
+ onChange: (event) => setRange((previous) => ({ ...previous, start: event.target.value })),
1480
+ }),
1481
+ ),
1482
+ h(
1483
+ "div",
1484
+ { className: "dsdr-field" },
1485
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-end" }, t("range.end", "结束日期")),
1486
+ h("input", {
1487
+ id: "dsdr-end",
1488
+ className: "dsdr-input",
1489
+ type: "date",
1490
+ value: range.end,
1491
+ onChange: (event) => setRange((previous) => ({ ...previous, end: event.target.value })),
1492
+ }),
1493
+ ),
1494
+ h(
1495
+ "div",
1496
+ { className: "dsdr-actions" },
1497
+ h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(currentMonthRange()) }, t("range.thisMonth", "本月")),
1498
+ h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(lastMonthRange()) }, t("range.lastMonth", "上月")),
1499
+ h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(last7Range()) }, t("range.last7", "最近 7 天")),
1500
+ ),
1501
+ ),
1502
+ ),
1503
+
1504
+ h(
1505
+ "section",
1506
+ { className: "dsdr-card" },
1507
+ h(
1508
+ "div",
1509
+ { className: "dsdr-card-head" },
1510
+ h(
1511
+ "div",
1512
+ null,
1513
+ h("div", { className: "dsdr-card-title" }, t("ai.title", "AI 分析")),
1514
+ h("div", { className: "dsdr-card-desc" }, t("ai.desc", "使用当前默认模型分析提交说明、代码变更和文档摘要,生成按天简要总结。")),
1515
+ ),
1516
+ h(
1517
+ "label",
1518
+ { className: "dsdr-check" + (aiInfo.available ? "" : " is-disabled") },
1519
+ h("input", {
1520
+ type: "checkbox",
1521
+ checked: useAi,
1522
+ disabled: !aiInfo.available,
1523
+ onChange: (event) => setUseAi(event.target.checked),
1524
+ }),
1525
+ h("span", null, t("ai.enable", "启用 AI 分析")),
1526
+ ),
1527
+ ),
1528
+ h(
1529
+ "div",
1530
+ { className: "dsdr-hint" },
1531
+ aiInfo.available
1532
+ ? t("ai.model", "当前模型:") + aiInfo.provider + "/" + aiInfo.model + " · " + t("ai.cost", "会增加生成时间和模型 token 消耗。")
1533
+ : t("ai.unavailable", "当前没有可用的默认模型。"),
1534
+ ),
1535
+ ),
1536
+
1537
+ h(
1538
+ "section",
1539
+ { className: "dsdr-card" },
1540
+ h(
1541
+ "div",
1542
+ { className: "dsdr-card-head" },
1543
+ h(
1544
+ "div",
1545
+ null,
1546
+ h("div", { className: "dsdr-card-title" }, t("git.title", "Git 仓库")),
1547
+ h("div", { className: "dsdr-card-desc" }, t("git.desc", "支持远程地址或本地仓库;多个仓库、多个分支会合并到同一份日报。")),
1548
+ ),
1549
+ h(
1550
+ "div",
1551
+ { className: "dsdr-actions" },
1552
+ h("button", { type: "button", className: "dsdr-btn", onClick: toggleScan }, t("scan.open", "扫描目录")),
1553
+ h("button", { type: "button", className: "dsdr-btn", onClick: addGit }, "+ " + t("git.add", "添加仓库")),
1554
+ ),
1555
+ ),
1556
+ h(
1557
+ "div",
1558
+ { className: "dsdr-filter-row" },
1559
+ h(
1560
+ "label",
1561
+ { className: "dsdr-check" },
1562
+ h("input", {
1563
+ type: "checkbox",
1564
+ checked: settings.mineOnly !== false,
1565
+ onChange: (event) => setSettings((previous) => ({ ...previous, mineOnly: event.target.checked })),
1566
+ }),
1567
+ h("span", null, t("git.mine", "仅统计我的提交")),
1568
+ ),
1569
+ h("span", { className: "dsdr-hint" }, t("git.mineHint", "按各仓库 git config user.email / user.name 过滤")),
1570
+ ),
1571
+ showScan
1572
+ ? h(
1573
+ "div",
1574
+ { className: "dsdr-scan" },
1575
+ h("div", { className: "dsdr-scan-title" }, t("scan.title", "从目录批量添加")),
1576
+ h("div", { className: "dsdr-card-desc" }, t("scan.desc", "扫描目录下最多 6 层的 Git 仓库,已存在的会自动跳过。")),
1577
+ h(
1578
+ "div",
1579
+ { className: "dsdr-scan-row" },
1580
+ h(
1581
+ "div",
1582
+ { className: "dsdr-field" },
1583
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-scan-dir" }, t("scan.placeholder", "要扫描的目录,例如 E:\\sunline\\SBG")),
1584
+ h("input", {
1585
+ id: "dsdr-scan-dir",
1586
+ className: "dsdr-input",
1587
+ value: scanDir,
1588
+ placeholder: t("scan.placeholder", "要扫描的目录,例如 E:\\sunline\\SBG"),
1589
+ onChange: (event) => setScanDir(event.target.value),
1590
+ }),
1591
+ ),
1592
+ h(
1593
+ "div",
1594
+ { className: "dsdr-actions" },
1595
+ h(
1596
+ "button",
1597
+ { type: "button", className: "dsdr-btn dsdr-btn-primary", onClick: scanAndAdd, disabled: scanning },
1598
+ scanning ? t("scan.scanning", "正在扫描…") : t("scan.scan", "扫描并添加"),
1599
+ ),
1600
+ h("button", { type: "button", className: "dsdr-btn", onClick: pickScanDirectory, disabled: scanning }, t("scan.pick", "选择目录")),
1601
+ ),
1602
+ ),
1603
+ scanError ? h("div", { className: "dsdr-error" }, scanError) : null,
1604
+ scanMessage ? h("div", { className: "dsdr-hint dsdr-ok" }, scanMessage) : null,
1605
+ )
1606
+ : null,
1607
+ settings.git.length === 0
1608
+ ? h("div", { className: "dsdr-empty" }, t("git.empty", "还没有 Git 仓库。"))
1609
+ : settings.git.map((item, index) =>
1610
+ h(
1611
+ "div",
1612
+ { className: "dsdr-source", key: item._key },
1613
+ h(
1614
+ "div",
1615
+ { className: "dsdr-field" },
1616
+ h("label", { className: "dsdr-label" }, t("git.location", "仓库地址或本地路径")),
1617
+ h("input", {
1618
+ className: "dsdr-input",
1619
+ value: item.location,
1620
+ placeholder: t("git.placeholder", "https://…/repo.git 或 D:\\code\\repo"),
1621
+ onChange: (event) => updateGit(index, { location: event.target.value }),
1622
+ }),
1623
+ ),
1624
+ h(
1625
+ "div",
1626
+ { className: "dsdr-field" },
1627
+ h("label", { className: "dsdr-label" }, t("git.branch", "分支")),
1628
+ h("input", {
1629
+ className: "dsdr-input",
1630
+ value: item.branch,
1631
+ placeholder: "main",
1632
+ onChange: (event) => updateGit(index, { branch: event.target.value }),
1633
+ }),
1634
+ ),
1635
+ h(
1636
+ "button",
1637
+ {
1638
+ type: "button",
1639
+ className: "dsdr-btn dsdr-btn-danger dsdr-icon-btn",
1640
+ title: t("remove", "移除"),
1641
+ "aria-label": t("remove", "移除"),
1642
+ onClick: () => removeGit(index),
1643
+ },
1644
+ "×",
1645
+ ),
1646
+ ),
1647
+ ),
1648
+ ),
1649
+
1650
+ h(
1651
+ "section",
1652
+ { className: "dsdr-card" },
1653
+ h(
1654
+ "div",
1655
+ { className: "dsdr-card-head" },
1656
+ h(
1657
+ "div",
1658
+ null,
1659
+ h("div", { className: "dsdr-card-title" }, t("docs.title", "本地文档")),
1660
+ h("div", { className: "dsdr-card-desc" }, t("docs.desc", "文件或目录;目录会递归扫描 md、txt、docx、html 等笔记文件。")),
1661
+ ),
1662
+ h(
1663
+ "div",
1664
+ { className: "dsdr-actions" },
1665
+ h("button", { type: "button", className: "dsdr-btn", onClick: addDoc }, "+ " + t("docs.add", "添加文档/目录")),
1666
+ ),
1667
+ ),
1668
+ settings.docs.length === 0
1669
+ ? h("div", { className: "dsdr-empty" }, t("docs.empty", "还没有本地文档。"))
1670
+ : settings.docs.map((item, index) =>
1671
+ h(
1672
+ "div",
1673
+ { className: "dsdr-source dsdr-source-docs", key: item._key },
1674
+ h(
1675
+ "div",
1676
+ { className: "dsdr-field" },
1677
+ h("label", { className: "dsdr-label" }, t("docs.location", "文件或目录路径")),
1678
+ h("input", {
1679
+ className: "dsdr-input",
1680
+ value: item.location,
1681
+ placeholder: t("docs.placeholder", "C:\\notes 或 ./docs"),
1682
+ onChange: (event) => updateDoc(index, { location: event.target.value }),
1683
+ }),
1684
+ ),
1685
+ h(
1686
+ "div",
1687
+ { className: "dsdr-field" },
1688
+ h("label", { className: "dsdr-label" }, t("docs.label", "显示名称(可选)")),
1689
+ h("input", {
1690
+ className: "dsdr-input",
1691
+ value: item.label,
1692
+ placeholder: t("optional", "可选"),
1693
+ onChange: (event) => updateDoc(index, { label: event.target.value }),
1694
+ }),
1695
+ ),
1696
+ h(
1697
+ "button",
1698
+ {
1699
+ type: "button",
1700
+ className: "dsdr-btn dsdr-btn-danger dsdr-icon-btn",
1701
+ title: t("remove", "移除"),
1702
+ "aria-label": t("remove", "移除"),
1703
+ onClick: () => removeDoc(index),
1704
+ },
1705
+ "×",
1706
+ ),
1707
+ ),
1708
+ ),
1709
+ ),
1710
+
1711
+ h("div", { className: "dsdr-footer" }, t("footer.tip", "Git 使用系统 git 与你的凭据配置;远程仓库会缓存在 DSH 数据目录。")),
1712
+
1713
+ result
1714
+ ? h(
1715
+ "section",
1716
+ { className: "dsdr-card" },
1717
+ h(
1718
+ "div",
1719
+ { className: "dsdr-card-head" },
1720
+ h(
1721
+ "div",
1722
+ null,
1723
+ h("div", { className: "dsdr-card-title" }, t("result.title", "生成结果")),
1724
+ h("div", { className: "dsdr-card-desc" }, result.filename || ""),
1725
+ ),
1726
+ h(
1727
+ "button",
1728
+ {
1729
+ type: "button",
1730
+ className: "dsdr-btn dsdr-btn-primary",
1731
+ onClick: () => triggerDownload(result.url, result.filename),
1732
+ },
1733
+ t("result.download", "下载 XLSX"),
1734
+ ),
1735
+ ),
1736
+ h(
1737
+ "div",
1738
+ { className: "dsdr-stats" },
1739
+ h(
1740
+ "div",
1741
+ { className: "dsdr-stat" },
1742
+ h("div", { className: "dsdr-stat-value" }, String((result.stats && result.stats.commits) || 0)),
1743
+ h("div", { className: "dsdr-stat-label" }, t("result.stats.commits", "Git 提交")),
1744
+ ),
1745
+ h(
1746
+ "div",
1747
+ { className: "dsdr-stat" },
1748
+ h("div", { className: "dsdr-stat-value" }, String((result.stats && result.stats.docs) || 0)),
1749
+ h("div", { className: "dsdr-stat-label" }, t("result.stats.docs", "文档")),
1750
+ ),
1751
+ h(
1752
+ "div",
1753
+ { className: "dsdr-stat" },
1754
+ h("div", { className: "dsdr-stat-value" }, String((result.stats && result.stats.activeDays) || 0)),
1755
+ h("div", { className: "dsdr-stat-label" }, t("result.stats.activeDays", "有记录天数")),
1756
+ ),
1757
+ h(
1758
+ "div",
1759
+ { className: "dsdr-stat" },
1760
+ h("div", { className: "dsdr-stat-value" }, String((result.stats && result.stats.sources) || 0)),
1761
+ h("div", { className: "dsdr-stat-label" }, t("result.stats.sources", "数据源")),
1762
+ ),
1763
+ ),
1764
+ result.ai
1765
+ ? h(
1766
+ "div",
1767
+ { className: "dsdr-ai-result" },
1768
+ h(
1769
+ "div",
1770
+ { className: "dsdr-hint" },
1771
+ t("result.ai.title", "AI 分析") +
1772
+ (result.ai.provider ? " · " + result.ai.provider + "/" + result.ai.model : "") +
1773
+ (result.ai.cachedDays > 0 ? " · " + (localeIsEnglish ? "cache " : "缓存 ") + result.ai.cachedDays + (localeIsEnglish ? " day(s)" : " 天") : ""),
1774
+ ),
1775
+ result.ai.ok
1776
+ ? h("div", { className: "dsdr-ai-text" }, result.ai.overall || t("result.ai.hint", "逐日 AI 摘要已写入 XLSX 的「AI分析」工作表。"))
1777
+ : h("div", { className: "dsdr-ai-text dsdr-bad" }, t("result.ai.failed", "AI 分析未完成") + (result.ai.error ? ":" + result.ai.error : "")),
1778
+ )
1779
+ : null,
1780
+ result.sources && result.sources.length > 0
1781
+ ? h(
1782
+ "div",
1783
+ { className: "dsdr-list" },
1784
+ h("div", { className: "dsdr-hint" }, t("result.sources", "数据源状态")),
1785
+ result.sources.map((source, index) =>
1786
+ h(
1787
+ "div",
1788
+ { className: "dsdr-row", key: String(index) },
1789
+ h("span", { className: "dsdr-dot " + (source.status || "ok") }),
1790
+ h(
1791
+ "div",
1792
+ null,
1793
+ h("span", { className: "dsdr-source-name" }, source.label || source.location || "—"),
1794
+ source.branch ? h("span", { className: "dsdr-source-meta" }, " · " + source.branch) : null,
1795
+ h("span", { className: "dsdr-source-meta" }, " · " + (source.count || 0) + " " + (source.kind === "git" ? t("result.unit.commits", "次提交") : t("result.unit.docs", "份文档"))),
1796
+ h("div", { className: "dsdr-source-meta" }, source.message || ""),
1797
+ ),
1798
+ ),
1799
+ ),
1800
+ )
1801
+ : null,
1802
+ result.warnings && result.warnings.length > 0
1803
+ ? h(
1804
+ "div",
1805
+ { className: "dsdr-list" },
1806
+ h("div", { className: "dsdr-hint dsdr-warn" }, t("result.warnings", "提示")),
1807
+ result.warnings.map((warning, index) => h("div", { className: "dsdr-row", key: String(index) }, "• " + warning)),
1808
+ )
1809
+ : null,
1810
+ )
1811
+ : null,
1812
+
1813
+ h("div", { className: "dsdr-footer" }, t("page.footer", "数据仅在本机处理;生成的 XLSX 包含日报汇总、Git明细、文档明细、数据源与说明。")),
1814
+ ),
1815
+ );
1816
+ }
1817
+
1818
+ function apply(ctx) {
1819
+ installStyles(ctx);
1820
+ ctx.effect(() => ctx.locale.register(NS, { zh: zh, en: en }), "daily-report: locale");
1821
+ const t = ctx.locale.bind(NS);
1822
+
1823
+ ctx.slots.inject("main", () =>
1824
+ ctx.slots.register(
1825
+ {
1826
+ name: "main",
1827
+ key: "daily-report",
1828
+ },
1829
+ (props) => h(DailyReportView, { ...props, ctx }),
1830
+ ),
1831
+ );
1832
+
1833
+ ctx.slots.inject("sidebar.panellist", () =>
1834
+ ctx.slots.register(
1835
+ {
1836
+ name: "sidebar.panellist",
1837
+ id: "daily-report",
1838
+ order: 20,
1839
+ label: () => t("panel.title"),
1840
+ },
1841
+ (props) => h(PanelIcon, props),
1842
+ ),
1843
+ );
1844
+ }
1845
+
1846
+ return { inject: ["slots", "locale"], apply: apply };
1847
+ },
1848
+ });
1849
+
1850
+
1851
+