@morehao/dsh-github-trending 0.5.2 → 0.6.1

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
@@ -1,4 +1,4 @@
1
- import { ErrorCode, FileArtifact, Usage, ReleasesReport, ErrorInfo, RunRequest, RunResult, State, StateRun, SubscriptionList } from '@morehao/trendradar-contract';
1
+ import { ErrorCode, FileArtifact, Usage, ReleasesReport, SubscriptionItem, SubscriptionDoc, SubscriptionStatus, ErrorInfo, RunRequest, RunResult, State, StateRun, SubscriptionList, SubscriptionVolume } from '@morehao/trendradar-contract';
2
2
  import http from 'node:http';
3
3
  import { Updater, PluginMeta, Logger } from '@morehao/dsh-plugin-kit/update';
4
4
 
@@ -112,6 +112,12 @@ interface RunOutcome {
112
112
  durationMs: number;
113
113
  coreVersion: string;
114
114
  }
115
+ declare function isSubscriptionStatus(value: unknown): value is SubscriptionStatus;
116
+ /**
117
+ * `SubscriptionDoc` 的运行时守卫:缺 fullName/volume/markdown 的负载宁可抛
118
+ * `IPC_INVALID`,也不要让界面拿到半份文档(正文缺失时渲染会崩,比报错更难查)。
119
+ */
120
+ declare function isSubscriptionDoc(value: unknown): value is SubscriptionDoc;
115
121
  declare class IpcError extends Error {
116
122
  readonly code: string;
117
123
  constructor(code: string, message: string);
@@ -139,24 +145,27 @@ interface ParseRunOptions {
139
145
  * 当成功写进状态。
140
146
  */
141
147
  declare function parseRunResult(text: string, options: ParseRunOptions): RunOutcome;
142
- /** 订阅命令的结果:list 带回 repos,add/remove 只看 ok/error。 */
148
+ /** 订阅命令的结果:list 带回 repos/items,releases 带回 doc,add/remove 只看 ok/error。 */
143
149
  interface SubscribeOutcome {
144
150
  ok: boolean;
145
151
  repos: string[];
152
+ /** list 的逐仓库明细(契约 0.4.0);旧核心不返回时为 undefined。 */
153
+ items?: SubscriptionItem[];
154
+ /** `action=releases` 读到的发版文档;其它 action 没有。 */
155
+ doc?: SubscriptionDoc;
146
156
  error?: RunError;
147
157
  }
148
158
  /** 订阅命令的一个动作(契约 `RunRequest.action`)。 */
149
- type SubscribeAction = 'list' | 'add' | 'remove';
159
+ type SubscribeAction = 'list' | 'add' | 'remove' | 'releases';
150
160
  /**
151
161
  * 解析核心 `subscribe --json` 的 stdout。
152
162
  *
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`:订阅结果读错会把失败当成功。
163
+ * 契约 0.4.0 起 payload 进了结果信封:`{command:"subscribe", subscriptions?, subscriptionDoc?}`。
164
+ * 这里**优先**读信封;旧核心(0.6.x 及以前)把 `list` 的结果逐行打到信封之前、信封里只有
165
+ * `ok/error`,因此保留逐行兜底——两种都能读,但绝不「猜字段」:
166
+ * - 信封不是 `ipcSchema=2` 的对象 → `IPC_INVALID`;
167
+ * - `subscriptions` 存在但形状不对 → `IPC_INVALID`(不是空列表);
168
+ * - `subscriptionDoc` 存在但不满足契约 → `IPC_INVALID`(界面不能拿到半份文档)。
160
169
  */
161
170
  declare function parseSubscribeResult(text: string): SubscribeOutcome;
162
171
 
@@ -178,16 +187,22 @@ declare const ipc_State: typeof State;
178
187
  declare const ipc_StateRun: typeof StateRun;
179
188
  type ipc_SubscribeAction = SubscribeAction;
180
189
  type ipc_SubscribeOutcome = SubscribeOutcome;
190
+ declare const ipc_SubscriptionDoc: typeof SubscriptionDoc;
191
+ declare const ipc_SubscriptionItem: typeof SubscriptionItem;
181
192
  declare const ipc_SubscriptionList: typeof SubscriptionList;
193
+ declare const ipc_SubscriptionStatus: typeof SubscriptionStatus;
194
+ declare const ipc_SubscriptionVolume: typeof SubscriptionVolume;
182
195
  declare const ipc_Usage: typeof Usage;
183
196
  type ipc_VersionInfo = VersionInfo;
197
+ declare const ipc_isSubscriptionDoc: typeof isSubscriptionDoc;
198
+ declare const ipc_isSubscriptionStatus: typeof isSubscriptionStatus;
184
199
  declare const ipc_isVersionInfo: typeof isVersionInfo;
185
200
  declare const ipc_parseJsonObject: typeof parseJsonObject;
186
201
  declare const ipc_parseRunResult: typeof parseRunResult;
187
202
  declare const ipc_parseSubscribeResult: typeof parseSubscribeResult;
188
203
  declare const ipc_parseVersionInfo: typeof parseVersionInfo;
189
204
  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 };
205
+ 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_SubscriptionDoc as SubscriptionDoc, ipc_SubscriptionItem as SubscriptionItem, ipc_SubscriptionList as SubscriptionList, ipc_SubscriptionStatus as SubscriptionStatus, 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
206
  }
