@morehao/dsh-github-trending 0.5.2 → 0.6.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/lib/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  createTrendingRunTool,
9
9
  createUpdateService,
10
10
  readPluginInfo
11
- } from "./chunk-6HITSHK6.js";
11
+ } from "./chunk-WDBZAMWN.js";
12
12
  import "./chunk-R7BK7SJK.js";
13
13
 
14
14
  // src/index.ts
@@ -57,6 +57,10 @@ function apply(ctx, config = {}) {
57
57
  const summary = await runner.run({ trigger: "schedule", period });
58
58
  log("schedule.run", { period, ok: summary.ok, skipped: summary.skipped, error: summary.error?.code });
59
59
  },
60
+ runReleases: async () => {
61
+ const record = await runner.runReleases({ trigger: "schedule" });
62
+ log("schedule.release", { id: record.id, status: record.status, checked: record.checked, changed: record.changed });
63
+ },
60
64
  onError: (error) => ctx.logger?.warn?.(`[dsh-github-trending] \u5B9A\u65F6\u8FD0\u884C\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`)
61
65
  });
62
66
  const pluginInfo = readPluginInfo();
package/lib/internal.d.ts CHANGED
@@ -112,6 +112,46 @@ interface RunOutcome {
112
112
  durationMs: number;
113
113
  coreVersion: string;
114
114
  }
