@morehao/dsh-github-trending 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +10 -6
- package/client.js +1289 -206
- package/lib/{chunk-6HITSHK6.js → chunk-WDBZAMWN.js} +632 -147
- package/lib/index.js +5 -1
- package/lib/internal.d.ts +251 -20
- package/lib/internal.js +1 -1
- package/package.json +2 -2
|
@@ -74,9 +74,14 @@ var DEFAULT_SETTINGS = {
|
|
|
74
74
|
includeDrafts: false,
|
|
75
75
|
fallbackToTags: true,
|
|
76
76
|
baselineOnFirstRun: true,
|
|
77
|
-
perRepoLimit:
|
|
77
|
+
perRepoLimit: 10,
|
|
78
78
|
maxNotesChars: 4e3,
|
|
79
|
-
|
|
79
|
+
summaryMaxChars: 1200,
|
|
80
|
+
docMaxLines: 600,
|
|
81
|
+
concurrency: "auto",
|
|
82
|
+
budgetSeconds: "auto",
|
|
83
|
+
period: "weekly",
|
|
84
|
+
schedule: { enabled: true, weekday: 1, time: "08:00:00", timezone: "Asia/Shanghai" }
|
|
80
85
|
},
|
|
81
86
|
output: { dir: "trending" },
|
|
82
87
|
schedule: { enabled: true, time: "08:30:00", timezone: "Asia/Shanghai", periods: ["daily", "weekly", "monthly"] },
|
|
@@ -148,6 +153,11 @@ function strArray(value, fallback) {
|
|
|
148
153
|
const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
149
154
|
return items.length > 0 ? items.map((item) => item.trim()) : fallback;
|
|
150
155
|
}
|
|
156
|
+
function autoNumber(value, fallback) {
|
|
157
|
+
if (value === "auto") return "auto";
|
|
158
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.round(value);
|
|
159
|
+
return fallback;
|
|
160
|
+
}
|
|
151
161
|
function periodList(_value, _fallback) {
|
|
152
162
|
return [...PERIODS];
|
|
153
163
|
}
|
|
@@ -160,6 +170,7 @@ function normalizeSettings(input) {
|
|
|
160
170
|
const trending = raw.trending ?? {};
|
|
161
171
|
const summarize = raw.summarize ?? {};
|
|
162
172
|
const releases = raw.releases ?? {};
|
|
173
|
+
const releasesSchedule = releases.schedule ?? {};
|
|
163
174
|
const output = raw.output ?? {};
|
|
164
175
|
const schedule = raw.schedule ?? {};
|
|
165
176
|
const commitMode = str(repo.commitMode, DEFAULT_SETTINGS.repo.commitMode);
|
|
@@ -220,7 +231,17 @@ function normalizeSettings(input) {
|
|
|
220
231
|
baselineOnFirstRun: bool(releases.baselineOnFirstRun, DEFAULT_SETTINGS.releases.baselineOnFirstRun),
|
|
221
232
|
perRepoLimit: Math.round(num(releases.perRepoLimit, DEFAULT_SETTINGS.releases.perRepoLimit)),
|
|
222
233
|
maxNotesChars: Math.round(num(releases.maxNotesChars, DEFAULT_SETTINGS.releases.maxNotesChars)),
|
|
223
|
-
|
|
234
|
+
summaryMaxChars: Math.round(num(releases.summaryMaxChars, DEFAULT_SETTINGS.releases.summaryMaxChars)),
|
|
235
|
+
docMaxLines: Math.round(num(releases.docMaxLines, DEFAULT_SETTINGS.releases.docMaxLines)),
|
|
236
|
+
concurrency: autoNumber(releases.concurrency, DEFAULT_SETTINGS.releases.concurrency),
|
|
237
|
+
budgetSeconds: autoNumber(releases.budgetSeconds, DEFAULT_SETTINGS.releases.budgetSeconds),
|
|
238
|
+
period: isPeriod(releasesPeriod) ? releasesPeriod : DEFAULT_SETTINGS.releases.period,
|
|
239
|
+
schedule: {
|
|
240
|
+
enabled: bool(releasesSchedule.enabled, DEFAULT_SETTINGS.releases.schedule.enabled),
|
|
241
|
+
weekday: Math.round(num(releasesSchedule.weekday, DEFAULT_SETTINGS.releases.schedule.weekday)),
|
|
242
|
+
time: str(releasesSchedule.time, DEFAULT_SETTINGS.releases.schedule.time),
|
|
243
|
+
timezone: str(releasesSchedule.timezone, DEFAULT_SETTINGS.releases.schedule.timezone)
|
|
244
|
+
}
|
|
224
245
|
},
|
|
225
246
|
output: {
|
|
226
247
|
dir: str(output.dir, DEFAULT_SETTINGS.output.dir).trim() || DEFAULT_SETTINGS.output.dir
|
|
@@ -245,6 +266,10 @@ function isValidTimezone(timezone) {
|
|
|
245
266
|
return false;
|
|
246
267
|
}
|
|
247
268
|
}
|
|
269
|
+
function autoInRange(value, min, max) {
|
|
270
|
+
if (value === "auto") return true;
|
|
271
|
+
return Number.isInteger(value) && value >= min && value <= max;
|
|
272
|
+
}
|
|
248
273
|
function validateSettings(settings, providers = CORE_PROVIDERS) {
|
|
249
274
|
const problems = [];
|
|
250
275
|
const { model, repo, trending, summarize, releases, output, schedule } = settings;
|
|
@@ -298,8 +323,12 @@ function validateSettings(settings, providers = CORE_PROVIDERS) {
|
|
|
298
323
|
if (!Number.isInteger(summarize.readmeChars) || summarize.readmeChars <= 0) {
|
|
299
324
|
problems.push({ field: "summarize.readmeChars", code: "CONFIG_INVALID", message: "readmeChars \u5FC5\u987B\u662F\u6B63\u6574\u6570" });
|
|
300
325
|
}
|
|
301
|
-
if (
|
|
302
|
-
problems.push({
|
|
326
|
+
if (releases.period !== "weekly") {
|
|
327
|
+
problems.push({
|
|
328
|
+
field: "releases.period",
|
|
329
|
+
code: "CONFIG_INVALID",
|
|
330
|
+
message: "\u672C\u7248\u672C\u53EA\u652F\u6301\u6BCF\u5468\uFF08weekly\uFF09\u53D1\u7248\u6458\u8981\uFF0C\u8BF7\u628A releases.period \u6539\u4E3A weekly"
|
|
331
|
+
});
|
|
303
332
|
}
|
|
304
333
|
if (!Number.isInteger(releases.perRepoLimit) || releases.perRepoLimit < 1 || releases.perRepoLimit > 20) {
|
|
305
334
|
problems.push({ field: "releases.perRepoLimit", code: "CONFIG_INVALID", message: "perRepoLimit \u5FC5\u987B\u662F 1\u201320 \u7684\u6574\u6570" });
|
|
@@ -307,6 +336,27 @@ function validateSettings(settings, providers = CORE_PROVIDERS) {
|
|
|
307
336
|
if (!Number.isInteger(releases.maxNotesChars) || releases.maxNotesChars < 500 || releases.maxNotesChars > 2e4) {
|
|
308
337
|
problems.push({ field: "releases.maxNotesChars", code: "CONFIG_INVALID", message: "maxNotesChars \u5FC5\u987B\u662F 500\u201320000 \u7684\u6574\u6570" });
|
|
309
338
|
}
|
|
339
|
+
if (!Number.isInteger(releases.summaryMaxChars) || releases.summaryMaxChars < 500 || releases.summaryMaxChars > 2e3) {
|
|
340
|
+
problems.push({ field: "releases.summaryMaxChars", code: "CONFIG_INVALID", message: "summaryMaxChars \u5FC5\u987B\u662F 500\u20132000 \u7684\u6574\u6570" });
|
|
341
|
+
}
|
|
342
|
+
if (!Number.isInteger(releases.docMaxLines) || releases.docMaxLines < 100 || releases.docMaxLines > 2e3) {
|
|
343
|
+
problems.push({ field: "releases.docMaxLines", code: "CONFIG_INVALID", message: "docMaxLines \u5FC5\u987B\u662F 100\u20132000 \u7684\u6574\u6570" });
|
|
344
|
+
}
|
|
345
|
+
if (!autoInRange(releases.concurrency, 1, 8)) {
|
|
346
|
+
problems.push({ field: "releases.concurrency", code: "CONFIG_INVALID", message: "concurrency \u5FC5\u987B\u662F auto \u6216 1\u20138 \u7684\u6574\u6570" });
|
|
347
|
+
}
|
|
348
|
+
if (!autoInRange(releases.budgetSeconds, 1, 300)) {
|
|
349
|
+
problems.push({ field: "releases.budgetSeconds", code: "CONFIG_INVALID", message: "budgetSeconds \u5FC5\u987B\u662F auto \u6216 1\u2013300 \u7684\u6574\u6570" });
|
|
350
|
+
}
|
|
351
|
+
if (!Number.isInteger(releases.schedule.weekday) || releases.schedule.weekday < 1 || releases.schedule.weekday > 7) {
|
|
352
|
+
problems.push({ field: "releases.schedule.weekday", code: "CONFIG_INVALID", message: "\u53D1\u7248\u65E5\u5FC5\u987B\u662F ISO \u661F\u671F 1\u20137\uFF081=\u5468\u4E00\uFF09" });
|
|
353
|
+
}
|
|
354
|
+
if (!TIME_RE.test(releases.schedule.time)) {
|
|
355
|
+
problems.push({ field: "releases.schedule.time", code: "CONFIG_INVALID", message: "\u65F6\u95F4\u683C\u5F0F\u5FC5\u987B\u662F HH:MM \u6216 HH:MM:SS" });
|
|
356
|
+
}
|
|
357
|
+
if (!isValidTimezone(releases.schedule.timezone)) {
|
|
358
|
+
problems.push({ field: "releases.schedule.timezone", code: "CONFIG_INVALID", message: "\u65F6\u533A\u5FC5\u987B\u662F IANA \u540D\u79F0\uFF08\u5982 Asia/Shanghai\uFF09" });
|
|
359
|
+
}
|
|
310
360
|
if (output.dir.trim() === "" || path.isAbsolute(output.dir) || output.dir.split(/[\\/]/).includes("..")) {
|
|
311
361
|
problems.push({ field: "output.dir", code: "CONFIG_INVALID", message: "\u8F93\u51FA\u76EE\u5F55\u5FC5\u987B\u662F\u4ED3\u5E93\u5185\u7684\u76F8\u5BF9\u8DEF\u5F84\uFF0C\u4E14\u4E0D\u542B .." });
|
|
312
362
|
}
|
|
@@ -321,7 +371,11 @@ function validateSettings(settings, providers = CORE_PROVIDERS) {
|
|
|
321
371
|
function effectiveBaseURL(model) {
|
|
322
372
|
return model.baseURL !== "" ? model.baseURL : PROVIDER_BASE_URLS[model.provider] ?? "";
|
|
323
373
|
}
|
|
324
|
-
function toCoreConfig(settings, apiKey, baseURLOverride, period) {
|
|
374
|
+
function toCoreConfig(settings, apiKey, baseURLOverride, period, repoCount = 0) {
|
|
375
|
+
const releases = settings.releases;
|
|
376
|
+
const repoTotal = Number.isFinite(repoCount) && repoCount > 0 ? Math.floor(repoCount) : 0;
|
|
377
|
+
const concurrency = releases.concurrency === "auto" ? Math.min(8, Math.max(4, Math.ceil(repoTotal / 10))) : releases.concurrency;
|
|
378
|
+
const budgetSeconds = releases.budgetSeconds === "auto" ? Math.min(300, 30 + 3 * repoTotal) : releases.budgetSeconds;
|
|
325
379
|
const config = {
|
|
326
380
|
model: {
|
|
327
381
|
provider: settings.model.provider,
|
|
@@ -363,13 +417,21 @@ function toCoreConfig(settings, apiKey, baseURLOverride, period) {
|
|
|
363
417
|
releases: {
|
|
364
418
|
// 发版订阅常开:插件侧不再提供开关,是否产出由订阅列表决定。
|
|
365
419
|
enabled: true,
|
|
366
|
-
includePrerelease:
|
|
367
|
-
includeDrafts:
|
|
368
|
-
fallbackToTags:
|
|
369
|
-
baselineOnFirstRun:
|
|
370
|
-
perRepoLimit:
|
|
371
|
-
maxNotesChars:
|
|
372
|
-
|
|
420
|
+
includePrerelease: releases.includePrerelease,
|
|
421
|
+
includeDrafts: releases.includeDrafts,
|
|
422
|
+
fallbackToTags: releases.fallbackToTags,
|
|
423
|
+
baselineOnFirstRun: releases.baselineOnFirstRun,
|
|
424
|
+
perRepoLimit: releases.perRepoLimit,
|
|
425
|
+
maxNotesChars: releases.maxNotesChars,
|
|
426
|
+
summaryMaxChars: releases.summaryMaxChars,
|
|
427
|
+
docMaxLines: releases.docMaxLines,
|
|
428
|
+
// 核心按这个时区切 ISO 周;必须与插件侧 lastReleaseWeek 的去重口径一致。
|
|
429
|
+
timezone: releases.schedule.timezone,
|
|
430
|
+
weekStart: "monday",
|
|
431
|
+
concurrency,
|
|
432
|
+
budgetSeconds,
|
|
433
|
+
// 核心只实现每周一次;非法值在校验阶段已拦下,这里不再透传枚举。
|
|
434
|
+
period: "weekly"
|
|
373
435
|
},
|
|
374
436
|
output: {
|
|
375
437
|
dir: settings.output.dir,
|
|
@@ -448,6 +510,7 @@ __export(state_exports, {
|
|
|
448
510
|
HISTORY_LIMIT: () => HISTORY_LIMIT,
|
|
449
511
|
PREVIEW_LIMIT: () => PREVIEW_LIMIT,
|
|
450
512
|
PREVIEW_MAX_BYTES: () => PREVIEW_MAX_BYTES,
|
|
513
|
+
RELEASE_RUN_LIMIT: () => RELEASE_RUN_LIMIT,
|
|
451
514
|
Store: () => Store,
|
|
452
515
|
newRunId: () => newRunId,
|
|
453
516
|
sumTokens: () => sumTokens,
|
|
@@ -460,6 +523,7 @@ var PREVIEW_LIMIT = 90;
|
|
|
460
523
|
var PREVIEW_MAX_BYTES = 512 * 1024;
|
|
461
524
|
var CONTENT_HASH_ALGO_V2 = "jcs-sha256-v1";
|
|
462
525
|
var V2_CORE_MIN = [0, 3, 0];
|
|
526
|
+
var RELEASE_RUN_LIMIT = 30;
|
|
463
527
|
function readJson(file, fallback) {
|
|
464
528
|
try {
|
|
465
529
|
const text = readFileSync(file, "utf8");
|
|
@@ -488,6 +552,11 @@ function coreVersionIsV2(coreVersion) {
|
|
|
488
552
|
function digestIsV2(digest) {
|
|
489
553
|
return digest !== void 0 && digest.contentHashAlgo === CONTENT_HASH_ALGO_V2 && coreVersionIsV2(digest.coreVersion);
|
|
490
554
|
}
|
|
555
|
+
function isReleaseRunRecord(value) {
|
|
556
|
+
if (typeof value !== "object" || value === null) return false;
|
|
557
|
+
const record = value;
|
|
558
|
+
return typeof record.id === "string" && typeof record.startedAt === "string" && typeof record.finishedAt === "string" && (record.status === "ok" || record.status === "failed" || record.status === "skipped" || record.status === "cancelled");
|
|
559
|
+
}
|
|
491
560
|
function normalizeState(state) {
|
|
492
561
|
const next = { ...state };
|
|
493
562
|
if (next.lastDigest !== void 0 && !digestIsV2(next.lastDigest)) {
|
|
@@ -582,6 +651,21 @@ var Store = class {
|
|
|
582
651
|
const state = this.readState();
|
|
583
652
|
this.writeState({ ...state, ...next, lastResult: record });
|
|
584
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* 记录一次发版运行:只写 `state.json`(不发版记录进 `history.json`,两者节奏不同)。
|
|
656
|
+
* 记录裁剪到 RELEASE_RUN_LIMIT 条,`next` 用来顺带更新 lastReleaseAt/lastReleaseWeek。
|
|
657
|
+
*/
|
|
658
|
+
appendReleaseRun(record, next = {}) {
|
|
659
|
+
const state = this.readState();
|
|
660
|
+
const runs = [record, ...Array.isArray(state.releaseRuns) ? state.releaseRuns : []].slice(0, RELEASE_RUN_LIMIT);
|
|
661
|
+
this.writeState({ ...state, ...next, releaseRuns: runs });
|
|
662
|
+
}
|
|
663
|
+
/** 读发版运行记录;缺失/损坏(不是数组或行不是对象)一律按空数组处理,不让界面崩。 */
|
|
664
|
+
readReleaseRuns() {
|
|
665
|
+
const state = this.readState();
|
|
666
|
+
if (!Array.isArray(state.releaseRuns)) return [];
|
|
667
|
+
return state.releaseRuns.filter(isReleaseRunRecord);
|
|
668
|
+
}
|
|
585
669
|
newLogPath(id) {
|
|
586
670
|
ensureDir(this.logsPath);
|
|
587
671
|
return path3.join(this.logsPath, `${id}.log`);
|
|
@@ -911,6 +995,8 @@ __export(ipc_exports, {
|
|
|
911
995
|
IpcError: () => IpcError,
|
|
912
996
|
SUPPORTED_CONTRACT_MAJOR: () => SUPPORTED_CONTRACT_MAJOR,
|
|
913
997
|
SUPPORTED_DIGEST_SCHEMA: () => SUPPORTED_DIGEST_SCHEMA,
|
|
998
|
+
isSubscriptionDoc: () => isSubscriptionDoc,
|
|
999
|
+
isSubscriptionStatus: () => isSubscriptionStatus,
|
|
914
1000
|
isVersionInfo: () => isVersionInfo,
|
|
915
1001
|
parseJsonObject: () => parseJsonObject,
|
|
916
1002
|
parseRunResult: () => parseRunResult,
|
|
@@ -921,6 +1007,17 @@ import { validators } from "@morehao/trendradar-contract";
|
|
|
921
1007
|
var IPC_SCHEMA_VERSION = 2;
|
|
922
1008
|
var SUPPORTED_DIGEST_SCHEMA = 2;
|
|
923
1009
|
var SUPPORTED_CONTRACT_MAJOR = 0;
|
|
1010
|
+
var SUBSCRIPTION_STATUSES = ["baseline", "ok", "no_change", "rate_limited", "failed"];
|
|
1011
|
+
function isSubscriptionStatus(value) {
|
|
1012
|
+
return typeof value === "string" && SUBSCRIPTION_STATUSES.includes(value);
|
|
1013
|
+
}
|
|
1014
|
+
function isSubscriptionDoc(value) {
|
|
1015
|
+
if (!isRecord(value)) return false;
|
|
1016
|
+
return typeof value.fullName === "string" && value.fullName !== "" && typeof value.volume === "number" && Number.isInteger(value.volume) && value.volume >= 1 && typeof value.markdown === "string";
|
|
1017
|
+
}
|
|
1018
|
+
function isSubscriptionItem(value) {
|
|
1019
|
+
return isRecord(value) && typeof value.fullName === "string" && value.fullName !== "" && isSubscriptionStatus(value.status);
|
|
1020
|
+
}
|
|
924
1021
|
var IpcError = class extends Error {
|
|
925
1022
|
code;
|
|
926
1023
|
constructor(code, message) {
|
|
@@ -996,6 +1093,16 @@ function timing(options) {
|
|
|
996
1093
|
return { startedAt: options.startedAt, finishedAt: options.finishedAt, durationMs: options.durationMs };
|
|
997
1094
|
}
|
|
998
1095
|
var REPO_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
1096
|
+
function parseSubscriptionList(value) {
|
|
1097
|
+
if (!isRecord(value)) throw new IpcError("IPC_INVALID", "\u6838\u5FC3 subscribe \u8FD4\u56DE\u7684 subscriptions \u4E0D\u662F\u5BF9\u8C61");
|
|
1098
|
+
const rawRepos = value.repos;
|
|
1099
|
+
if (!Array.isArray(rawRepos) || rawRepos.some((item) => typeof item !== "string")) {
|
|
1100
|
+
throw new IpcError("IPC_INVALID", "\u6838\u5FC3 subscribe \u8FD4\u56DE\u7684 subscriptions.repos \u4E0D\u662F\u5B57\u7B26\u4E32\u6570\u7EC4");
|
|
1101
|
+
}
|
|
1102
|
+
const repos = rawRepos.filter((item) => typeof item === "string");
|
|
1103
|
+
const items = Array.isArray(value.items) ? value.items.filter(isSubscriptionItem) : void 0;
|
|
1104
|
+
return { repos, ...items === void 0 ? {} : { items } };
|
|
1105
|
+
}
|
|
999
1106
|
function parseSubscribeResult(text) {
|
|
1000
1107
|
const trimmed = text.trim();
|
|
1001
1108
|
if (trimmed === "") throw new IpcError("IPC_INVALID", "\u6838\u5FC3 subscribe \u8F93\u51FA\u4E3A\u7A7A");
|
|
@@ -1012,15 +1119,24 @@ function parseSubscribeResult(text) {
|
|
|
1012
1119
|
throw new IpcError("IPC_INVALID", "\u6838\u5FC3 subscribe --json \u4E0D\u662F\u5951\u7EA6\u4FE1\u5C01\uFF08ipcSchema=2 / ok\uFF09");
|
|
1013
1120
|
}
|
|
1014
1121
|
const error = normalizeError(envelope.error);
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1122
|
+
const list = envelope.subscriptions === void 0 ? void 0 : parseSubscriptionList(envelope.subscriptions);
|
|
1123
|
+
const doc = envelope.subscriptionDoc;
|
|
1124
|
+
if (doc !== void 0 && !isSubscriptionDoc(doc)) {
|
|
1125
|
+
throw new IpcError("IPC_INVALID", "\u6838\u5FC3 subscribe \u8FD4\u56DE\u7684 subscriptionDoc \u4E0D\u662F\u5951\u7EA6 SubscriptionDoc\uFF08fullName/volume/markdown\uFF09");
|
|
1126
|
+
}
|
|
1127
|
+
const repos = list?.repos ?? [];
|
|
1128
|
+
if (list === void 0) {
|
|
1129
|
+
for (const line of lines.slice(0, -1)) {
|
|
1130
|
+
const value = line.trim();
|
|
1131
|
+
if (value === "") continue;
|
|
1132
|
+
if (REPO_PATTERN.test(value)) repos.push(value);
|
|
1133
|
+
}
|
|
1020
1134
|
}
|
|
1021
1135
|
return {
|
|
1022
1136
|
ok: envelope.ok === true,
|
|
1023
1137
|
repos,
|
|
1138
|
+
...list?.items === void 0 ? {} : { items: list.items },
|
|
1139
|
+
...doc === void 0 ? {} : { doc },
|
|
1024
1140
|
...error === void 0 ? {} : { error }
|
|
1025
1141
|
};
|
|
1026
1142
|
}
|
|
@@ -1494,6 +1610,7 @@ function buildCoreArgs(channelPath, options) {
|
|
|
1494
1610
|
if (options.dryRun) args.push("--dry-run");
|
|
1495
1611
|
if (options.date !== void 0 && options.date !== "") args.push("--date", options.date);
|
|
1496
1612
|
if (options.period !== void 0) args.push("--period", options.period);
|
|
1613
|
+
if (options.stages !== void 0 && options.stages.length > 0) args.push("--stages", options.stages.join(","));
|
|
1497
1614
|
return args;
|
|
1498
1615
|
}
|
|
1499
1616
|
async function runCore(options) {
|
|
@@ -1501,10 +1618,16 @@ async function runCore(options) {
|
|
|
1501
1618
|
const startedAt = nowIso();
|
|
1502
1619
|
const started = Date.now();
|
|
1503
1620
|
const channel = await openConfigChannel();
|
|
1504
|
-
const yaml = `${toYaml(toCoreConfig(options.settings, options.apiKey, options.baseURLOverride, options.period))}
|
|
1621
|
+
const yaml = `${toYaml(toCoreConfig(options.settings, options.apiKey, options.baseURLOverride, options.period, options.repoCount))}
|
|
1505
1622
|
`;
|
|
1506
1623
|
const secrets = [options.apiKey, options.token, ...options.secrets ?? []];
|
|
1507
|
-
const
|
|
1624
|
+
const args = buildCoreArgs(channel.path, {
|
|
1625
|
+
dryRun: options.dryRun,
|
|
1626
|
+
date: options.date,
|
|
1627
|
+
period: options.period,
|
|
1628
|
+
stages: options.stages
|
|
1629
|
+
});
|
|
1630
|
+
const child = spawn2(options.core.path, args, {
|
|
1508
1631
|
env: sanitizedEnv(process.env, {
|
|
1509
1632
|
GITHUB_TOKEN: options.token,
|
|
1510
1633
|
GH_TOKEN: options.token,
|
|
@@ -2154,18 +2277,23 @@ function authorized(request, token) {
|
|
|
2154
2277
|
// src/host/schedule.ts
|
|
2155
2278
|
var schedule_exports = {};
|
|
2156
2279
|
__export(schedule_exports, {
|
|
2280
|
+
RELEASE_CATCH_UP_MINUTES: () => RELEASE_CATCH_UP_MINUTES,
|
|
2157
2281
|
Scheduler: () => Scheduler,
|
|
2158
2282
|
daysInMonth: () => daysInMonth,
|
|
2159
2283
|
isDue: () => isDue,
|
|
2160
2284
|
isPeriodTriggerDay: () => isPeriodTriggerDay,
|
|
2285
|
+
isReleaseDue: () => isReleaseDue,
|
|
2286
|
+
isoWeekKey: () => isoWeekKey,
|
|
2161
2287
|
nextOccurrence: () => nextOccurrence,
|
|
2162
2288
|
nextOccurrenceFor: () => nextOccurrenceFor,
|
|
2289
|
+
nextReleaseOccurrence: () => nextReleaseOccurrence,
|
|
2163
2290
|
parseClock: () => parseClock,
|
|
2164
2291
|
zonedDateString: () => zonedDateString,
|
|
2165
2292
|
zonedTimeToUtc: () => zonedTimeToUtc
|
|
2166
2293
|
});
|
|
2167
2294
|
var MINUTE = 6e4;
|
|
2168
2295
|
var CATCH_UP_MINUTES = 6 * 60;
|
|
2296
|
+
var RELEASE_CATCH_UP_MINUTES = 6 * 60;
|
|
2169
2297
|
var MAX_ATTEMPTS_PER_DAY = 2;
|
|
2170
2298
|
var RETRY_GAP_MS = 5 * MINUTE;
|
|
2171
2299
|
function partsOf(date, timezone) {
|
|
@@ -2239,6 +2367,50 @@ function isDue(now, schedule) {
|
|
|
2239
2367
|
const late = nowMinutes - targetMinutes;
|
|
2240
2368
|
return late >= 0 && late <= CATCH_UP_MINUTES;
|
|
2241
2369
|
}
|
|
2370
|
+
function isoWeekOf(year, month, day) {
|
|
2371
|
+
const d = new Date(Date.UTC(year, month - 1, day));
|
|
2372
|
+
const dayIdx = (d.getUTCDay() + 6) % 7;
|
|
2373
|
+
d.setUTCDate(d.getUTCDate() - dayIdx + 3);
|
|
2374
|
+
const isoYear = d.getUTCFullYear();
|
|
2375
|
+
const firstThursday = new Date(Date.UTC(isoYear, 0, 4));
|
|
2376
|
+
const firstIdx = (firstThursday.getUTCDay() + 6) % 7;
|
|
2377
|
+
firstThursday.setUTCDate(firstThursday.getUTCDate() - firstIdx + 3);
|
|
2378
|
+
const week = 1 + Math.round((d.getTime() - firstThursday.getTime()) / (7 * 864e5));
|
|
2379
|
+
return `${isoYear}-W${String(week).padStart(2, "0")}`;
|
|
2380
|
+
}
|
|
2381
|
+
function isoWeekKey(date, timezone) {
|
|
2382
|
+
const parts = partsOf(date, timezone);
|
|
2383
|
+
return isoWeekOf(parts.year, parts.month, parts.day);
|
|
2384
|
+
}
|
|
2385
|
+
function isoWeekdayOf(date, timezone) {
|
|
2386
|
+
const parts = partsOf(date, timezone);
|
|
2387
|
+
const day = new Date(Date.UTC(parts.year, parts.month - 1, parts.day)).getUTCDay();
|
|
2388
|
+
return day === 0 ? 7 : day;
|
|
2389
|
+
}
|
|
2390
|
+
function nextReleaseOccurrence(now, cfg) {
|
|
2391
|
+
const { hour, minute, second } = parseClock(cfg.time);
|
|
2392
|
+
const today = partsOf(now, cfg.timezone);
|
|
2393
|
+
const start = Date.UTC(today.year, today.month - 1, today.day);
|
|
2394
|
+
for (let index = 0; index <= 15; index += 1) {
|
|
2395
|
+
const d = new Date(start + index * 864e5);
|
|
2396
|
+
const year = d.getUTCFullYear();
|
|
2397
|
+
const month = d.getUTCMonth() + 1;
|
|
2398
|
+
const day = d.getUTCDate();
|
|
2399
|
+
const dayOfWeek = d.getUTCDay();
|
|
2400
|
+
const weekday = dayOfWeek === 0 ? 7 : dayOfWeek;
|
|
2401
|
+
if (weekday !== cfg.weekday) continue;
|
|
2402
|
+
const candidate = zonedTimeToUtc(year, month, day, hour, minute, second, cfg.timezone);
|
|
2403
|
+
if (candidate.getTime() > now.getTime()) return candidate;
|
|
2404
|
+
}
|
|
2405
|
+
throw new Error(`\u65E0\u6CD5\u8BA1\u7B97\u4E0B\u4E00\u6B21\u53D1\u7248\u8FD0\u884C\u65F6\u523B\uFF08weekday=${String(cfg.weekday)}\uFF09`);
|
|
2406
|
+
}
|
|
2407
|
+
function isReleaseDue(now, cfg) {
|
|
2408
|
+
if (isoWeekdayOf(now, cfg.timezone) !== cfg.weekday) return false;
|
|
2409
|
+
const { hour, minute } = parseClock(cfg.time);
|
|
2410
|
+
const parts = partsOf(now, cfg.timezone);
|
|
2411
|
+
const late = parts.hour * 60 + parts.minute - (hour * 60 + minute);
|
|
2412
|
+
return late >= 0 && late <= RELEASE_CATCH_UP_MINUTES;
|
|
2413
|
+
}
|
|
2242
2414
|
function daysInMonth(year, month) {
|
|
2243
2415
|
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
2244
2416
|
}
|
|
@@ -2274,6 +2446,8 @@ var Scheduler = class {
|
|
|
2274
2446
|
stopped = false;
|
|
2275
2447
|
/** 当天已尝试次数与时间;只在内存里,进程重启后重新计数。 */
|
|
2276
2448
|
attempts = /* @__PURE__ */ new Map();
|
|
2449
|
+
/** 发版行程的尝试计数(键 = ISO 周 + 本地日期);理由同榜单:失败不能每分钟重试。 */
|
|
2450
|
+
releaseAttempts = /* @__PURE__ */ new Map();
|
|
2277
2451
|
constructor(deps) {
|
|
2278
2452
|
this.deps = deps;
|
|
2279
2453
|
}
|
|
@@ -2299,9 +2473,24 @@ var Scheduler = class {
|
|
|
2299
2473
|
else if (typeof handle === "number" || handle !== void 0) clearInterval(handle);
|
|
2300
2474
|
this.timerHandle = void 0;
|
|
2301
2475
|
}
|
|
2302
|
-
/**
|
|
2476
|
+
/**
|
|
2477
|
+
* 公开给测试:判断此刻是否该跑,并按需触发。
|
|
2478
|
+
*
|
|
2479
|
+
* 每个 tick 都**同时**评估榜单与发版两条行程(interval 已经是 60 s)。返回值合并规则
|
|
2480
|
+
* 保证老调用方不受影响:真的跑了 → `ran`;否则有人占着锁 → `busy`;再否则只要有
|
|
2481
|
+
* 一条行程是禁用状态就沿用旧的 `disabled`;都没有 → `skipped`。
|
|
2482
|
+
*/
|
|
2303
2483
|
async tick(now = /* @__PURE__ */ new Date()) {
|
|
2304
2484
|
if (this.stopped) return "skipped";
|
|
2485
|
+
const digest = await this.tickDigest(now);
|
|
2486
|
+
const releases = await this.tickReleases(now);
|
|
2487
|
+
if (digest === "ran" || releases === "ran") return "ran";
|
|
2488
|
+
if (digest === "busy" || releases === "busy") return "busy";
|
|
2489
|
+
if (digest === "disabled" || releases === "disabled") return "disabled";
|
|
2490
|
+
return "skipped";
|
|
2491
|
+
}
|
|
2492
|
+
/** 榜单行程(原 tick 的逻辑,不改)。 */
|
|
2493
|
+
async tickDigest(now) {
|
|
2305
2494
|
const schedule = this.readSchedule();
|
|
2306
2495
|
if (!schedule.enabled) return "disabled";
|
|
2307
2496
|
if (!isDue(now, schedule)) return "skipped";
|
|
@@ -2332,6 +2521,47 @@ var Scheduler = class {
|
|
|
2332
2521
|
}
|
|
2333
2522
|
return ran ? "ran" : "skipped";
|
|
2334
2523
|
}
|
|
2524
|
+
/**
|
|
2525
|
+
* 发版行程:每周一次、只跑 `--stages releases`。
|
|
2526
|
+
*
|
|
2527
|
+
* 跳过条件(全部满足才跑):开启、星期/时间命中且迟到 ≤ 6 h、本周尚未成功过
|
|
2528
|
+
* (`state.lastReleaseWeek`)、当前没有别的运行占锁。
|
|
2529
|
+
* 失败**不锁本周**,但也只有 2 次机会(间隔 5 min)——否则一个坏 token 会在 6 h 的
|
|
2530
|
+
* 补跑窗口里每分钟 spawn 一次核心、白白打 GitHub API。
|
|
2531
|
+
*/
|
|
2532
|
+
async tickReleases(now) {
|
|
2533
|
+
const cfg = this.readReleaseSchedule();
|
|
2534
|
+
if (!cfg.enabled) return "disabled";
|
|
2535
|
+
if (!isReleaseDue(now, cfg)) return "skipped";
|
|
2536
|
+
const week = (() => {
|
|
2537
|
+
try {
|
|
2538
|
+
return isoWeekKey(now, cfg.timezone);
|
|
2539
|
+
} catch {
|
|
2540
|
+
return void 0;
|
|
2541
|
+
}
|
|
2542
|
+
})();
|
|
2543
|
+
if (week === void 0) return "skipped";
|
|
2544
|
+
const state = this.deps.store.readState();
|
|
2545
|
+
if (state.lastReleaseWeek === week) return "skipped";
|
|
2546
|
+
const key = `${week}:${zonedDateString(now, cfg.timezone)}`;
|
|
2547
|
+
for (const existing of this.releaseAttempts.keys()) {
|
|
2548
|
+
if (!existing.startsWith(`${week}:`)) this.releaseAttempts.delete(existing);
|
|
2549
|
+
}
|
|
2550
|
+
const attempt = this.releaseAttempts.get(key);
|
|
2551
|
+
if (attempt !== void 0) {
|
|
2552
|
+
if (attempt.count >= MAX_ATTEMPTS_PER_DAY) return "skipped";
|
|
2553
|
+
if (now.getTime() - attempt.lastAt < RETRY_GAP_MS) return "skipped";
|
|
2554
|
+
}
|
|
2555
|
+
if (this.deps.isRunning()) return "busy";
|
|
2556
|
+
this.releaseAttempts.set(key, { count: (attempt?.count ?? 0) + 1, lastAt: now.getTime() });
|
|
2557
|
+
try {
|
|
2558
|
+
await this.deps.runReleases("schedule");
|
|
2559
|
+
return "ran";
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
this.deps.onError?.(error);
|
|
2562
|
+
return "skipped";
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2335
2565
|
/** 该周期今天是否已经成功过(含无变更跳过)。 */
|
|
2336
2566
|
succeededToday(state, period, today, timezone) {
|
|
2337
2567
|
const stamp = state.periodSuccess?.[period];
|
|
@@ -2350,6 +2580,16 @@ var Scheduler = class {
|
|
|
2350
2580
|
periods: settings.schedule.periods
|
|
2351
2581
|
};
|
|
2352
2582
|
}
|
|
2583
|
+
/** 发版行程的节奏来自 `settings.releases.schedule`(与榜单 `schedule.*` 相互独立)。 */
|
|
2584
|
+
readReleaseSchedule() {
|
|
2585
|
+
const { schedule } = this.deps.store.readSettings().releases;
|
|
2586
|
+
return {
|
|
2587
|
+
enabled: schedule.enabled,
|
|
2588
|
+
weekday: schedule.weekday,
|
|
2589
|
+
time: schedule.time,
|
|
2590
|
+
timezone: schedule.timezone
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2353
2593
|
/** 下一次运行(取所有已启用周期里最近的一次;禁用时为 undefined)。 */
|
|
2354
2594
|
next() {
|
|
2355
2595
|
const schedule = this.readSchedule();
|
|
@@ -2367,9 +2607,21 @@ var Scheduler = class {
|
|
|
2367
2607
|
return void 0;
|
|
2368
2608
|
}
|
|
2369
2609
|
}
|
|
2610
|
+
/** 下一次发版运行(禁用/非法时为 undefined)。 */
|
|
2611
|
+
nextRelease(now = /* @__PURE__ */ new Date()) {
|
|
2612
|
+
const cfg = this.readReleaseSchedule();
|
|
2613
|
+
if (!cfg.enabled) return void 0;
|
|
2614
|
+
try {
|
|
2615
|
+
return nextReleaseOccurrence(now, cfg);
|
|
2616
|
+
} catch {
|
|
2617
|
+
return void 0;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2370
2620
|
describe(state, now = /* @__PURE__ */ new Date()) {
|
|
2371
2621
|
const schedule = this.readSchedule();
|
|
2622
|
+
const release = this.readReleaseSchedule();
|
|
2372
2623
|
const next = schedule.enabled ? this.next() : void 0;
|
|
2624
|
+
const nextRelease = release.enabled ? this.nextRelease(now) : void 0;
|
|
2373
2625
|
return {
|
|
2374
2626
|
enabled: schedule.enabled,
|
|
2375
2627
|
time: schedule.time,
|
|
@@ -2378,11 +2630,98 @@ var Scheduler = class {
|
|
|
2378
2630
|
...next === void 0 ? {} : { nextRunAt: next.toISOString() },
|
|
2379
2631
|
...state.lastRunAt === void 0 ? {} : { lastRunDate: zonedDateString(new Date(state.lastRunAt), schedule.timezone) },
|
|
2380
2632
|
catchUpMinutes: CATCH_UP_MINUTES,
|
|
2381
|
-
source: "timer"
|
|
2633
|
+
source: "timer",
|
|
2634
|
+
// 发版行程的信息并列给出,界面/工具不必再去读设置才知道下一次何时跑。
|
|
2635
|
+
releaseEnabled: release.enabled,
|
|
2636
|
+
releaseWeekday: release.weekday,
|
|
2637
|
+
releaseTime: release.time,
|
|
2638
|
+
releaseTimezone: release.timezone,
|
|
2639
|
+
...nextRelease === void 0 ? {} : { nextReleaseAt: nextRelease.toISOString() },
|
|
2640
|
+
...state.lastReleaseAt === void 0 ? {} : { lastReleaseAt: state.lastReleaseAt },
|
|
2641
|
+
...state.lastReleaseWeek === void 0 ? {} : { lastReleaseWeek: state.lastReleaseWeek },
|
|
2642
|
+
releaseCatchUpMinutes: RELEASE_CATCH_UP_MINUTES
|
|
2382
2643
|
};
|
|
2383
2644
|
}
|
|
2384
2645
|
};
|
|
2385
2646
|
|
|
2647
|
+
// src/host/subscribe.ts
|
|
2648
|
+
var subscribe_exports = {};
|
|
2649
|
+
__export(subscribe_exports, {
|
|
2650
|
+
RELEASES_TIMEOUT_MS: () => RELEASES_TIMEOUT_MS,
|
|
2651
|
+
SUBSCRIBE_TIMEOUT_MS: () => SUBSCRIBE_TIMEOUT_MS,
|
|
2652
|
+
isValidRepo: () => isValidRepo,
|
|
2653
|
+
runSubscribe: () => runSubscribe
|
|
2654
|
+
});
|
|
2655
|
+
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
|
|
2656
|
+
import os3 from "os";
|
|
2657
|
+
import path8 from "path";
|
|
2658
|
+
var SUBSCRIBE_TIMEOUT_MS = 8e3;
|
|
2659
|
+
var RELEASES_TIMEOUT_MS = 3e4;
|
|
2660
|
+
var REPO_PATTERN2 = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
2661
|
+
function isValidRepo(repo) {
|
|
2662
|
+
return REPO_PATTERN2.test(repo.trim());
|
|
2663
|
+
}
|
|
2664
|
+
var SUBSCRIBE_PLACEHOLDER_BASE_URL = "http://127.0.0.1:9/v1";
|
|
2665
|
+
function writeTempConfig(settings) {
|
|
2666
|
+
const dir = mkdtempSync2(path8.join(os3.tmpdir(), "dsh-subscribe-"));
|
|
2667
|
+
const file = path8.join(dir, "config.yml");
|
|
2668
|
+
const placeholder = effectiveBaseURL(settings.model) === "" ? SUBSCRIBE_PLACEHOLDER_BASE_URL : void 0;
|
|
2669
|
+
const yaml = `${toYaml(toCoreConfig(settings, "", placeholder))}
|
|
2670
|
+
`;
|
|
2671
|
+
writeFileSync2(file, yaml, { mode: 384 });
|
|
2672
|
+
return { file, dir };
|
|
2673
|
+
}
|
|
2674
|
+
function requireUsableConfig(settings) {
|
|
2675
|
+
const problems = validateSettings(settings);
|
|
2676
|
+
if (problems.length > 0) {
|
|
2677
|
+
const first = problems[0];
|
|
2678
|
+
throw new IpcError("CONFIG_INVALID", `\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF0C\u65E0\u6CD5\u7BA1\u7406\u8BA2\u9605\uFF1A${first?.message ?? "\u672A\u77E5\u95EE\u9898"}`);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
async function runSubscribe(input, deps) {
|
|
2682
|
+
const env = deps.env ?? process.env;
|
|
2683
|
+
const settings = deps.store.readSettings();
|
|
2684
|
+
requireUsableConfig(settings);
|
|
2685
|
+
if (input.action !== "list") {
|
|
2686
|
+
const repo = (input.repo ?? "").trim();
|
|
2687
|
+
if (!isValidRepo(repo)) {
|
|
2688
|
+
throw new IpcError("CONFIG_INVALID", `\u4ED3\u5E93\u5FC5\u987B\u662F owner/repo \u5F62\u6001\uFF1A${repo || "(\u7A7A)"}`);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
if (input.volume !== void 0 && (!Number.isInteger(input.volume) || input.volume < 1)) {
|
|
2692
|
+
throw new IpcError("CONFIG_INVALID", `\u5377\u53F7\u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${String(input.volume)}`);
|
|
2693
|
+
}
|
|
2694
|
+
const corePath = deps.corePath !== void 0 ? deps.corePath : (await locateCore({ settings, env, allowDownload: settings.coreAutoDownload })).path;
|
|
2695
|
+
const { file, dir } = writeTempConfig(settings);
|
|
2696
|
+
try {
|
|
2697
|
+
const request = {
|
|
2698
|
+
ipcSchema: 2,
|
|
2699
|
+
command: "subscribe",
|
|
2700
|
+
action: input.action,
|
|
2701
|
+
...input.action === "list" ? {} : { repo: (input.repo ?? "").trim() },
|
|
2702
|
+
// 仅 `releases` 认卷号;核心对其它 action 传 volume 会直接判 CONFIG_INVALID。
|
|
2703
|
+
...input.action === "releases" && input.volume !== void 0 ? { volume: input.volume } : {},
|
|
2704
|
+
configPath: file
|
|
2705
|
+
};
|
|
2706
|
+
const timeoutMs = input.action === "releases" ? RELEASES_TIMEOUT_MS : SUBSCRIBE_TIMEOUT_MS;
|
|
2707
|
+
const result = await execCapture(corePath, ["subscribe", "--json"], {
|
|
2708
|
+
env: sanitizedEnv(env),
|
|
2709
|
+
input: JSON.stringify(request),
|
|
2710
|
+
timeoutMs
|
|
2711
|
+
});
|
|
2712
|
+
if (result.timedOut) {
|
|
2713
|
+
throw new IpcError("SUBSCRIBE_FAILED", `\u8BA2\u9605\u547D\u4EE4\u8D85\u65F6\uFF08${Math.round(timeoutMs / 1e3)} s\uFF09`);
|
|
2714
|
+
}
|
|
2715
|
+
if (result.stdout.trim() === "") {
|
|
2716
|
+
const detail = result.stderr.trim();
|
|
2717
|
+
throw new IpcError("SUBSCRIBE_FAILED", `\u8BA2\u9605\u547D\u4EE4\u65E0\u8F93\u51FA${detail === "" ? "" : `\uFF1A${detail}`}`);
|
|
2718
|
+
}
|
|
2719
|
+
return parseSubscribeResult(result.stdout);
|
|
2720
|
+
} finally {
|
|
2721
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2386
2725
|
// src/host/runner.ts
|
|
2387
2726
|
var runner_exports = {};
|
|
2388
2727
|
__export(runner_exports, {
|
|
@@ -2393,7 +2732,7 @@ __export(runner_exports, {
|
|
|
2393
2732
|
});
|
|
2394
2733
|
import { createHash as createHash2 } from "crypto";
|
|
2395
2734
|
import { existsSync as existsSync4, readFileSync as readFileSync3, realpathSync, statSync as statSync5 } from "fs";
|
|
2396
|
-
import
|
|
2735
|
+
import path9 from "path";
|
|
2397
2736
|
function repoProblem(repoPath) {
|
|
2398
2737
|
if (repoPath.trim() === "") return { code: "CONFIG_INVALID", message: "\u8FD8\u6CA1\u6709\u9009\u62E9\u5F52\u6863\u4ED3\u5E93" };
|
|
2399
2738
|
if (!existsSync4(repoPath)) return { code: "REPO_NOT_GIT", message: `\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${repoPath}` };
|
|
@@ -2402,7 +2741,7 @@ function repoProblem(repoPath) {
|
|
|
2402
2741
|
} catch {
|
|
2403
2742
|
return { code: "REPO_NOT_GIT", message: `\u65E0\u6CD5\u8BBF\u95EE\uFF1A${repoPath}` };
|
|
2404
2743
|
}
|
|
2405
|
-
if (!existsSync4(
|
|
2744
|
+
if (!existsSync4(path9.join(repoPath, ".git"))) {
|
|
2406
2745
|
return { code: "REPO_NOT_GIT", message: `\u4E0D\u662F git \u4ED3\u5E93\uFF1A${repoPath}\uFF08\u63D2\u4EF6\u4E0D\u4F1A\u66FF\u4F60 git init\uFF09` };
|
|
2407
2746
|
}
|
|
2408
2747
|
return void 0;
|
|
@@ -2415,27 +2754,23 @@ function runDate(settings, now = /* @__PURE__ */ new Date()) {
|
|
|
2415
2754
|
}
|
|
2416
2755
|
}
|
|
2417
2756
|
function isInside(root, target) {
|
|
2418
|
-
const rel =
|
|
2419
|
-
return rel !== "" && !rel.startsWith("..") && !
|
|
2757
|
+
const rel = path9.relative(root, target);
|
|
2758
|
+
return rel !== "" && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
2420
2759
|
}
|
|
2421
2760
|
function normalizeSha256(value) {
|
|
2422
2761
|
if (typeof value !== "string") return void 0;
|
|
2423
2762
|
const hex = value.trim().replace(/^sha256:/i, "").toLowerCase();
|
|
2424
2763
|
return /^[0-9a-f]{64}$/.test(hex) ? hex : void 0;
|
|
2425
2764
|
}
|
|
2765
|
+
function counterOf(value) {
|
|
2766
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
2767
|
+
}
|
|
2426
2768
|
function isoWeekName(date) {
|
|
2427
2769
|
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
|
2428
2770
|
if (match === null) return date;
|
|
2429
2771
|
const d = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
2430
2772
|
if (Number.isNaN(d.getTime())) return date;
|
|
2431
|
-
|
|
2432
|
-
d.setUTCDate(d.getUTCDate() - dayIdx + 3);
|
|
2433
|
-
const isoYear = d.getUTCFullYear();
|
|
2434
|
-
const firstThursday = new Date(Date.UTC(isoYear, 0, 4));
|
|
2435
|
-
const firstIdx = (firstThursday.getUTCDay() + 6) % 7;
|
|
2436
|
-
firstThursday.setUTCDate(firstThursday.getUTCDate() - firstIdx + 3);
|
|
2437
|
-
const week = 1 + Math.round((d.getTime() - firstThursday.getTime()) / (7 * 864e5));
|
|
2438
|
-
return `${isoYear}-W${String(week).padStart(2, "0")}`;
|
|
2773
|
+
return isoWeekKey(d, "UTC");
|
|
2439
2774
|
}
|
|
2440
2775
|
function periodFileBase(period, date) {
|
|
2441
2776
|
if (period === "monthly") return date.slice(0, 7);
|
|
@@ -2516,32 +2851,9 @@ var Runner = class {
|
|
|
2516
2851
|
`\u6838\u5FC3\u4E0D\u652F\u6301 provider\u300C${settings.model.provider}\u300D\uFF1B\u53EF\u7528\uFF1A${handshakeResult.supportedProviders.join(" / ")}`
|
|
2517
2852
|
);
|
|
2518
2853
|
}
|
|
2519
|
-
const
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
}
|
|
2523
|
-
let apiKey = modelKey.apiKey ?? "";
|
|
2524
|
-
let baseURLOverride;
|
|
2525
|
-
if (modelKey.mode === "proxy") {
|
|
2526
|
-
const llm = this.deps.get("llm");
|
|
2527
|
-
if (llm === void 0 || typeof llm.stream !== "function") {
|
|
2528
|
-
throw new IpcError("MODEL_KEY_MISSING", "\u8BE5\u6A21\u578B\u9700\u8981\u56DE\u73AF\u4EE3\u7406\uFF0C\u4F46\u5BBF\u4E3B\u6CA1\u6709\u53EF\u7528\u7684 ctx.llm \u670D\u52A1");
|
|
2529
|
-
}
|
|
2530
|
-
proxy = await startProxy({
|
|
2531
|
-
llm,
|
|
2532
|
-
provider: settings.model.hostProvider !== "" ? settings.model.hostProvider : settings.model.provider,
|
|
2533
|
-
model: settings.model.model,
|
|
2534
|
-
temperature: settings.model.temperature,
|
|
2535
|
-
maxTokens: settings.model.maxTokens,
|
|
2536
|
-
timeoutMs: 12e4
|
|
2537
|
-
});
|
|
2538
|
-
baseURLOverride = proxy.url;
|
|
2539
|
-
apiKey = proxy.token;
|
|
2540
|
-
store.appendLog(
|
|
2541
|
-
logFile,
|
|
2542
|
-
`[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`
|
|
2543
|
-
);
|
|
2544
|
-
}
|
|
2854
|
+
const model = await this.openModelChannel({ settings, credentials, env, logFile });
|
|
2855
|
+
const apiKey = model.apiKey;
|
|
2856
|
+
proxy = model.proxy;
|
|
2545
2857
|
const github = await resolveGithubToken(credentials, settings.github.authMode, settings.github.tokenRef, { env });
|
|
2546
2858
|
if (github.token === void 0) {
|
|
2547
2859
|
store.appendLog(logFile, "[github] \u672A\u627E\u5230 token\uFF1Apush/PR \u4F1A\u5931\u8D25\u5E76\u8FD4\u56DE GITHUB_AUTH\uFF08dryRun \u4E0D\u53D7\u5F71\u54CD\uFF09");
|
|
@@ -2556,7 +2868,7 @@ var Runner = class {
|
|
|
2556
2868
|
period,
|
|
2557
2869
|
dryRun,
|
|
2558
2870
|
trigger: options.trigger === "schedule" ? "schedule" : "manual",
|
|
2559
|
-
...baseURLOverride === void 0 ? {} : { baseURLOverride },
|
|
2871
|
+
...model.baseURLOverride === void 0 ? {} : { baseURLOverride: model.baseURLOverride },
|
|
2560
2872
|
signal: controller.signal,
|
|
2561
2873
|
secrets: [apiKey, github.token],
|
|
2562
2874
|
onStderr: (line) => store.appendLog(logFile, line)
|
|
@@ -2629,6 +2941,220 @@ var Runner = class {
|
|
|
2629
2941
|
}
|
|
2630
2942
|
return summaries;
|
|
2631
2943
|
}
|
|
2944
|
+
/**
|
|
2945
|
+
* 只跑发版订阅阶段(`run --stages releases`)。
|
|
2946
|
+
*
|
|
2947
|
+
* 与榜单运行共用同一把 `active` 锁:两条路径都会写同一份产物、读同一份订阅,
|
|
2948
|
+
* 并发跑会互相踩分支。和 `run()` 的区别只有三点:不带 dryRun、写完只落
|
|
2949
|
+
* `state.releaseRuns`(不碰 history.json)、自己**不**合并订阅 pending 队列
|
|
2950
|
+
* (`--stages releases` 让核心跳过那一步,合并只在完整 run 里发生)。
|
|
2951
|
+
*/
|
|
2952
|
+
async runReleases(options) {
|
|
2953
|
+
const env = this.deps.env ?? process.env;
|
|
2954
|
+
const store = this.deps.store;
|
|
2955
|
+
const settings = store.readSettings();
|
|
2956
|
+
const credentials = credentialServices(this.deps.get);
|
|
2957
|
+
if (this.active !== void 0 || this.batching) {
|
|
2958
|
+
throw new IpcError("CONFIG_INVALID", "\u5DF2\u6709\u4E00\u6B21\u8FD0\u884C\u5728\u8FDB\u884C\u4E2D");
|
|
2959
|
+
}
|
|
2960
|
+
const problems = validateSettings(settings);
|
|
2961
|
+
if (problems.length > 0) {
|
|
2962
|
+
const first = problems[0];
|
|
2963
|
+
throw new IpcError(first?.code ?? "CONFIG_INVALID", `\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A${first?.message ?? "\u672A\u77E5\u95EE\u9898"}`);
|
|
2964
|
+
}
|
|
2965
|
+
const repoIssue = repoProblem(settings.repo.path);
|
|
2966
|
+
if (repoIssue !== void 0) throw new IpcError(repoIssue.code, repoIssue.message);
|
|
2967
|
+
const id = newRunId();
|
|
2968
|
+
const controller = new AbortController();
|
|
2969
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2970
|
+
const startedMs = Date.now();
|
|
2971
|
+
this.active = { id, controller, startedAt };
|
|
2972
|
+
const logFile = store.newLogPath(id);
|
|
2973
|
+
let proxy;
|
|
2974
|
+
try {
|
|
2975
|
+
const core = await locateCore({ settings, env, allowDownload: settings.coreAutoDownload });
|
|
2976
|
+
if (core.downloaded === true) {
|
|
2977
|
+
store.appendLog(logFile, `[core] \u5DF2\u4E0B\u8F7D\u6838\u5FC3 ${core.tag ?? ""} \u2192 ${core.path}`);
|
|
2978
|
+
}
|
|
2979
|
+
const handshakeResult = await handshake(core, { signal: controller.signal });
|
|
2980
|
+
if (!handshakeResult.supportedProviders.includes(settings.model.provider)) {
|
|
2981
|
+
throw new IpcError(
|
|
2982
|
+
"MODEL_PROVIDER_UNSUPPORTED",
|
|
2983
|
+
`\u6838\u5FC3\u4E0D\u652F\u6301 provider\u300C${settings.model.provider}\u300D\uFF1B\u53EF\u7528\uFF1A${handshakeResult.supportedProviders.join(" / ")}`
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
const model = await this.openModelChannel({ settings, credentials, env, logFile });
|
|
2987
|
+
proxy = model.proxy;
|
|
2988
|
+
const github = await resolveGithubToken(credentials, settings.github.authMode, settings.github.tokenRef, { env });
|
|
2989
|
+
if (github.token === void 0) {
|
|
2990
|
+
store.appendLog(logFile, "[github] \u672A\u627E\u5230 token\uFF1A\u62C9\u53D6 Release \u4F1A\u5931\u8D25\u5E76\u8FD4\u56DE GITHUB_AUTH");
|
|
2991
|
+
}
|
|
2992
|
+
const repoCount = await this.subscriptionCount(core.path, env);
|
|
2993
|
+
const result = await runCore({
|
|
2994
|
+
core,
|
|
2995
|
+
version: handshakeResult.version,
|
|
2996
|
+
settings,
|
|
2997
|
+
apiKey: model.apiKey,
|
|
2998
|
+
...github.token === void 0 ? {} : { token: github.token },
|
|
2999
|
+
// 按发版调度时区算「今天」:与 lastReleaseWeek 的去重口径必须是同一天历。
|
|
3000
|
+
date: this.releaseDate(settings),
|
|
3001
|
+
period: settings.trending.since,
|
|
3002
|
+
dryRun: false,
|
|
3003
|
+
trigger: options.trigger === "schedule" ? "schedule" : "manual",
|
|
3004
|
+
stages: ["releases"],
|
|
3005
|
+
repoCount,
|
|
3006
|
+
...model.baseURLOverride === void 0 ? {} : { baseURLOverride: model.baseURLOverride },
|
|
3007
|
+
signal: controller.signal,
|
|
3008
|
+
secrets: [model.apiKey, github.token],
|
|
3009
|
+
onStderr: (line) => store.appendLog(logFile, line)
|
|
3010
|
+
});
|
|
3011
|
+
return this.recordReleaseRun({
|
|
3012
|
+
id,
|
|
3013
|
+
outcome: result.outcome,
|
|
3014
|
+
settings,
|
|
3015
|
+
startedAt,
|
|
3016
|
+
logFile,
|
|
3017
|
+
cancelled: controller.signal.aborted
|
|
3018
|
+
});
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
const aborted = controller.signal.aborted || error instanceof IpcError && error.code === "CORE_CANCELLED";
|
|
3021
|
+
return this.recordReleaseFailure({ id, error, aborted, settings, startedAt, startedMs, logFile });
|
|
3022
|
+
} finally {
|
|
3023
|
+
if (proxy !== void 0) {
|
|
3024
|
+
const port = proxy.port;
|
|
3025
|
+
await proxy.close().catch(() => void 0);
|
|
3026
|
+
this.deps.store.appendLog(logFile, `[proxy] \u5DF2\u5173\u95ED 127.0.0.1:${port}`);
|
|
3027
|
+
}
|
|
3028
|
+
this.active = void 0;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
/** 发版运行用哪一天:发版调度时区下的「今天」(不是榜单调度时区,两者可独立配置)。 */
|
|
3032
|
+
releaseDate(settings, now = /* @__PURE__ */ new Date()) {
|
|
3033
|
+
try {
|
|
3034
|
+
return zonedDateString(now, settings.releases.schedule.timezone);
|
|
3035
|
+
} catch {
|
|
3036
|
+
return now.toISOString().slice(0, 10);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
/** 订阅仓库数(best-effort,只影响 auto 解析):任何失败都当 0,不阻塞发版运行。 */
|
|
3040
|
+
async subscriptionCount(corePath, env) {
|
|
3041
|
+
try {
|
|
3042
|
+
const result = await runSubscribe({ action: "list" }, { store: this.deps.store, env, corePath });
|
|
3043
|
+
return result.ok ? result.repos.length : 0;
|
|
3044
|
+
} catch {
|
|
3045
|
+
return 0;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* 解析模型钥匙并(代理模式下)起回环代理。
|
|
3050
|
+
*
|
|
3051
|
+
* 榜单运行与发版运行共用这一段:代理的一次性 token 语义(空 key 会让核心静默降级成
|
|
3052
|
+
* 英文/离线假模型)只写一次,避免两份实现漂移。
|
|
3053
|
+
*/
|
|
3054
|
+
async openModelChannel(input) {
|
|
3055
|
+
const { settings, env, logFile } = input;
|
|
3056
|
+
const modelKey = await resolveModelKey(settings, input.credentials, env);
|
|
3057
|
+
if (modelKey.mode === "direct" && (modelKey.apiKey === void 0 || modelKey.apiKey === "")) {
|
|
3058
|
+
throw new IpcError("MODEL_KEY_MISSING", `\u6CA1\u6709\u89E3\u6790\u5230\u6A21\u578B\u5BC6\u94A5\uFF08\u5F15\u7528 ${settings.model.apiKeyRef}\uFF09`);
|
|
3059
|
+
}
|
|
3060
|
+
if (modelKey.mode !== "proxy") return { apiKey: modelKey.apiKey ?? "" };
|
|
3061
|
+
const llm = this.deps.get("llm");
|
|
3062
|
+
if (llm === void 0 || typeof llm.stream !== "function") {
|
|
3063
|
+
throw new IpcError("MODEL_KEY_MISSING", "\u8BE5\u6A21\u578B\u9700\u8981\u56DE\u73AF\u4EE3\u7406\uFF0C\u4F46\u5BBF\u4E3B\u6CA1\u6709\u53EF\u7528\u7684 ctx.llm \u670D\u52A1");
|
|
3064
|
+
}
|
|
3065
|
+
const proxy = await startProxy({
|
|
3066
|
+
llm,
|
|
3067
|
+
provider: settings.model.hostProvider !== "" ? settings.model.hostProvider : settings.model.provider,
|
|
3068
|
+
model: settings.model.model,
|
|
3069
|
+
temperature: settings.model.temperature,
|
|
3070
|
+
maxTokens: settings.model.maxTokens,
|
|
3071
|
+
timeoutMs: 12e4
|
|
3072
|
+
});
|
|
3073
|
+
this.deps.store.appendLog(logFile, `[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`);
|
|
3074
|
+
return { apiKey: proxy.token, baseURLOverride: proxy.url, proxy };
|
|
3075
|
+
}
|
|
3076
|
+
/** 把 `run --json` 的发版阶段结果投影成一条发版运行记录并落盘。 */
|
|
3077
|
+
recordReleaseRun(input) {
|
|
3078
|
+
const { outcome, settings } = input;
|
|
3079
|
+
const status = input.cancelled ? "cancelled" : outcome.skipped ? "skipped" : outcome.ok ? "ok" : "failed";
|
|
3080
|
+
const releases = outcome.releases;
|
|
3081
|
+
const record = {
|
|
3082
|
+
id: input.id,
|
|
3083
|
+
startedAt: input.startedAt,
|
|
3084
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3085
|
+
status,
|
|
3086
|
+
checked: counterOf(releases?.checked),
|
|
3087
|
+
changed: counterOf(releases?.changed),
|
|
3088
|
+
newTags: counterOf(releases?.newReleases),
|
|
3089
|
+
summarized: counterOf(releases?.summarized),
|
|
3090
|
+
degraded: counterOf(releases?.degraded),
|
|
3091
|
+
failed: counterOf(releases?.failed),
|
|
3092
|
+
rateLimited: counterOf(releases?.rateLimited),
|
|
3093
|
+
durationMs: counterOf(releases?.durationMs),
|
|
3094
|
+
...releases?.usage === void 0 ? {} : { tokens: releases.usage.totalTokens },
|
|
3095
|
+
...outcome.commit === void 0 ? {} : { commit: outcome.commit },
|
|
3096
|
+
...outcome.prUrl === void 0 ? {} : { prUrl: outcome.prUrl },
|
|
3097
|
+
...outcome.branch === void 0 ? {} : { branch: outcome.branch },
|
|
3098
|
+
...outcome.error === void 0 ? input.cancelled ? { errorCode: "CORE_CANCELLED", errorMessage: "\u8FD0\u884C\u5DF2\u53D6\u6D88\uFF1A\u6838\u5FC3\u8FDB\u7A0B\u5DF2\u7EC8\u6B62\uFF0C\u672A\u63D0\u4EA4\u4EFB\u4F55\u72B6\u6001" } : {} : { errorCode: outcome.error.code, errorMessage: redact(outcome.error.message) },
|
|
3099
|
+
...outcome.coreVersion === "" ? {} : { coreVersion: outcome.coreVersion }
|
|
3100
|
+
};
|
|
3101
|
+
this.persistReleaseRun(record, settings, input.logFile);
|
|
3102
|
+
return record;
|
|
3103
|
+
}
|
|
3104
|
+
/** 运行在拿到信封之前就失败(定位/握手/模型/代理/超时/取消):也要留一条可复盘记录。 */
|
|
3105
|
+
recordReleaseFailure(input) {
|
|
3106
|
+
const code = input.aborted ? "CORE_CANCELLED" : input.error instanceof IpcError ? input.error.code : "INTERNAL";
|
|
3107
|
+
const message = redact(input.error instanceof Error ? input.error.message : String(input.error));
|
|
3108
|
+
const record = {
|
|
3109
|
+
id: input.id,
|
|
3110
|
+
startedAt: input.startedAt,
|
|
3111
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3112
|
+
status: input.aborted ? "cancelled" : "failed",
|
|
3113
|
+
checked: 0,
|
|
3114
|
+
changed: 0,
|
|
3115
|
+
newTags: 0,
|
|
3116
|
+
summarized: 0,
|
|
3117
|
+
degraded: 0,
|
|
3118
|
+
failed: 0,
|
|
3119
|
+
rateLimited: 0,
|
|
3120
|
+
durationMs: Date.now() - input.startedMs,
|
|
3121
|
+
errorCode: code,
|
|
3122
|
+
errorMessage: message
|
|
3123
|
+
};
|
|
3124
|
+
this.persistReleaseRun(record, input.settings, input.logFile);
|
|
3125
|
+
return record;
|
|
3126
|
+
}
|
|
3127
|
+
/**
|
|
3128
|
+
* 落盘发版运行:只写 state(history.json 是榜单的),顺带推进 lastReleaseAt;
|
|
3129
|
+
* 只有 ok/skipped 才锁 `lastReleaseWeek`——失败不锁,调度下一 tick 才有机会重试。
|
|
3130
|
+
*/
|
|
3131
|
+
persistReleaseRun(record, settings, logFile) {
|
|
3132
|
+
const store = this.deps.store;
|
|
3133
|
+
const succeeded = record.status === "ok" || record.status === "skipped";
|
|
3134
|
+
let week;
|
|
3135
|
+
if (succeeded) {
|
|
3136
|
+
try {
|
|
3137
|
+
week = isoWeekKey(new Date(record.finishedAt), settings.releases.schedule.timezone);
|
|
3138
|
+
} catch {
|
|
3139
|
+
week = void 0;
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
store.appendReleaseRun(record, {
|
|
3143
|
+
lastReleaseAt: record.finishedAt,
|
|
3144
|
+
...week === void 0 ? {} : { lastReleaseWeek: week }
|
|
3145
|
+
});
|
|
3146
|
+
store.appendLog(
|
|
3147
|
+
logFile,
|
|
3148
|
+
`[release] ${record.status} checked=${record.checked} changed=${record.changed} newTags=${record.newTags} summarized=${record.summarized} degraded=${record.degraded} failed=${record.failed} rateLimited=${record.rateLimited} durationMs=${record.durationMs}`
|
|
3149
|
+
);
|
|
3150
|
+
this.deps.logger?.("release.finished", {
|
|
3151
|
+
id: record.id,
|
|
3152
|
+
status: record.status,
|
|
3153
|
+
checked: record.checked,
|
|
3154
|
+
changed: record.changed,
|
|
3155
|
+
coreVersion: record.coreVersion ?? null
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
2632
3158
|
summarize(input) {
|
|
2633
3159
|
const { outcome } = input;
|
|
2634
3160
|
const error = outcome.error === void 0 ? void 0 : (() => {
|
|
@@ -2703,19 +3229,19 @@ var Runner = class {
|
|
|
2703
3229
|
return void 0;
|
|
2704
3230
|
};
|
|
2705
3231
|
const markdown = outcome.files.filter((file) => {
|
|
2706
|
-
const ext =
|
|
2707
|
-
const base =
|
|
3232
|
+
const ext = path9.extname(file.path).toLowerCase();
|
|
3233
|
+
const base = path9.basename(file.path).toLowerCase();
|
|
2708
3234
|
const normalized = file.path.replace(/\\/g, "/");
|
|
2709
3235
|
const inReleases = normalized === "releases" || normalized.startsWith("releases/");
|
|
2710
3236
|
return ext === ".md" && base !== "readme.md" && base !== "index.md" && !inReleases;
|
|
2711
3237
|
});
|
|
2712
3238
|
if (markdown.length === 0) return skip("no-markdown");
|
|
2713
3239
|
const expected = period === void 0 ? `${date}.md` : `${periodFileBase(period, date)}.md`;
|
|
2714
|
-
const named = markdown.filter((file) =>
|
|
3240
|
+
const named = markdown.filter((file) => path9.basename(file.path) === expected);
|
|
2715
3241
|
const chosen = named.length > 0 ? named[0] : markdown.length === 1 ? markdown[0] : void 0;
|
|
2716
3242
|
if (chosen === void 0) return skip("ambiguous", { candidates: markdown.length, expected, period: period ?? null });
|
|
2717
3243
|
const repoPath = settings.repo.path;
|
|
2718
|
-
const resolved =
|
|
3244
|
+
const resolved = path9.resolve(repoPath, chosen.path);
|
|
2719
3245
|
if (!isInside(repoPath, resolved)) return skip("outside-repo", { path: chosen.path });
|
|
2720
3246
|
let fileReal;
|
|
2721
3247
|
try {
|
|
@@ -2748,7 +3274,7 @@ var Runner = class {
|
|
|
2748
3274
|
return skip("write-failed", { path: chosen.path });
|
|
2749
3275
|
}
|
|
2750
3276
|
return {
|
|
2751
|
-
file:
|
|
3277
|
+
file: path9.relative(repoPath, resolved).split(path9.sep).join("/"),
|
|
2752
3278
|
bytes: stored.length,
|
|
2753
3279
|
sha256: createHash2("sha256").update(stored).digest("hex")
|
|
2754
3280
|
};
|
|
@@ -2923,15 +3449,15 @@ __export(import_cli_exports, {
|
|
|
2923
3449
|
parseSimpleYaml: () => parseSimpleYaml
|
|
2924
3450
|
});
|
|
2925
3451
|
import { readFileSync as readFileSync4 } from "fs";
|
|
2926
|
-
import
|
|
2927
|
-
import
|
|
2928
|
-
function cliConfigCandidates(env = process.env, home =
|
|
3452
|
+
import os4 from "os";
|
|
3453
|
+
import path10 from "path";
|
|
3454
|
+
function cliConfigCandidates(env = process.env, home = os4.homedir()) {
|
|
2929
3455
|
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
2930
|
-
const base = xdg !== void 0 && xdg !== "" ?
|
|
3456
|
+
const base = xdg !== void 0 && xdg !== "" ? path10.resolve(xdg) : path10.join(home, ".config");
|
|
2931
3457
|
return [
|
|
2932
3458
|
env.TRENDradar_CONFIG?.trim() ?? "",
|
|
2933
|
-
|
|
2934
|
-
|
|
3459
|
+
path10.join(base, "trendradar", "config.yaml"),
|
|
3460
|
+
path10.join(home, ".trendradar", "config.yaml")
|
|
2935
3461
|
].filter((item) => item !== "");
|
|
2936
3462
|
}
|
|
2937
3463
|
function unquote(raw) {
|
|
@@ -3365,7 +3891,7 @@ __export(repo_exports, {
|
|
|
3365
3891
|
readDefaultBranch: () => readDefaultBranch
|
|
3366
3892
|
});
|
|
3367
3893
|
import { statSync as statSync6 } from "fs";
|
|
3368
|
-
import
|
|
3894
|
+
import path11 from "path";
|
|
3369
3895
|
function isDirectory(dir) {
|
|
3370
3896
|
try {
|
|
3371
3897
|
return statSync6(dir).isDirectory();
|
|
@@ -3384,7 +3910,7 @@ async function git(dir, args, env) {
|
|
|
3384
3910
|
async function readDefaultBranch(repoPath, options = {}) {
|
|
3385
3911
|
const dir = repoPath.trim();
|
|
3386
3912
|
if (dir === "") return { source: "none", message: "\u4ED3\u5E93\u8DEF\u5F84\u4E3A\u7A7A" };
|
|
3387
|
-
if (!
|
|
3913
|
+
if (!path11.isAbsolute(dir)) return { source: "none", message: "\u4ED3\u5E93\u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84" };
|
|
3388
3914
|
if (!isDirectory(dir)) return { source: "none", message: "\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u8BBF\u95EE" };
|
|
3389
3915
|
const env = options.env ?? process.env;
|
|
3390
3916
|
const origin = await git(dir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], env);
|
|
@@ -3399,76 +3925,6 @@ async function readDefaultBranch(repoPath, options = {}) {
|
|
|
3399
3925
|
return { source: "none", message: "\u672A\u8BFB\u53D6\u5230\u9ED8\u8BA4\u5206\u652F\uFF08\u53EF\u80FD\u662F\u7A7A\u4ED3\u5E93\u6216\u6CA1\u6709 origin\uFF09" };
|
|
3400
3926
|
}
|
|
3401
3927
|
|
|
3402
|
-
// src/host/subscribe.ts
|
|
3403
|
-
var subscribe_exports = {};
|
|
3404
|
-
__export(subscribe_exports, {
|
|
3405
|
-
SUBSCRIBE_TIMEOUT_MS: () => SUBSCRIBE_TIMEOUT_MS,
|
|
3406
|
-
isValidRepo: () => isValidRepo,
|
|
3407
|
-
runSubscribe: () => runSubscribe
|
|
3408
|
-
});
|
|
3409
|
-
import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
|
|
3410
|
-
import os4 from "os";
|
|
3411
|
-
import path11 from "path";
|
|
3412
|
-
var SUBSCRIBE_TIMEOUT_MS = 8e3;
|
|
3413
|
-
var REPO_PATTERN2 = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
3414
|
-
function isValidRepo(repo) {
|
|
3415
|
-
return REPO_PATTERN2.test(repo.trim());
|
|
3416
|
-
}
|
|
3417
|
-
var SUBSCRIBE_PLACEHOLDER_BASE_URL = "http://127.0.0.1:9/v1";
|
|
3418
|
-
function writeTempConfig(settings) {
|
|
3419
|
-
const dir = mkdtempSync2(path11.join(os4.tmpdir(), "dsh-subscribe-"));
|
|
3420
|
-
const file = path11.join(dir, "config.yml");
|
|
3421
|
-
const placeholder = effectiveBaseURL(settings.model) === "" ? SUBSCRIBE_PLACEHOLDER_BASE_URL : void 0;
|
|
3422
|
-
const yaml = `${toYaml(toCoreConfig(settings, "", placeholder))}
|
|
3423
|
-
`;
|
|
3424
|
-
writeFileSync2(file, yaml, { mode: 384 });
|
|
3425
|
-
return { file, dir };
|
|
3426
|
-
}
|
|
3427
|
-
function requireUsableConfig(settings) {
|
|
3428
|
-
const problems = validateSettings(settings);
|
|
3429
|
-
if (problems.length > 0) {
|
|
3430
|
-
const first = problems[0];
|
|
3431
|
-
throw new IpcError("CONFIG_INVALID", `\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF0C\u65E0\u6CD5\u7BA1\u7406\u8BA2\u9605\uFF1A${first?.message ?? "\u672A\u77E5\u95EE\u9898"}`);
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
async function runSubscribe(input, deps) {
|
|
3435
|
-
const env = deps.env ?? process.env;
|
|
3436
|
-
const settings = deps.store.readSettings();
|
|
3437
|
-
requireUsableConfig(settings);
|
|
3438
|
-
if (input.action !== "list") {
|
|
3439
|
-
const repo = (input.repo ?? "").trim();
|
|
3440
|
-
if (!isValidRepo(repo)) {
|
|
3441
|
-
throw new IpcError("CONFIG_INVALID", `\u4ED3\u5E93\u5FC5\u987B\u662F owner/repo \u5F62\u6001\uFF1A${repo || "(\u7A7A)"}`);
|
|
3442
|
-
}
|
|
3443
|
-
}
|
|
3444
|
-
const corePath = deps.corePath !== void 0 ? deps.corePath : (await locateCore({ settings, env, allowDownload: settings.coreAutoDownload })).path;
|
|
3445
|
-
const { file, dir } = writeTempConfig(settings);
|
|
3446
|
-
try {
|
|
3447
|
-
const request = {
|
|
3448
|
-
ipcSchema: 2,
|
|
3449
|
-
command: "subscribe",
|
|
3450
|
-
action: input.action,
|
|
3451
|
-
...input.action === "list" ? {} : { repo: (input.repo ?? "").trim() },
|
|
3452
|
-
configPath: file
|
|
3453
|
-
};
|
|
3454
|
-
const result = await execCapture(corePath, ["subscribe", "--json"], {
|
|
3455
|
-
env: sanitizedEnv(env),
|
|
3456
|
-
input: JSON.stringify(request),
|
|
3457
|
-
timeoutMs: SUBSCRIBE_TIMEOUT_MS
|
|
3458
|
-
});
|
|
3459
|
-
if (result.timedOut) {
|
|
3460
|
-
throw new IpcError("SUBSCRIBE_FAILED", `\u8BA2\u9605\u547D\u4EE4\u8D85\u65F6\uFF08${Math.round(SUBSCRIBE_TIMEOUT_MS / 1e3)} s\uFF09`);
|
|
3461
|
-
}
|
|
3462
|
-
if (result.stdout.trim() === "") {
|
|
3463
|
-
const detail = result.stderr.trim();
|
|
3464
|
-
throw new IpcError("SUBSCRIBE_FAILED", `\u8BA2\u9605\u547D\u4EE4\u65E0\u8F93\u51FA${detail === "" ? "" : `\uFF1A${detail}`}`);
|
|
3465
|
-
}
|
|
3466
|
-
return parseSubscribeResult(result.stdout);
|
|
3467
|
-
} finally {
|
|
3468
|
-
rmSync2(dir, { recursive: true, force: true });
|
|
3469
|
-
}
|
|
3470
|
-
}
|
|
3471
|
-
|
|
3472
3928
|
// src/host/routes.ts
|
|
3473
3929
|
var routes_exports = {};
|
|
3474
3930
|
__export(routes_exports, {
|
|
@@ -3625,7 +4081,32 @@ async function dispatch(method, pathname, url, request, deps) {
|
|
|
3625
4081
|
if (!result.ok) {
|
|
3626
4082
|
throw new IpcError("SUBSCRIBE_FAILED", result.error?.message ?? "\u8BFB\u53D6\u8BA2\u9605\u5217\u8868\u5931\u8D25");
|
|
3627
4083
|
}
|
|
3628
|
-
return { repos: result.repos };
|
|
4084
|
+
return { repos: result.repos, items: result.items ?? [] };
|
|
4085
|
+
}
|
|
4086
|
+
if (method === "GET" && pathname === "/subscriptions/releases") {
|
|
4087
|
+
const repo = (url.searchParams.get("repo") ?? "").trim();
|
|
4088
|
+
if (!isValidRepo(repo)) {
|
|
4089
|
+
throw new IpcError("CONFIG_INVALID", `\u4ED3\u5E93\u5FC5\u987B\u662F owner/repo \u5F62\u6001\uFF1A${repo || "(\u7A7A)"}`);
|
|
4090
|
+
}
|
|
4091
|
+
const volumeRaw = url.searchParams.get("volume");
|
|
4092
|
+
let volume;
|
|
4093
|
+
if (volumeRaw !== null && volumeRaw !== "") {
|
|
4094
|
+
if (!/^\d+$/.test(volumeRaw.trim()) || Number(volumeRaw) < 1) {
|
|
4095
|
+
throw new IpcError("CONFIG_INVALID", `\u5377\u53F7\u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${volumeRaw}`);
|
|
4096
|
+
}
|
|
4097
|
+
volume = Number(volumeRaw);
|
|
4098
|
+
}
|
|
4099
|
+
const result = await runSubscribe(
|
|
4100
|
+
{ action: "releases", repo, ...volume === void 0 ? {} : { volume } },
|
|
4101
|
+
{ store: deps.store, env }
|
|
4102
|
+
);
|
|
4103
|
+
if (!result.ok) {
|
|
4104
|
+
throw new IpcError("SUBSCRIBE_FAILED", result.error?.message ?? `\u8BFB\u53D6 ${repo} \u7684\u53D1\u7248\u6587\u6863\u5931\u8D25`);
|
|
4105
|
+
}
|
|
4106
|
+
if (result.doc === void 0) {
|
|
4107
|
+
throw new IpcError("SUBSCRIBE_FAILED", `\u6838\u5FC3\u6CA1\u6709\u8FD4\u56DE ${repo} \u7684\u53D1\u7248\u6587\u6863`);
|
|
4108
|
+
}
|
|
4109
|
+
return { ...result.doc };
|
|
3629
4110
|
}
|
|
3630
4111
|
if (method === "POST" && pathname === "/subscriptions") {
|
|
3631
4112
|
const body = await readBody2(request);
|
|
@@ -3640,7 +4121,7 @@ async function dispatch(method, pathname, url, request, deps) {
|
|
|
3640
4121
|
}
|
|
3641
4122
|
}
|
|
3642
4123
|
const latest = await runSubscribe({ action: "list" }, { store: deps.store, env });
|
|
3643
|
-
return { repos: latest.ok ? latest.repos : [] };
|
|
4124
|
+
return { repos: latest.ok ? latest.repos : [], items: latest.ok ? latest.items ?? [] : [] };
|
|
3644
4125
|
}
|
|
3645
4126
|
if (method === "POST" && pathname === "/github/test") {
|
|
3646
4127
|
const body = await readBody2(request);
|
|
@@ -3759,6 +4240,10 @@ async function statePayload(deps, settings) {
|
|
|
3759
4240
|
pendingPr,
|
|
3760
4241
|
...pendingPrState === void 0 ? {} : { pendingPrState },
|
|
3761
4242
|
history,
|
|
4243
|
+
/** 最近 30 次发版运行(HTTP 镜像:`Store.readReleaseRuns`,IPC 侧无对应命令,纯展示)。 */
|
|
4244
|
+
releaseRuns: deps.store.readReleaseRuns(),
|
|
4245
|
+
/** 最近一次发版运行结束时间(失败也算);界面「上次发版」读它。 */
|
|
4246
|
+
lastReleaseAt: state.lastReleaseAt ?? null,
|
|
3762
4247
|
/** 历史里全部运行的 token 累计(读时求和,不落盘);全无数据则省略。 */
|
|
3763
4248
|
...(() => {
|
|
3764
4249
|
const totalTokens = sumTokens(history);
|
|
@@ -4044,6 +4529,7 @@ export {
|
|
|
4044
4529
|
proxy_exports,
|
|
4045
4530
|
Scheduler,
|
|
4046
4531
|
schedule_exports,
|
|
4532
|
+
subscribe_exports,
|
|
4047
4533
|
Runner,
|
|
4048
4534
|
runner_exports,
|
|
4049
4535
|
readPluginInfo,
|
|
@@ -4053,7 +4539,6 @@ export {
|
|
|
4053
4539
|
pending_pr_exports,
|
|
4054
4540
|
preflight_exports,
|
|
4055
4541
|
repo_exports,
|
|
4056
|
-
subscribe_exports,
|
|
4057
4542
|
ROUTE_PREFIX,
|
|
4058
4543
|
createRoutes,
|
|
4059
4544
|
routes_exports,
|