192
207
 
193
208
  type Provider = string;
@@ -261,6 +276,18 @@ interface SummarizeSettings {
261
276
  enrichTimeoutSeconds: number;
262
277
  enrichBudgetSeconds: number;
263
278
  }
279
+ /**
280
+ * 每周发版运行的插件侧节奏(核心没有这个字段,**绝不**写进核心 YAML)。
281
+ * 与榜单调度(`schedule.*`)完全独立:两者各有自己的开关与时间点。
282
+ */
283
+ interface ReleaseScheduleSettings {
284
+ enabled: boolean;
285
+ /** ISO 星期:1=周一 … 7=周日(和 `Intl` 的 0=周日 不同,避免调用方来回换算)。 */
286
+ weekday: number;
287
+ /** `HH:MM[:SS]`(本地墙钟,按 timezone 解释)。 */
288
+ time: string;
289
+ timezone: string;
290
+ }
264
291
  /**
265
292
  * 发版订阅阶段的行为参数(核心 `releases.*`)。
266
293
  * 阶段**常开**(`releases.enabled` 恒为 true,不出现在设置里):有没有产出只由订阅列表决定,
@@ -273,10 +300,24 @@ interface ReleasesSettings {
273
300
  includeDrafts: boolean;
274
301
  fallbackToTags: boolean;
275
302
  baselineOnFirstRun: boolean;
303
+ /** 文档**列出**多少个版本(核心 0.7.0 起不再限制抓取条数,摘要覆盖全周)。 */
276
304
  perRepoLimit: number;
277
305
  maxNotesChars: number;
278
- /** 发版摘要周期,独立默认 daily,不与 trending.since 联动。 */
306
+ /** 每仓库每周一条中文摘要的字数上限(核心 `releases.summaryMaxChars`)。 */
307
+ summaryMaxChars: number;
308
+ /** 每仓库文档的行数预算,超了核心才开新卷(核心 `releases.docMaxLines`)。 */
309
+ docMaxLines: number;
310
+ /** 订阅检查并发;`auto` 由插件按订阅数解析成 1–8 的数字(核心只认数字)。 */
311
+ concurrency: 'auto' | number;
312
+ /** 订阅阶段的模型预算秒数;`auto` 由插件按订阅数解析(核心只认数字)。 */
313
+ budgetSeconds: 'auto' | number;
314
+ /**
315
+ * 发版摘要周期。核心 0.7.0 只实现 `weekly`(一个 ISO 周一次模型调用),
316
+ * 因此这里保留枚举类型只为让校验能明确告诉用户「daily/monthly 已不支持」。
317
+ */
279
318
  period: Period;
319
+ /** 插件侧每周触发节奏;核心配置里只用到它的 `timezone`(切周口径),不写整个对象。 */
320
+ schedule: ReleaseScheduleSettings;
280
321
  }
