@morehao/dsh-github-trending 0.4.2 → 0.4.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [v0.4.4] - 2026-10-31
4
+
5
+ ### 变更
6
+
7
+ - **概览榜单支持日/周/月三周期切换**:概览页「最近榜单」区块此前只展示日榜,现在按日榜 / 周榜 / 月榜分段展示,每个周期各自显示最近若干期,点格子可直接跳到榜单页对应期数;分段按钮上显示各周期期数。仅在调度启用多个周期时出现分段控件。
8
+ - **精简空日期展示**:不再把没有记录的日期逐个铺成「无记录」卡片,只渲染真正有记录的期数;完全无记录时显示空状态提示。今天尚未运行且调度开启时,日榜末尾保留一个「等待 {时间}」状态位。
9
+
10
+ ### 修复
11
+
12
+ - **时间格式统一为 24 小时制**:`localTime` / `shortTime` / 下次运行时间等展示改用 `hourCycle: 'h23'`,避免不同环境下出现 12 小时制与 24 小时制混用。
13
+
14
+ ## [v0.4.3] - 2026-10-31
15
+
16
+ ### 变更
17
+
18
+ - **默认三周期全产出,无需勾选**:`schedule.periods` 固定收敛为 `daily,weekly,monthly`,定时运行默认产出日榜、周榜、月榜;设置页移除周期多选框,改为静态说明。旧配置(只有 `daily` 或空数组)在归一化时自动升级为三周期。相应地移除了「调度开启却未选周期」的校验(周期已恒为三项)。
19
+ - **手动/工具运行也默认三榜**:新增 `Runner.runMany()`,`POST /run` 与 `trending_run` 在省略 `period` 时按顺序依次跑日/周/月三个周期(各自独立 id、日志、历史记录、`periodSuccess`),响应新增 `summaries` 数组;显式传 `period` 时仍只跑单周期。批次期间由 `batching` 标志挡住外部并发触发。
20
+
3
21
  ## [v0.4.2] - 2026-10-31
4
22
 
5
23
  ### Features
package/README.md CHANGED
@@ -97,7 +97,7 @@ dsh plugin --profile <profile> add @morehao/dsh-github-trending
97
97
  | `trending.since` / `languages` / `topN` | `daily` / `all,typescript,python,go,rust` / `25` | `since` 是单次运行周期(`daily`/`weekly`/`monthly`,抓取窗口=标的周期由核心保证);实际支持范围由核心决定 |
98
98
  | `output.dir` | `trending` | 仓库内相对目录 |
99
99
  | `schedule.enabled` / `time` / `timezone` | `true` / `08:30:00` / `Asia/Shanghai` | 插件自己的 60 s tick 驱动 |
100
- | `schedule.periods` | `daily` | 定时自动产出的周期(`daily`/`weekly`/`monthly` 多选):日=每天、周=周日、月=月末触发 |
100
+ | `schedule.periods` | `daily,weekly,monthly` | 定时自动产出的周期:默认三周期全产出(无需勾选),日=每天、周=周日、月=月末触发;旧配置自动升级为三周期 |
101
101
  | `corePath` | 空 | 等价于 `TRENDING_DIGEST_CORE`(仅开发/排障,界面不提供入口,用环境变量或直接改 `settings.json`;设了就完全不下载) |
102
102
  | `coreAutoDownload` | `true` | 缓存缺失时是否自动下载核心;关掉后只报 `CORE_NOT_DOWNLOADED`,由你在设置页点「下载核心」 |
103
103
 