115
+ /**
116
+ * 契约 0.4.0(核心 0.7.0)新增的 wire 类型。
117
+ *
118
+ * 契约包尚未发布:npm 上的 `@morehao/trendradar-contract` 还停在 0.3.0,
119
+ * 因此这三块按 `contract/schema/releases.v2.json` 原样在本地声明,等 0.4.0 发布后
120
+ * 改回 `@morehao/trendradar-contract` 的 `import type`。除这里列出的字段外,
121
+ * 不补任何契约里没有的东西——本地声明只是「包还没发」的临时载体,不是第二份契约。
122
+ */
123
+ type SubscriptionStatus = 'baseline' | 'ok' | 'no_change' | 'rate_limited' | 'failed';
124
+ /** 订阅表的一行:只放列表展示需要的字段,正文在 `SubscriptionDoc` 里(首屏体积预算)。 */
125
+ interface SubscriptionItem {
126
+ fullName: string;
127
+ url?: string;
128
+ latestTag?: string;
129
+ latestSummaryAt?: string;
130
+ lastCheckedAt?: string;
131
+ status: SubscriptionStatus;
132
+ }
133
+ /** 发版文档的一卷:每册都带两位序号,index N 是 `<repo>-0N.md`(第 1 册是 `-01`)。 */
134
+ interface SubscriptionVolume {
135
+ index: number;
136
+ file: string;
137
+ from?: string;
138
+ to?: string;
139
+ lines?: number;
140
+ }
141
+ /** 一个仓库某卷的发版文档;`markdown` 是文件原文,面板逐字渲染、不再二次摘要。 */
142
+ interface SubscriptionDoc {
143
+ fullName: string;
144
+ volume: number;
145
+ volumes?: SubscriptionVolume[];
146
+ updatedAt?: string;
147
+ markdown: string;
148
+ }
149
+ declare function isSubscriptionStatus(value: unknown): value is SubscriptionStatus;
150
+ /**
151
+ * `SubscriptionDoc` 的运行时守卫:缺 fullName/volume/markdown 的负载宁可抛
152
+ * `IPC_INVALID`,也不要让界面拿到半份文档(正文缺失时渲染会崩,比报错更难查)。
153
+ */
154
+ declare function isSubscriptionDoc(value: unknown): value is SubscriptionDoc;
115
155
  declare class IpcError extends Error {
116
156
  readonly code: string;
117
157
  constructor(code: string, message: string);
@@ -139,24 +179,27 @@ interface ParseRunOptions {
139
179
  * 当成功写进状态。
140
180
  */
141
181
  declare function parseRunResult(text: string, options: ParseRunOptions): RunOutcome;
142
- /** 订阅命令的结果:list 带回 repos,add/remove 只看 ok/error。 */
182
+ /** 订阅命令的结果:list 带回 repos/items,releases 带回 doc,add/remove 只看 ok/error。 */
143
183
  interface SubscribeOutcome {
144
184
  ok: boolean;
145
185
  repos: string[];
186
+ /** list 的逐仓库明细(契约 0.4.0);旧核心不返回时为 undefined。 */
187
+ items?: SubscriptionItem[];
188
+ /** `action=releases` 读到的发版文档;其它 action 没有。 */
189
+ doc?: SubscriptionDoc;
146
190
  error?: RunError;
147
191
  }
148
192
  /** 订阅命令的一个动作(契约 `RunRequest.action`)。 */
149
- type SubscribeAction = 'list' | 'add' | 'remove';
193
+ type SubscribeAction = 'list' | 'add' | 'remove' | 'releases';
150
194
  /**
151
195
  * 解析核心 `subscribe --json` 的 stdout。
152
196
  *
153
- * 核心先由 `runSubscribe` 把 `list` 的结果**逐行**打到 stdout(每行一个 owner/repo),
154
- * 再由 `emitSubscribeResult` 把契约 RunResult 信封打到 stdout(`command:"subscribe"`,
155
- * 只带 `ok/error`,**不含 repos**)。因此:
156
- * - 信封是**最后一段 JSON**;它前面的非空行才是 list 的仓库列表;
157
- * - 信封 `command` 是 `"subscribe"`(不在契约 RunResult 枚举里,故不能直接用
158
- * `validators.runResult` 校验),这里只按 envelope 必需字段做轻量校验。
159
- * 任何形状不对都抛 `IPC_INVALID`:订阅结果读错会把失败当成功。
197
+ * 契约 0.4.0 起 payload 进了结果信封:`{command:"subscribe", subscriptions?, subscriptionDoc?}`。
198
+ * 这里**优先**读信封;旧核心(0.6.x 及以前)把 `list` 的结果逐行打到信封之前、信封里只有
199
+ * `ok/error`,因此保留逐行兜底——两种都能读,但绝不「猜字段」:
200
+ * - 信封不是 `ipcSchema=2` 的对象 → `IPC_INVALID`;
201
+ * - `subscriptions` 存在但形状不对 → `IPC_INVALID`(不是空列表);
202
+ * - `subscriptionDoc` 存在但不满足契约 → `IPC_INVALID`(界面不能拿到半份文档)。
160
203
  */
161
204
  declare function parseSubscribeResult(text: string): SubscribeOutcome;
162
205
 
@@ -178,16 +221,22 @@ declare const ipc_State: typeof State;
178
221
  declare const ipc_StateRun: typeof StateRun;
179
222
  type ipc_SubscribeAction = SubscribeAction;
180
223
  type ipc_SubscribeOutcome = SubscribeOutcome;
224
+ type ipc_SubscriptionDoc = SubscriptionDoc;
225
+ type ipc_SubscriptionItem = SubscriptionItem;
181
226
  declare const ipc_SubscriptionList: typeof SubscriptionList;
227
+ type ipc_SubscriptionStatus = SubscriptionStatus;
228
+ type ipc_SubscriptionVolume = SubscriptionVolume;
182
229
  declare const ipc_Usage: typeof Usage;
183
230
  type ipc_VersionInfo = VersionInfo;
231
+ declare const ipc_isSubscriptionDoc: typeof isSubscriptionDoc;
232
+ declare const ipc_isSubscriptionStatus: typeof isSubscriptionStatus;
184
233
  declare const ipc_isVersionInfo: typeof isVersionInfo;
185
234
  declare const ipc_parseJsonObject: typeof parseJsonObject;
186
235
  declare const ipc_parseRunResult: typeof parseRunResult;
187
236
  declare const ipc_parseSubscribeResult: typeof parseSubscribeResult;
188
237
  declare const ipc_parseVersionInfo: typeof parseVersionInfo;
189
238
  declare namespace ipc {
190
- export { ipc_ErrorCode as ErrorCode, ipc_ErrorInfo as ErrorInfo, ipc_FileArtifact as FileArtifact, ipc_IPC_SCHEMA_VERSION as IPC_SCHEMA_VERSION, ipc_IpcError as IpcError, type ipc_ParseRunOptions as ParseRunOptions, ipc_ReleasesReport as ReleasesReport, type ipc_RunError as RunError, type ipc_RunOutcome as RunOutcome, ipc_RunRequest as RunRequest, ipc_RunResult as RunResult, ipc_SUPPORTED_CONTRACT_MAJOR as SUPPORTED_CONTRACT_MAJOR, ipc_SUPPORTED_DIGEST_SCHEMA as SUPPORTED_DIGEST_SCHEMA, ipc_State as State, ipc_StateRun as StateRun, type ipc_SubscribeAction as SubscribeAction, type ipc_SubscribeOutcome as SubscribeOutcome, ipc_SubscriptionList as SubscriptionList, ipc_Usage as Usage, type ipc_VersionInfo as VersionInfo, ipc_isVersionInfo as isVersionInfo, ipc_parseJsonObject as parseJsonObject, ipc_parseRunResult as parseRunResult, ipc_parseSubscribeResult as parseSubscribeResult, ipc_parseVersionInfo as parseVersionInfo };
239
+ export { ipc_ErrorCode as ErrorCode, ipc_ErrorInfo as ErrorInfo, ipc_FileArtifact as FileArtifact, ipc_IPC_SCHEMA_VERSION as IPC_SCHEMA_VERSION, ipc_IpcError as IpcError, type ipc_ParseRunOptions as ParseRunOptions, ipc_ReleasesReport as ReleasesReport, type ipc_RunError as RunError, type ipc_RunOutcome as RunOutcome, ipc_RunRequest as RunRequest, ipc_RunResult as RunResult, ipc_SUPPORTED_CONTRACT_MAJOR as SUPPORTED_CONTRACT_MAJOR, ipc_SUPPORTED_DIGEST_SCHEMA as SUPPORTED_DIGEST_SCHEMA, ipc_State as State, ipc_StateRun as StateRun, type ipc_SubscribeAction as SubscribeAction, type ipc_SubscribeOutcome as SubscribeOutcome, type ipc_SubscriptionDoc as SubscriptionDoc, type ipc_SubscriptionItem as SubscriptionItem, ipc_SubscriptionList as SubscriptionList, type ipc_SubscriptionStatus as SubscriptionStatus, type ipc_SubscriptionVolume as SubscriptionVolume, ipc_Usage as Usage, type ipc_VersionInfo as VersionInfo, ipc_isSubscriptionDoc as isSubscriptionDoc, ipc_isSubscriptionStatus as isSubscriptionStatus, ipc_isVersionInfo as isVersionInfo, ipc_parseJsonObject as parseJsonObject, ipc_parseRunResult as parseRunResult, ipc_parseSubscribeResult as parseSubscribeResult, ipc_parseVersionInfo as parseVersionInfo };
191
240
  }
192
241
 
193
242
  type Provider = string;
@@ -261,6 +310,18 @@ interface SummarizeSettings {
261
310
  enrichTimeoutSeconds: number;
262
311
  enrichBudgetSeconds: number;
263
312
  }
313
+ /**
314
+ * 每周发版运行的插件侧节奏(核心没有这个字段,**绝不**写进核心 YAML)。
315
+ * 与榜单调度(`schedule.*`)完全独立:两者各有自己的开关与时间点。
316
+ */
317
+ interface ReleaseScheduleSettings {
318
+ enabled: boolean;
319
+ /** ISO 星期:1=周一 … 7=周日(和 `Intl` 的 0=周日 不同,避免调用方来回换算)。 */
320
+ weekday: number;
321
+ /** `HH:MM[:SS]`(本地墙钟,按 timezone 解释)。 */
322
+ time: string;
323
+ timezone: string;
324
+ }
264
325
  /**
265
326
  * 发版订阅阶段的行为参数(核心 `releases.*`)。
266
327
  * 阶段**常开**(`releases.enabled` 恒为 true,不出现在设置里):有没有产出只由订阅列表决定,
@@ -273,10 +334,24 @@ interface ReleasesSettings {
273
334
  includeDrafts: boolean;
274
335
  fallbackToTags: boolean;
275
336
  baselineOnFirstRun: boolean;
337
+ /** 文档**列出**多少个版本(核心 0.7.0 起不再限制抓取条数,摘要覆盖全周)。 */
276
338
  perRepoLimit: number;
277
339
  maxNotesChars: number;
278
- /** 发版摘要周期,独立默认 daily,不与 trending.since 联动。 */
340
+ /** 每仓库每周一条中文摘要的字数上限(核心 `releases.summaryMaxChars`)。 */
341
+ summaryMaxChars: number;
342
+ /** 每仓库文档的行数预算,超了核心才开新卷(核心 `releases.docMaxLines`)。 */
343
+ docMaxLines: number;
344
+ /** 订阅检查并发;`auto` 由插件按订阅数解析成 1–8 的数字(核心只认数字)。 */
345
+ concurrency: 'auto' | number;
346
+ /** 订阅阶段的模型预算秒数;`auto` 由插件按订阅数解析(核心只认数字)。 */
347
+ budgetSeconds: 'auto' | number;
348
+ /**
349
+ * 发版摘要周期。核心 0.7.0 只实现 `weekly`(一个 ISO 周一次模型调用),
350
+ * 因此这里保留枚举类型只为让校验能明确告诉用户「daily/monthly 已不支持」。
351
+ */
279
352
  period: Period;
353
+ /** 插件侧每周触发节奏;核心配置里只用到它的 `timezone`(切周口径),不写整个对象。 */
354
+ schedule: ReleaseScheduleSettings;
280
355
  }
281
356
  interface ScheduleSettings {
282
357
  enabled: boolean;
@@ -385,7 +460,13 @@ interface CoreConfig {
385
460
  baselineOnFirstRun: boolean;
386
461
  perRepoLimit: number;
387
462
  maxNotesChars: number;
388
- period: string;
463
+ summaryMaxChars: number;
464
+ docMaxLines: number;
465
+ timezone: string;
466
+ weekStart: 'monday';
467
+ concurrency: number;
468
+ budgetSeconds: number;
469
+ period: 'weekly';
389
470
  };
390
471
  output: {
391
472
  dir: string;
@@ -397,8 +478,12 @@ interface CoreConfig {
397
478
  * 生成核心配置。`apiKey` 单独传入:它只能来自宿主凭据或用户当次输入,
398
479
  * 绝不来自 settings.json。`period` 是本次运行的标的周期;核心 `run --period`
399
480
  * 会用它覆盖 `trending.since`,这里同步写入以免旧核心/调试时口径不一致。
481
+ *
482
+ * `repoCount` 只用于把发版订阅的 `auto` 解析成数字(核心 0.7.0 的 YAML 只认整数):
483
+ * 并发 = clamp(4, ceil(n/10), 8),预算 = min(300, 30+3n)。显式数字永远优先。
484
+ * 不发版调度对象——`releases.schedule` 是纯插件概念,写进 YAML 核心会当成未知字段。
400
485
  */
401
- declare function toCoreConfig(settings: Settings, apiKey: string, baseURLOverride?: string, period?: Period): CoreConfig;
486
+ declare function toCoreConfig(settings: Settings, apiKey: string, baseURLOverride?: string, period?: Period, repoCount?: number): CoreConfig;
402
487
  /**
403
488
  * 极简 YAML 序列化:只覆盖「对象/数组/字符串/数字/布尔」。
404
489
  * 字符串一律用 JSON 双引号形式——JSON 字符串是合法 YAML 标量,且能避免
@@ -421,6 +506,7 @@ type config_PRSettings = PRSettings;
421
506
  type config_Period = Period;
422
507
  type config_Problem = Problem;
423
508
  type config_Provider = Provider;
509
+ type config_ReleaseScheduleSettings = ReleaseScheduleSettings;
424
510
  type config_ReleasesSettings = ReleasesSettings;
425
511
  type config_RepoSettings = RepoSettings;
426
512
  declare const config_SETTINGS_SECTIONS: typeof SETTINGS_SECTIONS;
@@ -441,7 +527,7 @@ declare const config_toCoreConfig: typeof toCoreConfig;
441
527
  declare const config_toYaml: typeof toYaml;
442
528
  declare const config_validateSettings: typeof validateSettings;
443
529
  declare namespace config {
444
- 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_Enrich as Enrich, 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_ReleasesSettings as ReleasesSettings, 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_SummarizeSettings as SummarizeSettings, type config_TrendingSettings as TrendingSettings, config_effectiveBaseURL as effectiveBaseURL, config_isEnrich as isEnrich, 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 };
530
+ 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_Enrich as Enrich, 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_ReleaseScheduleSettings as ReleaseScheduleSettings, type config_ReleasesSettings as ReleasesSettings, 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_SummarizeSettings as SummarizeSettings, type config_TrendingSettings as TrendingSettings, config_effectiveBaseURL as effectiveBaseURL, config_isEnrich as isEnrich, 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 };
445
531
  }
446
532
 
447
533
  declare const PLUGIN_DIR_NAME = "github-trending";
@@ -548,6 +634,38 @@ interface PendingPr {
548
634
  /** 该 PR 对应的 digest 日期(YYYY-MM-DD);用于判断「同日重跑」是否真的会撞分支。 */
549
635
  date?: string;
550
636
  }
637
+ /**
638
+ * 一次发版订阅运行的记录(与榜单的 `RunRecord` 分开)。
639
+ *
640
+ * 为什么不复用 `history.json`:发版文档按周累积、一跑就是十几次 GitHub 请求 + 每周一次
641
+ * 模型调用,它的失败/限流节奏与榜单完全无关;混在一起会污染「今天榜单有没有产出」的判断。
642
+ * 计数语义由契约 `ReleasesReport` 定义:checked=检查过的仓库数,changed=真正写了文档的仓库数。
643
+ */
644
+ interface ReleaseRunRecord {
645
+ id: string;
646
+ /** ISO 时间(UTC)。 */
647
+ startedAt: string;
648
+ /** ISO 时间(UTC)。 */
649
+ finishedAt: string;
650
+ status: 'ok' | 'failed' | 'skipped' | 'cancelled';
651
+ checked: number;
652
+ changed: number;
653
+ newTags: number;
654
+ summarized: number;
655
+ degraded: number;
656
+ failed: number;
657
+ rateLimited: number;
658
+ durationMs: number;
659
+ tokens?: number;
660
+ commit?: string;
661
+ prUrl?: string;
662
+ branch?: string;
663
+ errorCode?: string;
664
+ errorMessage?: string;
665
+ coreVersion?: string;
666
+ }
667
+ /** 发版运行记录上限:一周一次的话正好留半年,够复盘又不会让 state.json 膨胀。 */
668
+ declare const RELEASE_RUN_LIMIT = 30;
551
669
  interface PluginState {
552
670
  lastRunAt?: string;
553
671
  /** 最近一次成功(ok=true,含 skipped)的运行时间;工具与界面读它。 */
@@ -557,6 +675,12 @@ interface PluginState {
557
675
  lastDigest?: DigestIdentity;
558
676
  lastResult?: RunRecord;
559
677
  pendingPr?: PendingPr;
678
+ /** 最近 30 次发版运行,最新在前;旧 state 里没有这个字段。 */
679
+ releaseRuns?: ReleaseRunRecord[];
680
+ /** 最近一次发版运行结束时间(失败也算)。 */
681
+ lastReleaseAt?: string;
682
+ /** 最近一次**成功**发版运行所属的 ISO 周(如 `2026-W44`),周去重键。 */
683
+ lastReleaseWeek?: string;
560
684
  }
561
685
  interface HistoryFile {
562
686
  records: RunRecord[];
@@ -597,6 +721,13 @@ declare class Store {
597
721
  private readHistoryRaw;
598
722
  /** 记录一次运行:历史裁剪到 HISTORY_LIMIT 条,state 同步更新。 */
599
723
  recordRun(record: RunRecord, next?: Partial<PluginState>): void;
724
+ /**
725
+ * 记录一次发版运行:只写 `state.json`(不发版记录进 `history.json`,两者节奏不同)。
726
+ * 记录裁剪到 RELEASE_RUN_LIMIT 条,`next` 用来顺带更新 lastReleaseAt/lastReleaseWeek。
727
+ */
728
+ appendReleaseRun(record: ReleaseRunRecord, next?: Partial<PluginState>): void;
729
+ /** 读发版运行记录;缺失/损坏(不是数组或行不是对象)一律按空数组处理,不让界面崩。 */
730
+ readReleaseRuns(): ReleaseRunRecord[];
600
731
  newLogPath(id: string): string;
601
732
  appendLog(file: string, line: string): void;
602
733
  hasLog(file: string): boolean;
@@ -631,6 +762,8 @@ declare const state_PREVIEW_MAX_BYTES: typeof PREVIEW_MAX_BYTES;
631
762
  type state_PendingPr = PendingPr;
632
763
  type state_PluginState = PluginState;
633
764
  type state_PreviewMeta = PreviewMeta;
765
+ declare const state_RELEASE_RUN_LIMIT: typeof RELEASE_RUN_LIMIT;
766
+ type state_ReleaseRunRecord = ReleaseRunRecord;
634
767
  type state_ReleasesSummary = ReleasesSummary;
635
768
  type state_RunRecord = RunRecord;
636
769
  type state_Store = Store;
@@ -640,7 +773,7 @@ declare const state_newRunId: typeof newRunId;
640
773
  declare const state_sumTokens: typeof sumTokens;
641
774
  declare const state_writeJsonAtomic: typeof writeJsonAtomic;
642
775
  declare namespace state {
643
- export { state_CONTENT_HASH_ALGO_V2 as CONTENT_HASH_ALGO_V2, type state_DigestIdentity as DigestIdentity, state_HISTORY_LIMIT as HISTORY_LIMIT, type state_HistoryFile as HistoryFile, type state_LegacyMigrationResult as LegacyMigrationResult, state_PREVIEW_LIMIT as PREVIEW_LIMIT, state_PREVIEW_MAX_BYTES as PREVIEW_MAX_BYTES, type state_PendingPr as PendingPr, type state_PluginState as PluginState, type state_PreviewMeta as PreviewMeta, type state_ReleasesSummary as ReleasesSummary, type state_RunRecord as RunRecord, state_Store as Store, type state_StoreOptions as StoreOptions, state_newRunId as newRunId, state_sumTokens as sumTokens, state_writeJsonAtomic as writeJsonAtomic };
776
+ export { state_CONTENT_HASH_ALGO_V2 as CONTENT_HASH_ALGO_V2, type state_DigestIdentity as DigestIdentity, state_HISTORY_LIMIT as HISTORY_LIMIT, type state_HistoryFile as HistoryFile, type state_LegacyMigrationResult as LegacyMigrationResult, state_PREVIEW_LIMIT as PREVIEW_LIMIT, state_PREVIEW_MAX_BYTES as PREVIEW_MAX_BYTES, type state_PendingPr as PendingPr, type state_PluginState as PluginState, type state_PreviewMeta as PreviewMeta, state_RELEASE_RUN_LIMIT as RELEASE_RUN_LIMIT, type state_ReleaseRunRecord as ReleaseRunRecord, type state_ReleasesSummary as ReleasesSummary, type state_RunRecord as RunRecord, state_Store as Store, type state_StoreOptions as StoreOptions, state_newRunId as newRunId, state_sumTokens as sumTokens, state_writeJsonAtomic as writeJsonAtomic };
644
777
  }
645
778
 
646
779
  interface CoreRelease {
@@ -876,6 +1009,13 @@ interface RunCoreOptions {
876
1009
  trigger?: 'manual' | 'schedule';
877
1010
  /** 回环代理地址:覆盖配置里的 model.baseURL(代理模式专用)。 */
878
1011
  baseURLOverride?: string;
1012
+ /**
1013
+ * 只跑指定阶段(核心 `run --stages`)。缺省 = 跑所有已启用阶段;
1014
+ * `['releases']` 就是「只跑发版订阅」——不抓榜单、不渲染 digest、也不合并订阅 pending。
1015
+ */
1016
+ stages?: string[];
1017
+ /** 订阅仓库数:用于把配置里的 `auto` 并发/预算解析成数字(发版运行传入)。 */
1018
+ repoCount?: number;
879
1019
  signal?: AbortSignal;
880
1020
  timeoutMs?: number;
881
1021
  onStderr?: (line: string) => void;
@@ -892,6 +1032,7 @@ declare function buildCoreArgs(channelPath: string, options: {
892
1032
  dryRun: boolean;
893
1033
  date?: string;
894
1034
  period?: Period;
1035
+ stages?: string[];
895
1036
  }): string[];
896
1037
  declare function runCore(options: RunCoreOptions): Promise<RunCoreResult>;
897
1038
 
@@ -1422,6 +1563,37 @@ declare class Runner {
1422
1563
  runMany(options: RunRequestOptions & {
1423
1564
  periods?: Period[];
1424
1565
  }): Promise<RunSummary[]>;
1566
+ /**
1567
+ * 只跑发版订阅阶段(`run --stages releases`)。
1568
+ *
1569
+ * 与榜单运行共用同一把 `active` 锁:两条路径都会写同一份产物、读同一份订阅,
1570
+ * 并发跑会互相踩分支。和 `run()` 的区别只有三点:不带 dryRun、写完只落
1571
+ * `state.releaseRuns`(不碰 history.json)、自己**不**合并订阅 pending 队列
1572
+ * (`--stages releases` 让核心跳过那一步,合并只在完整 run 里发生)。
1573
+ */
1574
+ runReleases(options: {
1575
+ trigger: 'manual' | 'schedule';
1576
+ }): Promise<ReleaseRunRecord>;
1577
+ /** 发版运行用哪一天:发版调度时区下的「今天」(不是榜单调度时区,两者可独立配置)。 */
1578
+ private releaseDate;
1579
+ /** 订阅仓库数(best-effort,只影响 auto 解析):任何失败都当 0,不阻塞发版运行。 */
1580
+ private subscriptionCount;
1581
+ /**
1582
+ * 解析模型钥匙并(代理模式下)起回环代理。
1583
+ *
1584
+ * 榜单运行与发版运行共用这一段:代理的一次性 token 语义(空 key 会让核心静默降级成
1585
+ * 英文/离线假模型)只写一次,避免两份实现漂移。
1586
+ */
1587
+ private openModelChannel;
1588
+ /** 把 `run --json` 的发版阶段结果投影成一条发版运行记录并落盘。 */
1589
+ private recordReleaseRun;
1590
+ /** 运行在拿到信封之前就失败(定位/握手/模型/代理/超时/取消):也要留一条可复盘记录。 */
1591
+ private recordReleaseFailure;
1592
+ /**
1593
+ * 落盘发版运行:只写 state(history.json 是榜单的),顺带推进 lastReleaseAt;
1594
+ * 只有 ok/skipped 才锁 `lastReleaseWeek`——失败不锁,调度下一 tick 才有机会重试。
1595
+ */
1596
+ private persistReleaseRun;
1425
1597
  private summarize;
1426
1598
  private failureSummary;
1427
1599
  /**
@@ -1453,6 +1625,11 @@ declare namespace runner {
1453
1625
 
1454
1626
  /** 订阅命令超时:核心只做一次 GitHub 请求/本地文件读写,8 s 足够。 */
1455
1627
  declare const SUBSCRIBE_TIMEOUT_MS = 8000;
1628
+ /**
1629
+ * `releases` 要真的读一份可能几百行的文档文件、并在缺失时回退到卷索引,
1630
+ * 比 list/add/remove 重得多;给它 30 s,避免大仓库文档被判成「无输出」。
1631
+ */
1632
+ declare const RELEASES_TIMEOUT_MS = 30000;
1456
1633
  declare function isValidRepo(repo: string): boolean;
1457
1634
  interface SubscribeDeps {
1458
1635
  store: Store;
@@ -1461,20 +1638,23 @@ interface SubscribeDeps {
1461
1638
  corePath?: string;
1462
1639
  }
1463
1640
  /**
1464
- * 执行一次 subscribe 命令:`action=list` 读列表,`add/remove` 改 pending 队列。
1465
- * 返回 `{ ok, repos, error? }`;非 ok 由调用方(路由)决定转成 `SUBSCRIBE_FAILED`。
1641
+ * 执行一次 subscribe 命令:`action=list` 读列表,`add/remove` 改 pending 队列,
1642
+ * `releases` 读某个仓库的发版文档。
1643
+ * 返回 `{ ok, repos, items?, doc?, error? }`;非 ok 由调用方(路由)决定转成 `SUBSCRIBE_FAILED`。
1466
1644
  */
1467
1645
  declare function runSubscribe(input: {
1468
1646
  action: SubscribeAction;
1469
1647
  repo?: string;
1648
+ volume?: number;
1470
1649
  }, deps: SubscribeDeps): Promise<SubscribeOutcome>;
1471
1650
 
1651
+ declare const subscribe_RELEASES_TIMEOUT_MS: typeof RELEASES_TIMEOUT_MS;
1472
1652
  declare const subscribe_SUBSCRIBE_TIMEOUT_MS: typeof SUBSCRIBE_TIMEOUT_MS;
1473
1653
  type subscribe_SubscribeDeps = SubscribeDeps;
1474
1654
  declare const subscribe_isValidRepo: typeof isValidRepo;
1475
1655
  declare const subscribe_runSubscribe: typeof runSubscribe;
1476
1656
  declare namespace subscribe {
1477
- export { subscribe_SUBSCRIBE_TIMEOUT_MS as SUBSCRIBE_TIMEOUT_MS, type subscribe_SubscribeDeps as SubscribeDeps, subscribe_isValidRepo as isValidRepo, subscribe_runSubscribe as runSubscribe };
1657
+ export { subscribe_RELEASES_TIMEOUT_MS as RELEASES_TIMEOUT_MS, subscribe_SUBSCRIBE_TIMEOUT_MS as SUBSCRIBE_TIMEOUT_MS, type subscribe_SubscribeDeps as SubscribeDeps, subscribe_isValidRepo as isValidRepo, subscribe_runSubscribe as runSubscribe };
1478
1658
  }
1479
1659
 
1480
1660
  interface PluginInfo {
@@ -1500,6 +1680,10 @@ interface ScheduleConfig {
1500
1680
  /** 自动产出的周期;每个周期只在「它自己的触发日」跑(日=每天、周=周日、月=月末)。 */
1501
1681
  periods: Period[];
1502
1682
  }
1683
+ /** 每周发版行程的开关/时间点(纯插件概念,核心没有对应字段)。 */
1684
+ type ReleaseScheduleConfig = ReleaseScheduleSettings;
1685
+ /** 发版行程的补跑窗口:超过 6 h 就放弃本周(下一周那次会同时覆盖两周的内容)。 */
1686
+ declare const RELEASE_CATCH_UP_MINUTES: number;
1503
1687
  /** 目标时区的墙上时间 → UTC 时刻(按 DST 修正一次,够用且可测)。 */
1504
1688
  declare function zonedTimeToUtc(year: number, month: number, day: number, hour: number, minute: number, second: number, timezone: string): Date;
1505
1689
  declare function parseClock(time: string): {
@@ -1513,6 +1697,15 @@ declare function zonedDateString(date: Date, timezone: string): string;
1513
1697
  declare function nextOccurrence(now: Date, schedule: ScheduleConfig): Date;
1514
1698
  /** 今天这一次是否已过点且在补跑窗口内。 */
1515
1699
  declare function isDue(now: Date, schedule: ScheduleConfig): boolean;
1700
+ /** 某时刻在 `timezone` 下的 ISO 周键;发版周去重就用它。 */
1701
+ declare function isoWeekKey(date: Date, timezone: string): string;
1702
+ /** 下一次发版运行时刻:ISO 星期匹配 `cfg.weekday`、当天的墙钟 `cfg.time`,严格晚于 now。 */
1703
+ declare function nextReleaseOccurrence(now: Date, cfg: ReleaseScheduleConfig): Date;
1704
+ /**
1705
+ * 发版行程此刻是否该跑:星期匹配、已过点、且迟到不超过 6 h。
1706
+ * 超过窗口就**放弃本周**——下周那次会同时覆盖两周的内容(核心按周累积,不需要补跑)。
1707
+ */
1708
+ declare function isReleaseDue(now: Date, cfg: ReleaseScheduleConfig): boolean;
1516
1709
  declare function daysInMonth(year: number, month: number): number;
1517
1710
  /** 该时刻是否落在某个周期的结算日(按 timezone 的本地日历)。 */
1518
1711
  declare function isPeriodTriggerDay(period: Period, now: Date, timezone: string): boolean;
@@ -1521,6 +1714,8 @@ declare function nextOccurrenceFor(period: Period, now: Date, schedule: Schedule
1521
1714
  interface SchedulerDeps {
1522
1715
  store: Store;
1523
1716
  run: (trigger: 'schedule', period: Period) => Promise<unknown>;
1717
+ /** 每周发版行程:与榜单 `run` 共享 Runner 的单跑锁(Runner 自己会拒绝并发)。 */
1718
+ runReleases: (trigger: 'schedule') => Promise<unknown>;
1524
1719
  isRunning: () => boolean;
1525
1720
  getTimer: () => {
1526
1721
  interval?: (fn: () => void, ms: number) => unknown;
@@ -1534,16 +1729,39 @@ declare class Scheduler {
1534
1729
  private stopped;
1535
1730
  /** 当天已尝试次数与时间;只在内存里,进程重启后重新计数。 */
1536
1731
  private readonly attempts;
1732
+ /** 发版行程的尝试计数(键 = ISO 周 + 本地日期);理由同榜单:失败不能每分钟重试。 */
1733
+ private readonly releaseAttempts;
1537
1734
  constructor(deps: SchedulerDeps);
1538
1735
  start(): void;
1539
1736
  stop(): void;
1540
- /** 公开给测试:判断此刻是否该跑,并按需触发一次。 */
1737
+ /**
1738
+ * 公开给测试:判断此刻是否该跑,并按需触发。
1739
+ *
1740
+ * 每个 tick 都**同时**评估榜单与发版两条行程(interval 已经是 60 s)。返回值合并规则
1741
+ * 保证老调用方不受影响:真的跑了 → `ran`;否则有人占着锁 → `busy`;再否则只要有
1742
+ * 一条行程是禁用状态就沿用旧的 `disabled`;都没有 → `skipped`。
1743
+ */
1541
1744
  tick(now?: Date): Promise<'skipped' | 'ran' | 'disabled' | 'busy'>;
1745
+ /** 榜单行程(原 tick 的逻辑,不改)。 */
1746
+ private tickDigest;
1747
+ /**
1748
+ * 发版行程:每周一次、只跑 `--stages releases`。
1749
+ *
1750
+ * 跳过条件(全部满足才跑):开启、星期/时间命中且迟到 ≤ 6 h、本周尚未成功过
1751
+ * (`state.lastReleaseWeek`)、当前没有别的运行占锁。
1752
+ * 失败**不锁本周**,但也只有 2 次机会(间隔 5 min)——否则一个坏 token 会在 6 h 的
1753
+ * 补跑窗口里每分钟 spawn 一次核心、白白打 GitHub API。
1754
+ */
1755
+ private tickReleases;
1542
1756
  /** 该周期今天是否已经成功过(含无变更跳过)。 */
1543
1757
  private succeededToday;
1544
1758
  private readSchedule;
1759
+ /** 发版行程的节奏来自 `settings.releases.schedule`(与榜单 `schedule.*` 相互独立)。 */
1760
+ private readReleaseSchedule;
1545
1761
  /** 下一次运行(取所有已启用周期里最近的一次;禁用时为 undefined)。 */
1546
1762
  next(): Date | undefined;
1763
+ /** 下一次发版运行(禁用/非法时为 undefined)。 */
1764
+ nextRelease(now?: Date): Date | undefined;
1547
1765
  describe(state: PluginState, now?: Date): {
1548
1766
  enabled: boolean;
1549
1767
  time: string;
@@ -1553,9 +1771,19 @@ declare class Scheduler {
1553
1771
  lastRunDate?: string;
1554
1772
  catchUpMinutes: number;
1555
1773
  source: 'timer';
1774
+ releaseEnabled: boolean;
1775
+ releaseWeekday: number;
1776
+ releaseTime: string;
1777
+ releaseTimezone: string;
1778
+ nextReleaseAt?: string;
1779
+ lastReleaseAt?: string;
1780
+ lastReleaseWeek?: string;
1781
+ releaseCatchUpMinutes: number;
1556
1782
  };
1557
1783
  }
1558
1784
 
1785
+ declare const schedule_RELEASE_CATCH_UP_MINUTES: typeof RELEASE_CATCH_UP_MINUTES;
1786
+ type schedule_ReleaseScheduleConfig = ReleaseScheduleConfig;
1559
1787
  type schedule_ScheduleConfig = ScheduleConfig;
1560
1788
  type schedule_Scheduler = Scheduler;
1561
1789
  declare const schedule_Scheduler: typeof Scheduler;
@@ -1563,13 +1791,16 @@ type schedule_SchedulerDeps = SchedulerDeps;
1563
1791
  declare const schedule_daysInMonth: typeof daysInMonth;
1564
1792
  declare const schedule_isDue: typeof isDue;
1565
1793
  declare const schedule_isPeriodTriggerDay: typeof isPeriodTriggerDay;
1794
+ declare const schedule_isReleaseDue: typeof isReleaseDue;
1795
+ declare const schedule_isoWeekKey: typeof isoWeekKey;
1566
1796
  declare const schedule_nextOccurrence: typeof nextOccurrence;
1567
1797
  declare const schedule_nextOccurrenceFor: typeof nextOccurrenceFor;
1798
+ declare const schedule_nextReleaseOccurrence: typeof nextReleaseOccurrence;
1568
1799
  declare const schedule_parseClock: typeof parseClock;
1569
1800
  declare const schedule_zonedDateString: typeof zonedDateString;
1570
1801
  declare const schedule_zonedTimeToUtc: typeof zonedTimeToUtc;
1571
1802
  declare namespace schedule {
1572
- 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 };
1803
+ export { schedule_RELEASE_CATCH_UP_MINUTES as RELEASE_CATCH_UP_MINUTES, type schedule_ReleaseScheduleConfig as ReleaseScheduleConfig, 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_isReleaseDue as isReleaseDue, schedule_isoWeekKey as isoWeekKey, schedule_nextOccurrence as nextOccurrence, schedule_nextOccurrenceFor as nextOccurrenceFor, schedule_nextReleaseOccurrence as nextReleaseOccurrence, schedule_parseClock as parseClock, schedule_zonedDateString as zonedDateString, schedule_zonedTimeToUtc as zonedTimeToUtc };
1573
1804
  }
1574
1805
 
1575
1806
  /**
package/lib/internal.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  subscribe_exports,
26
26
  tools_exports,
27
27
  update_exports
28
- } from "./chunk-6HITSHK6.js";
28
+ } from "./chunk-WDBZAMWN.js";
29
29
  import {
30
30
  exec_exports
31
31
  } from "./chunk-R7BK7SJK.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-github-trending",
3
- "version": "0.5.2",
3
+ "version": "0.6.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.6.0"
62
+ "version": "0.7.0"
63
63
  },
64
64
  "client": {
65
65
  "platform": "web",