281
322
  interface ScheduleSettings {
282
323
  enabled: boolean;
@@ -385,7 +426,13 @@ interface CoreConfig {
385
426
  baselineOnFirstRun: boolean;
386
427
  perRepoLimit: number;
387
428
  maxNotesChars: number;
388
- period: string;
429
+ summaryMaxChars: number;
430
+ docMaxLines: number;
431
+ timezone: string;
432
+ weekStart: 'monday';
433
+ concurrency: number;
434
+ budgetSeconds: number;
435
+ period: 'weekly';
389
436
  };
390
437
  output: {
391
438
  dir: string;
@@ -397,8 +444,12 @@ interface CoreConfig {
397
444
  * 生成核心配置。`apiKey` 单独传入:它只能来自宿主凭据或用户当次输入,
398
445
  * 绝不来自 settings.json。`period` 是本次运行的标的周期;核心 `run --period`
399
446
  * 会用它覆盖 `trending.since`,这里同步写入以免旧核心/调试时口径不一致。
447
+ *
448
+ * `repoCount` 只用于把发版订阅的 `auto` 解析成数字(核心 0.7.0 的 YAML 只认整数):
449
+ * 并发 = clamp(4, ceil(n/10), 8),预算 = min(300, 30+3n)。显式数字永远优先。
450
+ * 不发版调度对象——`releases.schedule` 是纯插件概念,写进 YAML 核心会当成未知字段。
400
451
  */
401
- declare function toCoreConfig(settings: Settings, apiKey: string, baseURLOverride?: string, period?: Period): CoreConfig;
452
+ declare function toCoreConfig(settings: Settings, apiKey: string, baseURLOverride?: string, period?: Period, repoCount?: number): CoreConfig;
402
453
  /**
403
454
  * 极简 YAML 序列化:只覆盖「对象/数组/字符串/数字/布尔」。
404
455
  * 字符串一律用 JSON 双引号形式——JSON 字符串是合法 YAML 标量,且能避免
@@ -421,6 +472,7 @@ type config_PRSettings = PRSettings;
421
472
  type config_Period = Period;
422
473
  type config_Problem = Problem;
423
474
  type config_Provider = Provider;
475
+ type config_ReleaseScheduleSettings = ReleaseScheduleSettings;
424
476
  type config_ReleasesSettings = ReleasesSettings;
425
477
  type config_RepoSettings = RepoSettings;
426
478
  declare const config_SETTINGS_SECTIONS: typeof SETTINGS_SECTIONS;
@@ -441,7 +493,7 @@ declare const config_toCoreConfig: typeof toCoreConfig;
441
493
  declare const config_toYaml: typeof toYaml;
442
494
  declare const config_validateSettings: typeof validateSettings;
443
495
  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 };
496
+ 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
497
  }
446
498
 
447
499
  declare const PLUGIN_DIR_NAME = "github-trending";
@@ -548,6 +600,38 @@ interface PendingPr {
548
600
  /** 该 PR 对应的 digest 日期(YYYY-MM-DD);用于判断「同日重跑」是否真的会撞分支。 */
549
601
  date?: string;
550
602
  }
603
+ /**
604
+ * 一次发版订阅运行的记录(与榜单的 `RunRecord` 分开)。
605
+ *
606
+ * 为什么不复用 `history.json`:发版文档按周累积、一跑就是十几次 GitHub 请求 + 每周一次
607
+ * 模型调用,它的失败/限流节奏与榜单完全无关;混在一起会污染「今天榜单有没有产出」的判断。
608
+ * 计数语义由契约 `ReleasesReport` 定义:checked=检查过的仓库数,changed=真正写了文档的仓库数。
609
+ */
610
+ interface ReleaseRunRecord {
611
+ id: string;
612
+ /** ISO 时间(UTC)。 */
613
+ startedAt: string;
614
+ /** ISO 时间(UTC)。 */
615
+ finishedAt: string;
616
+ status: 'ok' | 'failed' | 'skipped' | 'cancelled';
617
+ checked: number;
618
+ changed: number;
619
+ newTags: number;
620
+ summarized: number;
621
+ degraded: number;
622
+ failed: number;
623
+ rateLimited: number;
624
+ durationMs: number;
625
+ tokens?: number;
626
+ commit?: string;
627
+ prUrl?: string;
628
+ branch?: string;
629
+ errorCode?: string;
630
+ errorMessage?: string;
631
+ coreVersion?: string;
632
+ }
633
+ /** 发版运行记录上限:一周一次的话正好留半年,够复盘又不会让 state.json 膨胀。 */
634
+ declare const RELEASE_RUN_LIMIT = 30;
551
635
  interface PluginState {
552
636
  lastRunAt?: string;
553
637
  /** 最近一次成功(ok=true,含 skipped)的运行时间;工具与界面读它。 */
@@ -557,6 +641,12 @@ interface PluginState {
557
641
  lastDigest?: DigestIdentity;
558
642
  lastResult?: RunRecord;
559
643
  pendingPr?: PendingPr;
644
+ /** 最近 30 次发版运行,最新在前;旧 state 里没有这个字段。 */
645
+ releaseRuns?: ReleaseRunRecord[];
646
+ /** 最近一次发版运行结束时间(失败也算)。 */
647
+ lastReleaseAt?: string;
648
+ /** 最近一次**成功**发版运行所属的 ISO 周(如 `2026-W44`),周去重键。 */
649
+ lastReleaseWeek?: string;
560
650
  }
561
651
  interface HistoryFile {
562
652
  records: RunRecord[];
@@ -597,6 +687,13 @@ declare class Store {
597
687
  private readHistoryRaw;
598
688
  /** 记录一次运行:历史裁剪到 HISTORY_LIMIT 条,state 同步更新。 */
599
689
  recordRun(record: RunRecord, next?: Partial<PluginState>): void;
690
+ /**
691
+ * 记录一次发版运行:只写 `state.json`(不发版记录进 `history.json`,两者节奏不同)。
692
+ * 记录裁剪到 RELEASE_RUN_LIMIT 条,`next` 用来顺带更新 lastReleaseAt/lastReleaseWeek。
693
+ */
694
+ appendReleaseRun(record: ReleaseRunRecord, next?: Partial<PluginState>): void;
695
+ /** 读发版运行记录;缺失/损坏(不是数组或行不是对象)一律按空数组处理,不让界面崩。 */
696
+ readReleaseRuns(): ReleaseRunRecord[];
600
697
  newLogPath(id: string): string;
601
698
  appendLog(file: string, line: string): void;
602
699
  hasLog(file: string): boolean;
@@ -631,6 +728,8 @@ declare const state_PREVIEW_MAX_BYTES: typeof PREVIEW_MAX_BYTES;
631
728
  type state_PendingPr = PendingPr;
632
729
  type state_PluginState = PluginState;
633
730
  type state_PreviewMeta = PreviewMeta;
731
+ declare const state_RELEASE_RUN_LIMIT: typeof RELEASE_RUN_LIMIT;
732
+ type state_ReleaseRunRecord = ReleaseRunRecord;
634
733
  type state_ReleasesSummary = ReleasesSummary;
635
734
  type state_RunRecord = RunRecord;
636
735
  type state_Store = Store;
@@ -640,7 +739,7 @@ declare const state_newRunId: typeof newRunId;
640
739
  declare const state_sumTokens: typeof sumTokens;
641
740
  declare const state_writeJsonAtomic: typeof writeJsonAtomic;
642
741
  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 };
742
+ 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
743
  }
645
744
 
646
745
  interface CoreRelease {
@@ -876,6 +975,13 @@ interface RunCoreOptions {
876
975
  trigger?: 'manual' | 'schedule';
877
976
  /** 回环代理地址:覆盖配置里的 model.baseURL(代理模式专用)。 */
878
977
  baseURLOverride?: string;
978
+ /**
979
+ * 只跑指定阶段(核心 `run --stages`)。缺省 = 跑所有已启用阶段;
980
+ * `['releases']` 就是「只跑发版订阅」——不抓榜单、不渲染 digest、也不合并订阅 pending。
981
+ */
982
+ stages?: string[];
983
+ /** 订阅仓库数:用于把配置里的 `auto` 并发/预算解析成数字(发版运行传入)。 */
984
+ repoCount?: number;
879
985
  signal?: AbortSignal;
880
986
  timeoutMs?: number;
881
987
  onStderr?: (line: string) => void;
@@ -892,6 +998,7 @@ declare function buildCoreArgs(channelPath: string, options: {
892
998
  dryRun: boolean;
893
999
  date?: string;
894
1000
  period?: Period;
1001
+ stages?: string[];
895
1002
  }): string[];
896
1003
  declare function runCore(options: RunCoreOptions): Promise<RunCoreResult>;
897
1004
 
@@ -1422,6 +1529,37 @@ declare class Runner {
1422
1529
  runMany(options: RunRequestOptions & {
1423
1530
  periods?: Period[];
1424
1531
  }): Promise<RunSummary[]>;
1532
+ /**
1533
+ * 只跑发版订阅阶段(`run --stages releases`)。
1534
+ *
1535
+ * 与榜单运行共用同一把 `active` 锁:两条路径都会写同一份产物、读同一份订阅,
1536
+ * 并发跑会互相踩分支。和 `run()` 的区别只有三点:不带 dryRun、写完只落
1537
+ * `state.releaseRuns`(不碰 history.json)、自己**不**合并订阅 pending 队列
1538
+ * (`--stages releases` 让核心跳过那一步,合并只在完整 run 里发生)。
1539
+ */
1540
+ runReleases(options: {
1541
+ trigger: 'manual' | 'schedule';
1542
+ }): Promise<ReleaseRunRecord>;
1543
+ /** 发版运行用哪一天:发版调度时区下的「今天」(不是榜单调度时区,两者可独立配置)。 */
1544
+ private releaseDate;
1545
+ /** 订阅仓库数(best-effort,只影响 auto 解析):任何失败都当 0,不阻塞发版运行。 */
1546
+ private subscriptionCount;
1547
+ /**
1548
+ * 解析模型钥匙并(代理模式下)起回环代理。
1549
+ *
1550
+ * 榜单运行与发版运行共用这一段:代理的一次性 token 语义(空 key 会让核心静默降级成
1551
+ * 英文/离线假模型)只写一次,避免两份实现漂移。
1552
+ */
1553
+ private openModelChannel;
1554
+ /** 把 `run --json` 的发版阶段结果投影成一条发版运行记录并落盘。 */
1555
+ private recordReleaseRun;
1556
+ /** 运行在拿到信封之前就失败(定位/握手/模型/代理/超时/取消):也要留一条可复盘记录。 */
1557
+ private recordReleaseFailure;
1558
+ /**
1559
+ * 落盘发版运行:只写 state(history.json 是榜单的),顺带推进 lastReleaseAt;
1560
+ * 只有 ok/skipped 才锁 `lastReleaseWeek`——失败不锁,调度下一 tick 才有机会重试。
1561
+ */
1562
+ private persistReleaseRun;
1425
1563
  private summarize;
1426
1564
  private failureSummary;
1427
1565
  /**
@@ -1453,6 +1591,11 @@ declare namespace runner {
1453
1591
 
1454
1592
  /** 订阅命令超时:核心只做一次 GitHub 请求/本地文件读写,8 s 足够。 */
1455
1593
  declare const SUBSCRIBE_TIMEOUT_MS = 8000;
1594
+ /**
1595
+ * `releases` 要真的读一份可能几百行的文档文件、并在缺失时回退到卷索引,
1596
+ * 比 list/add/remove 重得多;给它 30 s,避免大仓库文档被判成「无输出」。
1597
+ */
1598
+ declare const RELEASES_TIMEOUT_MS = 30000;
1456
1599
  declare function isValidRepo(repo: string): boolean;
1457
1600
  interface SubscribeDeps {
1458
1601
  store: Store;
@@ -1461,20 +1604,23 @@ interface SubscribeDeps {
1461
1604
  corePath?: string;
1462
1605
  }
1463
1606
  /**
1464
- * 执行一次 subscribe 命令:`action=list` 读列表,`add/remove` 改 pending 队列。
1465
- * 返回 `{ ok, repos, error? }`;非 ok 由调用方(路由)决定转成 `SUBSCRIBE_FAILED`。
1607
+ * 执行一次 subscribe 命令:`action=list` 读列表,`add/remove` 改 pending 队列,
1608
+ * `releases` 读某个仓库的发版文档。
1609
+ * 返回 `{ ok, repos, items?, doc?, error? }`;非 ok 由调用方(路由)决定转成 `SUBSCRIBE_FAILED`。
1466
1610
  */
1467
1611
  declare function runSubscribe(input: {
1468
1612
  action: SubscribeAction;
1469
1613
  repo?: string;
1614
+ volume?: number;
1470
1615
  }, deps: SubscribeDeps): Promise<SubscribeOutcome>;
1471
1616
 
1617
+ declare const subscribe_RELEASES_TIMEOUT_MS: typeof RELEASES_TIMEOUT_MS;
1472
1618
  declare const subscribe_SUBSCRIBE_TIMEOUT_MS: typeof SUBSCRIBE_TIMEOUT_MS;
1473
1619
  type subscribe_SubscribeDeps = SubscribeDeps;
1474
1620
  declare const subscribe_isValidRepo: typeof isValidRepo;
1475
1621
  declare const subscribe_runSubscribe: typeof runSubscribe;
1476
1622
  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 };
1623
+ 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
1624
  }
1479
1625
 
1480
1626
  interface PluginInfo {
@@ -1500,6 +1646,10 @@ interface ScheduleConfig {
1500
1646
  /** 自动产出的周期;每个周期只在「它自己的触发日」跑(日=每天、周=周日、月=月末)。 */
1501
1647
  periods: Period[];
1502
1648
  }
1649
+ /** 每周发版行程的开关/时间点(纯插件概念,核心没有对应字段)。 */
1650
+ type ReleaseScheduleConfig = ReleaseScheduleSettings;
1651
+ /** 发版行程的补跑窗口:超过 6 h 就放弃本周(下一周那次会同时覆盖两周的内容)。 */
1652
+ declare const RELEASE_CATCH_UP_MINUTES: number;
1503
1653
  /** 目标时区的墙上时间 → UTC 时刻(按 DST 修正一次,够用且可测)。 */
1504
1654
  declare function zonedTimeToUtc(year: number, month: number, day: number, hour: number, minute: number, second: number, timezone: string): Date;
1505
1655
  declare function parseClock(time: string): {
@@ -1513,6 +1663,15 @@ declare function zonedDateString(date: Date, timezone: string): string;
1513
1663
  declare function nextOccurrence(now: Date, schedule: ScheduleConfig): Date;
1514
1664
  /** 今天这一次是否已过点且在补跑窗口内。 */
1515
1665
  declare function isDue(now: Date, schedule: ScheduleConfig): boolean;
1666
+ /** 某时刻在 `timezone` 下的 ISO 周键;发版周去重就用它。 */
1667
+ declare function isoWeekKey(date: Date, timezone: string): string;
1668
+ /** 下一次发版运行时刻:ISO 星期匹配 `cfg.weekday`、当天的墙钟 `cfg.time`,严格晚于 now。 */
1669
+ declare function nextReleaseOccurrence(now: Date, cfg: ReleaseScheduleConfig): Date;
1670
+ /**
1671
+ * 发版行程此刻是否该跑:星期匹配、已过点、且迟到不超过 6 h。
1672
+ * 超过窗口就**放弃本周**——下周那次会同时覆盖两周的内容(核心按周累积,不需要补跑)。
1673
+ */
1674
+ declare function isReleaseDue(now: Date, cfg: ReleaseScheduleConfig): boolean;
1516
1675
  declare function daysInMonth(year: number, month: number): number;
1517
1676
  /** 该时刻是否落在某个周期的结算日(按 timezone 的本地日历)。 */
1518
1677
  declare function isPeriodTriggerDay(period: Period, now: Date, timezone: string): boolean;
@@ -1521,6 +1680,8 @@ declare function nextOccurrenceFor(period: Period, now: Date, schedule: Schedule
1521
1680
  interface SchedulerDeps {
1522
1681
  store: Store;
1523
1682
  run: (trigger: 'schedule', period: Period) => Promise<unknown>;
1683
+ /** 每周发版行程:与榜单 `run` 共享 Runner 的单跑锁(Runner 自己会拒绝并发)。 */
1684
+ runReleases: (trigger: 'schedule') => Promise<unknown>;
1524
1685
  isRunning: () => boolean;
1525
1686
  getTimer: () => {
1526
1687
  interval?: (fn: () => void, ms: number) => unknown;
@@ -1534,16 +1695,39 @@ declare class Scheduler {
1534
1695
  private stopped;
1535
1696
  /** 当天已尝试次数与时间;只在内存里,进程重启后重新计数。 */
1536
1697
  private readonly attempts;
1698
+ /** 发版行程的尝试计数(键 = ISO 周 + 本地日期);理由同榜单:失败不能每分钟重试。 */
1699
+ private readonly releaseAttempts;
1537
1700
  constructor(deps: SchedulerDeps);
1538
1701
  start(): void;
1539
1702
  stop(): void;
1540
- /** 公开给测试:判断此刻是否该跑,并按需触发一次。 */
1703
+ /**
1704
+ * 公开给测试:判断此刻是否该跑,并按需触发。
1705
+ *
1706
+ * 每个 tick 都**同时**评估榜单与发版两条行程(interval 已经是 60 s)。返回值合并规则
1707
+ * 保证老调用方不受影响:真的跑了 → `ran`;否则有人占着锁 → `busy`;再否则只要有
1708
+ * 一条行程是禁用状态就沿用旧的 `disabled`;都没有 → `skipped`。
1709
+ */
1541
1710
  tick(now?: Date): Promise<'skipped' | 'ran' | 'disabled' | 'busy'>;
1711
+ /** 榜单行程(原 tick 的逻辑,不改)。 */
1712
+ private tickDigest;
1713
+ /**
1714
+ * 发版行程:每周一次、只跑 `--stages releases`。
1715
+ *
1716
+ * 跳过条件(全部满足才跑):开启、星期/时间命中且迟到 ≤ 6 h、本周尚未成功过
1717
+ * (`state.lastReleaseWeek`)、当前没有别的运行占锁。
1718
+ * 失败**不锁本周**,但也只有 2 次机会(间隔 5 min)——否则一个坏 token 会在 6 h 的
1719
+ * 补跑窗口里每分钟 spawn 一次核心、白白打 GitHub API。
1720
+ */
1721
+ private tickReleases;
1542
1722
  /** 该周期今天是否已经成功过(含无变更跳过)。 */
1543
1723
  private succeededToday;
1544
1724
  private readSchedule;
1725
+ /** 发版行程的节奏来自 `settings.releases.schedule`(与榜单 `schedule.*` 相互独立)。 */
1726
+ private readReleaseSchedule;
1545
1727
  /** 下一次运行(取所有已启用周期里最近的一次;禁用时为 undefined)。 */
1546
1728
  next(): Date | undefined;
1729
+ /** 下一次发版运行(禁用/非法时为 undefined)。 */
1730
+ nextRelease(now?: Date): Date | undefined;
1547
1731
  describe(state: PluginState, now?: Date): {
1548
1732
  enabled: boolean;
1549
1733
  time: string;
@@ -1553,9 +1737,19 @@ declare class Scheduler {
1553
1737
  lastRunDate?: string;
1554
1738
  catchUpMinutes: number;
1555
1739
  source: 'timer';
1740
+ releaseEnabled: boolean;
1741
+ releaseWeekday: number;
1742
+ releaseTime: string;
1743
+ releaseTimezone: string;
1744
+ nextReleaseAt?: string;
1745
+ lastReleaseAt?: string;
1746
+ lastReleaseWeek?: string;
1747
+ releaseCatchUpMinutes: number;
1556
1748
  };
1557
1749
  }
1558
1750
 
1751
+ declare const schedule_RELEASE_CATCH_UP_MINUTES: typeof RELEASE_CATCH_UP_MINUTES;
1752
+ type schedule_ReleaseScheduleConfig = ReleaseScheduleConfig;
1559
1753
  type schedule_ScheduleConfig = ScheduleConfig;
1560
1754
  type schedule_Scheduler = Scheduler;
1561
1755
  declare const schedule_Scheduler: typeof Scheduler;
@@ -1563,13 +1757,16 @@ type schedule_SchedulerDeps = SchedulerDeps;
1563
1757
  declare const schedule_daysInMonth: typeof daysInMonth;
1564
1758
  declare const schedule_isDue: typeof isDue;
1565
1759
  declare const schedule_isPeriodTriggerDay: typeof isPeriodTriggerDay;
1760
+ declare const schedule_isReleaseDue: typeof isReleaseDue;
1761
+ declare const schedule_isoWeekKey: typeof isoWeekKey;
1566
1762
  declare const schedule_nextOccurrence: typeof nextOccurrence;
1567
1763
  declare const schedule_nextOccurrenceFor: typeof nextOccurrenceFor;
1764
+ declare const schedule_nextReleaseOccurrence: typeof nextReleaseOccurrence;
1568
1765
  declare const schedule_parseClock: typeof parseClock;
1569
1766
  declare const schedule_zonedDateString: typeof zonedDateString;
1570
1767
  declare const schedule_zonedTimeToUtc: typeof zonedTimeToUtc;
1571
1768
  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 };
1769
+ 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
1770
  }
1574
1771
 
1575
1772
  /**
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.1",
4
4
  "description": "GitHub 趋势日报插件:每天定时 spawn 本地 trendradar 核心,抓取 Trending、生成中文简介与总结,提交到你的仓库并自动合并 PR。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@morehao/dsh-plugin-kit": "0.1.0",
54
- "@morehao/trendradar-contract": "0.3.0"
54
+ "@morehao/trendradar-contract": "0.4.0"
55
55
  },
56
56
  "dsh": {
57
57
  "bundle": {
@@ -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",