@morehao/dsh-github-trending 0.2.0 → 0.3.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.
@@ -338,14 +338,21 @@ var HISTORY_FILE = "history.json";
338
338
  // src/state.ts
339
339
  var state_exports = {};
340
340
  __export(state_exports, {
341
+ CONTENT_HASH_ALGO_V2: () => CONTENT_HASH_ALGO_V2,
341
342
  HISTORY_LIMIT: () => HISTORY_LIMIT,
343
+ PREVIEW_LIMIT: () => PREVIEW_LIMIT,
344
+ PREVIEW_MAX_BYTES: () => PREVIEW_MAX_BYTES,
342
345
  Store: () => Store,
343
346
  newRunId: () => newRunId,
344
347
  writeJsonAtomic: () => writeJsonAtomic
345
348
  });
346
- import { mkdirSync, readFileSync, renameSync, writeFileSync, appendFileSync, existsSync } from "fs";
349
+ import { mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, appendFileSync, existsSync } from "fs";
347
350
  import path3 from "path";
348
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];
349
356
  function readJson(file, fallback) {
350
357
  try {
351
358
  const text = readFileSync(file, "utf8");
@@ -359,6 +366,32 @@ function readJson(file, fallback) {
359
366
  function ensureDir(dir) {
360
367
  mkdirSync(dir, { recursive: true, mode: 448 });
361
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
+ }
362
395
  function writeJsonAtomic(file, value) {
363
396
  ensureDir(path3.dirname(file));
364
397
  const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
@@ -385,6 +418,9 @@ var Store = class {
385
418
  get logsPath() {
386
419
  return path3.join(this.dir, "runs");
387
420
  }
421
+ get previewsPath() {
422
+ return path3.join(this.dir, "previews");
423
+ }
388
424
  readSettings() {
389
425
  return normalizeSettings(readJson(this.settingsPath, {}));
390
426
  }
@@ -392,18 +428,49 @@ var Store = class {
392
428
  writeJsonAtomic(this.settingsPath, settings);
393
429
  }
394
430
  readState() {
395
- return readJson(this.statePath, {});
431
+ return normalizeState(readJson(this.statePath, {}));
396
432
  }
397
433
  writeState(state) {
398
434
  writeJsonAtomic(this.statePath, state);
399
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
+ }
400
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() {
401
468
  const file = readJson(this.historyPath, { records: [] });
402
469
  return Array.isArray(file.records) ? file.records : [];
403
470
  }
404
471
  /** 记录一次运行:历史裁剪到 30 条,state 同步更新。 */
405
472
  recordRun(record, next = {}) {
406
- const records = [record, ...this.readHistory()].slice(0, HISTORY_LIMIT);
473
+ const records = [record, ...this.readHistoryRaw()].slice(0, HISTORY_LIMIT);
407
474
  writeJsonAtomic(this.historyPath, { records });
408
475
  const state = this.readState();
409
476
  this.writeState({ ...state, ...next, lastResult: record });
@@ -428,6 +495,61 @@ var Store = class {
428
495
  return "";
429
496
  }
430
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
+ }
431
553
  };
432
554
  function newRunId(now = /* @__PURE__ */ new Date()) {
433
555
  const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
@@ -443,6 +565,7 @@ __export(errors_exports, {
443
565
  classifyStderr: () => classifyStderr,
444
566
  hintOf: () => hintOf,
445
567
  isRetriable: () => isRetriable,
568
+ reasonOf: () => reasonOf,
446
569
  redact: () => redact
447
570
  });
448
571
  var PLUGIN_CODES = [
@@ -505,9 +628,37 @@ function areaOf(code) {
505
628
  if (code === "GITHUB_AUTH") return "auth";
506
629
  return "run";
507
630
  }
508
- function hintOf(code, message) {
509
- if (code === "GIT_OPERATION_FAILED" && typeof message === "string" && /non-fast-forward|\[rejected\]|failed to push|fetch first/i.test(message)) {
510
- return "\u63A8\u9001\u88AB\u62D2\uFF08non-fast-forward\uFF09\uFF1A\u8FDC\u7AEF\u5DF2\u6709\u540C\u540D\u5206\u652F\u4E14\u5386\u53F2\u5206\u53C9\u3002\u5148\u5408\u5E76/\u5173\u95ED\u5E76\u5220\u9664\u8BE5\u8FDC\u7AEF\u5206\u652F\uFF0C\u6216\u8BA9\u6838\u5FC3\u4EE5 --force-with-lease \u63A8\u9001\u540E\u91CD\u8BD5\u3002";
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
+ }
511
662
  }
512
663
  switch (code) {
513
664
  case "CORE_UNAVAILABLE":
@@ -636,18 +787,17 @@ var ipc_exports = {};
636
787
  __export(ipc_exports, {
637
788
  IPC_SCHEMA_VERSION: () => IPC_SCHEMA_VERSION,
638
789
  IpcError: () => IpcError,
790
+ SUPPORTED_CONTRACT_MAJOR: () => SUPPORTED_CONTRACT_MAJOR,
639
791
  SUPPORTED_DIGEST_SCHEMA: () => SUPPORTED_DIGEST_SCHEMA,
640
792
  isVersionInfo: () => isVersionInfo,
641
793
  parseJsonObject: () => parseJsonObject,
642
794
  parseRunResult: () => parseRunResult,
643
795
  parseVersionInfo: () => parseVersionInfo
644
796
  });
645
-
646
- // src/ipc.gen.ts
647
- var IPC_SCHEMA_VERSION = 1;
648
-
649
- // src/ipc.ts
650
- 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;
651
801
  var IpcError = class extends Error {
652
802
  code;
653
803
  constructor(code, message) {
@@ -683,19 +833,6 @@ function parseVersionInfo(text) {
683
833
  }
684
834
  return value;
685
835
  }
686
- function normalizeFiles(value) {
687
- if (!Array.isArray(value)) return [];
688
- const files = [];
689
- for (const item of value) {
690
- if (!isRecord(item) || typeof item.path !== "string") continue;
691
- files.push({
692
- path: item.path,
693
- bytes: typeof item.bytes === "number" ? item.bytes : 0,
694
- sha256: typeof item.sha256 === "string" ? item.sha256 : ""
695
- });
696
- }
697
- return files;
698
- }
699
836
  function normalizeError(value) {
700
837
  if (!isRecord(value)) return void 0;
701
838
  if (typeof value.code !== "string" || typeof value.message !== "string") return void 0;
@@ -708,37 +845,24 @@ function normalizeError(value) {
708
845
  }
709
846
  function parseRunResult(text, options) {
710
847
  const value = parseJsonObject(text);
711
- const contractShaped = value.ipcSchema === IPC_SCHEMA_VERSION && typeof value.ok === "boolean" && value.command === "run";
712
- if (contractShaped) {
713
- const result = value;
714
- return {
715
- ok: result.ok,
716
- skipped: result.skipped === true,
717
- dryRun: options.dryRun,
718
- files: normalizeFiles(result.files),
719
- contentHash: typeof value.contentHash === "string" ? value.contentHash : void 0,
720
- branch: typeof value.branch === "string" ? value.branch : void 0,
721
- commit: typeof value.commit === "string" ? value.commit : void 0,
722
- prUrl: typeof value.prUrl === "string" ? value.prUrl : void 0,
723
- error: normalizeError(result.error),
724
- warnings: Array.isArray(result.warnings) ? result.warnings.filter((item) => typeof item === "string") : [],
725
- ...timing(options),
726
- coreVersion: options.coreVersion
727
- };
728
- }
729
- if (!Array.isArray(value.files)) {
730
- throw new IpcError("IPC_INVALID", "\u6838\u5FC3 run --json \u65E2\u4E0D\u662F\u5951\u7EA6 RunResult\uFF0C\u4E5F\u4E0D\u542B files \u6570\u7EC4");
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");
731
850
  }
732
- const digest = isRecord(value.digest) ? value.digest : void 0;
851
+ const result = value;
852
+ const error = normalizeError(result.error);
853
+ const skipReason = value.skipReason;
733
854
  return {
734
- ok: true,
735
- skipped: value.skipped === true,
855
+ ok: result.ok,
856
+ skipped: result.skipped === true,
736
857
  dryRun: options.dryRun,
737
- files: normalizeFiles(value.files),
738
- contentHash: typeof digest?.contentHash === "string" ? digest.contentHash : void 0,
739
- branch: typeof value.branch === "string" ? value.branch : void 0,
740
- prUrl: typeof value.prUrl === "string" ? value.prUrl : void 0,
741
- 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") : [],
742
866
  ...timing(options),
743
867
  coreVersion: options.coreVersion
744
868
  };
@@ -782,10 +906,11 @@ async function handshake(core, options = {}) {
782
906
  const detail = error instanceof Error ? error.message : String(error);
783
907
  throw new IpcError("CORE_INCOMPATIBLE", `\u6838\u5FC3 version --json \u8F93\u51FA\u65E0\u6CD5\u8BC6\u522B\uFF1A${detail}`);
784
908
  }
785
- 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) {
786
911
  throw new IpcError(
787
912
  "CORE_INCOMPATIBLE",
788
- `\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`
789
914
  );
790
915
  }
791
916
  const supportedProviders = Array.isArray(version.supportedProviders) && version.supportedProviders.length > 0 ? version.supportedProviders : [...DEFAULT_PROVIDERS];
@@ -803,7 +928,7 @@ __export(release_exports, {
803
928
  verifyCached: () => verifyCached
804
929
  });
805
930
  import { createHash } from "crypto";
806
- 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";
807
932
  import { createRequire } from "module";
808
933
  import { pipeline } from "stream/promises";
809
934
  import { Readable } from "stream";
@@ -870,7 +995,7 @@ function markerPath(binaryPath) {
870
995
  function verifyCached(binaryPath) {
871
996
  let info;
872
997
  try {
873
- info = statSync(binaryPath);
998
+ info = statSync2(binaryPath);
874
999
  } catch {
875
1000
  return { ok: false, reason: "missing" };
876
1001
  }
@@ -984,7 +1109,7 @@ async function ensureCore(options = {}) {
984
1109
  );
985
1110
  }
986
1111
  try {
987
- unlinkSync(descriptor.binaryPath);
1112
+ unlinkSync2(descriptor.binaryPath);
988
1113
  } catch {
989
1114
  }
990
1115
  }
@@ -1005,11 +1130,11 @@ __export(locate_exports, {
1005
1130
  locateCore: () => locateCore,
1006
1131
  overridePath: () => overridePath
1007
1132
  });
1008
- import { accessSync, constants, existsSync as existsSync3, statSync as statSync2 } from "fs";
1133
+ import { accessSync, constants, existsSync as existsSync3, statSync as statSync3 } from "fs";
1009
1134
  import path5 from "path";
1010
1135
  function executable(file) {
1011
1136
  try {
1012
- if (!statSync2(file).isFile()) return false;
1137
+ if (!statSync3(file).isFile()) return false;
1013
1138
  accessSync(file, constants.X_OK);
1014
1139
  return true;
1015
1140
  } catch {
@@ -1225,7 +1350,12 @@ async function runCore(options) {
1225
1350
  `;
1226
1351
  const secrets = [options.apiKey, options.token, ...options.secrets ?? []];
1227
1352
  const child = spawn2(options.core.path, buildCoreArgs(channel.path, { dryRun: options.dryRun, date: options.date }), {
1228
- 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
+ }),
1229
1359
  stdio: ["ignore", "pipe", "pipe"],
1230
1360
  windowsHide: true
1231
1361
  });
@@ -1356,12 +1486,12 @@ __export(core_exports, {
1356
1486
  findOnPath: () => findOnPath,
1357
1487
  inspectCore: () => inspectCore
1358
1488
  });
1359
- 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";
1360
1490
  import path7 from "path";
1361
1491
  var CACHE_TTL_MS = 3e4;
1362
1492
  function executable2(file) {
1363
1493
  try {
1364
- if (!statSync3(file).isFile()) return false;
1494
+ if (!statSync4(file).isFile()) return false;
1365
1495
  accessSync2(file, constants3.X_OK);
1366
1496
  return true;
1367
1497
  } catch {
@@ -1457,7 +1587,7 @@ async function detectCoexistence(ownPath, options = {}) {
1457
1587
  };
1458
1588
  } else {
1459
1589
  const info = JSON.parse(result.stdout);
1460
- const compatible = info.ipcSchema === 1 && info.digestSchema === 1;
1590
+ const compatible = info.ipcSchema === IPC_SCHEMA_VERSION && info.digestSchema === SUPPORTED_DIGEST_SCHEMA;
1461
1591
  status = {
1462
1592
  found: true,
1463
1593
  path: found,
@@ -1863,6 +1993,8 @@ __export(schedule_exports, {
1863
1993
  });
1864
1994
  var MINUTE = 6e4;
1865
1995
  var CATCH_UP_MINUTES = 6 * 60;
1996
+ var MAX_ATTEMPTS_PER_DAY = 2;
1997
+ var RETRY_GAP_MS = 5 * MINUTE;
1866
1998
  function partsOf(date, timezone) {
1867
1999
  const formatter = new Intl.DateTimeFormat("en-US", {
1868
2000
  timeZone: timezone,
@@ -1938,7 +2070,8 @@ var Scheduler = class {
1938
2070
  deps;
1939
2071
  timerHandle;
1940
2072
  stopped = false;
1941
- lastFiredFor;
2073
+ /** 当天已尝试次数与时间;只在内存里,进程重启后重新计数。 */
2074
+ attempts = /* @__PURE__ */ new Map();
1942
2075
  constructor(deps) {
1943
2076
  this.deps = deps;
1944
2077
  }
@@ -1972,11 +2105,18 @@ var Scheduler = class {
1972
2105
  if (!isDue(now, schedule)) return "skipped";
1973
2106
  const today = zonedDateString(now, schedule.timezone);
1974
2107
  const state = this.deps.store.readState();
1975
- const lastDate = state.lastRunAt === void 0 ? void 0 : zonedDateString(new Date(state.lastRunAt), schedule.timezone);
1976
- if (lastDate === today) return "skipped";
1977
- if (this.lastFiredFor === today) return "skipped";
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
+ }
1978
2115
  if (this.deps.isRunning()) return "busy";
1979
- this.lastFiredFor = today;
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() });
1980
2120
  try {
1981
2121
  await this.deps.run("schedule");
1982
2122
  return "ran";
@@ -2023,13 +2163,14 @@ var runner_exports = {};
2023
2163
  __export(runner_exports, {
2024
2164
  Runner: () => Runner
2025
2165
  });
2026
- 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";
2027
2168
  import path8 from "path";
2028
2169
  function repoProblem(repoPath) {
2029
2170
  if (repoPath.trim() === "") return { code: "CONFIG_INVALID", message: "\u8FD8\u6CA1\u6709\u9009\u62E9\u5F52\u6863\u4ED3\u5E93" };
2030
2171
  if (!existsSync4(repoPath)) return { code: "REPO_NOT_GIT", message: `\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${repoPath}` };
2031
2172
  try {
2032
- 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}` };
2033
2174
  } catch {
2034
2175
  return { code: "REPO_NOT_GIT", message: `\u65E0\u6CD5\u8BBF\u95EE\uFF1A${repoPath}` };
2035
2176
  }
@@ -2045,6 +2186,15 @@ function runDate(settings, now = /* @__PURE__ */ new Date()) {
2045
2186
  return now.toISOString().slice(0, 10);
2046
2187
  }
2047
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
+ }
2048
2198
  var Runner = class {
2049
2199
  active;
2050
2200
  deps;
@@ -2133,6 +2283,7 @@ var Runner = class {
2133
2283
  ...github.token === void 0 ? {} : { token: github.token },
2134
2284
  date,
2135
2285
  dryRun,
2286
+ trigger: options.trigger === "schedule" ? "schedule" : "manual",
2136
2287
  ...baseURLOverride === void 0 ? {} : { baseURLOverride },
2137
2288
  signal: controller.signal,
2138
2289
  secrets: [apiKey, github.token],
@@ -2150,6 +2301,14 @@ var Runner = class {
2150
2301
  coreSource: core.source,
2151
2302
  ...core.tag === void 0 ? {} : { coreTag: core.tag }
2152
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;
2153
2312
  this.record(summary, settings);
2154
2313
  return summary;
2155
2314
  } catch (error) {
@@ -2177,10 +2336,12 @@ var Runner = class {
2177
2336
  const { outcome } = input;
2178
2337
  const error = outcome.error === void 0 ? void 0 : (() => {
2179
2338
  const message = redact(outcome.error.message);
2339
+ const reason = reasonOf(outcome.error.code, message, outcome.error.details);
2180
2340
  return {
2181
2341
  code: outcome.error.code,
2182
2342
  message,
2183
- hint: hintOf(outcome.error.code, message),
2343
+ ...reason === void 0 ? {} : { reason },
2344
+ hint: hintOf(outcome.error.code, message, reason),
2184
2345
  area: areaOf(outcome.error.code),
2185
2346
  retriable: outcome.error.retriable,
2186
2347
  inferred: outcome.error.details?.inferred === true
@@ -2197,7 +2358,9 @@ var Runner = class {
2197
2358
  files: outcome.files.length,
2198
2359
  ...outcome.contentHash === void 0 ? {} : { contentHash: outcome.contentHash },
2199
2360
  ...outcome.branch === void 0 ? {} : { branch: outcome.branch },
2361
+ ...outcome.commit === void 0 ? {} : { commit: outcome.commit },
2200
2362
  ...outcome.prUrl === void 0 ? {} : { prUrl: outcome.prUrl },
2363
+ ...outcome.skipReason === void 0 ? {} : { skipReason: outcome.skipReason },
2201
2364
  ...error === void 0 ? {} : { error },
2202
2365
  warnings: outcome.warnings,
2203
2366
  logFile: input.logFile,
@@ -2224,6 +2387,68 @@ var Runner = class {
2224
2387
  logFile: input.logFile
2225
2388
  };
2226
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
+ }
2227
2452
  /** 写历史与状态;同一天的 skipped 运行不覆盖 lastDigest。 */
2228
2453
  record(summary, settings) {
2229
2454
  const store = this.deps.store;
@@ -2233,12 +2458,20 @@ var Runner = class {
2233
2458
  kind: summary.dryRun ? "dry-run" : "run",
2234
2459
  ok: summary.ok,
2235
2460
  skipped: summary.skipped,
2236
- ...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 },
2237
2467
  durationMs: summary.durationMs,
2238
2468
  files: summary.files,
2469
+ ...summary.date === void 0 ? {} : { date: summary.date },
2239
2470
  ...summary.contentHash === void 0 ? {} : { contentHash: summary.contentHash },
2240
2471
  ...summary.branch === void 0 ? {} : { branch: summary.branch },
2472
+ ...summary.commit === void 0 ? {} : { commit: summary.commit },
2241
2473
  ...summary.prUrl === void 0 ? {} : { prUrl: summary.prUrl },
2474
+ ...summary.preview === void 0 ? {} : { preview: summary.preview },
2242
2475
  coreVersion: summary.coreVersion,
2243
2476
  model: { provider: settings.model.provider, model: settings.model.model },
2244
2477
  languages: settings.trending.languages,
@@ -2248,11 +2481,13 @@ var Runner = class {
2248
2481
  const state = store.readState();
2249
2482
  const next = {
2250
2483
  lastRunAt: record.at,
2484
+ ...summary.ok ? { lastSuccessAt: record.at } : {},
2251
2485
  ...summary.ok && summary.prUrl !== void 0 ? { pendingPr: { url: summary.prUrl, createdAt: record.at, ...summary.date === void 0 ? {} : { date: summary.date } } } : {},
2252
2486
  ...summary.ok && !summary.skipped && summary.contentHash !== void 0 ? {
2253
2487
  lastDigest: {
2254
2488
  date: summary.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
2255
2489
  contentHash: summary.contentHash,
2490
+ contentHashAlgo: CONTENT_HASH_ALGO_V2,
2256
2491
  coreVersion: summary.coreVersion
2257
2492
  },
2258
2493
  ...state.pendingPr !== void 0 && summary.prUrl === void 0 ? { pendingPr: state.pendingPr } : {}
@@ -2268,6 +2503,48 @@ var Runner = class {
2268
2503
  }
2269
2504
  };
2270
2505
 
2506
+ // src/host/plugin-info.ts
2507
+ var plugin_info_exports = {};
2508
+ __export(plugin_info_exports, {
2509
+ normalizeRepositoryUrl: () => normalizeRepositoryUrl,
2510
+ readPluginInfo: () => readPluginInfo
2511
+ });
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;
2531
+ }
2532
+ }
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 }
2542
+ };
2543
+ } catch {
2544
+ return { name: DEFAULT_NAME, version: "0.0.0" };
2545
+ }
2546
+ }
2547
+
2271
2548
  // src/host/github.ts
2272
2549
  var github_exports = {};
2273
2550
  __export(github_exports, {
@@ -2335,7 +2612,7 @@ __export(import_cli_exports, {
2335
2612
  importFromCli: () => importFromCli,
2336
2613
  parseSimpleYaml: () => parseSimpleYaml
2337
2614
  });
2338
- import { readFileSync as readFileSync3 } from "fs";
2615
+ import { readFileSync as readFileSync4 } from "fs";
2339
2616
  import os3 from "os";
2340
2617
  import path9 from "path";
2341
2618
  function cliConfigCandidates(env = process.env, home = os3.homedir()) {
@@ -2427,7 +2704,7 @@ function isSecretKey(key) {
2427
2704
  function importFromCli(filePath, base = DEFAULT_SETTINGS) {
2428
2705
  let text;
2429
2706
  try {
2430
- text = readFileSync3(filePath, "utf8");
2707
+ text = readFileSync4(filePath, "utf8");
2431
2708
  } catch (error) {
2432
2709
  return {
2433
2710
  ok: false,
@@ -2777,11 +3054,11 @@ var repo_exports = {};
2777
3054
  __export(repo_exports, {
2778
3055
  readDefaultBranch: () => readDefaultBranch
2779
3056
  });
2780
- import { statSync as statSync5 } from "fs";
3057
+ import { statSync as statSync6 } from "fs";
2781
3058
  import path10 from "path";
2782
3059
  function isDirectory(dir) {
2783
3060
  try {
2784
- return statSync5(dir).isDirectory();
3061
+ return statSync6(dir).isDirectory();
2785
3062
  } catch {
2786
3063
  return false;
2787
3064
  }
@@ -2819,6 +3096,7 @@ __export(routes_exports, {
2819
3096
  ROUTE_PREFIX: () => ROUTE_PREFIX,
2820
3097
  createRoutes: () => createRoutes
2821
3098
  });
3099
+ import { PluginUpdateError } from "@morehao/dsh-plugin-kit/update";
2822
3100
  var ROUTE_PREFIX = "/api/morehao/github-trending";
2823
3101
  var HEADER = "x-dsh-github-trending";
2824
3102
  var MAX_BODY2 = 512 * 1024;
@@ -2866,10 +3144,21 @@ function createRoutes(deps) {
2866
3144
  if (!response.writableEnded) sendJson2(response, 200, { ok: true, ...result });
2867
3145
  } catch (error) {
2868
3146
  if (response.writableEnded) return;
3147
+ if (error instanceof PluginUpdateError) {
3148
+ deps.logger?.("update.failed", { code: error.code, message: error.message });
3149
+ sendJson2(response, 409, {
3150
+ ok: false,
3151
+ code: "UPDATE_FAILED",
3152
+ updateCode: error.code,
3153
+ message: redact(error.message),
3154
+ update: deps.update?.view() ?? null
3155
+ });
3156
+ return;
3157
+ }
2869
3158
  const code = error instanceof IpcError ? error.code : "INTERNAL";
2870
3159
  const message = redact(error instanceof Error ? error.message : String(error));
2871
3160
  deps.logger?.("route.error", { pathname, method, code, message });
2872
- const status = code === "CONFIG_INVALID" ? 400 : 500;
3161
+ const status = code === "CONFIG_INVALID" ? 400 : code === "NOT_FOUND" ? 404 : 500;
2873
3162
  sendJson2(response, status, { ok: false, code, message });
2874
3163
  }
2875
3164
  };
@@ -2878,6 +3167,18 @@ async function dispatch(method, pathname, url, request, deps) {
2878
3167
  const env = deps.env ?? process.env;
2879
3168
  const settings = deps.store.readSettings();
2880
3169
  if (method === "GET" && pathname === "/state") return statePayload(deps, settings);
3170
+ if (method === "GET" && pathname === "/update") {
3171
+ if (deps.update === void 0) throw new IpcError("NOT_FOUND", "\u81EA\u66F4\u65B0\u670D\u52A1\u4E0D\u53EF\u7528");
3172
+ const update = await deps.update.check({ fresh: url.searchParams.get("fresh") === "1" });
3173
+ return { update };
3174
+ }
3175
+ if (method === "POST" && pathname === "/update") {
3176
+ if (deps.update === void 0) throw new IpcError("NOT_FOUND", "\u81EA\u66F4\u65B0\u670D\u52A1\u4E0D\u53EF\u7528");
3177
+ const body = await readBody2(request);
3178
+ const version = typeof body.version === "string" && body.version.trim() !== "" ? body.version.trim() : void 0;
3179
+ const result = await deps.update.apply(version === void 0 ? {} : { version });
3180
+ return { result, update: deps.update.view() };
3181
+ }
2881
3182
  if (method === "GET" && pathname === "/core") {
2882
3183
  const core = await inspectCore(settings, { env, fresh: url.searchParams.get("fresh") === "1" });
2883
3184
  const coexistence = await detectCoexistence(core.path, { env });
@@ -2986,6 +3287,19 @@ async function dispatch(method, pathname, url, request, deps) {
2986
3287
  if (result.ok && result.settings !== void 0) deps.store.writeSettings(result.settings);
2987
3288
  return { import: { ...result, settings: result.settings === void 0 ? void 0 : result.settings } };
2988
3289
  }
3290
+ if (method === "GET" && pathname === "/preview") {
3291
+ const id = url.searchParams.get("id") ?? "";
3292
+ if (!/^[A-Za-z0-9-]+$/.test(id)) throw new IpcError("CONFIG_INVALID", "\u975E\u6CD5\u7684\u8FD0\u884C id");
3293
+ const state = deps.store.readState();
3294
+ const history = deps.store.readHistory();
3295
+ const record = history.find((item) => item.id === id) ?? (state.lastResult?.id === id ? state.lastResult : void 0);
3296
+ if (record === void 0 || record.preview === void 0) {
3297
+ throw new IpcError("NOT_FOUND", "\u6CA1\u6709\u8FD9\u6B21\u8FD0\u884C\u7684\u9884\u89C8\u526F\u672C");
3298
+ }
3299
+ const preview = deps.store.readPreview(id);
3300
+ if (preview === void 0) throw new IpcError("NOT_FOUND", "\u9884\u89C8\u526F\u672C\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u6E05\u7406");
3301
+ return { id, preview };
3302
+ }
2989
3303
  if (method === "GET" && pathname === "/log") {
2990
3304
  const id = url.searchParams.get("id") ?? "";
2991
3305
  if (!/^[A-Za-z0-9-]+$/.test(id)) throw new IpcError("CONFIG_INVALID", "\u975E\u6CD5\u7684\u8FD0\u884C id");
@@ -2999,7 +3313,7 @@ async function statePayload(deps, settings) {
2999
3313
  const env = deps.env ?? process.env;
3000
3314
  const credentials = credentialServices(deps.get);
3001
3315
  const state = deps.store.readState();
3002
- const history = deps.store.readHistory().slice(0, 10);
3316
+ const history = deps.store.readHistory();
3003
3317
  const core = await inspectCore(settings, { env });
3004
3318
  const coexistence = await detectCoexistence(core.path, { env });
3005
3319
  const modelCredential = await describeRef(credentials, settings.model.apiKeyRef, env);
@@ -3021,7 +3335,11 @@ async function statePayload(deps, settings) {
3021
3335
  }
3022
3336
  }
3023
3337
  return {
3024
- plugin: { name: "@morehao/dsh-github-trending", version: deps.version },
3338
+ plugin: {
3339
+ name: deps.pluginName ?? "@morehao/dsh-github-trending",
3340
+ version: deps.version,
3341
+ ...deps.repository === void 0 ? {} : { repository: deps.repository }
3342
+ },
3025
3343
  settings,
3026
3344
  core,
3027
3345
  coexistence,
@@ -3053,6 +3371,57 @@ async function statePayload(deps, settings) {
3053
3371
  };
3054
3372
  }
3055
3373
 
3374
+ // src/host/update.ts
3375
+ var update_exports = {};
3376
+ __export(update_exports, {
3377
+ createUpdateService: () => createUpdateService
3378
+ });
3379
+ import { normalizeProfile } from "@morehao/dsh-plugin-kit";
3380
+ import { createPluginUpdater } from "@morehao/dsh-plugin-kit/update";
3381
+ function pnpmViewRunner(get, env, exec) {
3382
+ return async (spec) => {
3383
+ const profile = normalizeProfile(get("profileContext"));
3384
+ if (profile === null || profile.dir === void 0) return null;
3385
+ const command = profile.packageManager?.command ?? "pnpm";
3386
+ const prefix = profile.packageManager?.args ?? [];
3387
+ let result;
3388
+ try {
3389
+ result = await exec(command, [...prefix, "view", spec, "version", "--json"], {
3390
+ cwd: profile.dir,
3391
+ env: sanitizedEnv(env),
3392
+ timeoutMs: 8e3,
3393
+ maxStdout: 64 * 1024,
3394
+ maxStderr: 16 * 1024
3395
+ });
3396
+ } catch {
3397
+ return null;
3398
+ }
3399
+ if (result.code !== 0) return null;
3400
+ const text = result.stdout.trim();
3401
+ if (text === "") return null;
3402
+ let parsed;
3403
+ try {
3404
+ parsed = JSON.parse(text);
3405
+ } catch {
3406
+ return null;
3407
+ }
3408
+ const value = Array.isArray(parsed) ? parsed[parsed.length - 1] : parsed;
3409
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
3410
+ };
3411
+ }
3412
+ function createUpdateService(deps) {
3413
+ const exec = deps.exec ?? execCapture;
3414
+ return createPluginUpdater({
3415
+ meta: deps.meta,
3416
+ getPluginManager: () => deps.get("pluginManager"),
3417
+ getProfile: () => deps.get("profileContext"),
3418
+ runPnpmView: pnpmViewRunner(deps.get, deps.env, exec),
3419
+ ...deps.fetchImpl === void 0 ? {} : { fetchImpl: deps.fetchImpl },
3420
+ ...deps.readFile === void 0 ? {} : { readFile: deps.readFile },
3421
+ ...deps.logger === void 0 ? {} : { logger: deps.logger }
3422
+ });
3423
+ }
3424
+
3056
3425
  // src/tools.ts
3057
3426
  var tools_exports = {};
3058
3427
  __export(tools_exports, {
@@ -3236,6 +3605,8 @@ export {
3236
3605
  schedule_exports,
3237
3606
  Runner,
3238
3607
  runner_exports,
3608
+ readPluginInfo,
3609
+ plugin_info_exports,
3239
3610
  github_exports,
3240
3611
  import_cli_exports,
3241
3612
  pending_pr_exports,
@@ -3244,6 +3615,8 @@ export {
3244
3615
  ROUTE_PREFIX,
3245
3616
  createRoutes,
3246
3617
  routes_exports,
3618
+ createUpdateService,
3619
+ update_exports,
3247
3620
  createTrendingRunTool,
3248
3621
  createTrendingHistoryTool,
3249
3622
  tools_exports