@morehao/dsh-github-trending 0.1.4 → 0.3.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.
@@ -10,9 +10,12 @@ __export(config_exports, {
10
10
  CORE_PROVIDERS: () => CORE_PROVIDERS,
11
11
  DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
12
12
  PROVIDER_BASE_URLS: () => PROVIDER_BASE_URLS,
13
+ SETTINGS_SECTIONS: () => SETTINGS_SECTIONS,
13
14
  effectiveBaseURL: () => effectiveBaseURL,
14
15
  isValidTimezone: () => isValidTimezone,
16
+ mergeSettingsSection: () => mergeSettingsSection,
15
17
  normalizeSettings: () => normalizeSettings,
18
+ sectionOfField: () => sectionOfField,
16
19
  toCoreConfig: () => toCoreConfig,
17
20
  toYaml: () => toYaml,
18
21
  validateSettings: () => validateSettings
@@ -44,7 +47,7 @@ var DEFAULT_SETTINGS = {
44
47
  autoMerge: true,
45
48
  labels: []
46
49
  },
47
- github: { authMode: "device", tokenRef: "GITHUB_TOKEN" },
50
+ github: { authMode: "gh", tokenRef: "GITHUB_TOKEN" },
48
51
  trending: { since: "daily", languages: ["all", "typescript", "python", "go", "rust"], topN: 25 },
49
52
  output: { dir: "trending" },
50
53
  schedule: { enabled: true, time: "08:30:00", timezone: "Asia/Shanghai" },
@@ -57,6 +60,43 @@ var PROVIDER_BASE_URLS = {
57
60
  anthropic: ""
58
61
  };
59
62
  var CORE_PROVIDERS = ["deepseek", "openai-compatible"];
63
+ var SETTINGS_SECTIONS = ["source", "model", "repo", "auth", "scope", "schedule"];
64
+ function sectionOfField(field) {
65
+ if (field.startsWith("model.")) return "model";
66
+ if (field.startsWith("repo.") || field.startsWith("output.") || field.startsWith("pr.")) return "repo";
67
+ if (field.startsWith("github.")) return "auth";
68
+ if (field.startsWith("trending.")) return "scope";
69
+ if (field.startsWith("schedule.")) return "schedule";
70
+ if (field === "corePath" || field === "coreAutoDownload") return "source";
71
+ return void 0;
72
+ }
73
+ function mergeSettingsSection(stored, incoming, section) {
74
+ const next = JSON.parse(JSON.stringify(stored));
75
+ switch (section) {
76
+ case "source":
77
+ next.corePath = incoming.corePath;
78
+ next.coreAutoDownload = incoming.coreAutoDownload;
79
+ break;
80
+ case "model":
81
+ next.model = incoming.model;
82
+ break;
83
+ case "repo":
84
+ next.repo = incoming.repo;
85
+ next.pr = incoming.pr;
86
+ next.output = incoming.output;
87
+ break;
88
+ case "auth":
89
+ next.github = incoming.github;
90
+ break;
91
+ case "scope":
92
+ next.trending = incoming.trending;
93
+ break;
94
+ case "schedule":
95
+ next.schedule = incoming.schedule;
96
+ break;
97
+ }
98
+ return next;
99
+ }
60
100
  function str(value, fallback) {
61
101
  return typeof value === "string" ? value : fallback;
62
102
  }
@@ -110,7 +150,8 @@ function normalizeSettings(input) {
110
150
  labels: strArray(pr.labels, DEFAULT_SETTINGS.pr.labels)
111
151
  },
112
152
  github: {
113
- authMode: authMode === "pat" || authMode === "gh" ? authMode : "device",
153
+ // 旧版本可能存 'device':它没有对应实现,统一迁移到真正可用的「本机 gh」。
154
+ authMode: authMode === "pat" ? "pat" : "gh",
114
155
  tokenRef: str(github.tokenRef, DEFAULT_SETTINGS.github.tokenRef).trim() || DEFAULT_SETTINGS.github.tokenRef
115
156
  },
116
157
  trending: {
@@ -155,11 +196,17 @@ function validateSettings(settings, providers = CORE_PROVIDERS) {
155
196
  if (model.model.trim() === "") {
156
197
  problems.push({ field: "model.model", code: "CONFIG_INVALID", message: "\u5FC5\u987B\u586B\u6A21\u578B ID\uFF08\u63D2\u4EF6\u4E0D\u8BBE\u9690\u5F0F\u9ED8\u8BA4\u6A21\u578B\uFF09" });
157
198
  }
158
- const baseURL = model.baseURL || PROVIDER_BASE_URLS[model.provider] || "";
159
- if (baseURL === "") {
160
- problems.push({ field: "model.baseURL", code: "CONFIG_INVALID", message: "\u8BE5 provider \u9700\u8981\u663E\u5F0F baseURL" });
161
- } else if (!/^https?:\/\//.test(baseURL)) {
162
- problems.push({ field: "model.baseURL", code: "CONFIG_INVALID", message: "baseURL \u5FC5\u987B\u662F http(s) \u5730\u5740" });
199
+ if (model.mode === "proxy") {
200
+ if (model.baseURL !== "" && !/^https?:\/\//.test(model.baseURL)) {
201
+ problems.push({ field: "model.baseURL", code: "CONFIG_INVALID", message: "baseURL \u5FC5\u987B\u662F http(s) \u5730\u5740" });
202
+ }
203
+ } else {
204
+ const baseURL = model.baseURL || PROVIDER_BASE_URLS[model.provider] || "";
205
+ if (baseURL === "") {
206
+ problems.push({ field: "model.baseURL", code: "CONFIG_INVALID", message: "\u8BE5 provider \u9700\u8981\u663E\u5F0F baseURL" });
207
+ } else if (!/^https?:\/\//.test(baseURL)) {
208
+ problems.push({ field: "model.baseURL", code: "CONFIG_INVALID", message: "baseURL \u5FC5\u987B\u662F http(s) \u5730\u5740" });
209
+ }
163
210
  }
164
211
  if (model.temperature < 0 || model.temperature > 2) {
165
212
  problems.push({ field: "model.temperature", code: "CONFIG_INVALID", message: "temperature \u5FC5\u987B\u5728 0\u20132 \u4E4B\u95F4" });
@@ -291,14 +338,21 @@ var HISTORY_FILE = "history.json";
291
338
  // src/state.ts
292
339
  var state_exports = {};
293
340
  __export(state_exports, {
341
+ CONTENT_HASH_ALGO_V2: () => CONTENT_HASH_ALGO_V2,
294
342
  HISTORY_LIMIT: () => HISTORY_LIMIT,
343
+ PREVIEW_LIMIT: () => PREVIEW_LIMIT,
344
+ PREVIEW_MAX_BYTES: () => PREVIEW_MAX_BYTES,
295
345
  Store: () => Store,
296
346
  newRunId: () => newRunId,
297
347
  writeJsonAtomic: () => writeJsonAtomic
298
348
  });
299
- import { mkdirSync, readFileSync, renameSync, writeFileSync, appendFileSync, existsSync } from "fs";
349
+ import { mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, appendFileSync, existsSync } from "fs";
300
350
  import path3 from "path";
301
351
  var HISTORY_LIMIT = 30;
352
+ var PREVIEW_LIMIT = 30;
353
+ var PREVIEW_MAX_BYTES = 512 * 1024;
354
+ var CONTENT_HASH_ALGO_V2 = "jcs-sha256-v1";
355
+ var V2_CORE_MIN = [0, 3, 0];
302
356
  function readJson(file, fallback) {
303
357
  try {
304
358
  const text = readFileSync(file, "utf8");
@@ -312,6 +366,32 @@ function readJson(file, fallback) {
312
366
  function ensureDir(dir) {
313
367
  mkdirSync(dir, { recursive: true, mode: 448 });
314
368
  }
369
+ function coreVersionIsV2(coreVersion) {
370
+ if (typeof coreVersion !== "string") return false;
371
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(coreVersion.trim());
372
+ if (match === null) return false;
373
+ const major = Number(match[1]);
374
+ const minor = Number(match[2]);
375
+ const patch = Number(match[3]);
376
+ const [minMajor, minMinor, minPatch] = V2_CORE_MIN;
377
+ if (major !== minMajor) return major > minMajor;
378
+ if (minor !== minMinor) return minor > minMinor;
379
+ return patch >= minPatch;
380
+ }
381
+ function digestIsV2(digest) {
382
+ return digest !== void 0 && digest.contentHashAlgo === CONTENT_HASH_ALGO_V2 && coreVersionIsV2(digest.coreVersion);
383
+ }
384
+ function normalizeState(state) {
385
+ const next = { ...state };
386
+ if (next.lastDigest !== void 0 && !digestIsV2(next.lastDigest)) {
387
+ delete next.lastDigest;
388
+ delete next.pendingPr;
389
+ }
390
+ if (next.lastResult !== void 0 && next.lastResult.legacy !== true && !coreVersionIsV2(next.lastResult.coreVersion)) {
391
+ next.lastResult = { ...next.lastResult, legacy: true };
392
+ }
393
+ return next;
394
+ }
315
395
  function writeJsonAtomic(file, value) {
316
396
  ensureDir(path3.dirname(file));
317
397
  const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
@@ -336,7 +416,10 @@ var Store = class {
336
416
  return path3.join(this.dir, HISTORY_FILE);
337
417
  }
338
418
  get logsPath() {
339
- return runsDir(this.env);
419
+ return path3.join(this.dir, "runs");
420
+ }
421
+ get previewsPath() {
422
+ return path3.join(this.dir, "previews");
340
423
  }
341
424
  readSettings() {
342
425
  return normalizeSettings(readJson(this.settingsPath, {}));
@@ -345,18 +428,49 @@ var Store = class {
345
428
  writeJsonAtomic(this.settingsPath, settings);
346
429
  }
347
430
  readState() {
348
- return readJson(this.statePath, {});
431
+ return normalizeState(readJson(this.statePath, {}));
349
432
  }
350
433
  writeState(state) {
351
434
  writeJsonAtomic(this.statePath, state);
352
435
  }
436
+ /**
437
+ * 把 v1 时代遗留的 state / history 一次性清洗落盘:清掉不可信的 lastDigest 与
438
+ * 与之绑定的 pendingPr,并给旧历史记录打 legacy 标。插件启动时调用一次。
439
+ */
440
+ migrateLegacyState() {
441
+ const rawState = readJson(this.statePath, {});
442
+ const nextState = normalizeState(rawState);
443
+ const stateChanged = JSON.stringify(nextState) !== JSON.stringify(rawState);
444
+ if (stateChanged) this.writeState(nextState);
445
+ const rawHistory = readJson(this.historyPath, { records: [] });
446
+ const records = Array.isArray(rawHistory.records) ? rawHistory.records : [];
447
+ let historyMarked = 0;
448
+ const nextRecords = records.map((record) => {
449
+ if (record.legacy === true || coreVersionIsV2(record.coreVersion)) return record;
450
+ historyMarked += 1;
451
+ return { ...record, legacy: true };
452
+ });
453
+ if (historyMarked > 0) writeJsonAtomic(this.historyPath, { records: nextRecords });
454
+ return {
455
+ stateChanged,
456
+ clearedDigest: rawState.lastDigest !== void 0 && nextState.lastDigest === void 0,
457
+ clearedPendingPr: rawState.pendingPr !== void 0 && nextState.pendingPr === void 0,
458
+ historyMarked
459
+ };
460
+ }
353
461
  readHistory() {
462
+ return this.readHistoryRaw().map(
463
+ (record) => record.legacy === true || coreVersionIsV2(record.coreVersion) ? record : { ...record, legacy: true }
464
+ );
465
+ }
466
+ /** 原始历史(不做 legacy 标注):写入路径用它,避免把「读时标注」持久化。 */
467
+ readHistoryRaw() {
354
468
  const file = readJson(this.historyPath, { records: [] });
355
469
  return Array.isArray(file.records) ? file.records : [];
356
470
  }
357
471
  /** 记录一次运行:历史裁剪到 30 条,state 同步更新。 */
358
472
  recordRun(record, next = {}) {
359
- const records = [record, ...this.readHistory()].slice(0, HISTORY_LIMIT);
473
+ const records = [record, ...this.readHistoryRaw()].slice(0, HISTORY_LIMIT);
360
474
  writeJsonAtomic(this.historyPath, { records });
361
475
  const state = this.readState();
362
476
  this.writeState({ ...state, ...next, lastResult: record });
@@ -381,6 +495,61 @@ var Store = class {
381
495
  return "";
382
496
  }
383
497
  }
498
+ /** 预览副本路径;写入前才建目录,读接口不会凭空创建目录。 */
499
+ previewPath(id) {
500
+ return path3.join(this.previewsPath, `${id}.md`);
501
+ }
502
+ /** 写预览副本:同目录临时文件 + rename、0600;写完裁到 30 份。 */
503
+ writePreview(id, text) {
504
+ ensureDir(this.previewsPath);
505
+ const file = this.previewPath(id);
506
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
507
+ writeFileSync(temp, text, { mode: 384 });
508
+ renameSync(temp, file);
509
+ this.prunePreviews();
510
+ }
511
+ /** 读预览副本;不存在/不可读返回 undefined,超过上限时截断。 */
512
+ readPreview(id, maxBytes = PREVIEW_MAX_BYTES) {
513
+ const file = this.previewPath(id);
514
+ try {
515
+ const stat2 = statSync(file);
516
+ if (!stat2.isFile()) return void 0;
517
+ const truncated = stat2.size > maxBytes;
518
+ const buffer = readFileSync(file);
519
+ return {
520
+ text: (truncated ? buffer.subarray(0, maxBytes) : buffer).toString("utf8"),
521
+ truncated
522
+ };
523
+ } catch {
524
+ return void 0;
525
+ }
526
+ }
527
+ /** 只保留最近 `keep` 份预览副本;按 mtime 倒序,任何错误都忽略。 */
528
+ prunePreviews(keep = PREVIEW_LIMIT) {
529
+ let names;
530
+ try {
531
+ names = readdirSync(this.previewsPath);
532
+ } catch {
533
+ return;
534
+ }
535
+ const entries = [];
536
+ for (const name of names) {
537
+ if (!name.endsWith(".md")) continue;
538
+ const file = path3.join(this.previewsPath, name);
539
+ try {
540
+ const stat2 = statSync(file);
541
+ if (stat2.isFile()) entries.push({ file, mtimeMs: stat2.mtimeMs });
542
+ } catch {
543
+ }
544
+ }
545
+ entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
546
+ for (const entry of entries.slice(keep)) {
547
+ try {
548
+ unlinkSync(entry.file);
549
+ } catch {
550
+ }
551
+ }
552
+ }
384
553
  };
385
554
  function newRunId(now = /* @__PURE__ */ new Date()) {
386
555
  const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
@@ -396,6 +565,7 @@ __export(errors_exports, {
396
565
  classifyStderr: () => classifyStderr,
397
566
  hintOf: () => hintOf,
398
567
  isRetriable: () => isRetriable,
568
+ reasonOf: () => reasonOf,
399
569
  redact: () => redact
400
570
  });
401
571
  var PLUGIN_CODES = [
@@ -458,7 +628,38 @@ function areaOf(code) {
458
628
  if (code === "GITHUB_AUTH") return "auth";
459
629
  return "run";
460
630
  }
461
- function hintOf(code) {
631
+ function reasonOf(code, message, details) {
632
+ const structured = typeof details?.reason === "string" && details.reason !== "" ? details.reason : void 0;
633
+ if (structured !== void 0) return structured;
634
+ if (code !== "GIT_OPERATION_FAILED" || typeof message !== "string") return void 0;
635
+ if (/stale info/i.test(message)) return "stale-lease";
636
+ if (/non-fast-forward|fetch first/i.test(message)) return "non-fast-forward";
637
+ if (/permission denied|authentication failed|could not read Username|invalid username or password/i.test(message)) return "auth";
638
+ if (/protected branch|GH006/i.test(message)) return "protected";
639
+ if (/hook declined|pre-receive hook/i.test(message)) return "hook";
640
+ if (/pathspec/i.test(message)) return "pathspec";
641
+ return "unknown";
642
+ }
643
+ function hintOf(code, message, reason) {
644
+ const gitReason = reason ?? reasonOf(code, message);
645
+ if (code === "GIT_OPERATION_FAILED" && gitReason !== void 0) {
646
+ switch (gitReason) {
647
+ case "stale-lease":
648
+ return "\u63A8\u9001\u88AB\u62D2\uFF08stale info\uFF09\uFF1A\u672C\u5730\u5BF9\u8FDC\u7AEF\u5206\u652F\u7684\u8BB0\u5F55\u8FC7\u671F\uFF08\u5206\u652F\u88AB\u5220\u6216\u88AB\u6539\uFF09\uFF0C\u4E0D\u662F\u6709\u4EBA\u62A2\u5148\u63A8\u9001\u3002\u5728\u4ED3\u5E93\u91CC\u6267\u884C git fetch --prune origin \u540E\u91CD\u8BD5\uFF1B\u5347\u7EA7\u6838\u5FC3\u540E\u4E0D\u4F1A\u518D\u51FA\u73B0\u3002";
649
+ case "non-fast-forward":
650
+ return "\u63A8\u9001\u88AB\u62D2\uFF08non-fast-forward\uFF09\uFF1A\u8FDC\u7AEF\u5206\u652F\u5DF2\u524D\u8FDB\u3002\u5148 git fetch \u5E76 rebase \u5230\u8FDC\u7AEF\u5206\u652F\uFF0C\u6216\u5220\u9664\u8BE5\u8FDC\u7AEF\u5206\u652F\u540E\u91CD\u8BD5\u3002";
651
+ case "auth":
652
+ return "\u63A8\u9001\u8BA4\u8BC1\u5931\u8D25\uFF1A\u66F4\u65B0 GitHub token / gh \u767B\u5F55\u540E\u91CD\u8BD5\uFF1B\u672C\u5730 commit \u5DF2\u4FDD\u7559\u3002";
653
+ case "protected":
654
+ return "\u76EE\u6807\u5206\u652F\u53D7\u4FDD\u62A4\uFF1A\u6539\u4E3A\u901A\u8FC7 PR \u63D0\u4EA4\uFF0C\u6216\u8C03\u6574\u5206\u652F\u4FDD\u62A4\u89C4\u5219\u3002";
655
+ case "hook":
656
+ return "\u4ED3\u5E93 hook \u62D2\u7EDD\u4E86\u64CD\u4F5C\uFF1A\u6309 hook \u8F93\u51FA\u4FEE\u6B63\u540E\u91CD\u8BD5\uFF1B\u672C\u5730 commit \u5DF2\u4FDD\u7559\u3002";
657
+ case "pathspec":
658
+ return "\u6682\u5B58\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A\u68C0\u67E5\u8BBE\u7F6E\u91CC\u7684\u8F93\u51FA\u76EE\u5F55\uFF08output.dir\uFF09\u662F\u5426\u4E0E\u4ED3\u5E93\u5B9E\u9645\u76EE\u5F55\u4E00\u81F4\u3002";
659
+ default:
660
+ return "\u63A8\u9001\u5931\u8D25\uFF1A\u672C\u5730 commit \u5DF2\u4FDD\u7559\uFF0C\u67E5\u770B\u8FD0\u884C\u65E5\u5FD7\u91CC\u7684 git \u8F93\u51FA\u540E\u91CD\u8BD5\u3002";
661
+ }
662
+ }
462
663
  switch (code) {
463
664
  case "CORE_UNAVAILABLE":
464
665
  return "\u6838\u5FC3\u4E8C\u8FDB\u5236\u6CA1\u627E\u5230\uFF1A\u786E\u8BA4\u63D2\u4EF6\u5B89\u88C5\u5B8C\u6574\uFF08\u542B\u5E73\u53F0\u5305\uFF09\uFF0C\u6216\u7528 TRENDING_DIGEST_CORE \u6307\u5411\u4E00\u4E2A\u53EF\u6267\u884C\u7684 trendradar\u3002";
@@ -547,7 +748,8 @@ function classifyStderr(stderr) {
547
748
  const match = explicit.exec(line);
548
749
  if (match !== null && CORE_CODES.has(match[1] ?? "")) {
549
750
  const code = match[1];
550
- return { code, message: (match[2] ?? "").trim() || line, retriable: isRetriable(code), inferred: false };
751
+ const detail = [(match[2] ?? "").trim(), ...lines.slice(index + 1)].filter((part) => part !== "").join("\n").slice(0, 2e3);
752
+ return { code, message: detail || line, retriable: isRetriable(code), inferred: false };
551
753
  }
552
754
  }
553
755
  const message = lines[lines.length - 1] ?? "\u6838\u5FC3\u672A\u8F93\u51FA\u9519\u8BEF\u4FE1\u606F";
@@ -585,18 +787,17 @@ var ipc_exports = {};
585
787
  __export(ipc_exports, {
586
788
  IPC_SCHEMA_VERSION: () => IPC_SCHEMA_VERSION,
587
789
  IpcError: () => IpcError,
790
+ SUPPORTED_CONTRACT_MAJOR: () => SUPPORTED_CONTRACT_MAJOR,
588
791
  SUPPORTED_DIGEST_SCHEMA: () => SUPPORTED_DIGEST_SCHEMA,
589
792
  isVersionInfo: () => isVersionInfo,
590
793
  parseJsonObject: () => parseJsonObject,
591
794
  parseRunResult: () => parseRunResult,
592
795
  parseVersionInfo: () => parseVersionInfo
593
796
  });
594
-
595
- // src/ipc.gen.ts
596
- var IPC_SCHEMA_VERSION = 1;
597
-
598
- // src/ipc.ts
599
- var SUPPORTED_DIGEST_SCHEMA = 1;
797
+ import { validators } from "@morehao/trendradar-contract";
798
+ var IPC_SCHEMA_VERSION = 2;
799
+ var SUPPORTED_DIGEST_SCHEMA = 2;
800
+ var SUPPORTED_CONTRACT_MAJOR = 0;
600
801
  var IpcError = class extends Error {
601
802
  code;
602
803
  constructor(code, message) {
@@ -632,19 +833,6 @@ function parseVersionInfo(text) {
632
833
  }
633
834
  return value;
634
835
  }
635
- function normalizeFiles(value) {
636
- if (!Array.isArray(value)) return [];
637
- const files = [];
638
- for (const item of value) {
639
- if (!isRecord(item) || typeof item.path !== "string") continue;
640
- files.push({
641
- path: item.path,
642
- bytes: typeof item.bytes === "number" ? item.bytes : 0,
643
- sha256: typeof item.sha256 === "string" ? item.sha256 : ""
644
- });
645
- }
646
- return files;
647
- }
648
836
  function normalizeError(value) {
649
837
  if (!isRecord(value)) return void 0;
650
838
  if (typeof value.code !== "string" || typeof value.message !== "string") return void 0;
@@ -657,37 +845,24 @@ function normalizeError(value) {
657
845
  }
658
846
  function parseRunResult(text, options) {
659
847
  const value = parseJsonObject(text);
660
- const contractShaped = value.ipcSchema === IPC_SCHEMA_VERSION && typeof value.ok === "boolean" && value.command === "run";
661
- if (contractShaped) {
662
- const result = value;
663
- return {
664
- ok: result.ok,
665
- skipped: result.skipped === true,
666
- dryRun: options.dryRun,
667
- files: normalizeFiles(result.files),
668
- contentHash: typeof value.contentHash === "string" ? value.contentHash : void 0,
669
- branch: typeof value.branch === "string" ? value.branch : void 0,
670
- commit: typeof value.commit === "string" ? value.commit : void 0,
671
- prUrl: typeof value.prUrl === "string" ? value.prUrl : void 0,
672
- error: normalizeError(result.error),
673
- warnings: Array.isArray(result.warnings) ? result.warnings.filter((item) => typeof item === "string") : [],
674
- ...timing(options),
675
- coreVersion: options.coreVersion
676
- };
848
+ if (value.command !== "run" || value.ipcSchema !== IPC_SCHEMA_VERSION || !validators.runResult(value)) {
849
+ throw new IpcError("IPC_INVALID", "\u6838\u5FC3 run --json \u4E0D\u662F\u5951\u7EA6 RunResult\uFF08ipcSchema=2 / command=run\uFF09");
677
850
  }
678
- if (!Array.isArray(value.files)) {
679
- throw new IpcError("IPC_INVALID", "\u6838\u5FC3 run --json \u65E2\u4E0D\u662F\u5951\u7EA6 RunResult\uFF0C\u4E5F\u4E0D\u542B files \u6570\u7EC4");
680
- }
681
- const digest = isRecord(value.digest) ? value.digest : void 0;
851
+ const result = value;
852
+ const error = normalizeError(result.error);
853
+ const skipReason = value.skipReason;
682
854
  return {
683
- ok: true,
684
- skipped: value.skipped === true,
855
+ ok: result.ok,
856
+ skipped: result.skipped === true,
685
857
  dryRun: options.dryRun,
686
- files: normalizeFiles(value.files),
687
- contentHash: typeof digest?.contentHash === "string" ? digest.contentHash : void 0,
688
- branch: typeof value.branch === "string" ? value.branch : void 0,
689
- prUrl: typeof value.prUrl === "string" ? value.prUrl : void 0,
690
- warnings: [],
858
+ files: Array.isArray(result.files) ? result.files : [],
859
+ contentHash: typeof result.contentHash === "string" ? result.contentHash : void 0,
860
+ branch: typeof result.branch === "string" ? result.branch : void 0,
861
+ commit: typeof result.commit === "string" ? result.commit : void 0,
862
+ prUrl: typeof result.prUrl === "string" ? result.prUrl : void 0,
863
+ skipReason: typeof skipReason === "string" && skipReason !== "" ? skipReason : void 0,
864
+ error,
865
+ warnings: Array.isArray(result.warnings) ? result.warnings.filter((item) => typeof item === "string") : [],
691
866
  ...timing(options),
692
867
  coreVersion: options.coreVersion
693
868
  };
@@ -731,10 +906,11 @@ async function handshake(core, options = {}) {
731
906
  const detail = error instanceof Error ? error.message : String(error);
732
907
  throw new IpcError("CORE_INCOMPATIBLE", `\u6838\u5FC3 version --json \u8F93\u51FA\u65E0\u6CD5\u8BC6\u522B\uFF1A${detail}`);
733
908
  }
734
- if (version.ipcSchema !== IPC_SCHEMA_VERSION || version.digestSchema !== SUPPORTED_DIGEST_SCHEMA) {
909
+ const contractMajor = typeof version.contractVersion === "string" ? Number(version.contractVersion.split(".")[0]) : void 0;
910
+ if (version.ipcSchema !== IPC_SCHEMA_VERSION || version.digestSchema !== SUPPORTED_DIGEST_SCHEMA || contractMajor !== SUPPORTED_CONTRACT_MAJOR) {
735
911
  throw new IpcError(
736
912
  "CORE_INCOMPATIBLE",
737
- `\u6838\u5FC3\u534F\u8BAE\u4E0D\u5339\u914D\uFF1A\u6838\u5FC3 ipcSchema=${version.ipcSchema} digestSchema=${version.digestSchema}\uFF0C\u63D2\u4EF6\u652F\u6301 ipcSchema=${IPC_SCHEMA_VERSION} digestSchema=${SUPPORTED_DIGEST_SCHEMA}\u3002\u8BF7\u628A\u63D2\u4EF6\u4E0E\u6838\u5FC3\u5347\u7EA7\u5230\u540C\u4E00\u7248\u672C\uFF08\u63D2\u4EF6\u5E26\u5E73\u53F0\u5305\uFF0C\u5347\u7EA7\u63D2\u4EF6\u5373\u53EF\uFF09\u3002`
913
+ `\u6838\u5FC3\u534F\u8BAE\u4E0D\u5339\u914D\uFF1A\u6838\u5FC3 ipcSchema=${version.ipcSchema} digestSchema=${version.digestSchema} contractVersion=${version.contractVersion ?? "(\u7F3A\u5931)"}\uFF0C\u63D2\u4EF6\u652F\u6301 ipcSchema=${IPC_SCHEMA_VERSION} digestSchema=${SUPPORTED_DIGEST_SCHEMA} contractVersion \u4E3B\u7248\u672C=${SUPPORTED_CONTRACT_MAJOR}\u3002\u8BF7\u628A\u63D2\u4EF6\u4E0E\u6838\u5FC3\u5347\u7EA7\u5230\u540C\u4E00\u7248\u672C\uFF08\u63D2\u4EF6\u5E26\u5E73\u53F0\u5305\uFF0C\u5347\u7EA7\u63D2\u4EF6\u5373\u53EF\uFF09\u3002`
738
914
  );
739
915
  }
740
916
  const supportedProviders = Array.isArray(version.supportedProviders) && version.supportedProviders.length > 0 ? version.supportedProviders : [...DEFAULT_PROVIDERS];
@@ -752,7 +928,7 @@ __export(release_exports, {
752
928
  verifyCached: () => verifyCached
753
929
  });
754
930
  import { createHash } from "crypto";
755
- import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync, statSync, unlinkSync } from "fs";
931
+ import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync, statSync as statSync2, unlinkSync as unlinkSync2 } from "fs";
756
932
  import { createRequire } from "module";
757
933
  import { pipeline } from "stream/promises";
758
934
  import { Readable } from "stream";
@@ -819,7 +995,7 @@ function markerPath(binaryPath) {
819
995
  function verifyCached(binaryPath) {
820
996
  let info;
821
997
  try {
822
- info = statSync(binaryPath);
998
+ info = statSync2(binaryPath);
823
999
  } catch {
824
1000
  return { ok: false, reason: "missing" };
825
1001
  }
@@ -933,7 +1109,7 @@ async function ensureCore(options = {}) {
933
1109
  );
934
1110
  }
935
1111
  try {
936
- unlinkSync(descriptor.binaryPath);
1112
+ unlinkSync2(descriptor.binaryPath);
937
1113
  } catch {
938
1114
  }
939
1115
  }
@@ -954,11 +1130,11 @@ __export(locate_exports, {
954
1130
  locateCore: () => locateCore,
955
1131
  overridePath: () => overridePath
956
1132
  });
957
- import { accessSync, constants, existsSync as existsSync3, statSync as statSync2 } from "fs";
1133
+ import { accessSync, constants, existsSync as existsSync3, statSync as statSync3 } from "fs";
958
1134
  import path5 from "path";
959
1135
  function executable(file) {
960
1136
  try {
961
- if (!statSync2(file).isFile()) return false;
1137
+ if (!statSync3(file).isFile()) return false;
962
1138
  accessSync(file, constants.X_OK);
963
1139
  return true;
964
1140
  } catch {
@@ -1174,7 +1350,12 @@ async function runCore(options) {
1174
1350
  `;
1175
1351
  const secrets = [options.apiKey, options.token, ...options.secrets ?? []];
1176
1352
  const child = spawn2(options.core.path, buildCoreArgs(channel.path, { dryRun: options.dryRun, date: options.date }), {
1177
- env: sanitizedEnv(process.env, { GITHUB_TOKEN: options.token, GH_TOKEN: options.token }),
1353
+ env: sanitizedEnv(process.env, {
1354
+ GITHUB_TOKEN: options.token,
1355
+ GH_TOKEN: options.token,
1356
+ // 环境变量通道:旧核心会忽略它,不会因为多一个未知参数而失败。
1357
+ TRENDRADAR_TRIGGER: options.trigger ?? "manual"
1358
+ }),
1178
1359
  stdio: ["ignore", "pipe", "pipe"],
1179
1360
  windowsHide: true
1180
1361
  });
@@ -1305,12 +1486,12 @@ __export(core_exports, {
1305
1486
  findOnPath: () => findOnPath,
1306
1487
  inspectCore: () => inspectCore
1307
1488
  });
1308
- import { accessSync as accessSync2, constants as constants3, statSync as statSync3 } from "fs";
1489
+ import { accessSync as accessSync2, constants as constants3, statSync as statSync4 } from "fs";
1309
1490
  import path7 from "path";
1310
1491
  var CACHE_TTL_MS = 3e4;
1311
1492
  function executable2(file) {
1312
1493
  try {
1313
- if (!statSync3(file).isFile()) return false;
1494
+ if (!statSync4(file).isFile()) return false;
1314
1495
  accessSync2(file, constants3.X_OK);
1315
1496
  return true;
1316
1497
  } catch {
@@ -1406,7 +1587,7 @@ async function detectCoexistence(ownPath, options = {}) {
1406
1587
  };
1407
1588
  } else {
1408
1589
  const info = JSON.parse(result.stdout);
1409
- const compatible = info.ipcSchema === 1 && info.digestSchema === 1;
1590
+ const compatible = info.ipcSchema === IPC_SCHEMA_VERSION && info.digestSchema === SUPPORTED_DIGEST_SCHEMA;
1410
1591
  status = {
1411
1592
  found: true,
1412
1593
  path: found,
@@ -1474,7 +1655,7 @@ async function resolveRef(services, ref, env = process.env) {
1474
1655
  async function resolveGithubToken(services, authMode, tokenRef, deps = {}) {
1475
1656
  const env = deps.env ?? process.env;
1476
1657
  if (authMode === "gh") {
1477
- const execGh = deps.execGh ?? defaultGhToken;
1658
+ const execGh = deps.execGh ?? (() => defaultGhToken(deps.freshGh === true));
1478
1659
  try {
1479
1660
  const token = await execGh();
1480
1661
  if (token !== void 0 && token !== "") return { token, source: "gh" };
@@ -1485,22 +1666,30 @@ async function resolveGithubToken(services, authMode, tokenRef, deps = {}) {
1485
1666
  if (resolved.value !== void 0) return { token: resolved.value, source: resolved.source ?? "credentials" };
1486
1667
  return { source: "none" };
1487
1668
  }
1488
- async function defaultGhToken() {
1669
+ var ghTokenCache;
1670
+ var GH_TOKEN_TTL_MS = 3e4;
1671
+ async function defaultGhToken(fresh = false) {
1672
+ if (!fresh && ghTokenCache !== void 0 && Date.now() - ghTokenCache.at < GH_TOKEN_TTL_MS) {
1673
+ return ghTokenCache.value;
1674
+ }
1489
1675
  const { execCapture: execCapture2 } = await import("./exec-33EQXGUA.js");
1490
- const result = await execCapture2("gh", ["auth", "token"], { timeoutMs: 5e3 });
1491
- if (result.code !== 0) return void 0;
1492
- const token = result.stdout.trim();
1493
- return token === "" ? void 0 : token;
1676
+ const result = await execCapture2("gh", ["auth", "token"], { timeoutMs: 2e3 });
1677
+ const token = result.code === 0 ? result.stdout.trim() : "";
1678
+ const value = token === "" ? void 0 : token;
1679
+ ghTokenCache = { value, at: Date.now() };
1680
+ return value;
1494
1681
  }
1495
1682
 
1496
1683
  // src/host/quickfill.ts
1497
1684
  var quickfill_exports = {};
1498
1685
  __export(quickfill_exports, {
1499
1686
  applyCandidate: () => applyCandidate,
1687
+ applyDefaultSelection: () => applyDefaultSelection,
1500
1688
  coreProviderFor: () => coreProviderFor,
1501
1689
  credentialServices: () => credentialServices,
1502
1690
  listProviderCandidates: () => listProviderCandidates,
1503
1691
  llmServices: () => llmServices,
1692
+ readDefaultSelection: () => readDefaultSelection,
1504
1693
  resolveModelKey: () => resolveModelKey
1505
1694
  });
1506
1695
  function llmServices(get) {
@@ -1531,7 +1720,6 @@ async function listProviderCandidates(options) {
1531
1720
  const declared = new Map(configurable.map((entry) => [entry.provider, entry]));
1532
1721
  const ids = [];
1533
1722
  for (const provider of live) if (!ids.includes(provider.id)) ids.push(provider.id);
1534
- for (const entry of configurable) if (!ids.includes(entry.provider)) ids.push(entry.provider);
1535
1723
  const candidates = [];
1536
1724
  for (const id of ids) {
1537
1725
  const entry = declared.get(id);
@@ -1550,7 +1738,7 @@ async function listProviderCandidates(options) {
1550
1738
  break;
1551
1739
  }
1552
1740
  }
1553
- if (mode !== "direct" && oauth) mode = "proxy";
1741
+ if (mode !== "direct") mode = "proxy";
1554
1742
  const models = await listModelsWithTimeout(llm, id);
1555
1743
  candidates.push({
1556
1744
  id,
@@ -1568,7 +1756,37 @@ async function listProviderCandidates(options) {
1568
1756
  coreProvider: coreProviderFor(id)
1569
1757
  });
1570
1758
  }
1571
- return candidates;
1759
+ return candidates.filter((candidate) => candidate.models.length > 0);
1760
+ }
1761
+ function readDefaultSelection(get) {
1762
+ try {
1763
+ const service = get("agentDefaultModel");
1764
+ const selection = service?.currentSelection?.();
1765
+ if (selection !== void 0 && typeof selection.provider === "string" && selection.provider !== "" && typeof selection.model === "string" && selection.model !== "") {
1766
+ return {
1767
+ provider: selection.provider,
1768
+ model: selection.model,
1769
+ ...typeof selection.reasoningEffort === "string" ? { reasoningEffort: selection.reasoningEffort } : {}
1770
+ };
1771
+ }
1772
+ } catch {
1773
+ }
1774
+ return void 0;
1775
+ }
1776
+ function applyDefaultSelection(settings, selection, candidate) {
1777
+ if (candidate !== void 0) return applyCandidate(settings, candidate, selection.model);
1778
+ const coreProvider = coreProviderFor(selection.provider);
1779
+ return {
1780
+ ...settings,
1781
+ model: {
1782
+ ...settings.model,
1783
+ provider: coreProvider,
1784
+ mode: "proxy",
1785
+ hostProvider: selection.provider,
1786
+ baseURL: "",
1787
+ model: selection.model
1788
+ }
1789
+ };
1572
1790
  }
1573
1791
  function applyCandidate(settings, candidate, modelId) {
1574
1792
  const model = {
@@ -1763,18 +1981,196 @@ function authorized(request, token) {
1763
1981
  return provided.length === expected.length && timingSafeEqual(provided, expected);
1764
1982
  }
1765
1983
 
1984
+ // src/host/schedule.ts
1985
+ var schedule_exports = {};
1986
+ __export(schedule_exports, {
1987
+ Scheduler: () => Scheduler,
1988
+ isDue: () => isDue,
1989
+ nextOccurrence: () => nextOccurrence,
1990
+ parseClock: () => parseClock,
1991
+ zonedDateString: () => zonedDateString,
1992
+ zonedTimeToUtc: () => zonedTimeToUtc
1993
+ });
1994
+ var MINUTE = 6e4;
1995
+ var CATCH_UP_MINUTES = 6 * 60;
1996
+ var MAX_ATTEMPTS_PER_DAY = 2;
1997
+ var RETRY_GAP_MS = 5 * MINUTE;
1998
+ function partsOf(date, timezone) {
1999
+ const formatter = new Intl.DateTimeFormat("en-US", {
2000
+ timeZone: timezone,
2001
+ hourCycle: "h23",
2002
+ year: "numeric",
2003
+ month: "2-digit",
2004
+ day: "2-digit",
2005
+ hour: "2-digit",
2006
+ minute: "2-digit",
2007
+ second: "2-digit"
2008
+ });
2009
+ const map = /* @__PURE__ */ new Map();
2010
+ for (const part of formatter.formatToParts(date)) {
2011
+ if (part.type !== "literal") map.set(part.type, part.value);
2012
+ }
2013
+ return {
2014
+ year: Number(map.get("year")),
2015
+ month: Number(map.get("month")),
2016
+ day: Number(map.get("day")),
2017
+ hour: Number(map.get("hour")),
2018
+ minute: Number(map.get("minute")),
2019
+ second: Number(map.get("second"))
2020
+ };
2021
+ }
2022
+ function offsetMs(date, timezone) {
2023
+ const parts = partsOf(date, timezone);
2024
+ const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
2025
+ return asUtc - Math.floor(date.getTime() / 1e3) * 1e3;
2026
+ }
2027
+ function zonedTimeToUtc(year, month, day, hour, minute, second, timezone) {
2028
+ const guess = Date.UTC(year, month - 1, day, hour, minute, second);
2029
+ const first = new Date(guess - offsetMs(new Date(guess), timezone));
2030
+ return new Date(guess - offsetMs(first, timezone));
2031
+ }
2032
+ function parseClock(time) {
2033
+ const match = /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/.exec(time.trim());
2034
+ if (match === null) throw new Error(`\u975E\u6CD5\u65F6\u95F4\uFF1A${time}`);
2035
+ return {
2036
+ hour: Number(match[1]),
2037
+ minute: Number(match[2]),
2038
+ second: Number(match[3] ?? "0")
2039
+ };
2040
+ }
2041
+ function zonedDateString(date, timezone) {
2042
+ const parts = partsOf(date, timezone);
2043
+ return `${String(parts.year).padStart(4, "0")}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
2044
+ }
2045
+ function nextOccurrence(now, schedule) {
2046
+ const { hour, minute, second } = parseClock(schedule.time);
2047
+ const today = partsOf(now, schedule.timezone);
2048
+ const candidate = zonedTimeToUtc(today.year, today.month, today.day, hour, minute, second, schedule.timezone);
2049
+ if (candidate.getTime() > now.getTime()) return candidate;
2050
+ const tomorrow = new Date(Date.UTC(today.year, today.month - 1, today.day) + 864e5);
2051
+ return zonedTimeToUtc(
2052
+ tomorrow.getUTCFullYear(),
2053
+ tomorrow.getUTCMonth() + 1,
2054
+ tomorrow.getUTCDate(),
2055
+ hour,
2056
+ minute,
2057
+ second,
2058
+ schedule.timezone
2059
+ );
2060
+ }
2061
+ function isDue(now, schedule) {
2062
+ const { hour, minute } = parseClock(schedule.time);
2063
+ const parts = partsOf(now, schedule.timezone);
2064
+ const nowMinutes = parts.hour * 60 + parts.minute;
2065
+ const targetMinutes = hour * 60 + minute;
2066
+ const late = nowMinutes - targetMinutes;
2067
+ return late >= 0 && late <= CATCH_UP_MINUTES;
2068
+ }
2069
+ var Scheduler = class {
2070
+ deps;
2071
+ timerHandle;
2072
+ stopped = false;
2073
+ /** 当天已尝试次数与时间;只在内存里,进程重启后重新计数。 */
2074
+ attempts = /* @__PURE__ */ new Map();
2075
+ constructor(deps) {
2076
+ this.deps = deps;
2077
+ }
2078
+ start() {
2079
+ this.stopped = false;
2080
+ const timer = this.deps.getTimer();
2081
+ const tick = () => {
2082
+ void this.tick();
2083
+ };
2084
+ if (timer !== void 0 && typeof timer.interval === "function") {
2085
+ this.timerHandle = timer.interval(tick, MINUTE);
2086
+ } else {
2087
+ const handle = setInterval(tick, MINUTE);
2088
+ handle.unref?.();
2089
+ this.timerHandle = handle;
2090
+ }
2091
+ setTimeout(tick, 5e3).unref?.();
2092
+ }
2093
+ stop() {
2094
+ this.stopped = true;
2095
+ const handle = this.timerHandle;
2096
+ if (typeof handle === "object" && handle !== null && typeof handle.clear === "function") handle.clear();
2097
+ else if (typeof handle === "number" || handle !== void 0) clearInterval(handle);
2098
+ this.timerHandle = void 0;
2099
+ }
2100
+ /** 公开给测试:判断此刻是否该跑,并按需触发一次。 */
2101
+ async tick(now = /* @__PURE__ */ new Date()) {
2102
+ if (this.stopped) return "skipped";
2103
+ const schedule = this.readSchedule();
2104
+ if (!schedule.enabled) return "disabled";
2105
+ if (!isDue(now, schedule)) return "skipped";
2106
+ 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
+ for (const key of this.attempts.keys()) {
2117
+ if (key !== today) this.attempts.delete(key);
2118
+ }
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";
2126
+ }
2127
+ }
2128
+ readSchedule() {
2129
+ const settings = this.deps.store.readSettings();
2130
+ return {
2131
+ enabled: settings.schedule.enabled,
2132
+ time: settings.schedule.time,
2133
+ timezone: settings.schedule.timezone
2134
+ };
2135
+ }
2136
+ /** 下一次运行(禁用时为 undefined)。 */
2137
+ next() {
2138
+ const schedule = this.readSchedule();
2139
+ if (!schedule.enabled) return void 0;
2140
+ try {
2141
+ return nextOccurrence(/* @__PURE__ */ new Date(), schedule);
2142
+ } catch {
2143
+ return void 0;
2144
+ }
2145
+ }
2146
+ describe(state, now = /* @__PURE__ */ new Date()) {
2147
+ const schedule = this.readSchedule();
2148
+ const next = schedule.enabled ? this.next() : void 0;
2149
+ return {
2150
+ enabled: schedule.enabled,
2151
+ time: schedule.time,
2152
+ timezone: schedule.timezone,
2153
+ ...next === void 0 ? {} : { nextRunAt: next.toISOString() },
2154
+ ...state.lastRunAt === void 0 ? {} : { lastRunDate: zonedDateString(new Date(state.lastRunAt), schedule.timezone) },
2155
+ catchUpMinutes: CATCH_UP_MINUTES,
2156
+ source: "timer"
2157
+ };
2158
+ }
2159
+ };
2160
+
1766
2161
  // src/host/runner.ts
1767
2162
  var runner_exports = {};
1768
2163
  __export(runner_exports, {
1769
2164
  Runner: () => Runner
1770
2165
  });
1771
- import { existsSync as existsSync4, statSync as statSync4 } from "fs";
2166
+ import { createHash as createHash2 } from "crypto";
2167
+ import { existsSync as existsSync4, readFileSync as readFileSync3, realpathSync, statSync as statSync5 } from "fs";
1772
2168
  import path8 from "path";
1773
2169
  function repoProblem(repoPath) {
1774
2170
  if (repoPath.trim() === "") return { code: "CONFIG_INVALID", message: "\u8FD8\u6CA1\u6709\u9009\u62E9\u5F52\u6863\u4ED3\u5E93" };
1775
2171
  if (!existsSync4(repoPath)) return { code: "REPO_NOT_GIT", message: `\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${repoPath}` };
1776
2172
  try {
1777
- if (!statSync4(repoPath).isDirectory()) return { code: "REPO_NOT_GIT", message: `\u4E0D\u662F\u76EE\u5F55\uFF1A${repoPath}` };
2173
+ if (!statSync5(repoPath).isDirectory()) return { code: "REPO_NOT_GIT", message: `\u4E0D\u662F\u76EE\u5F55\uFF1A${repoPath}` };
1778
2174
  } catch {
1779
2175
  return { code: "REPO_NOT_GIT", message: `\u65E0\u6CD5\u8BBF\u95EE\uFF1A${repoPath}` };
1780
2176
  }
@@ -1783,6 +2179,22 @@ function repoProblem(repoPath) {
1783
2179
  }
1784
2180
  return void 0;
1785
2181
  }
2182
+ function runDate(settings, now = /* @__PURE__ */ new Date()) {
2183
+ try {
2184
+ return zonedDateString(now, settings.schedule.timezone);
2185
+ } catch {
2186
+ return now.toISOString().slice(0, 10);
2187
+ }
2188
+ }
2189
+ function isInside(root, target) {
2190
+ const rel = path8.relative(root, target);
2191
+ return rel !== "" && !rel.startsWith("..") && !path8.isAbsolute(rel);
2192
+ }
2193
+ function normalizeSha256(value) {
2194
+ if (typeof value !== "string") return void 0;
2195
+ const hex = value.trim().replace(/^sha256:/i, "").toLowerCase();
2196
+ return /^[0-9a-f]{64}$/.test(hex) ? hex : void 0;
2197
+ }
1786
2198
  var Runner = class {
1787
2199
  active;
1788
2200
  deps;
@@ -1812,12 +2224,13 @@ var Runner = class {
1812
2224
  }
1813
2225
  const repoIssue = repoProblem(settings.repo.path);
1814
2226
  if (repoIssue !== void 0) throw new IpcError(repoIssue.code, repoIssue.message);
2227
+ const dryRun = options.dryRun === true;
2228
+ const date = options.date !== void 0 && options.date !== "" ? options.date : runDate(settings);
1815
2229
  const id = newRunId();
1816
2230
  const controller = new AbortController();
1817
2231
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1818
2232
  this.active = { id, controller, startedAt };
1819
2233
  const logFile = store.newLogPath(id);
1820
- const dryRun = options.dryRun === true;
1821
2234
  const startedMs = Date.now();
1822
2235
  let proxy;
1823
2236
  try {
@@ -1836,6 +2249,7 @@ var Runner = class {
1836
2249
  if (modelKey.mode === "direct" && (modelKey.apiKey === void 0 || modelKey.apiKey === "")) {
1837
2250
  throw new IpcError("MODEL_KEY_MISSING", `\u6CA1\u6709\u89E3\u6790\u5230\u6A21\u578B\u5BC6\u94A5\uFF08\u5F15\u7528 ${settings.model.apiKeyRef}\uFF09`);
1838
2251
  }
2252
+ let apiKey = modelKey.apiKey ?? "";
1839
2253
  let baseURLOverride;
1840
2254
  if (modelKey.mode === "proxy") {
1841
2255
  const llm = this.deps.get("llm");
@@ -1851,6 +2265,7 @@ var Runner = class {
1851
2265
  timeoutMs: 12e4
1852
2266
  });
1853
2267
  baseURLOverride = proxy.url;
2268
+ apiKey = proxy.token;
1854
2269
  store.appendLog(
1855
2270
  logFile,
1856
2271
  `[proxy] \u56DE\u73AF\u4EE3\u7406\u5DF2\u542F\u52A8\uFF1A127.0.0.1:${proxy.port}\uFF08\u4E00\u6B21\u6027 token\uFF0C\u8FD0\u884C\u7ED3\u675F\u5373\u5173\u95ED\uFF09`
@@ -1864,13 +2279,14 @@ var Runner = class {
1864
2279
  core,
1865
2280
  version: handshakeResult.version,
1866
2281
  settings,
1867
- apiKey: modelKey.apiKey ?? "",
2282
+ apiKey,
1868
2283
  ...github.token === void 0 ? {} : { token: github.token },
1869
- ...options.date === void 0 ? {} : { date: options.date },
2284
+ date,
1870
2285
  dryRun,
2286
+ trigger: options.trigger === "schedule" ? "schedule" : "manual",
1871
2287
  ...baseURLOverride === void 0 ? {} : { baseURLOverride },
1872
2288
  signal: controller.signal,
1873
- secrets: [modelKey.apiKey, github.token],
2289
+ secrets: [apiKey, github.token],
1874
2290
  onStderr: (line) => store.appendLog(logFile, line)
1875
2291
  });
1876
2292
  const outcome = result.outcome;
@@ -1878,13 +2294,21 @@ var Runner = class {
1878
2294
  id,
1879
2295
  outcome,
1880
2296
  dryRun,
1881
- ...options.date === void 0 ? {} : { date: options.date },
2297
+ date,
1882
2298
  logFile,
1883
2299
  channel: result.channelKind,
1884
2300
  proxy: proxy !== void 0,
1885
2301
  coreSource: core.source,
1886
2302
  ...core.tag === void 0 ? {} : { coreTag: core.tag }
1887
2303
  });
2304
+ const preview = this.capturePreview({
2305
+ id,
2306
+ date,
2307
+ settings,
2308
+ outcome,
2309
+ secrets: [apiKey, github.token]
2310
+ });
2311
+ if (preview !== void 0) summary.preview = preview;
1888
2312
  this.record(summary, settings);
1889
2313
  return summary;
1890
2314
  } catch (error) {
@@ -1893,7 +2317,7 @@ var Runner = class {
1893
2317
  id,
1894
2318
  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,
1895
2319
  dryRun,
1896
- ...options.date === void 0 ? {} : { date: options.date },
2320
+ date,
1897
2321
  logFile,
1898
2322
  durationMs: Date.now() - startedMs
1899
2323
  });
@@ -1910,14 +2334,19 @@ var Runner = class {
1910
2334
  }
1911
2335
  summarize(input) {
1912
2336
  const { outcome } = input;
1913
- const error = outcome.error === void 0 ? void 0 : {
1914
- code: outcome.error.code,
1915
- message: redact(outcome.error.message),
1916
- hint: hintOf(outcome.error.code),
1917
- area: areaOf(outcome.error.code),
1918
- retriable: outcome.error.retriable,
1919
- inferred: outcome.error.details?.inferred === true
1920
- };
2337
+ const error = outcome.error === void 0 ? void 0 : (() => {
2338
+ const message = redact(outcome.error.message);
2339
+ const reason = reasonOf(outcome.error.code, message, outcome.error.details);
2340
+ return {
2341
+ code: outcome.error.code,
2342
+ message,
2343
+ ...reason === void 0 ? {} : { reason },
2344
+ hint: hintOf(outcome.error.code, message, reason),
2345
+ area: areaOf(outcome.error.code),
2346
+ retriable: outcome.error.retriable,
2347
+ inferred: outcome.error.details?.inferred === true
2348
+ };
2349
+ })();
1921
2350
  return {
1922
2351
  id: input.id,
1923
2352
  ok: outcome.ok,
@@ -1929,7 +2358,9 @@ var Runner = class {
1929
2358
  files: outcome.files.length,
1930
2359
  ...outcome.contentHash === void 0 ? {} : { contentHash: outcome.contentHash },
1931
2360
  ...outcome.branch === void 0 ? {} : { branch: outcome.branch },
2361
+ ...outcome.commit === void 0 ? {} : { commit: outcome.commit },
1932
2362
  ...outcome.prUrl === void 0 ? {} : { prUrl: outcome.prUrl },
2363
+ ...outcome.skipReason === void 0 ? {} : { skipReason: outcome.skipReason },
1933
2364
  ...error === void 0 ? {} : { error },
1934
2365
  warnings: outcome.warnings,
1935
2366
  logFile: input.logFile,
@@ -1951,11 +2382,73 @@ var Runner = class {
1951
2382
  durationMs: input.durationMs ?? 0,
1952
2383
  coreVersion: "",
1953
2384
  files: 0,
1954
- error: { code, message, hint: hintOf(code), area: areaOf(code), retriable: false },
2385
+ error: { code, message, hint: hintOf(code, message), area: areaOf(code), retriable: false },
1955
2386
  warnings: [],
1956
2387
  logFile: input.logFile
1957
2388
  };
1958
2389
  }
2390
+ /**
2391
+ * 从本次核心返回的 artifact 清单里选当天的 `.md`,只读复制到 previews/。
2392
+ *
2393
+ * 这是插件**唯一**读取用户仓库的场景,而且只读核心自己刚写出的那一份:
2394
+ * - 路径必须在 `repo.path` 内(词法 + realpath 各判一次,挡 `..`/绝对路径/符号链接);
2395
+ * - 上限 512 KB、拒绝 NUL、sha256 必须与契约声明一致;
2396
+ * - 任何一步失败都只记日志,返回 undefined;运行结果不受影响。
2397
+ */
2398
+ capturePreview(input) {
2399
+ const { outcome, settings, date, id } = input;
2400
+ if (!outcome.ok || outcome.dryRun || outcome.skipped || date === void 0) return void 0;
2401
+ const skip = (reason, extra = {}) => {
2402
+ this.deps.logger?.("preview.skip", { id, reason, ...extra });
2403
+ return void 0;
2404
+ };
2405
+ const markdown = outcome.files.filter((file) => {
2406
+ const ext = path8.extname(file.path).toLowerCase();
2407
+ return ext === ".md" && path8.basename(file.path).toLowerCase() !== "readme.md";
2408
+ });
2409
+ 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 });
2413
+ const repoPath = settings.repo.path;
2414
+ const resolved = path8.resolve(repoPath, chosen.path);
2415
+ if (!isInside(repoPath, resolved)) return skip("outside-repo", { path: chosen.path });
2416
+ let fileReal;
2417
+ try {
2418
+ const repoReal = realpathSync(repoPath);
2419
+ fileReal = realpathSync(resolved);
2420
+ if (!isInside(repoReal, fileReal)) return skip("symlink-outside-repo", { path: chosen.path });
2421
+ } catch {
2422
+ return skip("missing-file", { path: chosen.path });
2423
+ }
2424
+ let buffer;
2425
+ try {
2426
+ const stat2 = statSync5(fileReal);
2427
+ if (!stat2.isFile()) return skip("not-a-file", { path: chosen.path });
2428
+ if (stat2.size > PREVIEW_MAX_BYTES) return skip("too-large", { bytes: stat2.size });
2429
+ buffer = readFileSync3(fileReal);
2430
+ } catch {
2431
+ return skip("read-failed", { path: chosen.path });
2432
+ }
2433
+ if (buffer.includes(0)) return skip("contains-nul", { path: chosen.path });
2434
+ const declared = normalizeSha256(chosen.sha256);
2435
+ const actual = createHash2("sha256").update(buffer).digest("hex");
2436
+ if (declared === void 0 || declared !== actual) {
2437
+ return skip("sha-mismatch", { path: chosen.path, declared: declared ?? null, actual });
2438
+ }
2439
+ const text = redact(buffer.toString("utf8"), input.secrets);
2440
+ const stored = Buffer.from(text, "utf8");
2441
+ try {
2442
+ this.deps.store.writePreview(id, text);
2443
+ } catch {
2444
+ return skip("write-failed", { path: chosen.path });
2445
+ }
2446
+ return {
2447
+ file: path8.relative(repoPath, resolved).split(path8.sep).join("/"),
2448
+ bytes: stored.length,
2449
+ sha256: createHash2("sha256").update(stored).digest("hex")
2450
+ };
2451
+ }
1959
2452
  /** 写历史与状态;同一天的 skipped 运行不覆盖 lastDigest。 */
1960
2453
  record(summary, settings) {
1961
2454
  const store = this.deps.store;
@@ -1965,12 +2458,20 @@ var Runner = class {
1965
2458
  kind: summary.dryRun ? "dry-run" : "run",
1966
2459
  ok: summary.ok,
1967
2460
  skipped: summary.skipped,
1968
- ...summary.error === void 0 ? {} : { errorCode: summary.error.code, errorMessage: summary.error.message },
2461
+ ...summary.error === void 0 ? {} : {
2462
+ errorCode: summary.error.code,
2463
+ errorMessage: summary.error.message,
2464
+ ...summary.error.reason === void 0 ? {} : { errorReason: summary.error.reason }
2465
+ },
2466
+ ...summary.skipReason === void 0 ? {} : { skipReason: summary.skipReason },
1969
2467
  durationMs: summary.durationMs,
1970
2468
  files: summary.files,
2469
+ ...summary.date === void 0 ? {} : { date: summary.date },
1971
2470
  ...summary.contentHash === void 0 ? {} : { contentHash: summary.contentHash },
1972
2471
  ...summary.branch === void 0 ? {} : { branch: summary.branch },
2472
+ ...summary.commit === void 0 ? {} : { commit: summary.commit },
1973
2473
  ...summary.prUrl === void 0 ? {} : { prUrl: summary.prUrl },
2474
+ ...summary.preview === void 0 ? {} : { preview: summary.preview },
1974
2475
  coreVersion: summary.coreVersion,
1975
2476
  model: { provider: settings.model.provider, model: settings.model.model },
1976
2477
  languages: settings.trending.languages,
@@ -1980,11 +2481,13 @@ var Runner = class {
1980
2481
  const state = store.readState();
1981
2482
  const next = {
1982
2483
  lastRunAt: record.at,
1983
- ...summary.ok && summary.prUrl !== void 0 ? { pendingPr: { url: summary.prUrl, createdAt: record.at } } : {},
2484
+ ...summary.ok ? { lastSuccessAt: record.at } : {},
2485
+ ...summary.ok && summary.prUrl !== void 0 ? { pendingPr: { url: summary.prUrl, createdAt: record.at, ...summary.date === void 0 ? {} : { date: summary.date } } } : {},
1984
2486
  ...summary.ok && !summary.skipped && summary.contentHash !== void 0 ? {
1985
2487
  lastDigest: {
1986
2488
  date: summary.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1987
2489
  contentHash: summary.contentHash,
2490
+ contentHashAlgo: CONTENT_HASH_ALGO_V2,
1988
2491
  coreVersion: summary.coreVersion
1989
2492
  },
1990
2493
  ...state.pendingPr !== void 0 && summary.prUrl === void 0 ? { pendingPr: state.pendingPr } : {}
@@ -2000,172 +2503,106 @@ var Runner = class {
2000
2503
  }
2001
2504
  };
2002
2505
 
2003
- // src/host/schedule.ts
2004
- var schedule_exports = {};
2005
- __export(schedule_exports, {
2006
- Scheduler: () => Scheduler,
2007
- isDue: () => isDue,
2008
- nextOccurrence: () => nextOccurrence,
2009
- parseClock: () => parseClock,
2010
- zonedDateString: () => zonedDateString,
2011
- zonedTimeToUtc: () => zonedTimeToUtc
2506
+ // src/host/plugin-info.ts
2507
+ var plugin_info_exports = {};
2508
+ __export(plugin_info_exports, {
2509
+ normalizeRepositoryUrl: () => normalizeRepositoryUrl,
2510
+ readPluginInfo: () => readPluginInfo
2012
2511
  });
2013
- var MINUTE = 6e4;
2014
- var CATCH_UP_MINUTES = 6 * 60;
2015
- function partsOf(date, timezone) {
2016
- const formatter = new Intl.DateTimeFormat("en-US", {
2017
- timeZone: timezone,
2018
- hourCycle: "h23",
2019
- year: "numeric",
2020
- month: "2-digit",
2021
- day: "2-digit",
2022
- hour: "2-digit",
2023
- minute: "2-digit",
2024
- second: "2-digit"
2025
- });
2026
- const map = /* @__PURE__ */ new Map();
2027
- for (const part of formatter.formatToParts(date)) {
2028
- if (part.type !== "literal") map.set(part.type, part.value);
2512
+ import { createRequire as createRequire2 } from "module";
2513
+ var DEFAULT_NAME = "@morehao/dsh-github-trending";
2514
+ function normalizeRepositoryUrl(value) {
2515
+ if (typeof value !== "string") return void 0;
2516
+ let url = value.trim();
2517
+ if (url === "") return void 0;
2518
+ url = url.replace(/^git\+/i, "");
2519
+ const ssh = /^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/i.exec(url);
2520
+ if (ssh !== null) url = `https://github.com/${ssh[1]}/${ssh[2]}`;
2521
+ url = url.replace(/\.git$/i, "");
2522
+ try {
2523
+ const parsed = new URL(url);
2524
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return void 0;
2525
+ if (parsed.hostname.toLowerCase() !== "github.com") return void 0;
2526
+ const pathname = parsed.pathname.replace(/\/+$/, "");
2527
+ if (pathname === "") return void 0;
2528
+ return `https://github.com${pathname}`;
2529
+ } catch {
2530
+ return void 0;
2029
2531
  }
2030
- return {
2031
- year: Number(map.get("year")),
2032
- month: Number(map.get("month")),
2033
- day: Number(map.get("day")),
2034
- hour: Number(map.get("hour")),
2035
- minute: Number(map.get("minute")),
2036
- second: Number(map.get("second"))
2037
- };
2038
- }
2039
- function offsetMs(date, timezone) {
2040
- const parts = partsOf(date, timezone);
2041
- const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
2042
- return asUtc - Math.floor(date.getTime() / 1e3) * 1e3;
2043
- }
2044
- function zonedTimeToUtc(year, month, day, hour, minute, second, timezone) {
2045
- const guess = Date.UTC(year, month - 1, day, hour, minute, second);
2046
- const first = new Date(guess - offsetMs(new Date(guess), timezone));
2047
- return new Date(guess - offsetMs(first, timezone));
2048
- }
2049
- function parseClock(time) {
2050
- const match = /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/.exec(time.trim());
2051
- if (match === null) throw new Error(`\u975E\u6CD5\u65F6\u95F4\uFF1A${time}`);
2052
- return {
2053
- hour: Number(match[1]),
2054
- minute: Number(match[2]),
2055
- second: Number(match[3] ?? "0")
2056
- };
2057
2532
  }
2058
- function zonedDateString(date, timezone) {
2059
- const parts = partsOf(date, timezone);
2060
- return `${String(parts.year).padStart(4, "0")}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
2061
- }
2062
- function nextOccurrence(now, schedule) {
2063
- const { hour, minute, second } = parseClock(schedule.time);
2064
- const today = partsOf(now, schedule.timezone);
2065
- const candidate = zonedTimeToUtc(today.year, today.month, today.day, hour, minute, second, schedule.timezone);
2066
- if (candidate.getTime() > now.getTime()) return candidate;
2067
- const tomorrow = new Date(Date.UTC(today.year, today.month - 1, today.day) + 864e5);
2068
- return zonedTimeToUtc(
2069
- tomorrow.getUTCFullYear(),
2070
- tomorrow.getUTCMonth() + 1,
2071
- tomorrow.getUTCDate(),
2072
- hour,
2073
- minute,
2074
- second,
2075
- schedule.timezone
2076
- );
2077
- }
2078
- function isDue(now, schedule) {
2079
- const { hour, minute } = parseClock(schedule.time);
2080
- const parts = partsOf(now, schedule.timezone);
2081
- const nowMinutes = parts.hour * 60 + parts.minute;
2082
- const targetMinutes = hour * 60 + minute;
2083
- const late = nowMinutes - targetMinutes;
2084
- return late >= 0 && late <= CATCH_UP_MINUTES;
2085
- }
2086
- var Scheduler = class {
2087
- deps;
2088
- timerHandle;
2089
- stopped = false;
2090
- lastFiredFor;
2091
- constructor(deps) {
2092
- this.deps = deps;
2093
- }
2094
- start() {
2095
- this.stopped = false;
2096
- const timer = this.deps.getTimer();
2097
- const tick = () => {
2098
- void this.tick();
2533
+ function readPluginInfo() {
2534
+ try {
2535
+ const require2 = createRequire2(import.meta.url);
2536
+ const pkg = require2("../package.json");
2537
+ const repository = typeof pkg.repository === "string" ? normalizeRepositoryUrl(pkg.repository) : typeof pkg.repository === "object" && pkg.repository !== null ? normalizeRepositoryUrl(pkg.repository.url) : void 0;
2538
+ return {
2539
+ name: typeof pkg.name === "string" && pkg.name !== "" ? pkg.name : DEFAULT_NAME,
2540
+ version: typeof pkg.version === "string" ? pkg.version : "0.0.0",
2541
+ ...repository === void 0 ? {} : { repository }
2099
2542
  };
2100
- if (timer !== void 0 && typeof timer.interval === "function") {
2101
- this.timerHandle = timer.interval(tick, MINUTE);
2102
- } else {
2103
- const handle = setInterval(tick, MINUTE);
2104
- handle.unref?.();
2105
- this.timerHandle = handle;
2106
- }
2107
- setTimeout(tick, 5e3).unref?.();
2108
- }
2109
- stop() {
2110
- this.stopped = true;
2111
- const handle = this.timerHandle;
2112
- if (typeof handle === "object" && handle !== null && typeof handle.clear === "function") handle.clear();
2113
- else if (typeof handle === "number" || handle !== void 0) clearInterval(handle);
2114
- this.timerHandle = void 0;
2115
- }
2116
- /** 公开给测试:判断此刻是否该跑,并按需触发一次。 */
2117
- async tick(now = /* @__PURE__ */ new Date()) {
2118
- if (this.stopped) return "skipped";
2119
- const schedule = this.readSchedule();
2120
- if (!schedule.enabled) return "disabled";
2121
- if (!isDue(now, schedule)) return "skipped";
2122
- const today = zonedDateString(now, schedule.timezone);
2123
- const state = this.deps.store.readState();
2124
- const lastDate = state.lastRunAt === void 0 ? void 0 : zonedDateString(new Date(state.lastRunAt), schedule.timezone);
2125
- if (lastDate === today) return "skipped";
2126
- if (this.lastFiredFor === today) return "skipped";
2127
- if (this.deps.isRunning()) return "busy";
2128
- this.lastFiredFor = today;
2129
- try {
2130
- await this.deps.run("schedule");
2131
- return "ran";
2132
- } catch (error) {
2133
- this.deps.onError?.(error);
2134
- return "skipped";
2135
- }
2543
+ } catch {
2544
+ return { name: DEFAULT_NAME, version: "0.0.0" };
2136
2545
  }
2137
- readSchedule() {
2138
- const settings = this.deps.store.readSettings();
2546
+ }
2547
+
2548
+ // src/host/github.ts
2549
+ var github_exports = {};
2550
+ __export(github_exports, {
2551
+ probeGithubAuth: () => probeGithubAuth
2552
+ });
2553
+ async function probeGithubAuth(options) {
2554
+ const env = options.env ?? process.env;
2555
+ const resolved = await resolveGithubToken(options.credentials, options.authMode, options.tokenRef, {
2556
+ env,
2557
+ freshGh: true
2558
+ });
2559
+ if (resolved.token === void 0 || resolved.token === "") {
2139
2560
  return {
2140
- enabled: settings.schedule.enabled,
2141
- time: settings.schedule.time,
2142
- timezone: settings.schedule.timezone
2561
+ ok: false,
2562
+ code: "GITHUB_AUTH",
2563
+ source: resolved.source,
2564
+ message: options.authMode === "gh" ? "\u6CA1\u6709\u53D6\u5230\u672C\u673A gh \u7684\u767B\u5F55\u6001\uFF1A\u5148\u5728\u7EC8\u7AEF\u6267\u884C `gh auth login`\u3002" : `\u6CA1\u6709\u53D6\u5230\u51ED\u636E\u300C${options.tokenRef}\u300D\uFF1A\u8BF7\u5728\u73AF\u5883\u53D8\u91CF\u6216\u51ED\u636E\u91CC\u914D\u7F6E\u4E00\u4E2A Personal Access Token\u3002`
2143
2565
  };
2144
2566
  }
2145
- /** 下一次运行(禁用时为 undefined)。 */
2146
- next() {
2147
- const schedule = this.readSchedule();
2148
- if (!schedule.enabled) return void 0;
2149
- try {
2150
- return nextOccurrence(/* @__PURE__ */ new Date(), schedule);
2151
- } catch {
2152
- return void 0;
2567
+ const doFetch = options.fetchImpl ?? fetch;
2568
+ const controller = new AbortController();
2569
+ const timer = setTimeout(() => controller.abort(), 1e4);
2570
+ try {
2571
+ const response = await doFetch("https://api.github.com/user", {
2572
+ method: "GET",
2573
+ headers: {
2574
+ authorization: `Bearer ${resolved.token}`,
2575
+ accept: "application/vnd.github+json",
2576
+ "user-agent": "dsh-github-trending"
2577
+ },
2578
+ signal: controller.signal
2579
+ });
2580
+ if (!response.ok) {
2581
+ return {
2582
+ ok: false,
2583
+ code: "GITHUB_AUTH",
2584
+ source: resolved.source,
2585
+ message: `GitHub \u8FD4\u56DE HTTP ${response.status}\uFF08token \u65E0\u6548\u6216\u6743\u9650\u4E0D\u8DB3\uFF09`
2586
+ };
2153
2587
  }
2154
- }
2155
- describe(state, now = /* @__PURE__ */ new Date()) {
2156
- const schedule = this.readSchedule();
2157
- const next = schedule.enabled ? this.next() : void 0;
2588
+ const body = await response.json();
2158
2589
  return {
2159
- enabled: schedule.enabled,
2160
- time: schedule.time,
2161
- timezone: schedule.timezone,
2162
- ...next === void 0 ? {} : { nextRunAt: next.toISOString() },
2163
- ...state.lastRunAt === void 0 ? {} : { lastRunDate: zonedDateString(new Date(state.lastRunAt), schedule.timezone) },
2164
- catchUpMinutes: CATCH_UP_MINUTES,
2165
- source: "timer"
2590
+ ok: true,
2591
+ source: resolved.source,
2592
+ ...typeof body.login === "string" ? { login: body.login } : {}
2166
2593
  };
2594
+ } catch (error) {
2595
+ const aborted = error instanceof Error && error.name === "AbortError";
2596
+ return {
2597
+ ok: false,
2598
+ code: "GITHUB_AUTH",
2599
+ source: resolved.source,
2600
+ message: aborted ? "\u8BF7\u6C42 GitHub \u8D85\u65F6" : error instanceof Error ? error.message : String(error)
2601
+ };
2602
+ } finally {
2603
+ clearTimeout(timer);
2167
2604
  }
2168
- };
2605
+ }
2169
2606
 
2170
2607
  // src/host/import-cli.ts
2171
2608
  var import_cli_exports = {};
@@ -2175,7 +2612,7 @@ __export(import_cli_exports, {
2175
2612
  importFromCli: () => importFromCli,
2176
2613
  parseSimpleYaml: () => parseSimpleYaml
2177
2614
  });
2178
- import { readFileSync as readFileSync3 } from "fs";
2615
+ import { readFileSync as readFileSync4 } from "fs";
2179
2616
  import os3 from "os";
2180
2617
  import path9 from "path";
2181
2618
  function cliConfigCandidates(env = process.env, home = os3.homedir()) {
@@ -2267,7 +2704,7 @@ function isSecretKey(key) {
2267
2704
  function importFromCli(filePath, base = DEFAULT_SETTINGS) {
2268
2705
  let text;
2269
2706
  try {
2270
- text = readFileSync3(filePath, "utf8");
2707
+ text = readFileSync4(filePath, "utf8");
2271
2708
  } catch (error) {
2272
2709
  return {
2273
2710
  ok: false,
@@ -2399,6 +2836,92 @@ function importFirstAvailable(env = process.env, base) {
2399
2836
  };
2400
2837
  }
2401
2838
 
2839
+ // src/host/pending-pr.ts
2840
+ var pending_pr_exports = {};
2841
+ __export(pending_pr_exports, {
2842
+ checkPendingPr: () => checkPendingPr,
2843
+ clearPendingPrCache: () => clearPendingPrCache,
2844
+ parsePullUrl: () => parsePullUrl
2845
+ });
2846
+ function parsePullUrl(url) {
2847
+ const match = /^https?:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)(?:[/?#].*)?$/i.exec(url.trim());
2848
+ if (match === null) return void 0;
2849
+ const owner = match[1];
2850
+ const repo = match[2];
2851
+ const number = Number(match[3]);
2852
+ if (owner === void 0 || repo === void 0 || !Number.isInteger(number) || number <= 0) return void 0;
2853
+ return { owner, repo, number };
2854
+ }
2855
+ var CACHE_TTL_MS2 = 6e4;
2856
+ var cache = /* @__PURE__ */ new Map();
2857
+ function clearPendingPrCache() {
2858
+ cache.clear();
2859
+ }
2860
+ async function checkPendingPr(options) {
2861
+ const { url } = options;
2862
+ const parsed = parsePullUrl(url);
2863
+ if (parsed === void 0) {
2864
+ return { state: "unknown", url, reason: "PR \u94FE\u63A5\u65E0\u6CD5\u89E3\u6790\uFF0C\u8DF3\u8FC7\u5BF9\u8D26" };
2865
+ }
2866
+ const now = options.now ?? Date.now();
2867
+ if (options.cache !== false) {
2868
+ const hit = cache.get(url);
2869
+ if (hit !== void 0 && now - hit.at < CACHE_TTL_MS2) return hit.status;
2870
+ }
2871
+ if (options.token === void 0 || options.token === "") {
2872
+ return { state: "unknown", url, number: parsed.number, reason: "\u6CA1\u6709\u53EF\u7528\u7684 GitHub token\uFF0C\u65E0\u6CD5\u6838\u5BF9 PR \u72B6\u6001" };
2873
+ }
2874
+ const doFetch = options.fetchImpl ?? fetch;
2875
+ const controller = new AbortController();
2876
+ const timer = setTimeout(() => controller.abort(), 8e3);
2877
+ try {
2878
+ const response = await doFetch(
2879
+ `https://api.github.com/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`,
2880
+ {
2881
+ method: "GET",
2882
+ headers: {
2883
+ authorization: `Bearer ${options.token}`,
2884
+ accept: "application/vnd.github+json",
2885
+ "user-agent": "dsh-github-trending"
2886
+ },
2887
+ signal: controller.signal
2888
+ }
2889
+ );
2890
+ if (response.status === 404) {
2891
+ const status2 = { state: "unknown", url, number: parsed.number, reason: "PR \u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE" };
2892
+ cache.set(url, { at: now, status: status2 });
2893
+ return status2;
2894
+ }
2895
+ if (!response.ok) {
2896
+ const status2 = { state: "unknown", url, number: parsed.number, reason: `GitHub \u8FD4\u56DE HTTP ${response.status}` };
2897
+ cache.set(url, { at: now, status: status2 });
2898
+ return status2;
2899
+ }
2900
+ const body = await response.json();
2901
+ const merged = body.merged === true || typeof body.merged_at === "string" && body.merged_at !== "";
2902
+ const state = merged ? "merged" : body.state === "open" ? "open" : body.state === "closed" ? "closed" : "unknown";
2903
+ const status = {
2904
+ state,
2905
+ url,
2906
+ number: parsed.number,
2907
+ ...typeof body.title === "string" ? { title: body.title } : {}
2908
+ };
2909
+ cache.set(url, { at: now, status });
2910
+ return status;
2911
+ } catch (error) {
2912
+ const status = {
2913
+ state: "unknown",
2914
+ url,
2915
+ number: parsed.number,
2916
+ reason: error instanceof Error ? error.message : String(error)
2917
+ };
2918
+ cache.set(url, { at: now, status });
2919
+ return status;
2920
+ } finally {
2921
+ clearTimeout(timer);
2922
+ }
2923
+ }
2924
+
2402
2925
  // src/host/preflight.ts
2403
2926
  var preflight_exports = {};
2404
2927
  __export(preflight_exports, {
@@ -2526,6 +3049,46 @@ async function preflightModel(options) {
2526
3049
  }
2527
3050
  }
2528
3051
 
3052
+ // src/host/repo.ts
3053
+ var repo_exports = {};
3054
+ __export(repo_exports, {
3055
+ readDefaultBranch: () => readDefaultBranch
3056
+ });
3057
+ import { statSync as statSync6 } from "fs";
3058
+ import path10 from "path";
3059
+ function isDirectory(dir) {
3060
+ try {
3061
+ return statSync6(dir).isDirectory();
3062
+ } catch {
3063
+ return false;
3064
+ }
3065
+ }
3066
+ async function git(dir, args, env) {
3067
+ try {
3068
+ const result = await execCapture("git", ["-C", dir, ...args], { env, timeoutMs: 5e3 });
3069
+ return result.code === 0 ? result.stdout.trim() : void 0;
3070
+ } catch {
3071
+ return void 0;
3072
+ }
3073
+ }
3074
+ async function readDefaultBranch(repoPath, options = {}) {
3075
+ const dir = repoPath.trim();
3076
+ if (dir === "") return { source: "none", message: "\u4ED3\u5E93\u8DEF\u5F84\u4E3A\u7A7A" };
3077
+ if (!path10.isAbsolute(dir)) return { source: "none", message: "\u4ED3\u5E93\u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84" };
3078
+ if (!isDirectory(dir)) return { source: "none", message: "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u8BBF\u95EE" };
3079
+ const env = options.env ?? process.env;
3080
+ const origin = await git(dir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], env);
3081
+ if (origin !== void 0) {
3082
+ const branch = origin.replace(/^origin\//, "");
3083
+ if (branch !== "") return { branch, source: "origin-head" };
3084
+ }
3085
+ const head = await git(dir, ["rev-parse", "--abbrev-ref", "HEAD"], env);
3086
+ if (head !== void 0) {
3087
+ if (head !== "" && head !== "HEAD") return { branch: head, source: "head" };
3088
+ }
3089
+ return { source: "none", message: "\u672A\u8BFB\u53D6\u5230\u9ED8\u8BA4\u5206\u652F\uFF08\u53EF\u80FD\u662F\u7A7A\u4ED3\u5E93\u6216\u6CA1\u6709 origin\uFF09" };
3090
+ }
3091
+
2529
3092
  // src/host/routes.ts
2530
3093
  var routes_exports = {};
2531
3094
  __export(routes_exports, {
@@ -2583,7 +3146,8 @@ function createRoutes(deps) {
2583
3146
  const code = error instanceof IpcError ? error.code : "INTERNAL";
2584
3147
  const message = redact(error instanceof Error ? error.message : String(error));
2585
3148
  deps.logger?.("route.error", { pathname, method, code, message });
2586
- sendJson2(response, code === "CONFIG_INVALID" ? 400 : 500, { ok: false, code, message });
3149
+ const status = code === "CONFIG_INVALID" ? 400 : code === "NOT_FOUND" ? 404 : 500;
3150
+ sendJson2(response, status, { ok: false, code, message });
2587
3151
  }
2588
3152
  };
2589
3153
  }
@@ -2603,10 +3167,17 @@ async function dispatch(method, pathname, url, request, deps) {
2603
3167
  }
2604
3168
  if (method === "POST" && pathname === "/config") {
2605
3169
  const body = await readBody2(request);
2606
- const next = normalizeSettings(body.settings ?? body);
3170
+ const incoming = normalizeSettings(body.settings ?? body);
3171
+ const section = typeof body.section === "string" && SETTINGS_SECTIONS.includes(body.section) ? body.section : void 0;
3172
+ let next = section === void 0 ? incoming : mergeSettingsSection(settings, incoming, section);
3173
+ if (next.repo.path.trim() !== "" && (section === void 0 || section === "repo")) {
3174
+ const branch = await readDefaultBranch(next.repo.path, { env });
3175
+ if (branch.branch !== void 0) next.repo.baseBranch = branch.branch;
3176
+ }
2607
3177
  const core = await inspectCore({ ...next, corePath: settings.corePath }, { env });
2608
3178
  const providers = core.supportedProviders.length > 0 ? core.supportedProviders : void 0;
2609
- const problems = validateSettings(next, providers);
3179
+ const allProblems = validateSettings(next, providers);
3180
+ const problems = section === void 0 ? allProblems : allProblems.filter((problem) => sectionOfField(problem.field) === section);
2610
3181
  if (problems.length > 0) {
2611
3182
  return { saved: false, problems, settings: next };
2612
3183
  }
@@ -2636,25 +3207,52 @@ async function dispatch(method, pathname, url, request, deps) {
2636
3207
  if (method === "POST" && pathname === "/run/cancel") {
2637
3208
  return { cancelled: deps.runner.cancel() };
2638
3209
  }
3210
+ if (method === "GET" && pathname === "/repo/branch") {
3211
+ const repoPath = url.searchParams.get("path") ?? settings.repo.path;
3212
+ return { repo: await readDefaultBranch(repoPath, { env }) };
3213
+ }
3214
+ if (method === "POST" && pathname === "/github/test") {
3215
+ const body = await readBody2(request);
3216
+ const candidate = body.settings === void 0 ? settings : normalizeSettings(body.settings);
3217
+ const probe = await probeGithubAuth({
3218
+ credentials: credentialServices(deps.get),
3219
+ authMode: candidate.github.authMode,
3220
+ tokenRef: candidate.github.tokenRef,
3221
+ env
3222
+ });
3223
+ return { github: probe };
3224
+ }
2639
3225
  if (method === "GET" && pathname === "/quickfill") {
2640
3226
  const candidates = await listProviderCandidates({
2641
3227
  llm: llmServices(deps.get),
2642
3228
  credentials: credentialServices(deps.get),
2643
3229
  env
2644
3230
  });
2645
- return { providers: candidates };
3231
+ return { providers: candidates, defaultSelection: readDefaultSelection(deps.get) ?? null };
2646
3232
  }
2647
3233
  if (method === "POST" && pathname === "/quickfill") {
2648
3234
  const body = await readBody2(request);
2649
- const providerId = typeof body.providerId === "string" ? body.providerId : "";
2650
- const modelId = typeof body.modelId === "string" ? body.modelId : "";
2651
3235
  const candidates = await listProviderCandidates({
2652
3236
  llm: llmServices(deps.get),
2653
3237
  credentials: credentialServices(deps.get),
2654
3238
  env
2655
3239
  });
3240
+ if (body.useDefault === true) {
3241
+ const selection = readDefaultSelection(deps.get);
3242
+ if (selection === void 0) throw new IpcError("CONFIG_INVALID", "\u5BBF\u4E3B\u6CA1\u6709\u53EF\u6CBF\u7528\u7684\u9ED8\u8BA4\u6A21\u578B");
3243
+ const candidate2 = candidates.find((item) => item.id === selection.provider);
3244
+ const next2 = applyDefaultSelection(settings, selection, candidate2);
3245
+ deps.store.writeSettings(next2);
3246
+ return {
3247
+ saved: true,
3248
+ settings: next2,
3249
+ applied: { provider: selection.provider, model: selection.model, mode: next2.model.mode }
3250
+ };
3251
+ }
3252
+ const providerId = typeof body.providerId === "string" ? body.providerId : "";
3253
+ const modelId = typeof body.modelId === "string" ? body.modelId : "";
2656
3254
  const candidate = candidates.find((item) => item.id === providerId);
2657
- if (candidate === void 0) throw new IpcError("CONFIG_INVALID", `\u5BBF\u4E3B\u6CA1\u6709\u8FD9\u4E2A provider\uFF1A${providerId}`);
3255
+ if (candidate === void 0) throw new IpcError("CONFIG_INVALID", `\u5BBF\u4E3B\u6CA1\u6709\u8FD9\u4E2A\u5DF2\u914D\u7F6E\u7684 provider\uFF1A${providerId}`);
2658
3256
  if (modelId === "") throw new IpcError("CONFIG_INVALID", "\u5FC5\u987B\u9009\u62E9\u4E00\u4E2A\u6A21\u578B");
2659
3257
  const next = applyCandidate(settings, candidate, modelId);
2660
3258
  deps.store.writeSettings(next);
@@ -2665,6 +3263,19 @@ async function dispatch(method, pathname, url, request, deps) {
2665
3263
  if (result.ok && result.settings !== void 0) deps.store.writeSettings(result.settings);
2666
3264
  return { import: { ...result, settings: result.settings === void 0 ? void 0 : result.settings } };
2667
3265
  }
3266
+ if (method === "GET" && pathname === "/preview") {
3267
+ const id = url.searchParams.get("id") ?? "";
3268
+ if (!/^[A-Za-z0-9-]+$/.test(id)) throw new IpcError("CONFIG_INVALID", "\u975E\u6CD5\u7684\u8FD0\u884C id");
3269
+ const state = deps.store.readState();
3270
+ const history = deps.store.readHistory();
3271
+ const record = history.find((item) => item.id === id) ?? (state.lastResult?.id === id ? state.lastResult : void 0);
3272
+ if (record === void 0 || record.preview === void 0) {
3273
+ throw new IpcError("NOT_FOUND", "\u6CA1\u6709\u8FD9\u6B21\u8FD0\u884C\u7684\u9884\u89C8\u526F\u672C");
3274
+ }
3275
+ const preview = deps.store.readPreview(id);
3276
+ if (preview === void 0) throw new IpcError("NOT_FOUND", "\u9884\u89C8\u526F\u672C\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u6E05\u7406");
3277
+ return { id, preview };
3278
+ }
2668
3279
  if (method === "GET" && pathname === "/log") {
2669
3280
  const id = url.searchParams.get("id") ?? "";
2670
3281
  if (!/^[A-Za-z0-9-]+$/.test(id)) throw new IpcError("CONFIG_INVALID", "\u975E\u6CD5\u7684\u8FD0\u884C id");
@@ -2678,13 +3289,33 @@ async function statePayload(deps, settings) {
2678
3289
  const env = deps.env ?? process.env;
2679
3290
  const credentials = credentialServices(deps.get);
2680
3291
  const state = deps.store.readState();
2681
- const history = deps.store.readHistory().slice(0, 10);
3292
+ const history = deps.store.readHistory();
2682
3293
  const core = await inspectCore(settings, { env });
2683
3294
  const coexistence = await detectCoexistence(core.path, { env });
2684
3295
  const modelCredential = await describeRef(credentials, settings.model.apiKeyRef, env);
2685
3296
  const github = await resolveGithubToken(credentials, settings.github.authMode, settings.github.tokenRef, { env });
3297
+ let pendingPr = state.pendingPr ?? null;
3298
+ let pendingPrState;
3299
+ if (pendingPr !== null) {
3300
+ const status = await checkPendingPr({
3301
+ url: pendingPr.url,
3302
+ token: github.token,
3303
+ ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl }
3304
+ });
3305
+ pendingPrState = status.state;
3306
+ if (status.state === "merged" || status.state === "closed") {
3307
+ const next = { ...state };
3308
+ delete next.pendingPr;
3309
+ deps.store.writeState(next);
3310
+ pendingPr = null;
3311
+ }
3312
+ }
2686
3313
  return {
2687
- plugin: { name: "@morehao/dsh-github-trending", version: deps.version },
3314
+ plugin: {
3315
+ name: deps.pluginName ?? "@morehao/dsh-github-trending",
3316
+ version: deps.version,
3317
+ ...deps.repository === void 0 ? {} : { repository: deps.repository }
3318
+ },
2688
3319
  settings,
2689
3320
  core,
2690
3321
  coexistence,
@@ -2694,7 +3325,8 @@ async function statePayload(deps, settings) {
2694
3325
  lastRunAt: state.lastRunAt ?? null,
2695
3326
  lastDigest: state.lastDigest ?? null,
2696
3327
  lastResult: state.lastResult ?? null,
2697
- pendingPr: state.pendingPr ?? null,
3328
+ pendingPr,
3329
+ ...pendingPrState === void 0 ? {} : { pendingPrState },
2698
3330
  history
2699
3331
  },
2700
3332
  model: {
@@ -2792,7 +3424,7 @@ function createTrendingRunTool(runner) {
2792
3424
  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",
2793
3425
  parameters: {
2794
3426
  dryRun: { type: "boolean", description: "\u53EA\u8BD5\u8DD1\uFF1A\u4E0D\u5199\u6587\u4EF6\u3001\u4E0D\u63D0\u4EA4\u3001\u4E0D\u5F00 PR" },
2795
- date: { type: "string", description: "\u6307\u5B9A\u65E5\u671F\uFF08YYYY-MM-DD\uFF09\uFF1B\u7701\u7565\u5219\u7528\u4ECA\u5929" }
3427
+ date: { type: "string", description: "\u6307\u5B9A\u65E5\u671F\uFF08YYYY-MM-DD\uFF09\uFF1B\u7701\u7565\u5219\u7528\u8C03\u5EA6\u65F6\u533A\u7684\u4ECA\u5929" }
2796
3428
  },
2797
3429
  output: {
2798
3430
  schema: RUN_OUTPUT_SCHEMA,
@@ -2894,12 +3526,17 @@ export {
2894
3526
  credentials_exports,
2895
3527
  quickfill_exports,
2896
3528
  proxy_exports,
2897
- Runner,
2898
- runner_exports,
2899
3529
  Scheduler,
2900
3530
  schedule_exports,
3531
+ Runner,
3532
+ runner_exports,
3533
+ readPluginInfo,
3534
+ plugin_info_exports,
3535
+ github_exports,
2901
3536
  import_cli_exports,
3537
+ pending_pr_exports,
2902
3538
  preflight_exports,
3539
+ repo_exports,
2903
3540
  ROUTE_PREFIX,
2904
3541
  createRoutes,
2905
3542
  routes_exports,