@morehao/dsh-github-trending 0.3.1 → 0.4.0

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-10-31
4
+
5
+ ### 变更
6
+
7
+ - **适配核心 0.4.0 的三周期产物布局**:`dsh.core.version` 升到 `0.4.0`。核心产物改为 `trending/<period>/<bucket>/<name>.{md,json}`(daily/weekly/monthly),分支为 `trending/<period>/<name>`;插件每次运行按标的周期传 `run --period daily|weekly|monthly`,保证「抓取窗口 = 标注周期」。
8
+ - **预览按周期选取**:从本次 artifact 里挑本期 digest 的 `.md`(daily=`YYYY-MM-DD.md`、weekly=`YYYY-Wnn.md`、monthly=`YYYY-MM.md`),并排除 `index.md`;日榜预览行为不变。
9
+ - **调度按周期**:新增 `schedule.periods`(日/周/月多选,默认 `daily`)。日榜每天、周榜周日(ISO 周末)、月榜月末触发;去重键含周期与本地日期,同一周期当天只成功一次;「下次运行」取所有已启用周期里最近的一次。
10
+ - `trending_run` 工具与 `POST /run` 增加可选 `period` 参数(缺省用 `trending.since`)。
11
+
12
+ ## 0.3.2 - 2026-10-31
13
+
14
+ ### 改进
15
+
16
+ - **更新入口更显眼**:概览页新增「插件更新」卡片——当前 / 最新版本、检查更新、更新到 vX、本地安装时的手动命令都在这里;侧栏包名在有新版时显示红点。关于弹层与概览共用同一个 controller(模块级单例),打开多个位置不会重复请求。
17
+ - 关于弹层的更新区块改为复用同一组件(`client/update-panel.ts`),行为不变。
18
+
3
19
  ## 0.3.1 - 2026-10-31
4
20
 
