dsh-plugin-daily-report 1.0.1 → 1.0.3

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
@@ -20,6 +20,8 @@ window.__ModuleLoader__.load({
20
20
  "range.thisMonth": "本月",
21
21
  "range.lastMonth": "上月",
22
22
  "range.last7": "最近 7 天",
23
+ "range.yesterday": "昨天",
24
+ "range.thisWeek": "本周",
23
25
  "range.days": "天",
24
26
  "range.total": "共",
25
27
  "git.title": "Git 仓库",
@@ -32,6 +34,15 @@ window.__ModuleLoader__.load({
32
34
  "git.empty": "还没有 Git 仓库。",
33
35
  "git.mine": "仅统计我的提交",
34
36
  "git.mineHint": "按各仓库 git config user.email / user.name 过滤",
37
+ "git.excludeMerges": "排除 merge commit",
38
+ "git.authorEmails": "作者邮箱(可选)",
39
+ "git.authorEmailsHint": "留空自动读取各仓库 git config;多个邮箱用逗号分隔",
40
+ "test.button": "测试数据源",
41
+ "test.testing": "测试中…",
42
+ "test.failed": "测试数据源失败",
43
+ "test.ok": "正常",
44
+ cancel: "取消",
45
+ retry: "重试",
35
46
  "scan.open": "扫描目录",
36
47
  "scan.title": "从目录批量添加",
37
48
  "scan.desc": "扫描目录下最多 6 层的 Git 仓库,已存在的会自动跳过。",
@@ -57,13 +68,35 @@ window.__ModuleLoader__.load({
57
68
  "ai.model": "当前模型:",
58
69
  "ai.unavailable": "当前没有可用的默认模型。",
59
70
  "ai.cost": "会增加生成时间和模型 token 消耗。",
71
+ "ai.template": "分析风格",
72
+ "ai.template.concise": "简洁",
73
+ "ai.template.detailed": "详细",
74
+ "ai.template.manager": "管理汇报",
75
+ "ai.modelSelect": "模型",
76
+ "ai.defaultModel": "默认模型",
77
+ "schedule.title": "自动生成",
78
+ "schedule.desc": "在 DSH 运行期间按设定时间自动生成报告,并保存到本地历史目录。",
79
+ "schedule.enable": "启用自动生成",
80
+ "schedule.daily": "每天",
81
+ "schedule.weekly": "每周一",
82
+ "schedule.time": "生成时间",
83
+ "schedule.hint": "自动生成使用当前保存的数据源、AI 设置和格式。",
84
+ "datasource.title": "数据源配置",
85
+ "datasource.desc": "统一管理 Git 仓库与本地文档,生成时合并到同一份日报。",
86
+ "datasource.git": "Git 仓库",
87
+ "datasource.docs": "本地文档",
60
88
  "progress.start": "正在创建生成任务…",
61
89
  "progress.done": "生成完成,正在下载…",
62
90
  "progress.preview": "生成完成,请预览并编辑后导出 XLSX。",
63
91
  "progress.hint": "AI 分析需要调用模型,耗时通常比普通汇总更长;请保持页面打开。",
64
92
  optional: "可选",
65
93
  remove: "移除",
66
- generate: "生成 XLSX 日报",
94
+ generate: "生成日报",
95
+ "format.label": "格式",
96
+ "format.xlsx": "XLSX 工作簿",
97
+ "format.markdown": "Markdown 文档",
98
+ "format.csv": "CSV 表格",
99
+ "format.html": "HTML 网页",
67
100
  generating: "正在收集并生成…",
68
101
  "save.saving": "正在保存设置…",
69
102
  "save.savingShort": "正在保存…",
@@ -85,7 +118,7 @@ window.__ModuleLoader__.load({
85
118
  "result.ai.failed": "AI 分析未完成",
86
119
  "preview.title": "生成结果预览",
87
120
  "preview.desc": "先检查并编辑每日 AI 摘要、补充非代码工作,再导出 XLSX。",
88
- "preview.export": "导出 XLSX",
121
+ "preview.export": "导出",
89
122
  "preview.expandAll": "全部展开",
90
123
  "preview.collapseAll": "全部收起",
91
124
  "preview.exporting": "正在导出…",
@@ -128,6 +161,8 @@ window.__ModuleLoader__.load({
128
161
  "range.thisMonth": "This month",
129
162
  "range.lastMonth": "Last month",
130
163
  "range.last7": "Last 7 days",
164
+ "range.yesterday": "Yesterday",
165
+ "range.thisWeek": "This week",
131
166
  "range.days": "days",
132
167
  "range.total": "Total",
133
168
  "git.title": "Git repositories",
@@ -140,6 +175,15 @@ window.__ModuleLoader__.load({
140
175
  "git.empty": "No repositories yet.",
141
176
  "git.mine": "Only my commits",
142
177
  "git.mineHint": "Filter by each repository's git config user.email / user.name",
178
+ "git.excludeMerges": "Exclude merge commits",
179
+ "git.authorEmails": "Author emails (optional)",
180
+ "git.authorEmailsHint": "Leave empty to read each repository's git config; separate multiple emails with commas",
181
+ "test.button": "Test sources",
182
+ "test.testing": "Testing…",
183
+ "test.failed": "Could not test data sources",
184
+ "test.ok": "OK",
185
+ cancel: "Cancel",
186
+ retry: "Retry",
143
187
  "scan.open": "Scan directory",
144
188
  "scan.title": "Add repositories from a directory",
145
189
  "scan.desc": "Scans up to 6 levels deep for Git repositories and skips ones already added.",
@@ -165,13 +209,35 @@ window.__ModuleLoader__.load({
165
209
  "ai.model": "Current model: ",
166
210
  "ai.unavailable": "No default model is available.",
167
211
  "ai.cost": "This adds generation time and model token usage.",
212
+ "ai.template": "Analysis style",
213
+ "ai.template.concise": "Concise",
214
+ "ai.template.detailed": "Detailed",
215
+ "ai.template.manager": "Manager summary",
216
+ "ai.modelSelect": "Model",
217
+ "ai.defaultModel": "Default model",
218
+ "schedule.title": "Scheduled generation",
219
+ "schedule.desc": "While DSH is running, generate a report at the configured time and save it to the local history folder.",
220
+ "schedule.enable": "Enable scheduled generation",
221
+ "schedule.daily": "Daily",
222
+ "schedule.weekly": "Every Monday",
223
+ "schedule.time": "Time",
224
+ "schedule.hint": "Scheduled runs use the saved sources, AI settings and format.",
225
+ "datasource.title": "Data sources",
226
+ "datasource.desc": "Manage Git repositories and local documents together; both feed the same report.",
227
+ "datasource.git": "Git repositories",
228
+ "datasource.docs": "Local documents",
168
229
  "progress.start": "Creating the report job…",
169
230
  "progress.done": "Done. Preparing the download…",
170
231
  "progress.preview": "Generated. Review, edit, then export the XLSX.",
171
232
  "progress.hint": "AI analysis calls a model, so it usually takes longer than a plain digest. Keep this page open.",
172
233
  optional: "optional",
173
234
  remove: "Remove",
174
- generate: "Generate XLSX report",
235
+ generate: "Generate report",
236
+ "format.label": "Format",
237
+ "format.xlsx": "XLSX workbook",
238
+ "format.markdown": "Markdown document",
239
+ "format.csv": "CSV table",
240
+ "format.html": "HTML page",
175
241
  generating: "Collecting and generating…",
176
242
  "save.saving": "Saving settings…",
177
243
  "save.savingShort": "Saving…",
@@ -193,7 +259,7 @@ window.__ModuleLoader__.load({
193
259
  "result.ai.failed": "AI analysis did not complete",
194
260
  "preview.title": "Review before export",
195
261
  "preview.desc": "Check and edit each day's AI summary, add non-code work, then export the XLSX.",
196
- "preview.export": "Export XLSX",
262
+ "preview.export": "Export",
197
263
  "preview.expandAll": "Expand all",
198
264
  "preview.collapseAll": "Collapse all",
199
265
  "preview.exporting": "Exporting…",
@@ -258,6 +324,19 @@ window.__ModuleLoader__.load({
258
324
  return { start: formatDate(start), end: formatDate(now) };
259
325
  }
260
326
 
327
+ function yesterdayRange() {
328
+ const now = new Date();
329
+ const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
330
+ return { start: formatDate(yesterday), end: formatDate(yesterday) };
331
+ }
332
+
333
+ function thisWeekRange() {
334
+ const now = new Date();
335
+ const mondayOffset = (now.getDay() + 6) % 7;
336
+ const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - mondayOffset);
337
+ return { start: formatDate(monday), end: formatDate(now) };
338
+ }
339
+
261
340
  function daysBetween(start, end) {
262
341
  if (!start || !end) return 0;
263
342
  const first = new Date(start + "T00:00:00");
@@ -286,10 +365,22 @@ window.__ModuleLoader__.load({
286
365
  };
287
366
  })
288
367
  : [];
289
- return { git, docs, mineOnly: source.mineOnly !== false };
368
+ return {
369
+ git,
370
+ docs,
371
+ mineOnly: source.mineOnly !== false,
372
+ excludeMerges: source.excludeMerges !== false,
373
+ authorEmails: typeof source.authorEmails === "string" ? source.authorEmails : "",
374
+ aiTemplate: ["concise", "detailed", "manager"].includes(source.aiTemplate) ? source.aiTemplate : "concise",
375
+ aiProvider: typeof source.aiProvider === "string" ? source.aiProvider : "",
376
+ aiModel: typeof source.aiModel === "string" ? source.aiModel : "",
377
+ scheduleEnabled: source.scheduleEnabled === true,
378
+ scheduleFrequency: source.scheduleFrequency === "weekly" ? "weekly" : "daily",
379
+ scheduleTime: typeof source.scheduleTime === "string" ? source.scheduleTime : "18:30",
380
+ };
290
381
  }
291
382
 
292
- function cleanSettings(settings, aiEnabled, range) {
383
+ function cleanSettings(settings, aiEnabled, range, format) {
293
384
  return {
294
385
  git: settings.git
295
386
  .map((item) => ({
@@ -306,6 +397,15 @@ window.__ModuleLoader__.load({
306
397
  .filter((item) => item.location.length > 0),
307
398
  aiEnabled: aiEnabled !== false,
308
399
  mineOnly: settings.mineOnly !== false,
400
+ excludeMerges: settings.excludeMerges !== false,
401
+ authorEmails: (settings.authorEmails || "").trim(),
402
+ aiTemplate: ["concise", "detailed", "manager"].includes(settings.aiTemplate) ? settings.aiTemplate : "concise",
403
+ aiProvider: (settings.aiProvider || "").trim(),
404
+ aiModel: (settings.aiModel || "").trim(),
405
+ scheduleEnabled: settings.scheduleEnabled === true,
406
+ scheduleFrequency: settings.scheduleFrequency === "weekly" ? "weekly" : "daily",
407
+ scheduleTime: /^\d{2}:\d{2}$/.test(settings.scheduleTime) ? settings.scheduleTime : "18:30",
408
+ format: FORMAT_OPTIONS.includes(format) ? format : "xlsx",
309
409
  range:
310
410
  range && typeof range.start === "string" && typeof range.end === "string"
311
411
  ? { start: range.start, end: range.end }
@@ -339,6 +439,23 @@ window.__ModuleLoader__.load({
339
439
  }
340
440
  }
341
441
 
442
+ const FORMAT_OPTIONS = ["xlsx", "markdown", "csv", "html"];
443
+
444
+ function formatLabel(format, t) {
445
+ if (format === "markdown") return t("format.markdown", "Markdown");
446
+ if (format === "csv") return t("format.csv", "CSV");
447
+ if (format === "html") return t("format.html", "HTML");
448
+ return t("format.xlsx", "XLSX");
449
+ }
450
+
451
+ function formatExtension(format) {
452
+ return format === "markdown" ? "md" : format;
453
+ }
454
+
455
+ function formatShortLabel(format) {
456
+ return format === "markdown" ? "Markdown" : format.toUpperCase();
457
+ }
458
+
342
459
  function useTranslate(ctx) {
343
460
  const [, force] = React.useState(0);
344
461
  React.useEffect(() => {
@@ -359,8 +476,20 @@ window.__ModuleLoader__.load({
359
476
  ".dsdr-hero-copy { flex: 1; min-width: 0; }",
360
477
  ".dsdr-hero-action { display: flex; flex: none; flex-direction: column; align-items: flex-end; gap: 5px; margin-left: auto; }",
361
478
  ".dsdr-hero-buttons { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; }",
362
- ".dsdr-save-btn { min-height: 38px; }",
363
- ".dsdr-generate { min-height: 38px; padding: 9px 18px; font-size: 13px; white-space: nowrap; }",
479
+ ".dsdr-save-btn { min-height: 38px; background: #3b4046; border-color: #3b4046; color: #ffffff; font-weight: 600; }",
480
+ ".dsdr-save-btn:hover { background: #292d31; border-color: #292d31; }",
481
+ ".dsdr-save-btn:disabled { background: #666b70; border-color: #666b70; color: #ffffff; opacity: .8; }",
482
+ ".dsdr-generate { min-height: 38px; padding: 9px 16px; font-size: 13px; white-space: nowrap; }",
483
+ ".dsdr-btn-with-icon { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }",
484
+ ".dsdr-split { position: relative; display: inline-flex; align-items: stretch; }",
485
+ ".dsdr-split .dsdr-generate { border-radius: 8px 0 0 8px; }",
486
+ ".dsdr-split-toggle { min-height: 38px; padding: 0 9px; border-radius: 0 8px 8px 0; border-left: 1px solid rgba(255, 255, 255, .32); }",
487
+ ".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); }",
488
+ ".dsdr-menu-title { padding: 5px 8px 4px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
489
+ ".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; }",
490
+ ".dsdr-menu-item:hover { background: var(--dsw-alias-bg-layer-2); }",
491
+ ".dsdr-menu-item.is-active { background: var(--dsw-alias-bg-layer-2); font-weight: 600; }",
492
+ ".dsdr-menu-check { width: 14px; color: #146b5a; text-align: center; }",
364
493
  ".dsdr-save { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
365
494
  ".dsdr-mark { flex: none; width: 42px; height: 42px; border-radius: 11px; display: grid; place-items: center; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); }",
366
495
  ".dsdr-title { margin: 0; font-size: 22px; line-height: 30px; font-weight: 600; }",
@@ -368,7 +497,15 @@ window.__ModuleLoader__.load({
368
497
  ".dsdr-card { border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); padding: 16px 18px; }",
369
498
  ".dsdr-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }",
370
499
  ".dsdr-card-title { font-size: 14px; line-height: 22px; font-weight: 600; }",
500
+ ".dsdr-ai-title { display: inline-block; font-size: 15px; font-weight: 700; letter-spacing: .03em; background: linear-gradient(100deg, #0f766e 0%, #06b6d4 22%, #3b82f6 46%, #8b5cf6 70%, #0f766e 100%); background-size: 320% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; -webkit-text-fill-color: transparent; filter: drop-shadow(0 0 7px rgba(20, 184, 166, .22)); animation: dsdr-ai-gradient 5s ease-in-out infinite; }",
501
+ "@keyframes dsdr-ai-gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }",
502
+ "@media (prefers-reduced-motion: reduce) { .dsdr-ai-title { animation: none; } }",
371
503
  ".dsdr-card-desc { margin-top: 2px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
504
+ ".dsdr-title-with-info { display: flex; align-items: center; gap: 6px; min-width: 0; }",
505
+ ".dsdr-info { position: relative; display: inline-flex; flex: none; align-items: center; justify-content: center; width: 15px; height: 15px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 50%; color: var(--dsw-alias-label-secondary); font-family: Georgia, serif; font-size: 10px; font-style: italic; font-weight: 700; line-height: 1; cursor: help; }",
506
+ ".dsdr-info:hover, .dsdr-info:focus-visible { border-color: #146b5a; color: #146b5a; outline: none; }",
507
+ ".dsdr-info::after { content: attr(data-tip); position: absolute; top: calc(100% + 8px); left: 50%; z-index: 80; width: max-content; max-width: 280px; padding: 7px 9px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 7px; background: var(--dsw-alias-bg-overlay); color: var(--dsw-alias-label-primary); box-shadow: 0 7px 20px rgba(0, 0, 0, .2); font-family: inherit; font-size: 11px; font-style: normal; font-weight: 400; line-height: 17px; text-align: left; white-space: normal; opacity: 0; visibility: hidden; transform: translate(-50%, -3px); transition: opacity .15s ease, transform .15s ease, visibility .15s ease; pointer-events: none; }",
508
+ ".dsdr-info:hover::after, .dsdr-info:focus-visible::after { opacity: 1; visibility: visible; transform: translate(-50%, 0); }",
372
509
  ".dsdr-range { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: 10px; align-items: end; }",
373
510
  ".dsdr-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }",
374
511
  ".dsdr-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
@@ -380,9 +517,9 @@ window.__ModuleLoader__.load({
380
517
  ".dsdr-btn:hover { background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-brand-primary); }",
381
518
  ".dsdr-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
382
519
  ".dsdr-btn:disabled { opacity: .7; cursor: not-allowed; }",
383
- ".dsdr-btn-primary { background: #15557f; border-color: #15557f; color: #ffffff; font-weight: 600; box-shadow: 0 2px 6px rgba(0, 0, 0, .18); }",
384
- ".dsdr-btn-primary:hover { background: #103f5f; border-color: #103f5f; opacity: 1; }",
385
- ".dsdr-btn-primary:disabled { background: #5e7c8f; border-color: #5e7c8f; color: #ffffff; opacity: .8; }",
520
+ ".dsdr-btn-primary { background: #146b5a; border-color: #146b5a; color: #ffffff; font-weight: 600; box-shadow: 0 2px 6px rgba(0, 0, 0, .18); }",
521
+ ".dsdr-btn-primary:hover { background: #0f5547; border-color: #0f5547; opacity: 1; }",
522
+ ".dsdr-btn-primary:disabled { background: #6b8a82; border-color: #6b8a82; color: #ffffff; opacity: .8; }",
386
523
  ".dsdr-btn-danger { border-color: transparent; background: transparent; color: var(--dsw-alias-state-error-primary); font-weight: 600; }",
387
524
  ".dsdr-btn-danger:hover { background: var(--dsw-alias-bg-layer-2); border-color: transparent; }",
388
525
  ".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; }",
@@ -416,6 +553,16 @@ window.__ModuleLoader__.load({
416
553
  ".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; }",
417
554
  ".dsdr-ai-text { margin-top: 5px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 20px; white-space: pre-wrap; }",
418
555
  ".dsdr-filter-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin: 0 0 10px; }",
556
+ ".dsdr-datasource-tabs { margin-bottom: -16px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; }",
557
+ ".dsdr-datasource-panel { border-top-left-radius: 0; border-top-right-radius: 0; }",
558
+ ".dsdr-tabs { display: flex; align-items: center; gap: 4px; margin-top: 12px; border-bottom: 1px solid var(--dsw-alias-border-l1); }",
559
+ ".dsdr-tab { display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px 8px; border: 0; border-bottom: 2px solid transparent; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; line-height: 18px; cursor: pointer; }",
560
+ ".dsdr-tab:hover { color: var(--dsw-alias-label-primary); }",
561
+ ".dsdr-tab.is-active { color: var(--dsw-alias-label-primary); border-bottom-color: #146b5a; font-weight: 600; }",
562
+ ".dsdr-tab-count { display: inline-flex; min-width: 17px; height: 17px; align-items: center; justify-content: center; padding: 0 5px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-secondary); font-size: 10px; line-height: 17px; }",
563
+ ".dsdr-filter-author { flex: 1 1 280px; min-width: 220px; }",
564
+ ".dsdr-ai-options { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px; margin-top: 10px; }",
565
+ ".dsdr-test-results { display: flex; flex-direction: column; gap: 5px; margin: 0 0 10px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }",
419
566
  ".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); }",
420
567
  ".dsdr-scan-title { font-size: 13px; line-height: 20px; font-weight: 600; }",
421
568
  ".dsdr-scan-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: end; }",
@@ -423,6 +570,7 @@ window.__ModuleLoader__.load({
423
570
  ".dsdr-icon-btn { width: 32px; height: 32px; padding: 0; display: grid; place-items: center; line-height: 1; font-size: 18px; }",
424
571
  ".dsdr-hint { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
425
572
  ".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; }",
573
+ ".dsdr-error-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }",
426
574
  ".dsdr-notice { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }",
427
575
  ".dsdr-ok { color: var(--dsw-alias-state-success-primary); }",
428
576
  ".dsdr-warn { color: var(--dsw-alias-state-warn-primary); }",
@@ -443,7 +591,7 @@ window.__ModuleLoader__.load({
443
591
  ".dsdr-code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; }",
444
592
  ".dsdr-footer { color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 17px; }",
445
593
  ".dsdr-spacer { flex: 1; }",
446
- "@media (max-width: 780px) { .dsdr-page { padding: 20px 16px 40px; } .dsdr-hero-action { flex: 1 0 100%; width: 100%; align-items: stretch; margin-left: 0; } .dsdr-hero-buttons { width: 100%; } .dsdr-hero-buttons .dsdr-btn { flex: 1; } .dsdr-generate { width: 100%; } .dsdr-range { grid-template-columns: 1fr 1fr; } .dsdr-range .dsdr-actions { grid-column: 1 / -1; } .dsdr-stats { grid-template-columns: 1fr 1fr; } .dsdr-scan-row { grid-template-columns: 1fr; } .dsdr-manual-add { grid-template-columns: 1fr; } .dsdr-source, .dsdr-source-docs { grid-template-columns: 1fr; } .dsdr-source .dsdr-icon-btn { justify-self: end; } }",
594
+ "@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-ai-options { grid-template-columns: 1fr; } .dsdr-range { grid-template-columns: 1fr 1fr; } .dsdr-range .dsdr-actions { grid-column: 1 / -1; } .dsdr-stats { grid-template-columns: 1fr 1fr; } .dsdr-scan-row { grid-template-columns: 1fr; } .dsdr-manual-add { grid-template-columns: 1fr; } .dsdr-source, .dsdr-source-docs { grid-template-columns: 1fr; } .dsdr-source .dsdr-icon-btn { justify-self: end; } }",
447
595
  ].join(NL);
448
596
 
449
597
  function installStyles(ctx) {
@@ -488,12 +636,84 @@ window.__ModuleLoader__.load({
488
636
  );
489
637
  }
490
638
 
639
+ function SaveIcon(props) {
640
+ const size = Number(props && props.size) > 0 ? Number(props.size) : 15;
641
+ return h(
642
+ "svg",
643
+ {
644
+ viewBox: "0 0 24 24",
645
+ width: size,
646
+ height: size,
647
+ fill: "none",
648
+ stroke: "currentColor",
649
+ strokeWidth: 1.8,
650
+ strokeLinecap: "round",
651
+ strokeLinejoin: "round",
652
+ "aria-hidden": true,
653
+ },
654
+ h("path", { d: "M5 3.5h11l3.5 3.5v13.5H5z" }),
655
+ h("path", { d: "M8 3.5v5h7v-5M8 20.5v-6h8v6" }),
656
+ );
657
+ }
658
+
659
+ function GenerateIcon(props) {
660
+ const size = Number(props && props.size) > 0 ? Number(props.size) : 15;
661
+ return h(
662
+ "svg",
663
+ {
664
+ viewBox: "0 0 24 24",
665
+ width: size,
666
+ height: size,
667
+ fill: "none",
668
+ stroke: "currentColor",
669
+ strokeWidth: 1.8,
670
+ strokeLinecap: "round",
671
+ strokeLinejoin: "round",
672
+ "aria-hidden": true,
673
+ },
674
+ h("path", { d: "M6 3.5h8l4 4v13H6z" }),
675
+ h("path", { d: "M14 3.5v4h4M9 12h6M9 15.5h4" }),
676
+ h("path", { d: "M18.5 16.5v4M16.5 18.5h4" }),
677
+ );
678
+ }
679
+
680
+ function ChevronIcon(props) {
681
+ const size = Number(props && props.size) > 0 ? Number(props.size) : 12;
682
+ return h(
683
+ "svg",
684
+ {
685
+ viewBox: "0 0 24 24",
686
+ width: size,
687
+ height: size,
688
+ fill: "none",
689
+ stroke: "currentColor",
690
+ strokeWidth: 2,
691
+ strokeLinecap: "round",
692
+ strokeLinejoin: "round",
693
+ "aria-hidden": true,
694
+ },
695
+ h("path", { d: "M6 9l6 6 6-6" }),
696
+ );
697
+ }
698
+
699
+ function InfoDot(props) {
700
+ return h("span", {
701
+ className: "dsdr-info",
702
+ role: "img",
703
+ tabIndex: 0,
704
+ "aria-label": props.text,
705
+ "data-tip": props.text,
706
+ }, "i");
707
+ }
708
+
491
709
  function DailyReportView(props) {
492
710
  const ctx = props.ctx;
493
711
  const t = useTranslate(ctx);
494
712
  const [settings, setSettings] = React.useState({ git: [], docs: [] });
495
713
  const [range, setRange] = React.useState(currentMonthRange);
496
- const [aiInfo, setAiInfo] = React.useState({ available: false, provider: "", model: "" });
714
+ const [exportFormat, setExportFormat] = React.useState("xlsx");
715
+ const [formatMenuOpen, setFormatMenuOpen] = React.useState(false);
716
+ const [aiInfo, setAiInfo] = React.useState({ available: false, provider: "", model: "", models: [] });
497
717
  const [useAi, setUseAi] = React.useState(true);
498
718
  const [loading, setLoading] = React.useState(true);
499
719
  const [loadError, setLoadError] = React.useState("");
@@ -514,11 +734,17 @@ window.__ModuleLoader__.load({
514
734
  const [manualDate, setManualDate] = React.useState("");
515
735
  const [manualText, setManualText] = React.useState("");
516
736
  const [expandedPreviewDays, setExpandedPreviewDays] = React.useState({});
737
+ const [testingSources, setTestingSources] = React.useState(false);
738
+ const [testResults, setTestResults] = React.useState(null);
739
+ const [testError, setTestError] = React.useState("");
740
+ const [dataSourceTab, setDataSourceTab] = React.useState("git");
517
741
  const firstSave = React.useRef(true);
518
742
  const resultRef = React.useRef(null);
519
743
  const pollAbortRef = React.useRef(null);
520
744
  const saveAbortRef = React.useRef(null);
521
745
  const saveTimerRef = React.useRef(null);
746
+ const formatMenuRef = React.useRef(null);
747
+ const jobIdRef = React.useRef("");
522
748
  const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
523
749
  const localeIsEnglish = currentLocaleId(ctx).toLowerCase().startsWith("en");
524
750
 
@@ -535,6 +761,17 @@ window.__ModuleLoader__.load({
535
761
  };
536
762
  }, []);
537
763
 
764
+ React.useEffect(() => {
765
+ if (!formatMenuOpen) return;
766
+ const close = (event) => {
767
+ if (formatMenuRef.current && !formatMenuRef.current.contains(event.target)) {
768
+ setFormatMenuOpen(false);
769
+ }
770
+ };
771
+ document.addEventListener("mousedown", close);
772
+ return () => document.removeEventListener("mousedown", close);
773
+ }, [formatMenuOpen]);
774
+
538
775
  React.useEffect(() => {
539
776
  let alive = true;
540
777
  const query = sessionId.length > 0 ? "?sessionId=" + encodeURIComponent(sessionId) : "";
@@ -547,11 +784,16 @@ window.__ModuleLoader__.load({
547
784
  if (!alive) return;
548
785
  setSettings(normalizeClientSettings(payload && payload.settings));
549
786
  setUseAi(!(payload && payload.settings && payload.settings.aiEnabled === false));
787
+ const savedFormat = payload && payload.settings && payload.settings.format;
788
+ if (typeof savedFormat === "string" && FORMAT_OPTIONS.includes(savedFormat)) {
789
+ setExportFormat(savedFormat);
790
+ }
550
791
  const nextAi = payload && payload.ai ? payload.ai : { available: false, provider: "", model: "" };
551
792
  setAiInfo({
552
793
  available: nextAi.available === true,
553
794
  provider: typeof nextAi.provider === "string" ? nextAi.provider : "",
554
795
  model: typeof nextAi.model === "string" ? nextAi.model : "",
796
+ models: Array.isArray(nextAi.models) ? nextAi.models : [],
555
797
  });
556
798
  const savedRange = payload && payload.settings && payload.settings.range;
557
799
  if (savedRange && typeof savedRange.start === "string" && typeof savedRange.end === "string") {
@@ -590,7 +832,7 @@ window.__ModuleLoader__.load({
590
832
  saveTimerRef.current = null;
591
833
  }
592
834
  };
593
- }, [settings, useAi, range, loading]);
835
+ }, [settings, useAi, range, exportFormat, loading]);
594
836
 
595
837
  function updateGit(index, patch) {
596
838
  setSettings((previous) => ({
@@ -637,7 +879,7 @@ window.__ModuleLoader__.load({
637
879
  const response = await fetch(API + "/state", {
638
880
  method: "PUT",
639
881
  headers: { "content-type": "application/json" },
640
- body: JSON.stringify({ settings: cleanSettings(settings, useAi, range) }),
882
+ body: JSON.stringify({ settings: cleanSettings(settings, useAi, range, exportFormat) }),
641
883
  signal: controller.signal,
642
884
  });
643
885
  if (!response.ok) throw new Error("save-failed");
@@ -658,6 +900,52 @@ window.__ModuleLoader__.load({
658
900
  persistSettings();
659
901
  }
660
902
 
903
+ async function testSources() {
904
+ if (testingSources) return;
905
+ setTestingSources(true);
906
+ setTestError("");
907
+ setTestResults(null);
908
+ try {
909
+ const response = await fetch(API + "/test-sources", {
910
+ method: "POST",
911
+ headers: { "content-type": "application/json", accept: "application/json" },
912
+ body: JSON.stringify({ settings: cleanSettings(settings, useAi, range, exportFormat), sessionId }),
913
+ });
914
+ if (!response.ok) {
915
+ let payload = null;
916
+ try {
917
+ payload = await response.json();
918
+ } catch {
919
+ payload = null;
920
+ }
921
+ throw new Error(errorMessageFromPayload(payload));
922
+ }
923
+ const payload = await response.json();
924
+ setTestResults(Array.isArray(payload.sources) ? payload.sources : []);
925
+ } catch (err) {
926
+ setTestError(err && err.message ? err.message : t("test.failed", "测试数据源失败"));
927
+ } finally {
928
+ setTestingSources(false);
929
+ }
930
+ }
931
+
932
+ async function cancelGeneration() {
933
+ const jobId = jobIdRef.current;
934
+ if (jobId.length === 0) return;
935
+ try {
936
+ await fetch(API + "/job/cancel", {
937
+ method: "POST",
938
+ headers: { "content-type": "application/json" },
939
+ body: JSON.stringify({ jobId }),
940
+ });
941
+ } catch {
942
+ // The local abort below still stops the UI polling.
943
+ }
944
+ if (pollAbortRef.current) pollAbortRef.current.abort();
945
+ setProgress(null);
946
+ setGenerating(false);
947
+ }
948
+
661
949
  function toggleScan() {
662
950
  const next = !showScan;
663
951
  setShowScan(next);
@@ -821,7 +1109,7 @@ window.__ModuleLoader__.load({
821
1109
  const response = await fetch(API + "/job/export", {
822
1110
  method: "POST",
823
1111
  headers: { "content-type": "application/json", accept: "application/octet-stream" },
824
- body: JSON.stringify({ jobId: previewJobId, edits: previewEdits() }),
1112
+ body: JSON.stringify({ jobId: previewJobId, edits: previewEdits(), format: exportFormat }),
825
1113
  });
826
1114
  if (!response.ok) {
827
1115
  let payload = null;
@@ -837,7 +1125,7 @@ window.__ModuleLoader__.load({
837
1125
  const filenameHeader = response.headers.get("x-report-filename");
838
1126
  const filename = filenameHeader
839
1127
  ? decodeURIComponent(filenameHeader)
840
- : "日报汇总_" + preview.range.start + "_" + preview.range.end + ".xlsx";
1128
+ : "日报汇总_" + preview.range.start + "_" + preview.range.end + "." + formatExtension(exportFormat);
841
1129
  const blob = await response.blob();
842
1130
  const url = window.URL.createObjectURL(blob);
843
1131
  const next = Object.assign({}, summary, { filename, url, size: blob.size });
@@ -919,9 +1207,12 @@ window.__ModuleLoader__.load({
919
1207
  });
920
1208
  }
921
1209
 
922
- async function generate() {
1210
+ async function generate(formatOverride) {
923
1211
  if (generating) return;
924
- const cleaned = cleanSettings(settings, useAi, range);
1212
+ const format = FORMAT_OPTIONS.includes(formatOverride) ? formatOverride : exportFormat;
1213
+ setExportFormat(format);
1214
+ setFormatMenuOpen(false);
1215
+ const cleaned = cleanSettings(settings, useAi, range, format);
925
1216
  if (cleaned.git.length === 0 && cleaned.docs.length === 0) {
926
1217
  setError(t("error.noSources", "请至少添加一个 Git 仓库或本地文档。"));
927
1218
  return;
@@ -971,6 +1262,7 @@ window.__ModuleLoader__.load({
971
1262
  const started = await startResponse.json();
972
1263
  const jobId = started && typeof started.jobId === "string" ? started.jobId : "";
973
1264
  if (jobId.length === 0) throw new Error(t("error.generate", "生成失败"));
1265
+ jobIdRef.current = jobId;
974
1266
  while (true) {
975
1267
  if (controller.signal.aborted) {
976
1268
  const aborted = new Error("aborted");
@@ -1032,6 +1324,7 @@ window.__ModuleLoader__.load({
1032
1324
  setError(err && err.message ? err.message : t("error.generate", "生成失败"));
1033
1325
  } finally {
1034
1326
  if (pollAbortRef.current === controller) pollAbortRef.current = null;
1327
+ jobIdRef.current = "";
1035
1328
  setGenerating(false);
1036
1329
  }
1037
1330
  }
@@ -1080,16 +1373,66 @@ window.__ModuleLoader__.load({
1080
1373
  "button",
1081
1374
  {
1082
1375
  type: "button",
1083
- className: "dsdr-btn dsdr-save-btn",
1376
+ className: "dsdr-btn dsdr-save-btn dsdr-btn-with-icon",
1084
1377
  onClick: saveSettingsNow,
1085
1378
  disabled: saveState === "saving",
1086
1379
  },
1087
- saveState === "saving" ? t("save.savingShort", "正在保存…") : t("save.button", "保存设置"),
1380
+ h(SaveIcon, { size: 15 }),
1381
+ h("span", null, saveState === "saving" ? t("save.savingShort", "正在保存…") : t("save.button", "保存设置")),
1088
1382
  ),
1089
1383
  h(
1090
- "button",
1091
- { type: "button", className: "dsdr-btn dsdr-btn-primary dsdr-generate", onClick: generate, disabled: generating },
1092
- generating ? t("generating", "正在收集并生成…") : t("generate", "生成 XLSX 日报"),
1384
+ "div",
1385
+ { className: "dsdr-split", ref: formatMenuRef },
1386
+ h(
1387
+ "button",
1388
+ {
1389
+ type: "button",
1390
+ className: "dsdr-btn dsdr-btn-primary dsdr-generate dsdr-btn-with-icon",
1391
+ onClick: () => generate(),
1392
+ disabled: generating,
1393
+ },
1394
+ h(GenerateIcon, { size: 15 }),
1395
+ h("span", null, generating ? t("generating", "正在收集并生成…") : t("generate", "生成日报")),
1396
+ ),
1397
+ h(
1398
+ "button",
1399
+ {
1400
+ type: "button",
1401
+ className: "dsdr-btn dsdr-btn-primary dsdr-split-toggle",
1402
+ onClick: () => setFormatMenuOpen((open) => !open),
1403
+ disabled: generating,
1404
+ "aria-haspopup": "menu",
1405
+ "aria-expanded": formatMenuOpen,
1406
+ "aria-label": t("format.label", "格式"),
1407
+ },
1408
+ h(ChevronIcon, { size: 12 }),
1409
+ ),
1410
+ formatMenuOpen
1411
+ ? h(
1412
+ "div",
1413
+ { className: "dsdr-menu", role: "menu" },
1414
+ h("div", { className: "dsdr-menu-title" }, t("format.label", "格式")),
1415
+ FORMAT_OPTIONS.map((format) =>
1416
+ h(
1417
+ "button",
1418
+ {
1419
+ type: "button",
1420
+ role: "menuitemradio",
1421
+ "aria-checked": exportFormat === format,
1422
+ className: "dsdr-menu-item" + (exportFormat === format ? " is-active" : ""),
1423
+ key: format,
1424
+ onClick: () => {
1425
+ setExportFormat(format);
1426
+ setFormatMenuOpen(false);
1427
+ generate(format);
1428
+ },
1429
+ },
1430
+ h("span", null, formatLabel(format, t)),
1431
+ h("span", { className: "dsdr-menu-check" }, exportFormat === format ? "✓" : ""),
1432
+ ),
1433
+ ),
1434
+ )
1435
+ : null,
1093
1436
  ),
1094
1437
  ),
1095
1438
  saveText ? h("div", { className: "dsdr-save" }, saveText) : null,
@@ -1104,7 +1447,12 @@ window.__ModuleLoader__.load({
1104
1447
  "div",
1105
1448
  { className: "dsdr-progress-head" },
1106
1449
  h("span", null, (progress && progress.message) || t("generating", "正在收集并生成…")),
1107
- h("span", { className: "dsdr-progress-value" }, progress && !progress.indeterminate ? Math.round(Number(progress.percent) || 0) + "%" : "…"),
1450
+ h(
1451
+ "div",
1452
+ { className: "dsdr-actions" },
1453
+ h("span", { className: "dsdr-progress-value" }, progress && !progress.indeterminate ? Math.round(Number(progress.percent) || 0) + "%" : "…"),
1454
+ h("button", { type: "button", className: "dsdr-btn", onClick: cancelGeneration }, t("cancel", "取消")),
1455
+ ),
1108
1456
  ),
1109
1457
  h(
1110
1458
  "div",
@@ -1119,7 +1467,14 @@ window.__ModuleLoader__.load({
1119
1467
  : null,
1120
1468
 
1121
1469
  loadError ? h("div", { className: "dsdr-error" }, loadError) : null,
1122
- error ? h("div", { className: "dsdr-error", role: "alert" }, error) : null,
1470
+ error
1471
+ ? h(
1472
+ "div",
1473
+ { className: "dsdr-error dsdr-error-row", role: "alert" },
1474
+ h("span", null, error),
1475
+ h("button", { type: "button", className: "dsdr-btn", onClick: () => generate() }, t("retry", "重试")),
1476
+ )
1477
+ : null,
1123
1478
 
1124
1479
  preview
1125
1480
  ? h(
@@ -1131,8 +1486,12 @@ window.__ModuleLoader__.load({
1131
1486
  h(
1132
1487
  "div",
1133
1488
  null,
1134
- h("div", { className: "dsdr-card-title" }, t("preview.title", "生成结果预览")),
1135
- h("div", { className: "dsdr-card-desc" }, t("preview.desc", "先检查并编辑每日 AI 摘要、补充非代码工作,再导出 XLSX。")),
1489
+ h(
1490
+ "div",
1491
+ { className: "dsdr-title-with-info" },
1492
+ h("div", { className: "dsdr-card-title" }, t("preview.title", "生成结果预览")),
1493
+ h(InfoDot, { text: t("preview.desc", "先检查并编辑每日 AI 摘要、补充非代码工作,再导出 XLSX。") }),
1494
+ ),
1136
1495
  ),
1137
1496
  h(
1138
1497
  "div",
@@ -1150,7 +1509,7 @@ window.__ModuleLoader__.load({
1150
1509
  h(
1151
1510
  "button",
1152
1511
  { type: "button", className: "dsdr-btn dsdr-btn-primary", onClick: exportPreview, disabled: exporting || regenerating.length > 0 },
1153
- exporting ? t("preview.exporting", "正在导出…") : t("preview.export", "导出 XLSX"),
1512
+ exporting ? t("preview.exporting", "正在导出…") : t("preview.export", "导出") + " " + formatShortLabel(exportFormat),
1154
1513
  ),
1155
1514
  ),
1156
1515
  ),
@@ -1288,8 +1647,12 @@ window.__ModuleLoader__.load({
1288
1647
  h(
1289
1648
  "div",
1290
1649
  null,
1291
- h("div", { className: "dsdr-card-title" }, t("range.title", "汇总时间范围")),
1292
- h("div", { className: "dsdr-card-desc" }, t("range.desc", "默认当月月初到月底;可改为任意日期范围。")),
1650
+ h(
1651
+ "div",
1652
+ { className: "dsdr-title-with-info" },
1653
+ h("div", { className: "dsdr-card-title" }, t("range.title", "汇总时间范围")),
1654
+ h(InfoDot, { text: t("range.desc", "默认当月月初到月底;可改为任意日期范围。") }),
1655
+ ),
1293
1656
  ),
1294
1657
  h("div", { className: "dsdr-hint" }, rangeDays > 0 ? t("range.total", "共") + " " + rangeDays + " " + t("range.days", "天") : ""),
1295
1658
  ),
@@ -1323,6 +1686,8 @@ window.__ModuleLoader__.load({
1323
1686
  h(
1324
1687
  "div",
1325
1688
  { className: "dsdr-actions" },
1689
+ h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(yesterdayRange()) }, t("range.yesterday", "昨天")),
1690
+ h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(thisWeekRange()) }, t("range.thisWeek", "本周")),
1326
1691
  h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(currentMonthRange()) }, t("range.thisMonth", "本月")),
1327
1692
  h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(lastMonthRange()) }, t("range.lastMonth", "上月")),
1328
1693
  h("button", { type: "button", className: "dsdr-btn", onClick: () => setRange(last7Range()) }, t("range.last7", "最近 7 天")),
@@ -1339,8 +1704,12 @@ window.__ModuleLoader__.load({
1339
1704
  h(
1340
1705
  "div",
1341
1706
  null,
1342
- h("div", { className: "dsdr-card-title" }, t("ai.title", "AI 分析")),
1343
- h("div", { className: "dsdr-card-desc" }, t("ai.desc", "使用当前默认模型分析提交说明、代码变更和文档摘要,生成按天简要总结。")),
1707
+ h(
1708
+ "div",
1709
+ { className: "dsdr-title-with-info" },
1710
+ h("div", { className: "dsdr-card-title dsdr-ai-title" }, t("ai.title", "AI 分析")),
1711
+ h(InfoDot, { text: t("ai.desc", "使用当前默认模型分析提交说明、代码变更和文档摘要,生成按天简要总结。") }),
1712
+ ),
1344
1713
  ),
1345
1714
  h(
1346
1715
  "label",
@@ -1358,9 +1727,66 @@ window.__ModuleLoader__.load({
1358
1727
  "div",
1359
1728
  { className: "dsdr-hint" },
1360
1729
  aiInfo.available
1361
- ? t("ai.model", "当前模型:") + aiInfo.provider + "/" + aiInfo.model + " · " + t("ai.cost", "会增加生成时间和模型 token 消耗。")
1730
+ ? t("ai.model", "当前模型:") +
1731
+ (settings.aiModel ? (settings.aiProvider ? settings.aiProvider + "/" : "") + settings.aiModel : aiInfo.provider + "/" + aiInfo.model) +
1732
+ " · " +
1733
+ t("ai.cost", "会增加生成时间和模型 token 消耗。")
1362
1734
  : t("ai.unavailable", "当前没有可用的默认模型。"),
1363
1735
  ),
1736
+ h(
1737
+ "div",
1738
+ { className: "dsdr-ai-options" },
1739
+ h(
1740
+ "div",
1741
+ { className: "dsdr-field" },
1742
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-ai-template" }, t("ai.template", "分析风格")),
1743
+ h(
1744
+ "select",
1745
+ {
1746
+ id: "dsdr-ai-template",
1747
+ className: "dsdr-input",
1748
+ value: settings.aiTemplate || "concise",
1749
+ onChange: (event) => setSettings((previous) => ({ ...previous, aiTemplate: event.target.value })),
1750
+ },
1751
+ h("option", { value: "concise" }, t("ai.template.concise", "简洁")),
1752
+ h("option", { value: "detailed" }, t("ai.template.detailed", "详细")),
1753
+ h("option", { value: "manager" }, t("ai.template.manager", "管理汇报")),
1754
+ ),
1755
+ ),
1756
+ h(
1757
+ "div",
1758
+ { className: "dsdr-field" },
1759
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-ai-model" }, t("ai.modelSelect", "模型")),
1760
+ aiInfo.models && aiInfo.models.length > 0
1761
+ ? h(
1762
+ "select",
1763
+ {
1764
+ id: "dsdr-ai-model",
1765
+ className: "dsdr-input",
1766
+ value: settings.aiModel || "",
1767
+ onChange: (event) => {
1768
+ const selected = aiInfo.models.find((model) => model.id === event.target.value);
1769
+ setSettings((previous) => ({
1770
+ ...previous,
1771
+ aiProvider: selected ? selected.provider : "",
1772
+ aiModel: selected ? selected.id : "",
1773
+ }));
1774
+ },
1775
+ },
1776
+ h("option", { value: "" }, t("ai.defaultModel", "默认模型") + "(" + aiInfo.provider + "/" + aiInfo.model + ")"),
1777
+ aiInfo.models.map((model) =>
1778
+ h("option", { value: model.id, key: model.provider + "/" + model.id }, model.name || model.id),
1779
+ ),
1780
+ )
1781
+ : h("input", {
1782
+ id: "dsdr-ai-model",
1783
+ className: "dsdr-input",
1784
+ value: settings.aiModel || "",
1785
+ placeholder: t("ai.defaultModel", "默认模型"),
1786
+ onChange: (event) => setSettings((previous) => ({ ...previous, aiProvider: "", aiModel: event.target.value })),
1787
+ }),
1788
+ ),
1789
+ ),
1364
1790
  ),
1365
1791
 
1366
1792
  h(
@@ -1372,8 +1798,150 @@ window.__ModuleLoader__.load({
1372
1798
  h(
1373
1799
  "div",
1374
1800
  null,
1375
- h("div", { className: "dsdr-card-title" }, t("git.title", "Git 仓库")),
1376
- h("div", { className: "dsdr-card-desc" }, t("git.desc", "支持远程地址或本地仓库;多个仓库、多个分支会合并到同一份日报。")),
1801
+ h(
1802
+ "div",
1803
+ { className: "dsdr-title-with-info" },
1804
+ h("div", { className: "dsdr-card-title" }, t("schedule.title", "自动生成")),
1805
+ h(InfoDot, { text: t("schedule.desc", "在 DSH 运行期间按设定时间自动生成报告,并保存到本地历史目录。") }),
1806
+ ),
1807
+ ),
1808
+ h(
1809
+ "label",
1810
+ { className: "dsdr-check" },
1811
+ h("input", {
1812
+ type: "checkbox",
1813
+ checked: settings.scheduleEnabled === true,
1814
+ onChange: (event) => setSettings((previous) => ({ ...previous, scheduleEnabled: event.target.checked })),
1815
+ }),
1816
+ h("span", null, t("schedule.enable", "启用自动生成")),
1817
+ ),
1818
+ ),
1819
+ h(
1820
+ "div",
1821
+ { className: "dsdr-ai-options" },
1822
+ h(
1823
+ "div",
1824
+ { className: "dsdr-field" },
1825
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-schedule-frequency" }, t("schedule.title", "自动生成")),
1826
+ h(
1827
+ "select",
1828
+ {
1829
+ id: "dsdr-schedule-frequency",
1830
+ className: "dsdr-input",
1831
+ value: settings.scheduleFrequency || "daily",
1832
+ onChange: (event) => setSettings((previous) => ({ ...previous, scheduleFrequency: event.target.value })),
1833
+ },
1834
+ h("option", { value: "daily" }, t("schedule.daily", "每天")),
1835
+ h("option", { value: "weekly" }, t("schedule.weekly", "每周一")),
1836
+ ),
1837
+ ),
1838
+ h(
1839
+ "div",
1840
+ { className: "dsdr-field" },
1841
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-schedule-time" }, t("schedule.time", "生成时间")),
1842
+ h("input", {
1843
+ id: "dsdr-schedule-time",
1844
+ className: "dsdr-input",
1845
+ type: "time",
1846
+ value: settings.scheduleTime || "18:30",
1847
+ onChange: (event) => setSettings((previous) => ({ ...previous, scheduleTime: event.target.value })),
1848
+ }),
1849
+ ),
1850
+ ),
1851
+ h("div", { className: "dsdr-hint" }, t("schedule.hint", "自动生成使用当前保存的数据源、AI 设置和格式。")),
1852
+ ),
1853
+
1854
+ h(
1855
+ "section",
1856
+ { className: "dsdr-card dsdr-datasource-tabs" },
1857
+ h(
1858
+ "div",
1859
+ { className: "dsdr-card-head" },
1860
+ h(
1861
+ "div",
1862
+ null,
1863
+ h(
1864
+ "div",
1865
+ { className: "dsdr-title-with-info" },
1866
+ h("div", { className: "dsdr-card-title" }, t("datasource.title", "数据源配置")),
1867
+ h(InfoDot, { text: t("datasource.desc", "统一管理 Git 仓库与本地文档,生成时合并到同一份日报。") }),
1868
+ ),
1869
+ ),
1870
+ h(
1871
+ "button",
1872
+ { type: "button", className: "dsdr-btn", onClick: testSources, disabled: testingSources },
1873
+ testingSources ? t("test.testing", "测试中…") : t("test.button", "测试数据源"),
1874
+ ),
1875
+ ),
1876
+ h(
1877
+ "div",
1878
+ { className: "dsdr-tabs", role: "tablist" },
1879
+ h(
1880
+ "button",
1881
+ {
1882
+ type: "button",
1883
+ role: "tab",
1884
+ "aria-selected": dataSourceTab === "git",
1885
+ className: "dsdr-tab" + (dataSourceTab === "git" ? " is-active" : ""),
1886
+ onClick: () => setDataSourceTab("git"),
1887
+ },
1888
+ t("datasource.git", "Git 仓库"),
1889
+ h("span", { className: "dsdr-tab-count" }, String(settings.git.length)),
1890
+ ),
1891
+ h(
1892
+ "button",
1893
+ {
1894
+ type: "button",
1895
+ role: "tab",
1896
+ "aria-selected": dataSourceTab === "docs",
1897
+ className: "dsdr-tab" + (dataSourceTab === "docs" ? " is-active" : ""),
1898
+ onClick: () => setDataSourceTab("docs"),
1899
+ },
1900
+ t("datasource.docs", "本地文档"),
1901
+ h("span", { className: "dsdr-tab-count" }, String(settings.docs.length)),
1902
+ ),
1903
+ ),
1904
+ testError ? h("div", { className: "dsdr-error" }, testError) : null,
1905
+ testResults && testResults.length > 0
1906
+ ? h(
1907
+ "div",
1908
+ { className: "dsdr-test-results" },
1909
+ testResults.map((source, index) =>
1910
+ h(
1911
+ "div",
1912
+ { className: "dsdr-row", key: String(index) },
1913
+ h("span", { className: "dsdr-dot " + (source.status || "ok") }),
1914
+ h(
1915
+ "div",
1916
+ null,
1917
+ h("span", { className: "dsdr-source-name" }, source.label || source.location || "—"),
1918
+ h("span", { className: "dsdr-source-meta" }, " · " + (source.status === "error" ? t("status.error", "失败") : t("test.ok", "正常"))),
1919
+ h("div", { className: "dsdr-source-meta" }, source.message || ""),
1920
+ ),
1921
+ ),
1922
+ ),
1923
+ )
1924
+ : null,
1925
+ ),
1926
+
1927
+ h(
1928
+ "section",
1929
+ {
1930
+ className: "dsdr-card dsdr-datasource-panel",
1931
+ style: { display: dataSourceTab === "git" ? undefined : "none" },
1932
+ },
1933
+ h(
1934
+ "div",
1935
+ { className: "dsdr-card-head" },
1936
+ h(
1937
+ "div",
1938
+ null,
1939
+ h(
1940
+ "div",
1941
+ { className: "dsdr-title-with-info" },
1942
+ h("div", { className: "dsdr-card-title" }, t("git.title", "Git 仓库")),
1943
+ h(InfoDot, { text: t("git.desc", "支持远程地址或本地仓库;多个仓库、多个分支会合并到同一份日报。") }),
1944
+ ),
1377
1945
  ),
1378
1946
  h(
1379
1947
  "div",
@@ -1395,7 +1963,30 @@ window.__ModuleLoader__.load({
1395
1963
  }),
1396
1964
  h("span", null, t("git.mine", "仅统计我的提交")),
1397
1965
  ),
1398
- h("span", { className: "dsdr-hint" }, t("git.mineHint", "按各仓库 git config user.email / user.name 过滤")),
1966
+ h(
1967
+ "label",
1968
+ { className: "dsdr-check" },
1969
+ h("input", {
1970
+ type: "checkbox",
1971
+ checked: settings.excludeMerges !== false,
1972
+ onChange: (event) => setSettings((previous) => ({ ...previous, excludeMerges: event.target.checked })),
1973
+ }),
1974
+ h("span", null, t("git.excludeMerges", "排除 merge commit")),
1975
+ ),
1976
+ settings.mineOnly !== false
1977
+ ? h(
1978
+ "div",
1979
+ { className: "dsdr-field dsdr-filter-author" },
1980
+ h("label", { className: "dsdr-label", htmlFor: "dsdr-author-emails" }, t("git.authorEmails", "作者邮箱(可选)")),
1981
+ h("input", {
1982
+ id: "dsdr-author-emails",
1983
+ className: "dsdr-input",
1984
+ value: settings.authorEmails || "",
1985
+ placeholder: t("git.authorEmailsHint", "留空自动读取各仓库 git config;多个邮箱用逗号分隔"),
1986
+ onChange: (event) => setSettings((previous) => ({ ...previous, authorEmails: event.target.value })),
1987
+ }),
1988
+ )
1989
+ : null,
1399
1990
  ),
1400
1991
  showScan
1401
1992
  ? h(
@@ -1478,15 +2069,22 @@ window.__ModuleLoader__.load({
1478
2069
 
1479
2070
  h(
1480
2071
  "section",
1481
- { className: "dsdr-card" },
2072
+ {
2073
+ className: "dsdr-card dsdr-datasource-panel",
2074
+ style: { display: dataSourceTab === "docs" ? undefined : "none" },
2075
+ },
1482
2076
  h(
1483
2077
  "div",
1484
2078
  { className: "dsdr-card-head" },
1485
2079
  h(
1486
2080
  "div",
1487
2081
  null,
1488
- h("div", { className: "dsdr-card-title" }, t("docs.title", "本地文档")),
1489
- h("div", { className: "dsdr-card-desc" }, t("docs.desc", "文件或目录;目录会递归扫描 md、txt、docx、html 等笔记文件。")),
2082
+ h(
2083
+ "div",
2084
+ { className: "dsdr-title-with-info" },
2085
+ h("div", { className: "dsdr-card-title" }, t("docs.title", "本地文档")),
2086
+ h(InfoDot, { text: t("docs.desc", "文件或目录;目录会递归扫描 md、txt、docx、html 等笔记文件。") }),
2087
+ ),
1490
2088
  ),
1491
2089
  h(
1492
2090
  "div",