package/client.js CHANGED
@@ -143,6 +143,7 @@ var DICT = {
143
143
  "card.schedule.title": "调度",
144
144
  "schedule.enabled": "定时运行",
145
145
  "schedule.periods": "自动产出周期",
146
+ "schedule.periods.value": "日榜 / 周榜 / 月榜(默认全部产出,无需勾选)",
146
147
  "schedule.time": "时间",
147
148
  "schedule.timezone": "时区",
148
149
  "schedule.next": "下次运行",
@@ -279,9 +280,10 @@ var DICT = {
279
280
  "overview.ready.repo": "仓库",
280
281
  "overview.ready.github": "GitHub",
281
282
  "overview.ready.view": "查看",
282
- "overview.week.title": "最近 7 天(日榜)",
283
+ "overview.week.title": "最近榜单",
283
284
  "overview.week.all": "全部运行记录",
284
- "overview.week.none": "无记录",
285
+ "overview.week.empty": "还没有运行记录",
286
+ "overview.week.selectPeriod": "选择榜单周期",
285
287
  "overview.week.wait": "等待 {time}",
286
288
  "overview.live.title": "正在运行",
287
289
  "overview.live.core": "核心执行中",
@@ -469,6 +471,7 @@ var DICT = {
469
471
  "card.schedule.title": "Schedule",
470
472
  "schedule.enabled": "Scheduled runs",
471
473
  "schedule.periods": "Auto periods",
474
+ "schedule.periods.value": "Daily / Weekly / Monthly (all produced by default, no selection needed)",
472
475
  "schedule.time": "Time",
473
476
  "schedule.timezone": "Time zone",
474
477
  "schedule.next": "Next run",
@@ -605,9 +608,10 @@ var DICT = {
605
608
  "overview.ready.repo": "Repository",
606
609
  "overview.ready.github": "GitHub",
607
610
  "overview.ready.view": "View",
608
- "overview.week.title": "Last 7 days (daily)",
611
+ "overview.week.title": "Recent leaderboards",
609
612
  "overview.week.all": "All runs",
610
- "overview.week.none": "No record",
613
+ "overview.week.empty": "No runs yet",
614
+ "overview.week.selectPeriod": "Choose leaderboard period",
611
615
  "overview.week.wait": "Waiting for {time}",
612
616
  "overview.live.title": "Running",
613
617
  "overview.live.core": "Core is working",
@@ -968,7 +972,7 @@ var CSS = `
968
972
  .dshgt-md a:hover{text-decoration:underline;}
969
973
 
970
974
  /* ——— 周期标签 / 分段 ——— */
971
- .dshgt-pchip{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:999px;font-size:11px;font-weight:600;line-height:18px;white-space:nowrap;border:1px solid var(--gt-border);color:var(--gt-muted);background:var(--gt-bg);}
975
+ .dshgt-pchip{display:inline-flex;align-items:center;justify-content:center;height:20px;padding:0 8px;border-radius:999px;font-size:11px;font-weight:600;line-height:18px;white-space:nowrap;border:1px solid var(--gt-border);color:var(--gt-muted);background:var(--gt-bg);}
972
976
  .dshgt-pchip.daily{color:var(--gt-accent);border-color:color-mix(in srgb,var(--gt-accent) 35%,var(--gt-border));background:color-mix(in srgb,var(--gt-accent) 9%,var(--gt-bg));}
973
977
  .dshgt-pchip.weekly{color:#7c3aed;border-color:#ddd6fe;background:#f5f3ff;}
974
978
  .dshgt-pchip.monthly{color:#0f766e;border-color:#99f6e4;background:#f0fdfa;}
@@ -978,6 +982,8 @@ var CSS = `
978
982
  .dshgt-period-seg button.on{background:var(--gt-bg);color:var(--gt-fg);font-weight:600;box-shadow:0 1px 2px rgba(15,23,42,.08);}
979
983
  .dshgt-period-count{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--gt-muted);background:color-mix(in srgb,var(--gt-fg) 6%,var(--gt-bg));border-radius:999px;padding:0 6px;line-height:16px;}
980
984
  .dshgt-period-seg button.on .dshgt-period-count{color:var(--gt-accent);background:color-mix(in srgb,var(--gt-accent) 12%,var(--gt-bg));}
985
+ .dshgt-overview-seg{margin-bottom:8px;}
986
+ .dshgt-overview-empty{padding:14px 2px;}
981
987
 
982
988
  /* ——— 运行记录 ——— */
983
989
  .dshgt-runs-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
@@ -1200,7 +1206,15 @@ function localTime(iso) {
1200
1206
  if (iso === null || iso === void 0) return "—";
1201
1207
  const date = new Date(iso);
1202
1208
  if (Number.isNaN(date.getTime())) return "—";
1203
- return date.toLocaleString();
1209
+ return date.toLocaleString(void 0, {
1210
+ year: "numeric",
1211
+ month: "2-digit",
1212
+ day: "2-digit",
1213
+ hour: "2-digit",
1214
+ minute: "2-digit",
1215
+ second: "2-digit",
1216
+ hourCycle: "h23"
1217
+ });
1204
1218
  }
1205
1219
  async function copyText(value) {
1206
1220
  try {
@@ -1215,7 +1229,13 @@ function shortTime(iso) {
1215
1229
  if (iso === null || iso === void 0) return "—";
1216
1230
  const date = new Date(iso);
1217
1231
  if (Number.isNaN(date.getTime())) return "—";
1218
- return date.toLocaleString(void 0, { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
1232
+ return date.toLocaleString(void 0, {
1233
+ month: "2-digit",
1234
+ day: "2-digit",
1235
+ hour: "2-digit",
1236
+ minute: "2-digit",
1237
+ hourCycle: "h23"
1238
+ });
1219
1239
  }
1220
1240
 
1221
1241
  // client/api.ts
@@ -1785,19 +1805,7 @@ function SettingsView(props) {
1785
1805
  h(
1786
1806
  Row,
1787
1807
  { label: t("schedule.periods") },
1788
- ...["daily", "weekly", "monthly"].map(
1789
- (period) => h(Toggle, {
1790
- key: period,
1791
- checked: draft.schedule.periods.includes(period),
1792
- onChange: (on) => update((next) => {
1793
- const set = new Set(next.schedule.periods);
1794
- if (on) set.add(period);
1795
- else set.delete(period);
1796
- next.schedule.periods = ["daily", "weekly", "monthly"].filter((p) => set.has(p));
1797
- }),
1798
- label: t(`scope.${period}`)
1799
- })
1800
- )
1808
+ h("span", { className: "dshgt-hint" }, t("schedule.periods.value"))
1801
1809
  ),
1802
1810
  h("p", { className: "dshgt-hint" }, t("schedule.source"))
1803
1811
  );
@@ -2198,38 +2206,6 @@ function digestTarget(record, timeZone) {
2198
2206
  const period = recordPeriod(record);
2199
2207
  return { period, bucket: periodBucket(period, info.date) };
2200
2208
  }
2201
- function buildDigestDates(records, options) {
2202
- const now = options.now ?? /* @__PURE__ */ new Date();
2203
- const today = zonedDateString(now, options.timeZone);
2204
- const earliest = addDays(today, -(DIGEST_WINDOW_DAYS - 1));
2205
- const groups = /* @__PURE__ */ new Map();
2206
- for (const record of records) {
2207
- if (recordPeriod(record) !== "daily") continue;
2208
- const info = recordDate(record, options.timeZone);
2209
- if (info === void 0) continue;
2210
- if (info.date < earliest || info.date > today) continue;
2211
- const group = groups.get(info.date);
2212
- if (group === void 0) groups.set(info.date, { records: [record], derivedFromAt: info.derivedFromAt });
2213
- else {
2214
- group.records.push(record);
2215
- group.derivedFromAt = group.derivedFromAt || info.derivedFromAt;
2216
- }
2217
- }
2218
- const days = [];
2219
- for (const [date, group] of groups) {
2220
- const sorted = [...group.records].sort((a, b) => b.at.localeCompare(a.at));
2221
- const preferred = sorted.find(
2222
- (record2) => record2.preview !== void 0 && record2.ok && record2.kind !== "dry-run" && !record2.skipped
2223
- );
2224
- const record = preferred ?? sorted[0];
2225
- if (record === void 0) continue;
2226
- const info = recordDate(record, options.timeZone);
2227
- const legacy = record.legacy === true || info?.derivedFromAt === true || group.derivedFromAt;
2228
- days.push({ date, record, status: digestStatus(record), legacy });
2229
- }
2230
- days.sort((a, b) => b.date.localeCompare(a.date));
2231
- return days;
2232
- }
2233
2209
  function buildPeriodGroups(records, options) {
2234
2210
  const now = options.now ?? /* @__PURE__ */ new Date();
2235
2211
  const today = zonedDateString(now, options.timeZone);
@@ -2275,11 +2251,6 @@ function buildPeriodGroups(records, options) {
2275
2251
  return { period, days, previewCount: days.filter((day) => day.status === "preview").length };
2276
2252
  });
2277
2253
  }
2278
- function recentDates(today, count) {
2279
- const dates = [];
2280
- for (let offset = count - 1; offset >= 0; offset -= 1) dates.push(addDays(today, -offset));
2281
- return dates;
2282
- }
2283
2254
  function formatDayLabel(date, lang) {
2284
2255
  let weekday = "";
2285
2256
  try {
@@ -2379,6 +2350,7 @@ function readyRows(t, state) {
2379
2350
  function OverviewView(props) {
2380
2351
  const { t, lang, store, controller } = props;
2381
2352
  const state = store.state;
2353
+ const [activePeriod, setActivePeriod] = import_react.default.useState("daily");
2382
2354
  if (state === null) {
2383
2355
  return h("div", { className: "dshgt-empty" }, store.error ?? t("quickfill.loading"));
2384
2356
  }
@@ -2388,9 +2360,15 @@ function OverviewView(props) {
2388
2360
  const readyCount = rows.filter((row) => row.ok).length;
2389
2361
  const timeZone = state.schedule.timezone;
2390
2362
  const today = zonedDateString(/* @__PURE__ */ new Date(), timeZone);
2391
- const days = buildDigestDates(state.status.history, { timeZone });
2392
- const byDate = new Map(days.map((day) => [day.date, day]));
2393
- const week = recentDates(today, 7);
2363
+ const groups = buildPeriodGroups(state.status.history, { timeZone });
2364
+ const groupMap = new Map(groups.map((group) => [group.period, group]));
2365
+ const enabledPeriods = PERIODS.filter((period2) => state.schedule.periods.includes(period2));
2366
+ const periods = enabledPeriods.length > 0 ? enabledPeriods : PERIODS;
2367
+ const period = periods.includes(activePeriod) ? activePeriod : periods[0] ?? "daily";
2368
+ const activeDays = groupMap.get(period)?.days ?? [];
2369
+ const recent = period === "daily" ? activeDays.slice(0, 7) : activeDays;
2370
+ const todayHasDaily = groupMap.get("daily")?.days.some((day) => day.date === today) ?? false;
2371
+ const showWaiting = period === "daily" && !todayHasDaily && state.schedule.enabled;
2394
2372
  const lastTarget = last === null ? void 0 : digestTarget(last, timeZone);
2395
2373
  return h(
2396
2374
  "div",
@@ -2502,42 +2480,59 @@ function OverviewView(props) {
2502
2480
  t("overview.week.title"),
2503
2481
  h("button", { type: "button", className: "dshgt-linkbtn dshgt-section-tail", onClick: props.onOpenRuns }, t("overview.week.all"))
2504
2482
  ),
2505
- h(
2483
+ periods.length > 1 ? h(
2506
2484
  "div",
2507
- { className: "dshgt-week" },
2508
- week.map((date) => {
2509
- const day = byDate.get(date);
2510
- if (day === void 0) {
2511
- const waiting = date === today && state.schedule.enabled;
2512
- return h(
2513
- "div",
2514
- { className: "dshgt-day", key: date },
2515
- h("div", { className: "dshgt-day-date" }, date.slice(5).replace("-", "/")),
2516
- h("div", { className: "dshgt-day-status" }, waiting ? t("overview.week.wait", { time: state.schedule.time }) : t("overview.week.none"))
2517
- );
2518
- }
2485
+ { className: "dshgt-period-seg dshgt-overview-seg", role: "tablist", "aria-label": t("overview.week.selectPeriod") },
2486
+ periods.map((item) => {
2487
+ const on = item === period;
2488
+ const count = groupMap.get(item)?.days.length ?? 0;
2519
2489
  return h(
2520
2490
  "button",
2521
2491
  {
2492
+ key: item,
2522
2493
  type: "button",
2523
- className: "dshgt-day dshgt-day-clickable",
2524
- key: date,
2525
- onClick: () => props.onOpenDigest({ period: "daily", bucket: date }),
2526
- title: day.record.errorCode === void 0 ? formatDayLabel(date, lang) : `${formatDayLabel(date, lang)} · ${day.record.errorCode}`
2494
+ role: "tab",
2495
+ "aria-selected": on ? "true" : "false",
2496
+ className: on ? "on" : "",
2497
+ onClick: () => setActivePeriod(item)
2527
2498
  },
2528
- h("div", { className: "dshgt-day-date" }, date.slice(5).replace("-", "/")),
2529
- h(
2530
- "div",
2531
- { className: "dshgt-day-status" },
2532
- h("span", { className: `dshgt-lamp ${STATUS_LAMP[day.status]}` }),
2533
- dayLabel(t, day)
2534
- )
2499
+ h("span", null, t(`period.${item}`)),
2500
+ h("span", { className: "dshgt-period-count" }, String(count))
2535
2501
  );
2536
2502
  })
2503
+ ) : null,
2504
+ recent.length === 0 && !showWaiting ? h("div", { className: "dshgt-hint dshgt-overview-empty" }, t("overview.week.empty")) : h(
2505
+ "div",
2506
+ { className: "dshgt-week" },
2507
+ recent.map(
2508
+ (day) => h(
2509
+ "button",
2510
+ {
2511
+ type: "button",
2512
+ className: "dshgt-day dshgt-day-clickable",
2513
+ key: day.bucket,
2514
+ onClick: () => props.onOpenDigest({ period, bucket: day.bucket }),
2515
+ title: day.record.errorCode === void 0 ? formatBucketLabel(period, day.bucket, lang) : `${formatBucketLabel(period, day.bucket, lang)} · ${day.record.errorCode}`
2516
+ },
2517
+ h("div", { className: "dshgt-day-date" }, shortBucket(period, day.bucket)),
2518
+ h("div", { className: "dshgt-day-status" }, h("span", { className: `dshgt-lamp ${STATUS_LAMP[day.status]}` }), dayLabel(t, day))
2519
+ )
2520
+ ),
2521
+ showWaiting ? h(
2522
+ "div",
2523
+ { className: "dshgt-day" },
2524
+ h("div", { className: "dshgt-day-date" }, today.slice(5).replace("-", "/")),
2525
+ h("div", { className: "dshgt-day-status" }, t("overview.week.wait", { time: state.schedule.time }))
2526
+ ) : null
2537
2527
  )
2538
2528
  )
2539
2529
  );
2540
2530
  }
2531
+ function shortBucket(period, bucket) {
2532
+ if (period === "daily") return bucket.slice(5).replace("-", "/");
2533
+ if (period === "weekly") return bucket.slice(5);
2534
+ return bucket.slice(2).replace("-", "/");
2535
+ }
2541
2536
 
2542
2537
  // client/runs.ts
2543
2538
  var FILTERS = [
@@ -3902,7 +3897,13 @@ function nextRunText(iso) {
3902
3897
  if (iso === void 0) return "—";
3903
3898
  const date = new Date(iso);
3904
3899
  if (Number.isNaN(date.getTime())) return "—";
3905
- return date.toLocaleString(void 0, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" });
3900
+ return date.toLocaleString(void 0, {
3901
+ month: "numeric",
3902
+ day: "numeric",
3903
+ hour: "2-digit",
3904
+ minute: "2-digit",
3905
+ hourCycle: "h23"
3906
+ });
3906
3907
  }
3907
3908
  function apply(ctx) {
3908
3909
  const slots = ctx.get?.("slots");
@@ -56,7 +56,7 @@ var DEFAULT_SETTINGS = {
56
56
  github: { authMode: "gh", tokenRef: "GITHUB_TOKEN" },
57
57
  trending: { since: "daily", languages: ["all", "typescript", "python", "go", "rust"], topN: 25 },
58
58
  output: { dir: "trending" },
59
- schedule: { enabled: true, time: "08:30:00", timezone: "Asia/Shanghai", periods: ["daily"] },
59
+ schedule: { enabled: true, time: "08:30:00", timezone: "Asia/Shanghai", periods: ["daily", "weekly", "monthly"] },
60
60
  corePath: "",
61
61
  coreAutoDownload: true
62
62
  };
@@ -117,10 +117,8 @@ function strArray(value, fallback) {
117
117
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
118
118
  return items.length > 0 ? items.map((item) => item.trim()) : fallback;
119
119
  }
120
- function periodList(value, fallback) {
121
- if (value === void 0 || !Array.isArray(value)) return [...fallback];
122
- const set = new Set(value.filter(isPeriod));
123
- return PERIODS.filter((period) => set.has(period));
120
+ function periodList(_value, _fallback) {
121
+ return [...PERIODS];
124
122
  }
125
123
  function normalizeSettings(input) {
126
124
  const raw = typeof input === "object" && input !== null ? input : {};
@@ -243,9 +241,6 @@ function validateSettings(settings, providers = CORE_PROVIDERS) {
243
241
  if (!isValidTimezone(schedule.timezone)) {
244
242
  problems.push({ field: "schedule.timezone", code: "CONFIG_INVALID", message: "\u65F6\u533A\u5FC5\u987B\u662F IANA \u540D\u79F0\uFF08\u5982 Asia/Shanghai\uFF09" });
245
243
  }
246
- if (schedule.enabled && schedule.periods.length === 0) {
247
- problems.push({ field: "schedule.periods", code: "CONFIG_INVALID", message: "\u5B9A\u65F6\u8FD0\u884C\u5F00\u542F\u65F6\u81F3\u5C11\u8981\u9009\u62E9\u4E00\u4E2A\u4EA7\u51FA\u5468\u671F\uFF08\u65E5/\u5468/\u6708\uFF09" });
248
- }
249
244
  return problems;
250
245
  }
251
246
  function effectiveBaseURL(model) {
@@ -2170,7 +2165,7 @@ var Scheduler = class {
2170
2165
  for (const key of this.attempts.keys()) {
2171
2166
  if (!key.endsWith(`:${today}`)) this.attempts.delete(key);
2172
2167
  }
2173
- const periods = schedule.periods.length > 0 ? schedule.periods : ["daily"];
2168
+ const periods = schedule.periods.length > 0 ? schedule.periods : PERIODS;
2174
2169
  let ran = false;
2175
2170
  for (const period of periods) {
2176
2171
  if (!isPeriodTriggerDay(period, now, schedule.timezone)) continue;
@@ -2215,7 +2210,7 @@ var Scheduler = class {
2215
2210
  next() {
2216
2211
  const schedule = this.readSchedule();
2217
2212
  if (!schedule.enabled) return void 0;
2218
- const periods = schedule.periods.length > 0 ? schedule.periods : ["daily"];
2213
+ const periods = schedule.periods.length > 0 ? schedule.periods : PERIODS;
2219
2214
  try {
2220
2215
  const now = /* @__PURE__ */ new Date();
2221
2216
  let earliest;
@@ -2304,6 +2299,8 @@ function periodFileBase(period, date) {
2304
2299
  }
2305
2300
  var Runner = class {
2306
2301
  active;
2302
+ /** 批量运行(一次触发跑多个周期)期间为 true,挡住外部并发触发。 */
2303
+ batching = false;
2307
2304
  deps;
2308
2305
  constructor(deps) {
2309
2306
  this.deps = deps;
@@ -2316,13 +2313,13 @@ var Runner = class {
2316
2313
  this.active.controller.abort();
2317
2314
  return true;
2318
2315
  }
2319
- async run(options) {
2316
+ async run(options, _internal = false) {
2320
2317
  const env = this.deps.env ?? process.env;
2321
2318
  const store = this.deps.store;
2322
2319
  const settings = store.readSettings();
2323
2320
  const credentials = credentialServices(this.deps.get);
2324
- if (this.active !== void 0) {
2325
- throw new IpcError("CONFIG_INVALID", `\u5DF2\u6709\u4E00\u6B21\u8FD0\u884C\u5728\u8FDB\u884C\u4E2D\uFF08${this.active.id}\uFF09\uFF0C\u8BF7\u5148\u7B49\u5F85\u6216\u53D6\u6D88`);
2321
+ if (!_internal && (this.active !== void 0 || this.batching)) {
2322
+ throw new IpcError("CONFIG_INVALID", `\u5DF2\u6709\u4E00\u6B21\u8FD0\u884C\u5728\u8FDB\u884C\u4E2D\uFF08${this.active?.id ?? "\u6279\u91CF\u8FD0\u884C"}\uFF09\uFF0C\u8BF7\u5148\u7B49\u5F85\u6216\u53D6\u6D88`);
2326
2323
  }
2327
2324
  const problems = validateSettings(settings);
2328
2325
  if (problems.length > 0) {
@@ -2444,6 +2441,28 @@ var Runner = class {
2444
2441
  this.active = void 0;
2445
2442
  }
2446
2443
  }
2444
+ /**
2445
+ * 批量运行:按顺序对多个周期各跑一次(手动/工具触发时默认产出日/周/月三榜)。
2446
+ * 每次内部独享 id / 日志 / 摘要;整个批次期间置 `batching`,挡住外部并发触发。
2447
+ * 取消只中止当前周期,批次继续跑后续周期(用户取消单次即可)。
2448
+ */
2449
+ async runMany(options) {
2450
+ if (this.active !== void 0 || this.batching) {
2451
+ throw new IpcError("CONFIG_INVALID", `\u5DF2\u6709\u4E00\u6B21\u8FD0\u884C\u5728\u8FDB\u884C\u4E2D\uFF08${this.active?.id ?? "\u6279\u91CF\u8FD0\u884C"}\uFF09\uFF0C\u8BF7\u5148\u7B49\u5F85\u6216\u53D6\u6D88`);
2452
+ }
2453
+ const periods = options.periods !== void 0 && options.periods.length > 0 ? options.periods : [...PERIODS];
2454
+ this.batching = true;
2455
+ const summaries = [];
2456
+ try {
2457
+ for (const period of periods) {
2458
+ summaries.push(await this.run({ ...options, period }, true));
2459
+ }
2460
+ } finally {
2461
+ this.batching = false;
2462
+ this.active = void 0;
2463
+ }
2464
+ return summaries;
2465
+ }
2447
2466
  summarize(input) {
2448
2467
  const { outcome } = input;
2449
2468
  const error = outcome.error === void 0 ? void 0 : (() => {
@@ -3341,13 +3360,17 @@ async function dispatch(method, pathname, url, request, deps) {
3341
3360
  }
3342
3361
  if (method === "POST" && pathname === "/run") {
3343
3362
  const body = await readBody2(request);
3344
- const summary = await deps.runner.run({
3363
+ const base = {
3345
3364
  dryRun: body.dryRun === true,
3346
3365
  ...typeof body.date === "string" && body.date !== "" ? { date: body.date } : {},
3347
- ...isPeriod(body.period) ? { period: body.period } : {},
3348
3366
  trigger: "manual"
3349
- });
3350
- return { summary };
3367
+ };
3368
+ if (isPeriod(body.period)) {
3369
+ const summary = await deps.runner.run({ ...base, period: body.period });
3370
+ return { summary, summaries: [summary] };
3371
+ }
3372
+ const summaries = await deps.runner.runMany(base);
3373
+ return { summary: summaries[summaries.length - 1], summaries };
3351
3374
  }
3352
3375
  if (method === "POST" && pathname === "/run/cancel") {
3353
3376
  return { cancelled: deps.runner.cancel() };
@@ -3617,11 +3640,11 @@ function describeRecord(record) {
3617
3640
  function createTrendingRunTool(runner) {
3618
3641
  return {
3619
3642
  name: "trending_run",
3620
- description: "\u6267\u884C\u4E00\u6B21 GitHub \u8D8B\u52BF\u699C\u5355\uFF1A\u5B9A\u4F4D\u5E76\u63E1\u624B\u672C\u5730\u6838\u5FC3\uFF0Cspawn \u4E00\u6B21 run\uFF0C\u8FD4\u56DE\u7ED3\u6784\u5316\u6458\u8981\uFF08\u6587\u4EF6\u6570\u3001commit\u3001PR\u3001\u9519\u8BEF\u7801\uFF09\u3002dryRun=true \u53EA\u6293\u53D6\u4E0E\u6E32\u67D3\u3001\u4E0D\u5199\u6587\u4EF6\u4E5F\u4E0D\u63D0\u4EA4\u3002\u540C\u65E5\u540C\u5185\u5BB9\u91CD\u8DD1\u4F1A\u8FD4\u56DE skipped=true\uFF08\u96F6\u65B0\u589E commit\uFF09\u3002",
3643
+ description: "\u6267\u884C GitHub \u8D8B\u52BF\u699C\u5355\uFF1A\u5B9A\u4F4D\u5E76\u63E1\u624B\u672C\u5730\u6838\u5FC3\uFF0Cspawn \u6838\u5FC3 run\uFF0C\u8FD4\u56DE\u7ED3\u6784\u5316\u6458\u8981\uFF08\u6587\u4EF6\u6570\u3001commit\u3001PR\u3001\u9519\u8BEF\u7801\uFF09\u3002\u7701\u7565 period \u65F6\u9ED8\u8BA4\u4EA7\u51FA\u65E5/\u5468/\u6708\u4E09\u699C\uFF08\u4F9D\u6B21\u8FD0\u884C\u4E09\u6B21\uFF0C\u8FD4\u56DE summaries\uFF09\u3002dryRun=true \u53EA\u6293\u53D6\u4E0E\u6E32\u67D3\u3001\u4E0D\u5199\u6587\u4EF6\u4E5F\u4E0D\u63D0\u4EA4\u3002\u540C\u65E5\u540C\u5185\u5BB9\u91CD\u8DD1\u4F1A\u8FD4\u56DE skipped=true\uFF08\u96F6\u65B0\u589E commit\uFF09\u3002",
3621
3644
  parameters: {
3622
3645
  dryRun: { type: "boolean", description: "\u53EA\u8BD5\u8DD1\uFF1A\u4E0D\u5199\u6587\u4EF6\u3001\u4E0D\u63D0\u4EA4\u3001\u4E0D\u5F00 PR" },
3623
3646
  date: { type: "string", description: "\u6307\u5B9A\u65E5\u671F\uFF08YYYY-MM-DD\uFF09\uFF1B\u7701\u7565\u5219\u7528\u8C03\u5EA6\u65F6\u533A\u7684\u4ECA\u5929" },
3624
- period: { type: "string", enum: ["daily", "weekly", "monthly"], description: "\u4EA7\u51FA\u5468\u671F\uFF1B\u7701\u7565\u5219\u7528\u8BBE\u7F6E\u91CC\u7684\u9ED8\u8BA4\u5468\u671F" }
3647
+ period: { type: "string", enum: ["daily", "weekly", "monthly"], description: "\u53EA\u8DD1\u6307\u5B9A\u5468\u671F\uFF1B\u7701\u7565\u5219\u9ED8\u8BA4\u4EA7\u51FA\u65E5/\u5468/\u6708\u4E09\u699C" }
3625
3648
  },
3626
3649
  output: {
3627
3650
  schema: RUN_OUTPUT_SCHEMA,
@@ -3629,37 +3652,51 @@ function createTrendingRunTool(runner) {
3629
3652
  },
3630
3653
  async execute(args = {}, exec = {}) {
3631
3654
  void exec;
3632
- const summary = await runner.run({
3655
+ const base = {
3633
3656
  dryRun: args.dryRun === true,
3634
3657
  ...typeof args.date === "string" && args.date !== "" ? { date: args.date } : {},
3635
- ...isPeriod(args.period) ? { period: args.period } : {},
3636
3658
  trigger: "tool"
3637
- });
3659
+ };
3660
+ const summaries = isPeriod(args.period) ? [await runner.run({ ...base, period: args.period })] : await runner.runMany(base);
3638
3661
  const lines = [];
3639
- const head = summary.ok ? summary.skipped ? "\u65E0\u53D8\u66F4\uFF08skipped\uFF09" : summary.dryRun ? "\u8BD5\u8DD1\u5B8C\u6210" : "\u8FD0\u884C\u6210\u529F" : `\u8FD0\u884C\u5931\u8D25\uFF1A${summary.error?.code ?? "INTERNAL"}`;
3640
- lines.push(`${summary.ok ? "\u2713" : "\u2717"} ${head}`);
3641
- lines.push(`\xB7 \u6838\u5FC3 ${summary.coreVersion || "(\u672A\u77E5)"} \xB7 ${formatDuration(summary.durationMs)} \xB7 ${summary.files} \u4E2A\u6587\u4EF6`);
3642
- if (summary.contentHash !== void 0) lines.push(`\xB7 contentHash ${summary.contentHash.slice(0, 12)}`);
3643
- if (summary.branch !== void 0) lines.push(`\xB7 \u5206\u652F ${summary.branch}`);
3644
- if (summary.prUrl !== void 0) lines.push(`\xB7 PR ${summary.prUrl}`);
3645
- if (summary.error !== void 0) {
3646
- lines.push(`\xB7 ${summary.error.message}`);
3647
- lines.push(`\xB7 \u4E0B\u4E00\u6B65\uFF1A${summary.error.hint}`);
3662
+ for (const summary of summaries) {
3663
+ const periodLabel = summary.period ?? "\u2014";
3664
+ const head = summary.ok ? summary.skipped ? "\u65E0\u53D8\u66F4\uFF08skipped\uFF09" : summary.dryRun ? "\u8BD5\u8DD1\u5B8C\u6210" : "\u8FD0\u884C\u6210\u529F" : `\u8FD0\u884C\u5931\u8D25\uFF1A${summary.error?.code ?? "INTERNAL"}`;
3665
+ lines.push(`${summary.ok ? "\u2713" : "\u2717"} [${periodLabel}] ${head}`);
3666
+ lines.push(`\xB7 \u6838\u5FC3 ${summary.coreVersion || "(\u672A\u77E5)"} \xB7 ${formatDuration(summary.durationMs)} \xB7 ${summary.files} \u4E2A\u6587\u4EF6`);
3667
+ if (summary.contentHash !== void 0) lines.push(`\xB7 contentHash ${summary.contentHash.slice(0, 12)}`);
3668
+ if (summary.branch !== void 0) lines.push(`\xB7 \u5206\u652F ${summary.branch}`);
3669
+ if (summary.prUrl !== void 0) lines.push(`\xB7 PR ${summary.prUrl}`);
3670
+ if (summary.error !== void 0) {
3671
+ lines.push(`\xB7 ${summary.error.message}`);
3672
+ lines.push(`\xB7 \u4E0B\u4E00\u6B65\uFF1A${summary.error.hint}`);
3673
+ }
3648
3674
  }
3649
- if (summary.dryRun) lines.push("\xB7 \u8FD9\u662F\u8BD5\u8DD1\uFF0C\u6CA1\u6709\u5199\u6587\u4EF6\u3001\u6CA1\u6709\u63D0\u4EA4");
3675
+ if (summaries.length > 1) lines.push(`\xB7 \u672C\u6B21\u5171\u8DD1 ${summaries.length} \u4E2A\u5468\u671F\uFF08\u65E5/\u5468/\u6708\uFF09`);
3676
+ if (summaries.some((summary) => summary.dryRun)) lines.push("\xB7 \u8FD9\u662F\u8BD5\u8DD1\uFF0C\u6CA1\u6709\u5199\u6587\u4EF6\u3001\u6CA1\u6709\u63D0\u4EA4");
3677
+ const ok = summaries.every((summary) => summary.ok);
3678
+ const primary = summaries[summaries.length - 1];
3650
3679
  return {
3651
- id: summary.id,
3652
- ok: summary.ok,
3653
- skipped: summary.skipped,
3654
- dryRun: summary.dryRun,
3655
- durationMs: summary.durationMs,
3656
- coreVersion: summary.coreVersion,
3657
- files: summary.files,
3658
- ...summary.contentHash === void 0 ? {} : { contentHash: summary.contentHash },
3659
- ...summary.branch === void 0 ? {} : { branch: summary.branch },
3660
- ...summary.prUrl === void 0 ? {} : { prUrl: summary.prUrl },
3661
- ...summary.error === void 0 ? {} : { errorCode: summary.error.code, errorMessage: summary.error.message, errorHint: summary.error.hint },
3662
- warnings: summary.warnings,
3680
+ ok,
3681
+ summaries: summaries.map((summary) => ({
3682
+ id: summary.id,
3683
+ period: summary.period,
3684
+ ok: summary.ok,
3685
+ skipped: summary.skipped,
3686
+ dryRun: summary.dryRun,
3687
+ durationMs: summary.durationMs,
3688
+ files: summary.files,
3689
+ ...summary.branch === void 0 ? {} : { branch: summary.branch },
3690
+ ...summary.prUrl === void 0 ? {} : { prUrl: summary.prUrl },
3691
+ ...summary.error === void 0 ? {} : { errorCode: summary.error.code, errorMessage: summary.error.message }
3692
+ })),
3693
+ id: primary?.id,
3694
+ skipped: summaries.every((summary) => summary.skipped),
3695
+ dryRun: base.dryRun,
3696
+ durationMs: summaries.reduce((sum, summary) => sum + summary.durationMs, 0),
3697
+ files: summaries.reduce((sum, summary) => sum + summary.files, 0),
3698
+ warnings: summaries.flatMap((summary) => summary.warnings),
3699
+ ...primary?.error === void 0 ? {} : { errorCode: primary.error.code, errorMessage: primary.error.message, errorHint: primary.error.hint },
3663
3700
  markdown: lines.join("\n")
3664
3701
  };
3665
3702
  }
package/lib/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  createTrendingRunTool,
9
9
  createUpdateService,
10
10
  readPluginInfo
11
- } from "./chunk-WTVBXQYN.js";
11
+ } from "./chunk-GBXP2NGW.js";
12
12
  import "./chunk-XLIFTAVL.js";
13
13
 
14
14
  // src/index.ts
package/lib/internal.d.ts CHANGED
@@ -222,6 +222,8 @@ interface ScheduleSettings {
222
222
  /**
223
223
  * 定时自动产出的周期(插件侧调度概念,核心无此字段)。
224
224
  * 每个周期按自己的节奏触发:日=每天、周=周日、月=月末。
225
+ * 现在固定为三周期全产出(`['daily','weekly','monthly']`),不再由用户勾选;
226
+ * 保留该字段以兼容旧设置、并驱动调度与去重逻辑。
225
227
  */
226
228
  periods: Period[];
227
229
  }
@@ -1272,6 +1274,8 @@ declare function isoWeekName(date: string): string;
1272
1274
  declare function periodFileBase(period: Period, date: string): string;
1273
1275
  declare class Runner {
1274
1276
  private active;
1277
+ /** 批量运行(一次触发跑多个周期)期间为 true,挡住外部并发触发。 */
1278
+ private batching;
1275
1279
  private readonly deps;
1276
1280
  constructor(deps: RunnerDeps);
1277
1281
  get running(): {
@@ -1279,7 +1283,15 @@ declare class Runner {
1279
1283
  startedAt: string;
1280
1284
  } | undefined;
1281
1285
  cancel(): boolean;
1282
- run(options: RunRequestOptions): Promise<RunSummary>;
1286
+ run(options: RunRequestOptions, _internal?: boolean): Promise<RunSummary>;
1287
+ /**
1288
+ * 批量运行:按顺序对多个周期各跑一次(手动/工具触发时默认产出日/周/月三榜)。
1289
+ * 每次内部独享 id / 日志 / 摘要;整个批次期间置 `batching`,挡住外部并发触发。
1290
+ * 取消只中止当前周期,批次继续跑后续周期(用户取消单次即可)。
1291
+ */
1292
+ runMany(options: RunRequestOptions & {
1293
+ periods?: Period[];
1294
+ }): Promise<RunSummary[]>;
1283
1295
  private summarize;
1284
1296
  private failureSummary;
1285
1297
  /**
@@ -1324,17 +1336,6 @@ declare namespace pluginInfo {
1324
1336
  export { type pluginInfo_PluginInfo as PluginInfo, pluginInfo_normalizeRepositoryUrl as normalizeRepositoryUrl, pluginInfo_readPluginInfo as readPluginInfo };
1325
1337
  }
1326
1338
 
1327
- /**
1328
- * 调度(FR-P-08 / 规格 §9)。
1329
- *
1330
- * 规格原本设想「复用宿主 schedule 服务」,但实测:宿主的 `ctx.schedule` 是 **会话本地**
1331
- * 的提醒(rc.5 才引入,create 需要 sessionId,冷会话不投递),而本插件的运行完全不依赖会话。
1332
- * 因此这里实现规格 §9 已经写明的兜底:宿主 `timer` 服务(或 `setInterval`)60 s tick、
1333
- * 按 IANA 时区判当日、6 h 内补跑、同日不重复。
1334
- *
1335
- * 「下一次运行」也由同一份时区算术给出,界面与工具都用它。
1336
- */
1337
-
1338
1339
  interface ScheduleConfig {
1339
1340
  enabled: boolean;
1340
1341
  time: string;
@@ -1591,21 +1592,30 @@ declare function createTrendingRunTool(runner: Runner): {
1591
1592
  }, exec?: {
1592
1593
  signal?: AbortSignal;
1593
1594
  }): Promise<{
1594
- warnings: string[];
1595
1595
  markdown: string;
1596
1596
  errorCode?: string | undefined;
1597
1597
  errorMessage?: string | undefined;
1598
1598
  errorHint?: string | undefined;
1599
- prUrl?: string | undefined;
1600
- branch?: string | undefined;
1601
- contentHash?: string | undefined;
1602
- id: string;
1603
1599
  ok: boolean;
1600
+ summaries: {
1601
+ errorCode?: string | undefined;
1602
+ errorMessage?: string | undefined;
1603
+ prUrl?: string | undefined;
1604
+ branch?: string | undefined;
1605
+ id: string;
1606
+ period: Period | undefined;
1607
+ ok: boolean;
1608
+ skipped: boolean;
1609
+ dryRun: boolean;
1610
+ durationMs: number;
1611
+ files: number;
1612
+ }[];
1613
+ id: string | undefined;
1604
1614
  skipped: boolean;
1605
1615
  dryRun: boolean;
1606
1616
  durationMs: number;
1607
- coreVersion: string;
1608
1617
  files: number;
1618
+ warnings: string[];
1609
1619
  }>;
1610
1620
  };
1611
1621
  declare function createTrendingHistoryTool(store: Store, scheduler: Scheduler): {
package/lib/internal.js CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  state_exports,
25
25
  tools_exports,
26
26
  update_exports
27
- } from "./chunk-WTVBXQYN.js";
27
+ } from "./chunk-GBXP2NGW.js";
28
28
  import {
29
29
  exec_exports
30
30
  } from "./chunk-XLIFTAVL.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-github-trending",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "GitHub 趋势日报插件:每天定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与总结,提交到你的仓库并自动合并 PR。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",