5
21
  ### 新增
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![check](https://github.com/morehao/dsh-github-trending/actions/workflows/check.yml/badge.svg)](https://github.com/morehao/dsh-github-trending/actions/workflows/check.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/@morehao/dsh-github-trending.svg)](https://www.npmjs.com/package/@morehao/dsh-github-trending)
5
5
 
6
- > GitHub 趋势日报的 **DSH 入口**:每天定时 spawn 本地 [`trendradar`](https://github.com/morehao/trendradar) 核心,抓取 Trending、生成中文简介与当日总结,提交到你自己的仓库并自动合并 PR。
6
+ > GitHub 趋势榜的 **DSH 入口**:按日/周/月周期定时 spawn 本地 [`trendradar`](https://github.com/morehao/trendradar) 核心,抓取 Trending、生成中文简介与周期总结,提交到你自己的仓库并自动合并 PR。
7
7
 
8
8
  **状态:0.3.0。** 插件侧(配置、调度、核心调用、状态与工作台、两个工具)已完成。核心二进制走 **GitHub Release**:首次运行按 `os/arch` 下载、用 `checksums.txt` 校验 sha256、缓存到插件数据目录,之后离线复用。核心仓库打 tag 出资产之前,运行会明确报 `CORE_NOT_DOWNLOADED` / `CORE_UNAVAILABLE` 并给出下载地址——不会写空文件,也不会退回旧内容。
9
9
 
@@ -94,9 +94,10 @@ dsh plugin --profile <profile> add @morehao/dsh-github-trending
94
94
  | `pr.titleTemplate` | `chore(trending): add {date} digest` | 默认规则,界面不展示 |
95
95
  | `github.authMode` | `gh` | `gh`(本机 gh 登录态)/ `pat` |
96
96
  | `github.tokenRef` | `GITHUB_TOKEN` | `ctx.credentials` 引用名(`pat` 用) |
97
- | `trending.since` / `languages` / `topN` | `daily` / `all,typescript,python,go,rust` / `25` | 实际支持范围由核心决定 |
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
101
  | `corePath` | 空 | 等价于 `TRENDING_DIGEST_CORE`(仅开发/排障,界面不提供入口,用环境变量或直接改 `settings.json`;设了就完全不下载) |
101
102
  | `coreAutoDownload` | `true` | 缓存缺失时是否自动下载核心;关掉后只报 `CORE_NOT_DOWNLOADED`,由你在设置页点「下载核心」 |
102
103
 
@@ -104,7 +105,7 @@ dsh plugin --profile <profile> add @morehao/dsh-github-trending
104
105
 
105
106
  | 工具 | 参数 | 返回 |
106
107
  |---|---|---|
107
- | `trending_run` | `{ dryRun?: boolean, date?: string }` | 结构化摘要:文件数、commit/分支、PR、内容哈希、错误码与下一步 |
108
+ | `trending_run` | `{ dryRun?: boolean, date?: string, period?: 'daily'\|'weekly'\|'monthly' }` | 结构化摘要:文件数、commit/分支、PR、内容哈希、错误码与下一步 |
108
109
  | `trending_history` | `{ limit?: number }` | 上次/下次运行、最近 N 条记录、核心版本、模型、未合并 PR |
109
110
 
110
111
  工具名是稳定对外接口:**只新增不改名**。
@@ -141,7 +142,7 @@ dsh plugin --profile <profile> add @morehao/dsh-github-trending
141
142
  1. **结果信封**:核心 `run --config <path> --json` 输出契约 v2 `RunResult`(`ipcSchema=2`,可选 `branch/commit/prUrl/contentHash`);插件只解析这一种形状,旧核心的 `runOutput` 不再兼容。
142
143
  2. **密钥传输**:POSIX 上配置经 **FIFO** 交给核心(文件系统里不出现明文);Windows 无 FIFO,退化为 0600 临时文件并在读完后立即删除。
143
144
  3. **GitHub token** 经子进程环境变量传给核心(核心只认 `GITHUB_TOKEN`/`GH_TOKEN`);不进 argv、不进配置、不进日志。
144
- 4. **调度**用插件自己的 60 s tick(宿主 `schedule` 是会话本地提醒,覆盖不到无人值守场景)。
145
+ 4. **调度**用插件自己的 60 s tick(宿主 `schedule` 是会话本地提醒,覆盖不到无人值守场景)。按 `schedule.periods` 分别触发:日榜每天、周榜周日(ISO 周末)、月榜月末;去重键含周期与本地日期,同一周期当天只成功一次。
145
146
  5. **二进制改为下载**(2026-10-30 决策 B):不再随包发 npm 平台分包。首次运行需要能访问 GitHub Release;断网环境请用 `TRENDING_DIGEST_CORE` 指向本地核心。核心版本由 `package.json` 的 `dsh.core.version` 固定(用 `node scripts/set-core-version.mjs <x.y.z>` 写入)。
146
147
  6. **核心版本固定**:`package.json` 的 `dsh.core.version` 是首次运行下载哪个核心 Release 的唯一来源(用 `node scripts/set-core-version.mjs <x.y.z>` 写入);下载失败会给出下载地址与错误码,不会退回旧内容。
147
148
  7. **预览副本是派生数据**:不是第二份真相;仓库里的 canonical 文件才是。旧记录没有 `preview` 元数据,日报里只能看到对应的空状态,不承诺回填。
package/client.js CHANGED
@@ -137,10 +137,12 @@ var DICT = {
137
137
  "scope.period": "周期",
138
138
  "scope.daily": "每日",
139
139
  "scope.weekly": "每周",
140
+ "scope.monthly": "每月",
140
141
  "scope.languages": "语言",
141
142
  "scope.topN": "每语言条数",
142
143
  "card.schedule.title": "调度",
143
144
  "schedule.enabled": "定时运行",
145
+ "schedule.periods": "自动产出周期",
144
146
  "schedule.time": "时间",
145
147
  "schedule.timezone": "时区",
146
148
  "schedule.next": "下次运行",
@@ -357,7 +359,8 @@ var DICT = {
357
359
  "about.update.manual": "当前是本地 {kind} 安装,不能一键更新(会把开发链接换成 npm 版)。手动更新:",
358
360
  "about.update.copy": "复制命令",
359
361
  "about.update.copied": "已复制",
360
- "about.update.hint": "只检查 npm 上的 latest 稳定版;更新后需要重启 DSH。"
362
+ "about.update.hint": "只检查 npm 上的 latest 稳定版;更新后需要重启 DSH。",
363
+ "update.badge": "有新版本可用"
361
364
  },
362
365
  en: {
363
366
  "section.title": "GitHub Trending",
@@ -451,10 +454,12 @@ var DICT = {
451
454
  "scope.period": "Period",
452
455
  "scope.daily": "Daily",
453
456
  "scope.weekly": "Weekly",
457
+ "scope.monthly": "Monthly",
454
458
  "scope.languages": "Languages",
455
459
  "scope.topN": "Per language",
456
460
  "card.schedule.title": "Schedule",
457
461
  "schedule.enabled": "Scheduled runs",
462
+ "schedule.periods": "Auto periods",
458
463
  "schedule.time": "Time",
459
464
  "schedule.timezone": "Time zone",
460
465
  "schedule.next": "Next run",
@@ -671,7 +676,8 @@ var DICT = {
671
676
  "about.update.manual": "Installed from a local {kind} spec; one-click update is disabled (it would replace the dev link). Update manually:",
672
677
  "about.update.copy": "Copy command",
673
678
  "about.update.copied": "Copied",
674
- "about.update.hint": "Checks the npm latest stable tag only; applying requires a DSH restart."
679
+ "about.update.hint": "Checks the npm latest stable tag only; applying requires a DSH restart.",
680
+ "update.badge": "Update available"
675
681
  }
676
682
  };
677
683
  function activeLang(locale) {
@@ -952,6 +958,9 @@ var CSS = `
952
958
  .dshgt-update-manual{margin-top:9px;padding:9px 10px;border:1px solid color-mix(in srgb,var(--gt-warn) 35%,var(--gt-border));border-radius:8px;background:color-mix(in srgb,var(--gt-warn) 6%,var(--gt-card));}
953
959
  .dshgt-update-manual .dshgt-update-warn{margin:0 0 6px;}
954
960
  .dshgt-update-cmd{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:1.5;color:var(--gt-fg);word-break:break-all;user-select:all;}
961
+ .dshgt-update-dot{display:inline-block;width:6px;height:6px;margin-left:6px;border-radius:50%;background:var(--gt-err);vertical-align:middle;flex:none;}
962
+ .dshgt-update-bare{margin-top:0;padding-top:0;border-top:0;}
963
+ .dshgt-update-card .dshgt-card-h{margin-bottom:2px;}
955
964
 
956
965
  @media (max-width:1100px){
957
966
  .dshgt-app{grid-template-columns:190px minmax(0,1fr);}
@@ -1658,10 +1667,11 @@ function SettingsView(props) {
1658
1667
  value: draft.trending.since,
1659
1668
  options: [
1660
1669
  { value: "daily", label: t("scope.daily") },
1661
- { value: "weekly", label: t("scope.weekly") }
1670
+ { value: "weekly", label: t("scope.weekly") },
1671
+ { value: "monthly", label: t("scope.monthly") }
1662
1672
  ],
1663
1673
  onChange: (value) => update((next) => {
1664
- next.trending.since = value === "weekly" ? "weekly" : "daily";
1674
+ next.trending.since = value === "weekly" ? "weekly" : value === "monthly" ? "monthly" : "daily";
1665
1675
  })
1666
1676
  }),
1667
1677
  h("span", { className: "dshgt-hint" }, t("scope.topN")),
@@ -1699,6 +1709,23 @@ function SettingsView(props) {
1699
1709
  next.schedule.timezone = value;
1700
1710
  }) })
1701
1711
  ),
1712
+ h(
1713
+ Row,
1714
+ { label: t("schedule.periods") },
1715
+ ...["daily", "weekly", "monthly"].map(
1716
+ (period) => h(Toggle, {
1717
+ key: period,
1718
+ checked: draft.schedule.periods.includes(period),
1719
+ onChange: (on) => update((next) => {
1720
+ const set = new Set(next.schedule.periods);
1721
+ if (on) set.add(period);
1722
+ else set.delete(period);
1723
+ next.schedule.periods = ["daily", "weekly", "monthly"].filter((p) => set.has(p));
1724
+ }),
1725
+ label: t(`scope.${period}`)
1726
+ })
1727
+ )
1728
+ ),
1702
1729
  h("p", { className: "dshgt-hint" }, t("schedule.source"))
1703
1730
  );
1704
1731
  const currentCard = section === "source" ? sourceCard : section === "model" ? modelCard : section === "repo" ? repoCard : section === "auth" ? authCard : section === "scope" ? scopeCard : scheduleCard;
@@ -1995,6 +2022,196 @@ function LiveRun(props) {
1995
2022
  );
1996
2023
  }
1997
2024
 
2025
+ // node_modules/@morehao/dsh-plugin-kit/dist/update-client.js
2026
+ var IDLE = { status: "idle", view: null, error: null };
2027
+ function errorOf(error) {
2028
+ if (error !== null && typeof error === "object") {
2029
+ const value = error;
2030
+ const message = typeof value.message === "string" && value.message !== "" ? value.message : "操作失败";
2031
+ return typeof value.code === "string" ? { code: value.code, message } : { message };
2032
+ }
2033
+ return { message: String(error ?? "操作失败") };
2034
+ }
2035
+ function createUpdateController(options) {
2036
+ const now = options.now ?? Date.now;
2037
+ let state = { ...IDLE };
2038
+ const listeners = /* @__PURE__ */ new Set();
2039
+ let inFlight = null;
2040
+ function emit(next) {
2041
+ state = next;
2042
+ options.onChange?.(state);
2043
+ for (const listener of listeners) listener(state);
2044
+ }
2045
+ async function check(checkOptions = {}) {
2046
+ if (inFlight !== null) return;
2047
+ inFlight = "check";
2048
+ emit({ ...state, status: "checking", error: null });
2049
+ try {
2050
+ const view = await options.request.check(checkOptions);
2051
+ const checkedAt = new Date(now()).toISOString();
2052
+ if (view.checkFailed) {
2053
+ emit({
2054
+ status: "failed",
2055
+ view,
2056
+ error: { code: "CHECK_FAILED", message: "检查更新失败:拿不到 registry 上的最新版本" },
2057
+ checkedAt
2058
+ });
2059
+ } else {
2060
+ emit({ status: view.updateAvailable ? "available" : "current", view, error: null, checkedAt });
2061
+ }
2062
+ } catch (error) {
2063
+ emit({ ...state, status: "failed", error: errorOf(error) });
2064
+ } finally {
2065
+ inFlight = null;
2066
+ }
2067
+ }
2068
+ async function apply2() {
2069
+ if (inFlight !== null) return;
2070
+ const view = state.view;
2071
+ if (view === null || !view.updateAvailable) {
2072
+ emit({ ...state, status: "failed", error: { code: "NO_UPDATE", message: "没有可更新的版本,请先检查更新" } });
2073
+ return;
2074
+ }
2075
+ if (!view.canApply) {
2076
+ emit({
2077
+ ...state,
2078
+ status: "failed",
2079
+ error: { code: "NO_APPLY", message: `当前环境不支持一键更新,请执行:${view.manualCommand}` }
2080
+ });
2081
+ return;
2082
+ }
2083
+ inFlight = "apply";
2084
+ emit({ ...state, status: "applying", error: null });
2085
+ try {
2086
+ const result = await options.request.apply();
2087
+ const restartRequired = result.restartRequired ?? result.application !== "applied";
2088
+ emit({ ...state, status: restartRequired ? "restart-required" : "applied", error: null });
2089
+ } catch (error) {
2090
+ emit({ ...state, status: "failed", error: errorOf(error) });
2091
+ } finally {
2092
+ inFlight = null;
2093
+ }
2094
+ }
2095
+ return {
2096
+ getState: () => state,
2097
+ subscribe: (listener) => {
2098
+ listeners.add(listener);
2099
+ return () => {
2100
+ listeners.delete(listener);
2101
+ };
2102
+ },
2103
+ check,
2104
+ apply: apply2,
2105
+ reset: () => emit({ ...IDLE })
2106
+ };
2107
+ }
2108
+
2109
+ // client/update-panel.ts
2110
+ var singleton = null;
2111
+ function getUpdateController() {
2112
+ if (singleton === null) {
2113
+ singleton = createUpdateController({
2114
+ request: {
2115
+ check: (options) => getPluginUpdate(options.fresh === true ? { fresh: true } : {}),
2116
+ apply: () => applyPluginUpdate()
2117
+ }
2118
+ });
2119
+ }
2120
+ return singleton;
2121
+ }
2122
+ function usePluginUpdate() {
2123
+ const controller = getUpdateController();
2124
+ const [state, setState] = import_react.default.useState(() => controller.getState());
2125
+ import_react.default.useEffect(() => {
2126
+ const unsubscribe = controller.subscribe(setState);
2127
+ if (controller.getState().status === "idle") void controller.check();
2128
+ return unsubscribe;
2129
+ }, [controller]);
2130
+ return [state, controller];
2131
+ }
2132
+ function UpdateDot(props) {
2133
+ const [state] = usePluginUpdate();
2134
+ if (state.view?.updateAvailable !== true) return null;
2135
+ return h("span", { className: "dshgt-update-dot", title: props.t("update.badge") });
2136
+ }
2137
+ function PluginUpdateSection(props) {
2138
+ const { t } = props;
2139
+ const [state, controller] = usePluginUpdate();
2140
+ const [confirming, setConfirming] = import_react.default.useState(false);
2141
+ const [copied, setCopied] = import_react.default.useState(false);
2142
+ import_react.default.useEffect(() => {
2143
+ if (state.status === "applying" || state.status === "restart-required" || state.status === "applied") setConfirming(false);
2144
+ }, [state.status]);
2145
+ const copyCommand = () => {
2146
+ const command = state.view?.manualCommand;
2147
+ if (command === void 0) return;
2148
+ void copyText(command).then((ok) => {
2149
+ if (!ok) return;
2150
+ setCopied(true);
2151
+ setTimeout(() => setCopied(false), 1600);
2152
+ });
2153
+ };
2154
+ const view = state.view;
2155
+ const status = state.status;
2156
+ const target = view !== null && view.updateAvailable ? view.latestVersion ?? "" : "";
2157
+ const busy = status === "checking" || status === "applying";
2158
+ const settled = status === "restart-required" || status === "applied";
2159
+ const actionable = view !== null && view.updateAvailable && view.canApply && !settled && !busy;
2160
+ const manual = view !== null && (!view.canApply || view.checkFailed) ? view.manualCommand : null;
2161
+ const statusLine = () => {
2162
+ if (status === "checking" || view === null) return t("about.update.checking");
2163
+ if (status === "applying") return t("about.update.applying", { version: target });
2164
+ if (status === "restart-required") return t("about.update.done", { version: view.latestVersion ?? target });
2165
+ if (status === "applied") return t("about.update.applied", { version: view.latestVersion ?? target });
2166
+ if (status === "failed") {
2167
+ if (state.error?.code === "CHECK_FAILED") return t("about.update.checkFailed");
2168
+ return t("about.update.failed", { message: state.error?.message ?? "" });
2169
+ }
2170
+ if (view.checkFailed) return t("about.update.checkFailed");
2171
+ if (view.updateAvailable) return t("about.update.available", { latest: view.latestVersion ?? "", current: view.currentVersion });
2172
+ return t("about.update.current", { version: view.currentVersion });
2173
+ };
2174
+ const actions = [
2175
+ h(Btn, { key: "check", small: true, disabled: busy, onClick: () => void controller.check({ fresh: true }) }, t("about.update.check")),
2176
+ manual === null ? null : h(Btn, { key: "copy", small: true, onClick: copyCommand }, copied ? t("about.update.copied") : t("about.update.copy")),
2177
+ actionable ? confirming ? [
2178
+ h(
2179
+ Btn,
2180
+ {
2181
+ key: "confirm",
2182
+ small: true,
2183
+ variant: "danger",
2184
+ onClick: () => {
2185
+ setConfirming(false);
2186
+ void controller.apply();
2187
+ }
2188
+ },
2189
+ t("about.update.confirmYes")
2190
+ ),
2191
+ h(Btn, { key: "cancel", small: true, onClick: () => setConfirming(false) }, t("about.update.cancel"))
2192
+ ] : h(Btn, { key: "apply", small: true, variant: "primary", onClick: () => setConfirming(true) }, t("about.update.apply", { version: target })) : null
2193
+ ].filter((item) => item !== null);
2194
+ return h(
2195
+ "div",
2196
+ { className: `dshgt-update${props.bare === true ? " dshgt-update-bare" : ""}` },
2197
+ props.bare === true ? null : h(
2198
+ "div",
2199
+ { className: "dshgt-update-head" },
2200
+ h("h4", null, t("about.update.title")),
2201
+ h("span", { className: "dshgt-hint" }, t("about.update.hint"))
2202
+ ),
2203
+ h("p", { className: `dshgt-update-line${status === "failed" ? " dshgt-update-err" : ""}` }, statusLine()),
2204
+ confirming ? h("p", { className: "dshgt-update-warn" }, t("about.update.confirm", { version: target })) : null,
2205
+ h("div", { className: "dshgt-update-actions" }, actions),
2206
+ manual === null ? null : h(
2207
+ "div",
2208
+ { className: "dshgt-update-manual" },
2209
+ h("p", { className: "dshgt-update-warn" }, t("about.update.manual", { kind: view?.installKind ?? "unknown" })),
2210
+ h("code", { className: "dshgt-update-cmd" }, manual)
2211
+ )
2212
+ );
2213
+ }
2214
+
1998
2215
  // client/digest-core.ts
1999
2216
  var DIGEST_WINDOW_DAYS = 30;
2000
2217
  var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
@@ -2328,6 +2545,12 @@ function OverviewView(props) {
2328
2545
  )
2329
2546
  );
2330
2547
  })
2548
+ ),
2549
+ h(
2550
+ "section",
2551
+ { className: "dshgt-card dshgt-update-card" },
2552
+ h("div", { className: "dshgt-card-h" }, h("h3", null, t("about.update.title"))),
2553
+ h(PluginUpdateSection, { t, bare: true })
2331
2554
  )
2332
2555
  )
2333
2556
  );
@@ -3179,117 +3402,10 @@ function metaLine(rendered) {
3179
3402
  );
3180
3403
  }
3181
3404
 
3182
- // node_modules/@morehao/dsh-plugin-kit/dist/update-client.js
3183
- var IDLE = { status: "idle", view: null, error: null };
3184
- function errorOf(error) {
3185
- if (error !== null && typeof error === "object") {
3186
- const value = error;
3187
- const message = typeof value.message === "string" && value.message !== "" ? value.message : "操作失败";
3188
- return typeof value.code === "string" ? { code: value.code, message } : { message };
3189
- }
3190
- return { message: String(error ?? "操作失败") };
3191
- }
3192
- function createUpdateController(options) {
3193
- const now = options.now ?? Date.now;
3194
- let state = { ...IDLE };
3195
- const listeners = /* @__PURE__ */ new Set();
3196
- let inFlight = null;
3197
- function emit(next) {
3198
- state = next;
3199
- options.onChange?.(state);
3200
- for (const listener of listeners) listener(state);
3201
- }
3202
- async function check(checkOptions = {}) {
3203
- if (inFlight !== null) return;
3204
- inFlight = "check";
3205
- emit({ ...state, status: "checking", error: null });
3206
- try {
3207
- const view = await options.request.check(checkOptions);
3208
- const checkedAt = new Date(now()).toISOString();
3209
- if (view.checkFailed) {
3210
- emit({
3211
- status: "failed",
3212
- view,
3213
- error: { code: "CHECK_FAILED", message: "检查更新失败:拿不到 registry 上的最新版本" },
3214
- checkedAt
3215
- });
3216
- } else {
3217
- emit({ status: view.updateAvailable ? "available" : "current", view, error: null, checkedAt });
3218
- }
3219
- } catch (error) {
3220
- emit({ ...state, status: "failed", error: errorOf(error) });
3221
- } finally {
3222
- inFlight = null;
3223
- }
3224
- }
3225
- async function apply2() {
3226
- if (inFlight !== null) return;
3227
- const view = state.view;
3228
- if (view === null || !view.updateAvailable) {
3229
- emit({ ...state, status: "failed", error: { code: "NO_UPDATE", message: "没有可更新的版本,请先检查更新" } });
3230
- return;
3231
- }
3232
- if (!view.canApply) {
3233
- emit({
3234
- ...state,
3235
- status: "failed",
3236
- error: { code: "NO_APPLY", message: `当前环境不支持一键更新,请执行:${view.manualCommand}` }
3237
- });
3238
- return;
3239
- }
3240
- inFlight = "apply";
3241
- emit({ ...state, status: "applying", error: null });
3242
- try {
3243
- const result = await options.request.apply();
3244
- const restartRequired = result.restartRequired ?? result.application !== "applied";
3245
- emit({ ...state, status: restartRequired ? "restart-required" : "applied", error: null });
3246
- } catch (error) {
3247
- emit({ ...state, status: "failed", error: errorOf(error) });
3248
- } finally {
3249
- inFlight = null;
3250
- }
3251
- }
3252
- return {
3253
- getState: () => state,
3254
- subscribe: (listener) => {
3255
- listeners.add(listener);
3256
- return () => {
3257
- listeners.delete(listener);
3258
- };
3259
- },
3260
- check,
3261
- apply: apply2,
3262
- reset: () => emit({ ...IDLE })
3263
- };
3264
- }
3265
-
3266
3405
  // client/about.ts
3267
3406
  function AboutDialog(props) {
3268
3407
  const { t, state } = props;
3269
3408
  const [copied, setCopied] = import_react.default.useState(false);
3270
- const [copiedCommand, setCopiedCommand] = import_react.default.useState(false);
3271
- const [confirming, setConfirming] = import_react.default.useState(false);
3272
- const controllerRef = import_react.default.useRef(null);
3273
- if (controllerRef.current === null) {
3274
- controllerRef.current = createUpdateController({
3275
- request: {
3276
- check: (options) => getPluginUpdate(options.fresh === true ? { fresh: true } : {}),
3277
- apply: () => applyPluginUpdate()
3278
- }
3279
- });
3280
- }
3281
- const controller = controllerRef.current;
3282
- const [updateState, setUpdateState] = import_react.default.useState(() => controller.getState());
3283
- import_react.default.useEffect(() => {
3284
- const unsubscribe = controller.subscribe(setUpdateState);
3285
- if (controller.getState().status === "idle") void controller.check();
3286
- return unsubscribe;
3287
- }, [controller]);
3288
- import_react.default.useEffect(() => {
3289
- if (updateState.status === "applying" || updateState.status === "restart-required" || updateState.status === "applied") {
3290
- setConfirming(false);
3291
- }
3292
- }, [updateState.status]);
3293
3409
  const plugin = state.plugin;
3294
3410
  const fullName = `${plugin.name}@v${plugin.version}`;
3295
3411
  const repository = plugin.repository;
@@ -3309,59 +3425,6 @@ function AboutDialog(props) {
3309
3425
  setTimeout(() => setCopied(false), 1600);
3310
3426
  });
3311
3427
  };
3312
- const copyCommand = () => {
3313
- const command = updateState.view?.manualCommand;
3314
- if (command === void 0) return;
3315
- void copyText(command).then((ok) => {
3316
- if (!ok) return;
3317
- setCopiedCommand(true);
3318
- setTimeout(() => setCopiedCommand(false), 1600);
3319
- });
3320
- };
3321
- const view = updateState.view;
3322
- const status = updateState.status;
3323
- const target = view !== null && view.updateAvailable ? view.latestVersion ?? "" : "";
3324
- const busy = status === "checking" || status === "applying";
3325
- const settled = status === "restart-required" || status === "applied";
3326
- const actionable = view !== null && view.updateAvailable && view.canApply && !settled && !busy;
3327
- const manual = view !== null && (!view.canApply || view.checkFailed) ? view.manualCommand : null;
3328
- const statusLine = () => {
3329
- if (status === "checking" || view === null) return t("about.update.checking");
3330
- if (status === "applying") return t("about.update.applying", { version: target });
3331
- if (status === "restart-required") return t("about.update.done", { version: view.latestVersion ?? target });
3332
- if (status === "applied") return t("about.update.applied", { version: view.latestVersion ?? target });
3333
- if (status === "failed") {
3334
- if (updateState.error?.code === "CHECK_FAILED") return t("about.update.checkFailed");
3335
- return t("about.update.failed", { message: updateState.error?.message ?? "" });
3336
- }
3337
- if (view.checkFailed) return t("about.update.checkFailed");
3338
- if (view.updateAvailable) return t("about.update.available", { latest: view.latestVersion ?? "", current: view.currentVersion });
3339
- return t("about.update.current", { version: view.currentVersion });
3340
- };
3341
- const updateActions = [
3342
- h(Btn, { key: "check", small: true, disabled: busy, onClick: () => void controller.check({ fresh: true }) }, t("about.update.check")),
3343
- manual === null ? null : h(
3344
- Btn,
3345
- { key: "copy-command", small: true, onClick: copyCommand },
3346
- copiedCommand ? t("about.update.copied") : t("about.update.copy")
3347
- ),
3348
- actionable ? confirming ? [
3349
- h(
3350
- Btn,
3351
- {
3352
- key: "confirm",
3353
- small: true,
3354
- variant: "danger",
3355
- onClick: () => {
3356
- setConfirming(false);
3357
- void controller.apply();
3358
- }
3359
- },
3360
- t("about.update.confirmYes")
3361
- ),
3362
- h(Btn, { key: "cancel", small: true, onClick: () => setConfirming(false) }, t("about.update.cancel"))
3363
- ] : h(Btn, { key: "apply", small: true, variant: "primary", onClick: () => setConfirming(true) }, t("about.update.apply", { version: target })) : null
3364
- ].filter((item) => item !== null);
3365
3428
  const dialog = h(
3366
3429
  "div",
3367
3430
  { className: "dshgt-modal-mask", onClick: (event) => {
@@ -3393,25 +3456,7 @@ function AboutDialog(props) {
3393
3456
  [t("about.env"), t("about.env.value")]
3394
3457
  ]
3395
3458
  }),
3396
- h(
3397
- "div",
3398
- { className: "dshgt-update" },
3399
- h(
3400
- "div",
3401
- { className: "dshgt-update-head" },
3402
- h("h4", null, t("about.update.title")),
3403
- h("span", { className: "dshgt-hint" }, t("about.update.hint"))
3404
- ),
3405
- h("p", { className: `dshgt-update-line${status === "failed" ? " dshgt-update-err" : ""}` }, statusLine()),
3406
- confirming ? h("p", { className: "dshgt-update-warn" }, t("about.update.confirm", { version: target })) : null,
3407
- h("div", { className: "dshgt-update-actions" }, updateActions),
3408
- manual === null ? null : h(
3409
- "div",
3410
- { className: "dshgt-update-manual" },
3411
- h("p", { className: "dshgt-update-warn" }, t("about.update.manual", { kind: view?.installKind ?? "unknown" })),
3412
- h("code", { className: "dshgt-update-cmd" }, manual)
3413
- )
3414
- )
3459
+ h(PluginUpdateSection, { t })
3415
3460
  )
3416
3461
  );
3417
3462
  return h("div", { className: "dshgt-root" }, dialog);
@@ -3659,7 +3704,8 @@ function apply(ctx) {
3659
3704
  onClick: () => setAboutOpen(true),
3660
3705
  title: plugin === void 0 ? "" : fullName
3661
3706
  },
3662
- plugin?.name ?? t("section.title")
3707
+ plugin?.name ?? t("section.title"),
3708
+ h(UpdateDot, { t })
3663
3709
  ),
3664
3710
  h(
3665
3711
  "div",
@@ -9,9 +9,11 @@ var config_exports = {};
9
9
  __export(config_exports, {
10
10
  CORE_PROVIDERS: () => CORE_PROVIDERS,
11
11
  DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
12
+ PERIODS: () => PERIODS,
12
13
  PROVIDER_BASE_URLS: () => PROVIDER_BASE_URLS,
13
14
  SETTINGS_SECTIONS: () => SETTINGS_SECTIONS,
14
15
  effectiveBaseURL: () => effectiveBaseURL,
16
+ isPeriod: () => isPeriod,
15
17
  isValidTimezone: () => isValidTimezone,
16
18
  mergeSettingsSection: () => mergeSettingsSection,
17
19
  normalizeSettings: () => normalizeSettings,
@@ -21,6 +23,10 @@ __export(config_exports, {
21
23
  validateSettings: () => validateSettings
22
24
  });
23
25
  import path from "path";
26
+ var PERIODS = ["daily", "weekly", "monthly"];
27
+ function isPeriod(value) {
28
+ return value === "daily" || value === "weekly" || value === "monthly";
29
+ }
24
30
  var DEFAULT_SETTINGS = {
25
31
  model: {
26
32
  provider: "deepseek",
@@ -50,7 +56,7 @@ var DEFAULT_SETTINGS = {
50
56
  github: { authMode: "gh", tokenRef: "GITHUB_TOKEN" },
51
57
  trending: { since: "daily", languages: ["all", "typescript", "python", "go", "rust"], topN: 25 },
52
58
  output: { dir: "trending" },
53
- schedule: { enabled: true, time: "08:30:00", timezone: "Asia/Shanghai" },
59
+ schedule: { enabled: true, time: "08:30:00", timezone: "Asia/Shanghai", periods: ["daily"] },
54
60
  corePath: "",
55
61
  coreAutoDownload: true
56
62
  };
@@ -111,6 +117,11 @@ function strArray(value, fallback) {
111
117
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
112
118
  return items.length > 0 ? items.map((item) => item.trim()) : fallback;
113
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));
124
+ }
114
125
  function normalizeSettings(input) {
115
126
  const raw = typeof input === "object" && input !== null ? input : {};
116
127
  const model = raw.model ?? {};
@@ -155,7 +166,7 @@ function normalizeSettings(input) {
155
166
  tokenRef: str(github.tokenRef, DEFAULT_SETTINGS.github.tokenRef).trim() || DEFAULT_SETTINGS.github.tokenRef
156
167
  },
157
168
  trending: {
158
- since: since === "weekly" ? "weekly" : "daily",
169
+ since: isPeriod(since) ? since : "daily",
159
170
  languages: strArray(trending.languages, DEFAULT_SETTINGS.trending.languages),
160
171
  topN: Math.round(num(trending.topN, DEFAULT_SETTINGS.trending.topN))
161
172
  },
@@ -165,7 +176,8 @@ function normalizeSettings(input) {
165
176
  schedule: {
166
177
  enabled: bool(schedule.enabled, DEFAULT_SETTINGS.schedule.enabled),
167
178
  time: str(schedule.time, DEFAULT_SETTINGS.schedule.time),
168
- timezone: str(schedule.timezone, DEFAULT_SETTINGS.schedule.timezone)
179
+ timezone: str(schedule.timezone, DEFAULT_SETTINGS.schedule.timezone),
180
+ periods: periodList(schedule.periods, DEFAULT_SETTINGS.schedule.periods)
169
181
  },
170
182
  corePath: str(raw.corePath, DEFAULT_SETTINGS.corePath).trim(),
171
183
  coreAutoDownload: bool(raw.coreAutoDownload, DEFAULT_SETTINGS.coreAutoDownload)
@@ -231,12 +243,15 @@ function validateSettings(settings, providers = CORE_PROVIDERS) {
231
243
  if (!isValidTimezone(schedule.timezone)) {
232
244
  problems.push({ field: "schedule.timezone", code: "CONFIG_INVALID", message: "\u65F6\u533A\u5FC5\u987B\u662F IANA \u540D\u79F0\uFF08\u5982 Asia/Shanghai\uFF09" });
233
245
  }
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
+ }
234
249
  return problems;
235
250
  }
236
251
  function effectiveBaseURL(model) {
237
252
  return model.baseURL !== "" ? model.baseURL : PROVIDER_BASE_URLS[model.provider] ?? "";
238
253
  }
239
- function toCoreConfig(settings, apiKey, baseURLOverride) {
254
+ function toCoreConfig(settings, apiKey, baseURLOverride, period) {
240
255
  const config = {
241
256
  model: {
242
257
  provider: settings.model.provider,
@@ -262,7 +277,7 @@ function toCoreConfig(settings, apiKey, baseURLOverride) {
262
277
  },
263
278
  trending: {
264
279
  languages: settings.trending.languages,
265
- since: settings.trending.since,
280
+ since: period ?? settings.trending.since,
266
281
  perLanguage: settings.trending.topN
267
282
  },
268
283
  output: {
@@ -1339,6 +1354,7 @@ function buildCoreArgs(channelPath, options) {
1339
1354
  const args = ["run", "--config", channelPath, "--json"];
1340
1355
  if (options.dryRun) args.push("--dry-run");
1341
1356
  if (options.date !== void 0 && options.date !== "") args.push("--date", options.date);
1357
+ if (options.period !== void 0) args.push("--period", options.period);
1342
1358
  return args;
1343
1359
  }
1344
1360
  async function runCore(options) {
@@ -1346,10 +1362,10 @@ async function runCore(options) {
1346
1362
  const startedAt = nowIso();
1347
1363
  const started = Date.now();
1348
1364
  const channel = await openConfigChannel();
1349
- const yaml = `${toYaml(toCoreConfig(options.settings, options.apiKey, options.baseURLOverride))}
1365
+ const yaml = `${toYaml(toCoreConfig(options.settings, options.apiKey, options.baseURLOverride, options.period))}
1350
1366
  `;
1351
1367
  const secrets = [options.apiKey, options.token, ...options.secrets ?? []];
1352
- const child = spawn2(options.core.path, buildCoreArgs(channel.path, { dryRun: options.dryRun, date: options.date }), {
1368
+ const child = spawn2(options.core.path, buildCoreArgs(channel.path, { dryRun: options.dryRun, date: options.date, period: options.period }), {
1353
1369
  env: sanitizedEnv(process.env, {
1354
1370
  GITHUB_TOKEN: options.token,
1355
1371
  GH_TOKEN: options.token,
@@ -1985,8 +2001,11 @@ function authorized(request, token) {
1985
2001
  var schedule_exports = {};
1986
2002
  __export(schedule_exports, {
1987
2003
  Scheduler: () => Scheduler,
2004
+ daysInMonth: () => daysInMonth,
1988
2005
  isDue: () => isDue,
2006
+ isPeriodTriggerDay: () => isPeriodTriggerDay,
1989
2007
  nextOccurrence: () => nextOccurrence,
2008
+ nextOccurrenceFor: () => nextOccurrenceFor,
1990
2009
  parseClock: () => parseClock,
1991
2010
  zonedDateString: () => zonedDateString,
1992
2011
  zonedTimeToUtc: () => zonedTimeToUtc
@@ -2066,6 +2085,35 @@ function isDue(now, schedule) {
2066
2085
  const late = nowMinutes - targetMinutes;
2067
2086
  return late >= 0 && late <= CATCH_UP_MINUTES;
2068
2087
  }
2088
+ function daysInMonth(year, month) {
2089
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
2090
+ }
2091
+ function isPeriodTriggerDate(period, year, month, day) {
2092
+ if (period === "weekly") {
2093
+ return new Date(Date.UTC(year, month - 1, day)).getUTCDay() === 0;
2094
+ }
2095
+ if (period === "monthly") return day === daysInMonth(year, month);
2096
+ return true;
2097
+ }
2098
+ function isPeriodTriggerDay(period, now, timezone) {
2099
+ const parts = partsOf(now, timezone);
2100
+ return isPeriodTriggerDate(period, parts.year, parts.month, parts.day);
2101
+ }
2102
+ function nextOccurrenceFor(period, now, schedule) {
2103
+ const { hour, minute, second } = parseClock(schedule.time);
2104
+ const today = partsOf(now, schedule.timezone);
2105
+ const start = Date.UTC(today.year, today.month - 1, today.day);
2106
+ for (let i = 0; i < 400; i += 1) {
2107
+ const d = new Date(start + i * 864e5);
2108
+ const year = d.getUTCFullYear();
2109
+ const month = d.getUTCMonth() + 1;
2110
+ const day = d.getUTCDate();
2111
+ if (!isPeriodTriggerDate(period, year, month, day)) continue;
2112
+ const candidate = zonedTimeToUtc(year, month, day, hour, minute, second, schedule.timezone);
2113
+ if (candidate.getTime() > now.getTime()) return candidate;
2114
+ }
2115
+ return nextOccurrence(now, schedule);
2116
+ }
2069
2117
  var Scheduler = class {
2070
2118
  deps;
2071
2119
  timerHandle;
@@ -2104,41 +2152,63 @@ var Scheduler = class {
2104
2152
  if (!schedule.enabled) return "disabled";
2105
2153
  if (!isDue(now, schedule)) return "skipped";
2106
2154
  const today = zonedDateString(now, schedule.timezone);
2107
- const state = this.deps.store.readState();
2108
- const lastSuccess = state.lastSuccessAt === void 0 ? void 0 : zonedDateString(new Date(state.lastSuccessAt), schedule.timezone);
2109
- if (lastSuccess === today) return "skipped";
2110
- const attempt = this.attempts.get(today);
2111
- if (attempt !== void 0) {
2112
- if (attempt.count >= MAX_ATTEMPTS_PER_DAY) return "skipped";
2113
- if (now.getTime() - attempt.lastAt < RETRY_GAP_MS) return "skipped";
2114
- }
2115
- if (this.deps.isRunning()) return "busy";
2116
2155
  for (const key of this.attempts.keys()) {
2117
- if (key !== today) this.attempts.delete(key);
2156
+ if (!key.endsWith(`:${today}`)) this.attempts.delete(key);
2157
+ }
2158
+ const periods = schedule.periods.length > 0 ? schedule.periods : ["daily"];
2159
+ let ran = false;
2160
+ for (const period of periods) {
2161
+ if (!isPeriodTriggerDay(period, now, schedule.timezone)) continue;
2162
+ const state = this.deps.store.readState();
2163
+ if (this.succeededToday(state, period, today, schedule.timezone)) continue;
2164
+ const key = `${period}:${today}`;
2165
+ const attempt = this.attempts.get(key);
2166
+ if (attempt !== void 0) {
2167
+ if (attempt.count >= MAX_ATTEMPTS_PER_DAY) continue;
2168
+ if (now.getTime() - attempt.lastAt < RETRY_GAP_MS) continue;
2169
+ }
2170
+ if (this.deps.isRunning()) return "busy";
2171
+ this.attempts.set(key, { count: (attempt?.count ?? 0) + 1, lastAt: now.getTime() });
2172
+ try {
2173
+ await this.deps.run("schedule", period);
2174
+ ran = true;
2175
+ } catch (error) {
2176
+ this.deps.onError?.(error);
2177
+ }
2118
2178
  }
2119
- this.attempts.set(today, { count: (attempt?.count ?? 0) + 1, lastAt: now.getTime() });
2120
- try {
2121
- await this.deps.run("schedule");
2122
- return "ran";
2123
- } catch (error) {
2124
- this.deps.onError?.(error);
2125
- return "skipped";
2179
+ return ran ? "ran" : "skipped";
2180
+ }
2181
+ /** 该周期今天是否已经成功过(含无变更跳过)。 */
2182
+ succeededToday(state, period, today, timezone) {
2183
+ const stamp = state.periodSuccess?.[period];
2184
+ if (stamp !== void 0 && zonedDateString(new Date(stamp), timezone) === today) return true;
2185
+ if (period === "daily" && state.lastSuccessAt !== void 0 && zonedDateString(new Date(state.lastSuccessAt), timezone) === today) {
2186
+ return true;
2126
2187
  }
2188
+ return false;
2127
2189
  }
2128
2190
  readSchedule() {
2129
2191
  const settings = this.deps.store.readSettings();
2130
2192
  return {
2131
2193
  enabled: settings.schedule.enabled,
2132
2194
  time: settings.schedule.time,
2133
- timezone: settings.schedule.timezone
2195
+ timezone: settings.schedule.timezone,
2196
+ periods: settings.schedule.periods
2134
2197
  };
2135
2198
  }
2136
- /** 下一次运行(禁用时为 undefined)。 */
2199
+ /** 下一次运行(取所有已启用周期里最近的一次;禁用时为 undefined)。 */
2137
2200
  next() {
2138
2201
  const schedule = this.readSchedule();
2139
2202
  if (!schedule.enabled) return void 0;
2203
+ const periods = schedule.periods.length > 0 ? schedule.periods : ["daily"];
2140
2204
  try {
2141
- return nextOccurrence(/* @__PURE__ */ new Date(), schedule);
2205
+ const now = /* @__PURE__ */ new Date();
2206
+ let earliest;
2207
+ for (const period of periods) {
2208
+ const occurrence = nextOccurrenceFor(period, now, schedule);
2209
+ if (earliest === void 0 || occurrence.getTime() < earliest.getTime()) earliest = occurrence;
2210
+ }
2211
+ return earliest;
2142
2212
  } catch {
2143
2213
  return void 0;
2144
2214
  }
@@ -2150,6 +2220,7 @@ var Scheduler = class {
2150
2220
  enabled: schedule.enabled,
2151
2221
  time: schedule.time,
2152
2222
  timezone: schedule.timezone,
2223
+ periods: schedule.periods,
2153
2224
  ...next === void 0 ? {} : { nextRunAt: next.toISOString() },
2154
2225
  ...state.lastRunAt === void 0 ? {} : { lastRunDate: zonedDateString(new Date(state.lastRunAt), schedule.timezone) },
2155
2226
  catchUpMinutes: CATCH_UP_MINUTES,
@@ -2161,7 +2232,9 @@ var Scheduler = class {
2161
2232
  // src/host/runner.ts
2162
2233
  var runner_exports = {};
2163
2234
  __export(runner_exports, {
2164
- Runner: () => Runner
2235
+ Runner: () => Runner,
2236
+ isoWeekName: () => isoWeekName,
2237
+ periodFileBase: () => periodFileBase
2165
2238
  });
2166
2239
  import { createHash as createHash2 } from "crypto";
2167
2240
  import { existsSync as existsSync4, readFileSync as readFileSync3, realpathSync, statSync as statSync5 } from "fs";
@@ -2195,6 +2268,25 @@ function normalizeSha256(value) {
2195
2268
  const hex = value.trim().replace(/^sha256:/i, "").toLowerCase();
2196
2269
  return /^[0-9a-f]{64}$/.test(hex) ? hex : void 0;
2197
2270
  }
2271
+ function isoWeekName(date) {
2272
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
2273
+ if (match === null) return date;
2274
+ const d = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
2275
+ if (Number.isNaN(d.getTime())) return date;
2276
+ const dayIdx = (d.getUTCDay() + 6) % 7;
2277
+ d.setUTCDate(d.getUTCDate() - dayIdx + 3);
2278
+ const isoYear = d.getUTCFullYear();
2279
+ const firstThursday = new Date(Date.UTC(isoYear, 0, 4));
2280
+ const firstIdx = (firstThursday.getUTCDay() + 6) % 7;
2281
+ firstThursday.setUTCDate(firstThursday.getUTCDate() - firstIdx + 3);
2282
+ const week = 1 + Math.round((d.getTime() - firstThursday.getTime()) / (7 * 864e5));
2283
+ return `${isoYear}-W${String(week).padStart(2, "0")}`;
2284
+ }
2285
+ function periodFileBase(period, date) {
2286
+ if (period === "monthly") return date.slice(0, 7);
2287
+ if (period === "weekly") return isoWeekName(date);
2288
+ return date;
2289
+ }
2198
2290
  var Runner = class {
2199
2291
  active;
2200
2292
  deps;
@@ -2226,6 +2318,7 @@ var Runner = class {
2226
2318
  if (repoIssue !== void 0) throw new IpcError(repoIssue.code, repoIssue.message);
2227
2319
  const dryRun = options.dryRun === true;
2228
2320
  const date = options.date !== void 0 && options.date !== "" ? options.date : runDate(settings);
2321
+ const period = options.period ?? settings.trending.since;
2229
2322
  const id = newRunId();
2230
2323
  const controller = new AbortController();
2231
2324
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -2282,6 +2375,7 @@ var Runner = class {
2282
2375
  apiKey,
2283
2376
  ...github.token === void 0 ? {} : { token: github.token },
2284
2377
  date,
2378
+ period,
2285
2379
  dryRun,
2286
2380
  trigger: options.trigger === "schedule" ? "schedule" : "manual",
2287
2381
  ...baseURLOverride === void 0 ? {} : { baseURLOverride },
@@ -2295,6 +2389,7 @@ var Runner = class {
2295
2389
  outcome,
2296
2390
  dryRun,
2297
2391
  date,
2392
+ period,
2298
2393
  logFile,
2299
2394
  channel: result.channelKind,
2300
2395
  proxy: proxy !== void 0,
@@ -2304,6 +2399,7 @@ var Runner = class {
2304
2399
  const preview = this.capturePreview({
2305
2400
  id,
2306
2401
  date,
2402
+ period,
2307
2403
  settings,
2308
2404
  outcome,
2309
2405
  secrets: [apiKey, github.token]
@@ -2318,6 +2414,7 @@ var Runner = class {
2318
2414
  error: cancelled && !(error instanceof IpcError && error.code === "CORE_CANCELLED") ? new IpcError("CORE_CANCELLED", "\u8FD0\u884C\u5DF2\u53D6\u6D88\uFF1A\u6838\u5FC3\u8FDB\u7A0B\u5DF2\u7EC8\u6B62\uFF0C\u672A\u63D0\u4EA4\u4EFB\u4F55\u72B6\u6001") : error,
2319
2415
  dryRun,
2320
2416
  date,
2417
+ period,
2321
2418
  logFile,
2322
2419
  durationMs: Date.now() - startedMs
2323
2420
  });
@@ -2353,6 +2450,7 @@ var Runner = class {
2353
2450
  skipped: outcome.skipped,
2354
2451
  dryRun: input.dryRun,
2355
2452
  ...input.date === void 0 ? {} : { date: input.date },
2453
+ ...input.period === void 0 ? {} : { period: input.period },
2356
2454
  durationMs: outcome.durationMs,
2357
2455
  coreVersion: outcome.coreVersion,
2358
2456
  files: outcome.files.length,
@@ -2379,6 +2477,7 @@ var Runner = class {
2379
2477
  skipped: false,
2380
2478
  dryRun: input.dryRun,
2381
2479
  ...input.date === void 0 ? {} : { date: input.date },
2480
+ ...input.period === void 0 ? {} : { period: input.period },
2382
2481
  durationMs: input.durationMs ?? 0,
2383
2482
  coreVersion: "",
2384
2483
  files: 0,
@@ -2396,7 +2495,7 @@ var Runner = class {
2396
2495
  * - 任何一步失败都只记日志,返回 undefined;运行结果不受影响。
2397
2496
  */
2398
2497
  capturePreview(input) {
2399
- const { outcome, settings, date, id } = input;
2498
+ const { outcome, settings, date, period, id } = input;
2400
2499
  if (!outcome.ok || outcome.dryRun || outcome.skipped || date === void 0) return void 0;
2401
2500
  const skip = (reason, extra = {}) => {
2402
2501
  this.deps.logger?.("preview.skip", { id, reason, ...extra });
@@ -2404,12 +2503,14 @@ var Runner = class {
2404
2503
  };
2405
2504
  const markdown = outcome.files.filter((file) => {
2406
2505
  const ext = path8.extname(file.path).toLowerCase();
2407
- return ext === ".md" && path8.basename(file.path).toLowerCase() !== "readme.md";
2506
+ const base = path8.basename(file.path).toLowerCase();
2507
+ return ext === ".md" && base !== "readme.md" && base !== "index.md";
2408
2508
  });
2409
2509
  if (markdown.length === 0) return skip("no-markdown");
2410
- const dated = markdown.filter((file) => path8.basename(file.path) === `${date}.md`);
2411
- const chosen = dated.length > 0 ? dated[0] : markdown.length === 1 ? markdown[0] : void 0;
2412
- if (chosen === void 0) return skip("ambiguous", { candidates: markdown.length });
2510
+ const expected = period === void 0 ? `${date}.md` : `${periodFileBase(period, date)}.md`;
2511
+ const named = markdown.filter((file) => path8.basename(file.path) === expected);
2512
+ const chosen = named.length > 0 ? named[0] : markdown.length === 1 ? markdown[0] : void 0;
2513
+ if (chosen === void 0) return skip("ambiguous", { candidates: markdown.length, expected, period: period ?? null });
2413
2514
  const repoPath = settings.repo.path;
2414
2515
  const resolved = path8.resolve(repoPath, chosen.path);
2415
2516
  if (!isInside(repoPath, resolved)) return skip("outside-repo", { path: chosen.path });
@@ -2467,6 +2568,7 @@ var Runner = class {
2467
2568
  durationMs: summary.durationMs,
2468
2569
  files: summary.files,
2469
2570
  ...summary.date === void 0 ? {} : { date: summary.date },
2571
+ ...summary.period === void 0 ? {} : { period: summary.period },
2470
2572
  ...summary.contentHash === void 0 ? {} : { contentHash: summary.contentHash },
2471
2573
  ...summary.branch === void 0 ? {} : { branch: summary.branch },
2472
2574
  ...summary.commit === void 0 ? {} : { commit: summary.commit },
@@ -2479,9 +2581,12 @@ var Runner = class {
2479
2581
  logFile: summary.logFile
2480
2582
  };
2481
2583
  const state = store.readState();
2584
+ const periodSuccess = { ...state.periodSuccess ?? {} };
2585
+ if (summary.ok && summary.period !== void 0) periodSuccess[summary.period] = record.at;
2482
2586
  const next = {
2483
2587
  lastRunAt: record.at,
2484
2588
  ...summary.ok ? { lastSuccessAt: record.at } : {},
2589
+ ...summary.ok && summary.period !== void 0 ? { periodSuccess } : {},
2485
2590
  ...summary.ok && summary.prUrl !== void 0 ? { pendingPr: { url: summary.prUrl, createdAt: record.at, ...summary.date === void 0 ? {} : { date: summary.date } } } : {},
2486
2591
  ...summary.ok && !summary.skipped && summary.contentHash !== void 0 ? {
2487
2592
  lastDigest: {
@@ -3224,6 +3329,7 @@ async function dispatch(method, pathname, url, request, deps) {
3224
3329
  const summary = await deps.runner.run({
3225
3330
  dryRun: body.dryRun === true,
3226
3331
  ...typeof body.date === "string" && body.date !== "" ? { date: body.date } : {},
3332
+ ...isPeriod(body.period) ? { period: body.period } : {},
3227
3333
  trigger: "manual"
3228
3334
  });
3229
3335
  return { summary };
@@ -3496,10 +3602,11 @@ function describeRecord(record) {
3496
3602
  function createTrendingRunTool(runner) {
3497
3603
  return {
3498
3604
  name: "trending_run",
3499
- description: "\u6267\u884C\u4E00\u6B21 GitHub \u8D8B\u52BF\u65E5\u62A5\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",
3605
+ 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",
3500
3606
  parameters: {
3501
3607
  dryRun: { type: "boolean", description: "\u53EA\u8BD5\u8DD1\uFF1A\u4E0D\u5199\u6587\u4EF6\u3001\u4E0D\u63D0\u4EA4\u3001\u4E0D\u5F00 PR" },
3502
- date: { type: "string", description: "\u6307\u5B9A\u65E5\u671F\uFF08YYYY-MM-DD\uFF09\uFF1B\u7701\u7565\u5219\u7528\u8C03\u5EA6\u65F6\u533A\u7684\u4ECA\u5929" }
3608
+ date: { type: "string", description: "\u6307\u5B9A\u65E5\u671F\uFF08YYYY-MM-DD\uFF09\uFF1B\u7701\u7565\u5219\u7528\u8C03\u5EA6\u65F6\u533A\u7684\u4ECA\u5929" },
3609
+ 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" }
3503
3610
  },
3504
3611
  output: {
3505
3612
  schema: RUN_OUTPUT_SCHEMA,
@@ -3510,6 +3617,7 @@ function createTrendingRunTool(runner) {
3510
3617
  const summary = await runner.run({
3511
3618
  dryRun: args.dryRun === true,
3512
3619
  ...typeof args.date === "string" && args.date !== "" ? { date: args.date } : {},
3620
+ ...isPeriod(args.period) ? { period: args.period } : {},
3513
3621
  trigger: "tool"
3514
3622
  });
3515
3623
  const lines = [];
package/lib/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  createTrendingRunTool,
9
9
  createUpdateService,
10
10
  readPluginInfo
11
- } from "./chunk-NMINPPWO.js";
11
+ } from "./chunk-QXAKMNHQ.js";
12
12
  import "./chunk-XLIFTAVL.js";
13
13
 
14
14
  // src/index.ts
@@ -53,9 +53,9 @@ function apply(ctx, config = {}) {
53
53
  store,
54
54
  isRunning: () => runner.running !== void 0,
55
55
  getTimer: () => get("timer"),
56
- run: async () => {
57
- const summary = await runner.run({ trigger: "schedule" });
58
- log("schedule.run", { ok: summary.ok, skipped: summary.skipped, error: summary.error?.code });
56
+ run: async (_trigger, period) => {
57
+ const summary = await runner.run({ trigger: "schedule", period });
58
+ log("schedule.run", { period, ok: summary.ok, skipped: summary.skipped, error: summary.error?.code });
59
59
  },
60
60
  onError: (error) => ctx.logger?.warn?.(`[dsh-github-trending] \u5B9A\u65F6\u8FD0\u884C\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`)
61
61
  });
package/lib/internal.d.ts CHANGED
@@ -164,7 +164,16 @@ type Provider = string;
164
164
  type CommitMode = 'branch' | 'direct';
165
165
  /** 只保留两种真正可用的方式:本机 `gh` 登录态、手动 PAT。历史的 `device` 在归一化时迁移为 `gh`。 */
166
166
  type AuthMode = 'pat' | 'gh';
167
- type Since = 'daily' | 'weekly';
167
+ /**
168
+ * 榜单周期(core 0.4.0 的 `--period` 三取值)。
169
+ * `trending.since` 复用同一枚举:单次运行「抓取窗口 = 标注周期」由核心保证
170
+ * (`run --period` 会覆盖 `config.trending.since`)。
171
+ */
172
+ type Period = 'daily' | 'weekly' | 'monthly';
173
+ type Since = Period;
174
+ /** 规范顺序(索引分组、去重、UI 展示都以它为准)。 */
175
+ declare const PERIODS: readonly Period[];
176
+ declare function isPeriod(value: unknown): value is Period;
168
177
  interface ModelSettings {
169
178
  provider: Provider;
170
179
  /** `direct` = 密钥经配置通道直传核心;`proxy` = 宿主 provider 无可导出密钥,走回环代理。 */
@@ -210,6 +219,11 @@ interface ScheduleSettings {
210
219
  /** `HH:MM:SS`(本地墙钟,按 timezone 解释)。 */
211
220
  time: string;
212
221
  timezone: string;
222
+ /**
223
+ * 定时自动产出的周期(插件侧调度概念,核心无此字段)。
224
+ * 每个周期按自己的节奏触发:日=每天、周=周日、月=月末。
225
+ */
226
+ periods: Period[];
213
227
  }
214
228
  interface Settings {
215
229
  model: ModelSettings;
@@ -293,9 +307,10 @@ interface CoreConfig {
293
307
  }
294
308
  /**
295
309
  * 生成核心配置。`apiKey` 单独传入:它只能来自宿主凭据或用户当次输入,
296
- * 绝不来自 settings.json。
310
+ * 绝不来自 settings.json。`period` 是本次运行的标的周期;核心 `run --period`
311
+ * 会用它覆盖 `trending.since`,这里同步写入以免旧核心/调试时口径不一致。
297
312
  */
298
- declare function toCoreConfig(settings: Settings, apiKey: string, baseURLOverride?: string): CoreConfig;
313
+ declare function toCoreConfig(settings: Settings, apiKey: string, baseURLOverride?: string, period?: Period): CoreConfig;
299
314
  /**
300
315
  * 极简 YAML 序列化:只覆盖「对象/数组/字符串/数字/布尔」。
301
316
  * 字符串一律用 JSON 双引号形式——JSON 字符串是合法 YAML 标量,且能避免
@@ -311,8 +326,10 @@ declare const config_DEFAULT_SETTINGS: typeof DEFAULT_SETTINGS;
311
326
  type config_GithubSettings = GithubSettings;
312
327
  type config_ModelSettings = ModelSettings;
313
328
  type config_OutputSettings = OutputSettings;
329
+ declare const config_PERIODS: typeof PERIODS;
314
330
  declare const config_PROVIDER_BASE_URLS: typeof PROVIDER_BASE_URLS;
315
331
  type config_PRSettings = PRSettings;
332
+ type config_Period = Period;
316
333
  type config_Problem = Problem;
317
334
  type config_Provider = Provider;
318
335
  type config_RepoSettings = RepoSettings;
@@ -323,6 +340,7 @@ type config_SettingsSection = SettingsSection;
323
340
  type config_Since = Since;
324
341
  type config_TrendingSettings = TrendingSettings;
325
342
  declare const config_effectiveBaseURL: typeof effectiveBaseURL;
343
+ declare const config_isPeriod: typeof isPeriod;
326
344
  declare const config_isValidTimezone: typeof isValidTimezone;
327
345
  declare const config_mergeSettingsSection: typeof mergeSettingsSection;
328
346
  declare const config_normalizeSettings: typeof normalizeSettings;
@@ -331,7 +349,7 @@ declare const config_toCoreConfig: typeof toCoreConfig;
331
349
  declare const config_toYaml: typeof toYaml;
332
350
  declare const config_validateSettings: typeof validateSettings;
333
351
  declare namespace config {
334
- export { type config_AuthMode as AuthMode, config_CORE_PROVIDERS as CORE_PROVIDERS, type config_CommitMode as CommitMode, type config_CoreConfig as CoreConfig, config_DEFAULT_SETTINGS as DEFAULT_SETTINGS, type config_GithubSettings as GithubSettings, type config_ModelSettings as ModelSettings, type config_OutputSettings as OutputSettings, config_PROVIDER_BASE_URLS as PROVIDER_BASE_URLS, type config_PRSettings as PRSettings, type config_Problem as Problem, type config_Provider as Provider, type config_RepoSettings as RepoSettings, config_SETTINGS_SECTIONS as SETTINGS_SECTIONS, type config_ScheduleSettings as ScheduleSettings, type config_Settings as Settings, type config_SettingsSection as SettingsSection, type config_Since as Since, type config_TrendingSettings as TrendingSettings, config_effectiveBaseURL as effectiveBaseURL, config_isValidTimezone as isValidTimezone, config_mergeSettingsSection as mergeSettingsSection, config_normalizeSettings as normalizeSettings, config_sectionOfField as sectionOfField, config_toCoreConfig as toCoreConfig, config_toYaml as toYaml, config_validateSettings as validateSettings };
352
+ export { type config_AuthMode as AuthMode, config_CORE_PROVIDERS as CORE_PROVIDERS, type config_CommitMode as CommitMode, type config_CoreConfig as CoreConfig, config_DEFAULT_SETTINGS as DEFAULT_SETTINGS, type config_GithubSettings as GithubSettings, type config_ModelSettings as ModelSettings, type config_OutputSettings as OutputSettings, config_PERIODS as PERIODS, config_PROVIDER_BASE_URLS as PROVIDER_BASE_URLS, type config_PRSettings as PRSettings, type config_Period as Period, type config_Problem as Problem, type config_Provider as Provider, type config_RepoSettings as RepoSettings, config_SETTINGS_SECTIONS as SETTINGS_SECTIONS, type config_ScheduleSettings as ScheduleSettings, type config_Settings as Settings, type config_SettingsSection as SettingsSection, type config_Since as Since, type config_TrendingSettings as TrendingSettings, config_effectiveBaseURL as effectiveBaseURL, config_isPeriod as isPeriod, config_isValidTimezone as isValidTimezone, config_mergeSettingsSection as mergeSettingsSection, config_normalizeSettings as normalizeSettings, config_sectionOfField as sectionOfField, config_toCoreConfig as toCoreConfig, config_toYaml as toYaml, config_validateSettings as validateSettings };
335
353
  }
336
354
 
337
355
  declare const PLUGIN_DIR_NAME = "github-trending";
@@ -397,6 +415,8 @@ interface RunRecord {
397
415
  files: number;
398
416
  /** 运行所属的调度时区日期(YYYY-MM-DD);旧记录可能没有。 */
399
417
  date?: string;
418
+ /** 本次产出周期(daily|weekly|monthly);旧记录可能没有。 */
419
+ period?: Period;
400
420
  contentHash?: string;
401
421
  branch?: string;
402
422
  commit?: string;
@@ -421,8 +441,10 @@ interface PendingPr {
421
441
  }
422
442
  interface PluginState {
423
443
  lastRunAt?: string;
424
- /** 最近一次成功(ok=true,含 skipped)的运行时间;调度用它锁当天。 */
444
+ /** 最近一次成功(ok=true,含 skipped)的运行时间;工具与界面读它。 */
425
445
  lastSuccessAt?: string;
446
+ /** 各周期最近一次成功的 ISO 时间;调度按「周期 + 本地日期」判重。 */
447
+ periodSuccess?: Partial<Record<Period, string>>;
426
448
  lastDigest?: DigestIdentity;
427
449
  lastResult?: RunRecord;
428
450
  pendingPr?: PendingPr;
@@ -729,6 +751,8 @@ interface RunCoreOptions {
729
751
  /** GitHub token;核心只从 env 读(GITHUB_TOKEN/GH_TOKEN)。 */
730
752
  token?: string;
731
753
  date?: string;
754
+ /** 本次运行的标的周期(daily|weekly|monthly);核心 `--period` 覆盖抓取 since。 */
755
+ period: Period;
732
756
  dryRun: boolean;
733
757
  /** 谁触发的运行:核心用它在定时运行时尊重「人工关闭的 PR」。 */
734
758
  trigger?: 'manual' | 'schedule';
@@ -749,6 +773,7 @@ interface RunCoreResult {
749
773
  declare function buildCoreArgs(channelPath: string, options: {
750
774
  dryRun: boolean;
751
775
  date?: string;
776
+ period?: Period;
752
777
  }): string[];
753
778
  declare function runCore(options: RunCoreOptions): Promise<RunCoreResult>;
754
779
 
@@ -1187,6 +1212,8 @@ interface RunSummary {
1187
1212
  skipped: boolean;
1188
1213
  dryRun: boolean;
1189
1214
  date?: string;
1215
+ /** 本次产出周期(daily|weekly|monthly)。 */
1216
+ period?: Period;
1190
1217
  durationMs: number;
1191
1218
  coreVersion: string;
1192
1219
  files: number;
@@ -1218,6 +1245,8 @@ interface RunSummary {
1218
1245
  interface RunRequestOptions {
1219
1246
  dryRun?: boolean;
1220
1247
  date?: string;
1248
+ /** 标的周期;缺省取 settings.trending.since(手动/工具运行)。 */
1249
+ period?: Period;
1221
1250
  trigger: 'manual' | 'schedule' | 'tool';
1222
1251
  }
1223
1252
  interface RunnerDeps {
@@ -1229,6 +1258,13 @@ interface RunnerDeps {
1229
1258
  /** 预览副本这类「可失败但不阻断运行」的事件走这里,不写进 warnings。 */
1230
1259
  logger?: (event: string, data?: Record<string, unknown>) => void;
1231
1260
  }
1261
+ /**
1262
+ * 与 core `render.NameOf` 对齐的产物基名(不含扩展名):
1263
+ * daily → YYYY-MM-DD;weekly → YYYY-Wnn(ISO 周年/周);monthly → YYYY-MM。
1264
+ * 用于在 outcome.files 里按周期精确挑出本期 digest 的 `.md`(而不是猜)。
1265
+ */
1266
+ declare function isoWeekName(date: string): string;
1267
+ declare function periodFileBase(period: Period, date: string): string;
1232
1268
  declare class Runner {
1233
1269
  private active;
1234
1270
  private readonly deps;
@@ -1261,8 +1297,10 @@ type runner_RunSummary = RunSummary;
1261
1297
  type runner_Runner = Runner;
1262
1298
  declare const runner_Runner: typeof Runner;
1263
1299
  type runner_RunnerDeps = RunnerDeps;
1300
+ declare const runner_isoWeekName: typeof isoWeekName;
1301
+ declare const runner_periodFileBase: typeof periodFileBase;
1264
1302
  declare namespace runner {
1265
- export { type runner_RunRequestOptions as RunRequestOptions, type runner_RunSummary as RunSummary, runner_Runner as Runner, type runner_RunnerDeps as RunnerDeps };
1303
+ export { type runner_RunRequestOptions as RunRequestOptions, type runner_RunSummary as RunSummary, runner_Runner as Runner, type runner_RunnerDeps as RunnerDeps, runner_isoWeekName as isoWeekName, runner_periodFileBase as periodFileBase };
1266
1304
  }
1267
1305
 
1268
1306
  interface PluginInfo {
@@ -1296,6 +1334,8 @@ interface ScheduleConfig {
1296
1334
  enabled: boolean;
1297
1335
  time: string;
1298
1336
  timezone: string;
1337
+ /** 自动产出的周期;每个周期只在「它自己的触发日」跑(日=每天、周=周日、月=月末)。 */
1338
+ periods: Period[];
1299
1339
  }
1300
1340
  /** 目标时区的墙上时间 → UTC 时刻(按 DST 修正一次,够用且可测)。 */
1301
1341
  declare function zonedTimeToUtc(year: number, month: number, day: number, hour: number, minute: number, second: number, timezone: string): Date;
@@ -1310,9 +1350,14 @@ declare function zonedDateString(date: Date, timezone: string): string;
1310
1350
  declare function nextOccurrence(now: Date, schedule: ScheduleConfig): Date;
1311
1351
  /** 今天这一次是否已过点且在补跑窗口内。 */
1312
1352
  declare function isDue(now: Date, schedule: ScheduleConfig): boolean;
1353
+ declare function daysInMonth(year: number, month: number): number;
1354
+ /** 该时刻是否落在某个周期的结算日(按 timezone 的本地日历)。 */
1355
+ declare function isPeriodTriggerDay(period: Period, now: Date, timezone: string): boolean;
1356
+ /** 某周期下一次触发时刻(触发日的墙钟 time,严格晚于 now)。 */
1357
+ declare function nextOccurrenceFor(period: Period, now: Date, schedule: ScheduleConfig): Date;
1313
1358
  interface SchedulerDeps {
1314
1359
  store: Store;
1315
- run: (trigger: 'schedule') => Promise<unknown>;
1360
+ run: (trigger: 'schedule', period: Period) => Promise<unknown>;
1316
1361
  isRunning: () => boolean;
1317
1362
  getTimer: () => {
1318
1363
  interval?: (fn: () => void, ms: number) => unknown;
@@ -1331,13 +1376,16 @@ declare class Scheduler {
1331
1376
  stop(): void;
1332
1377
  /** 公开给测试:判断此刻是否该跑,并按需触发一次。 */
1333
1378
  tick(now?: Date): Promise<'skipped' | 'ran' | 'disabled' | 'busy'>;
1379
+ /** 该周期今天是否已经成功过(含无变更跳过)。 */
1380
+ private succeededToday;
1334
1381
  private readSchedule;
1335
- /** 下一次运行(禁用时为 undefined)。 */
1382
+ /** 下一次运行(取所有已启用周期里最近的一次;禁用时为 undefined)。 */
1336
1383
  next(): Date | undefined;
1337
1384
  describe(state: PluginState, now?: Date): {
1338
1385
  enabled: boolean;
1339
1386
  time: string;
1340
1387
  timezone: string;
1388
+ periods: Period[];
1341
1389
  nextRunAt?: string;
1342
1390
  lastRunDate?: string;
1343
1391
  catchUpMinutes: number;
@@ -1349,13 +1397,16 @@ type schedule_ScheduleConfig = ScheduleConfig;
1349
1397
  type schedule_Scheduler = Scheduler;
1350
1398
  declare const schedule_Scheduler: typeof Scheduler;
1351
1399
  type schedule_SchedulerDeps = SchedulerDeps;
1400
+ declare const schedule_daysInMonth: typeof daysInMonth;
1352
1401
  declare const schedule_isDue: typeof isDue;
1402
+ declare const schedule_isPeriodTriggerDay: typeof isPeriodTriggerDay;
1353
1403
  declare const schedule_nextOccurrence: typeof nextOccurrence;
1404
+ declare const schedule_nextOccurrenceFor: typeof nextOccurrenceFor;
1354
1405
  declare const schedule_parseClock: typeof parseClock;
1355
1406
  declare const schedule_zonedDateString: typeof zonedDateString;
1356
1407
  declare const schedule_zonedTimeToUtc: typeof zonedTimeToUtc;
1357
1408
  declare namespace schedule {
1358
- export { type schedule_ScheduleConfig as ScheduleConfig, schedule_Scheduler as Scheduler, type schedule_SchedulerDeps as SchedulerDeps, schedule_isDue as isDue, schedule_nextOccurrence as nextOccurrence, schedule_parseClock as parseClock, schedule_zonedDateString as zonedDateString, schedule_zonedTimeToUtc as zonedTimeToUtc };
1409
+ export { type schedule_ScheduleConfig as ScheduleConfig, schedule_Scheduler as Scheduler, type schedule_SchedulerDeps as SchedulerDeps, schedule_daysInMonth as daysInMonth, schedule_isDue as isDue, schedule_isPeriodTriggerDay as isPeriodTriggerDay, schedule_nextOccurrence as nextOccurrence, schedule_nextOccurrenceFor as nextOccurrenceFor, schedule_parseClock as parseClock, schedule_zonedDateString as zonedDateString, schedule_zonedTimeToUtc as zonedTimeToUtc };
1359
1410
  }
1360
1411
 
1361
1412
  /**
@@ -1447,13 +1498,6 @@ declare namespace importCli {
1447
1498
  export { type importCli_ImportCandidate as ImportCandidate, type importCli_ImportResult as ImportResult, importCli_cliConfigCandidates as cliConfigCandidates, importCli_importFirstAvailable as importFirstAvailable, importCli_importFromCli as importFromCli, importCli_parseSimpleYaml as parseSimpleYaml };
1448
1499
  }
1449
1500
 
1450
- /**
1451
- * 两个对外工具:`trending_run` / `trending_history`(规格 §8)。
1452
- *
1453
- * 工具名是**稳定对外接口**:一旦发布不再改名;需要新能力时新增工具。
1454
- * 参数方言用宿主 `dsh-tools` 的「内联 required: true」,输出用标准 JSON Schema。
1455
- */
1456
-
1457
1501
  declare function createTrendingRunTool(runner: Runner): {
1458
1502
  name: string;
1459
1503
  description: string;
@@ -1466,6 +1510,11 @@ declare function createTrendingRunTool(runner: Runner): {
1466
1510
  type: string;
1467
1511
  description: string;
1468
1512
  };
1513
+ period: {
1514
+ type: string;
1515
+ enum: string[];
1516
+ description: string;
1517
+ };
1469
1518
  };
1470
1519
  output: {
1471
1520
  schema: {
@@ -1533,6 +1582,7 @@ declare function createTrendingRunTool(runner: Runner): {
1533
1582
  execute(args?: {
1534
1583
  dryRun?: boolean;
1535
1584
  date?: string;
1585
+ period?: string;
1536
1586
  }, exec?: {
1537
1587
  signal?: AbortSignal;
1538
1588
  }): Promise<{
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-NMINPPWO.js";
27
+ } from "./chunk-QXAKMNHQ.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.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "GitHub 趋势日报插件:每天定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与总结,提交到你的仓库并自动合并 PR。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "core": {
61
61
  "repo": "morehao/trendradar",
62
- "version": "0.3.1"
62
+ "version": "0.4.0"
63
63
  },
64
64
  "client": {
65
65
  "platform": "web",