@morehao/dsh-github-trending 0.0.1 → 0.1.2
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 +61 -1
- package/README.md +115 -9
- package/client.js +1408 -0
- package/contract/CONTRACT.sha256 +10 -0
- package/contract/UPSTREAM.json +37 -0
- package/contract/fixtures/digest-degraded.json +39 -0
- package/contract/fixtures/digest-empty-lang.json +39 -0
- package/contract/fixtures/digest-normal.json +91 -0
- package/contract/schema/api.v1.yaml +70 -0
- package/contract/schema/digest.v1.json +24 -0
- package/contract/schema/ipc.v1.json +103 -0
- package/contract/schema/model.v1.json +46 -0
- package/cordis.patch.yml +3 -0
- package/lib/chunk-RXYA5WFW.js +2909 -0
- package/lib/chunk-XLIFTAVL.js +117 -0
- package/lib/exec-33EQXGUA.js +8 -0
- package/lib/index.d.ts +20 -9
- package/lib/index.js +82 -4
- package/lib/internal.d.ts +1443 -0
- package/lib/internal.js +48 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +46 -7
- package/lib/index.js.map +0 -1
|
@@ -0,0 +1,2909 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__export,
|
|
3
|
+
execCapture,
|
|
4
|
+
sanitizedEnv
|
|
5
|
+
} from "./chunk-XLIFTAVL.js";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
var config_exports = {};
|
|
9
|
+
__export(config_exports, {
|
|
10
|
+
CORE_PROVIDERS: () => CORE_PROVIDERS,
|
|
11
|
+
DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
|
|
12
|
+
PROVIDER_BASE_URLS: () => PROVIDER_BASE_URLS,
|
|
13
|
+
effectiveBaseURL: () => effectiveBaseURL,
|
|
14
|
+
isValidTimezone: () => isValidTimezone,
|
|
15
|
+
normalizeSettings: () => normalizeSettings,
|
|
16
|
+
toCoreConfig: () => toCoreConfig,
|
|
17
|
+
toYaml: () => toYaml,
|
|
18
|
+
validateSettings: () => validateSettings
|
|
19
|
+
});
|
|
20
|
+
import path from "path";
|
|
21
|
+
var DEFAULT_SETTINGS = {
|
|
22
|
+
model: {
|
|
23
|
+
provider: "deepseek",
|
|
24
|
+
mode: "direct",
|
|
25
|
+
hostProvider: "",
|
|
26
|
+
baseURL: "",
|
|
27
|
+
model: "",
|
|
28
|
+
apiKeyRef: "TRENDING_MODEL_KEY",
|
|
29
|
+
temperature: 0.2,
|
|
30
|
+
maxTokens: 2048,
|
|
31
|
+
allowDegraded: true
|
|
32
|
+
},
|
|
33
|
+
repo: {
|
|
34
|
+
path: "",
|
|
35
|
+
baseBranch: "main",
|
|
36
|
+
remote: "origin",
|
|
37
|
+
commitMode: "branch",
|
|
38
|
+
authorName: "",
|
|
39
|
+
authorEmail: ""
|
|
40
|
+
},
|
|
41
|
+
pr: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
titleTemplate: "chore(trending): add {date} digest",
|
|
44
|
+
autoMerge: true,
|
|
45
|
+
labels: []
|
|
46
|
+
},
|
|
47
|
+
github: { authMode: "device", tokenRef: "GITHUB_TOKEN" },
|
|
48
|
+
trending: { since: "daily", languages: ["all", "typescript", "python", "go", "rust"], topN: 25 },
|
|
49
|
+
output: { dir: "trending" },
|
|
50
|
+
schedule: { enabled: true, time: "08:30:00", timezone: "Asia/Shanghai" },
|
|
51
|
+
corePath: "",
|
|
52
|
+
coreAutoDownload: true
|
|
53
|
+
};
|
|
54
|
+
var PROVIDER_BASE_URLS = {
|
|
55
|
+
deepseek: "https://api.deepseek.com/v1",
|
|
56
|
+
"openai-compatible": "",
|
|
57
|
+
anthropic: ""
|
|
58
|
+
};
|
|
59
|
+
var CORE_PROVIDERS = ["deepseek", "openai-compatible"];
|
|
60
|
+
function str(value, fallback) {
|
|
61
|
+
return typeof value === "string" ? value : fallback;
|
|
62
|
+
}
|
|
63
|
+
function bool(value, fallback) {
|
|
64
|
+
return typeof value === "boolean" ? value : fallback;
|
|
65
|
+
}
|
|
66
|
+
function num(value, fallback) {
|
|
67
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
68
|
+
}
|
|
69
|
+
function strArray(value, fallback) {
|
|
70
|
+
if (!Array.isArray(value)) return fallback;
|
|
71
|
+
const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
72
|
+
return items.length > 0 ? items.map((item) => item.trim()) : fallback;
|
|
73
|
+
}
|
|
74
|
+
function normalizeSettings(input) {
|
|
75
|
+
const raw = typeof input === "object" && input !== null ? input : {};
|
|
76
|
+
const model = raw.model ?? {};
|
|
77
|
+
const repo = raw.repo ?? {};
|
|
78
|
+
const pr = raw.pr ?? {};
|
|
79
|
+
const github = raw.github ?? {};
|
|
80
|
+
const trending = raw.trending ?? {};
|
|
81
|
+
const output = raw.output ?? {};
|
|
82
|
+
const schedule = raw.schedule ?? {};
|
|
83
|
+
const commitMode = str(repo.commitMode, DEFAULT_SETTINGS.repo.commitMode);
|
|
84
|
+
const authMode = str(github.authMode, DEFAULT_SETTINGS.github.authMode);
|
|
85
|
+
const since = str(trending.since, DEFAULT_SETTINGS.trending.since);
|
|
86
|
+
return {
|
|
87
|
+
model: {
|
|
88
|
+
provider: str(model.provider, DEFAULT_SETTINGS.model.provider),
|
|
89
|
+
mode: str(model.mode, DEFAULT_SETTINGS.model.mode) === "proxy" ? "proxy" : "direct",
|
|
90
|
+
hostProvider: str(model.hostProvider, DEFAULT_SETTINGS.model.hostProvider).trim(),
|
|
91
|
+
baseURL: str(model.baseURL, DEFAULT_SETTINGS.model.baseURL).trim(),
|
|
92
|
+
model: str(model.model, DEFAULT_SETTINGS.model.model).trim(),
|
|
93
|
+
apiKeyRef: str(model.apiKeyRef, DEFAULT_SETTINGS.model.apiKeyRef).trim() || DEFAULT_SETTINGS.model.apiKeyRef,
|
|
94
|
+
temperature: num(model.temperature, DEFAULT_SETTINGS.model.temperature),
|
|
95
|
+
maxTokens: num(model.maxTokens, DEFAULT_SETTINGS.model.maxTokens),
|
|
96
|
+
allowDegraded: bool(model.allowDegraded, DEFAULT_SETTINGS.model.allowDegraded)
|
|
97
|
+
},
|
|
98
|
+
repo: {
|
|
99
|
+
path: str(repo.path, DEFAULT_SETTINGS.repo.path).trim(),
|
|
100
|
+
baseBranch: str(repo.baseBranch, DEFAULT_SETTINGS.repo.baseBranch).trim() || "main",
|
|
101
|
+
remote: str(repo.remote, DEFAULT_SETTINGS.repo.remote).trim() || "origin",
|
|
102
|
+
commitMode: commitMode === "direct" ? "direct" : "branch",
|
|
103
|
+
authorName: str(repo.authorName, DEFAULT_SETTINGS.repo.authorName).trim(),
|
|
104
|
+
authorEmail: str(repo.authorEmail, DEFAULT_SETTINGS.repo.authorEmail).trim()
|
|
105
|
+
},
|
|
106
|
+
pr: {
|
|
107
|
+
enabled: bool(pr.enabled, DEFAULT_SETTINGS.pr.enabled),
|
|
108
|
+
titleTemplate: str(pr.titleTemplate, DEFAULT_SETTINGS.pr.titleTemplate),
|
|
109
|
+
autoMerge: bool(pr.autoMerge, DEFAULT_SETTINGS.pr.autoMerge),
|
|
110
|
+
labels: strArray(pr.labels, DEFAULT_SETTINGS.pr.labels)
|
|
111
|
+
},
|
|
112
|
+
github: {
|
|
113
|
+
authMode: authMode === "pat" || authMode === "gh" ? authMode : "device",
|
|
114
|
+
tokenRef: str(github.tokenRef, DEFAULT_SETTINGS.github.tokenRef).trim() || DEFAULT_SETTINGS.github.tokenRef
|
|
115
|
+
},
|
|
116
|
+
trending: {
|
|
117
|
+
since: since === "weekly" ? "weekly" : "daily",
|
|
118
|
+
languages: strArray(trending.languages, DEFAULT_SETTINGS.trending.languages),
|
|
119
|
+
topN: Math.round(num(trending.topN, DEFAULT_SETTINGS.trending.topN))
|
|
120
|
+
},
|
|
121
|
+
output: {
|
|
122
|
+
dir: str(output.dir, DEFAULT_SETTINGS.output.dir).trim() || DEFAULT_SETTINGS.output.dir
|
|
123
|
+
},
|
|
124
|
+
schedule: {
|
|
125
|
+
enabled: bool(schedule.enabled, DEFAULT_SETTINGS.schedule.enabled),
|
|
126
|
+
time: str(schedule.time, DEFAULT_SETTINGS.schedule.time),
|
|
127
|
+
timezone: str(schedule.timezone, DEFAULT_SETTINGS.schedule.timezone)
|
|
128
|
+
},
|
|
129
|
+
corePath: str(raw.corePath, DEFAULT_SETTINGS.corePath).trim(),
|
|
130
|
+
coreAutoDownload: bool(raw.coreAutoDownload, DEFAULT_SETTINGS.coreAutoDownload)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
var TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/;
|
|
134
|
+
function isValidTimezone(timezone) {
|
|
135
|
+
if (timezone.trim() === "") return false;
|
|
136
|
+
try {
|
|
137
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone });
|
|
138
|
+
return true;
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function validateSettings(settings, providers = CORE_PROVIDERS) {
|
|
144
|
+
const problems = [];
|
|
145
|
+
const { model, repo, trending, output, schedule } = settings;
|
|
146
|
+
if (model.provider.trim() === "") {
|
|
147
|
+
problems.push({ field: "model.provider", code: "CONFIG_INVALID", message: "\u7F3A\u5C11 provider" });
|
|
148
|
+
} else if (!providers.includes(model.provider)) {
|
|
149
|
+
problems.push({
|
|
150
|
+
field: "model.provider",
|
|
151
|
+
code: "MODEL_PROVIDER_UNSUPPORTED",
|
|
152
|
+
message: `\u6838\u5FC3\u4E0D\u652F\u6301 provider\u300C${model.provider}\u300D\uFF1B\u53EF\u7528\uFF1A${providers.join(" / ")}`
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (model.model.trim() === "") {
|
|
156
|
+
problems.push({ field: "model.model", code: "CONFIG_INVALID", message: "\u5FC5\u987B\u586B\u6A21\u578B ID\uFF08\u63D2\u4EF6\u4E0D\u8BBE\u9690\u5F0F\u9ED8\u8BA4\u6A21\u578B\uFF09" });
|
|
157
|
+
}
|
|
158
|
+
const baseURL = model.baseURL || PROVIDER_BASE_URLS[model.provider] || "";
|
|
159
|
+
if (baseURL === "") {
|
|
160
|
+
problems.push({ field: "model.baseURL", code: "CONFIG_INVALID", message: "\u8BE5 provider \u9700\u8981\u663E\u5F0F baseURL" });
|
|
161
|
+
} else if (!/^https?:\/\//.test(baseURL)) {
|
|
162
|
+
problems.push({ field: "model.baseURL", code: "CONFIG_INVALID", message: "baseURL \u5FC5\u987B\u662F http(s) \u5730\u5740" });
|
|
163
|
+
}
|
|
164
|
+
if (model.temperature < 0 || model.temperature > 2) {
|
|
165
|
+
problems.push({ field: "model.temperature", code: "CONFIG_INVALID", message: "temperature \u5FC5\u987B\u5728 0\u20132 \u4E4B\u95F4" });
|
|
166
|
+
}
|
|
167
|
+
if (!Number.isInteger(model.maxTokens) || model.maxTokens <= 0) {
|
|
168
|
+
problems.push({ field: "model.maxTokens", code: "CONFIG_INVALID", message: "maxTokens \u5FC5\u987B\u662F\u6B63\u6574\u6570" });
|
|
169
|
+
}
|
|
170
|
+
if (repo.path.trim() === "") {
|
|
171
|
+
problems.push({ field: "repo.path", code: "CONFIG_INVALID", message: "\u5FC5\u987B\u9009\u62E9\u5F52\u6863\u4ED3\u5E93\u76EE\u5F55" });
|
|
172
|
+
} else if (!path.isAbsolute(repo.path)) {
|
|
173
|
+
problems.push({ field: "repo.path", code: "CONFIG_INVALID", message: "\u4ED3\u5E93\u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84" });
|
|
174
|
+
}
|
|
175
|
+
if (!Number.isInteger(trending.topN) || trending.topN <= 0) {
|
|
176
|
+
problems.push({ field: "trending.topN", code: "CONFIG_INVALID", message: "\u6BCF\u8BED\u8A00\u6761\u6570\u5FC5\u987B\u662F\u6B63\u6574\u6570" });
|
|
177
|
+
}
|
|
178
|
+
if (output.dir.trim() === "" || path.isAbsolute(output.dir) || output.dir.split(/[\\/]/).includes("..")) {
|
|
179
|
+
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 .." });
|
|
180
|
+
}
|
|
181
|
+
if (!TIME_RE.test(schedule.time)) {
|
|
182
|
+
problems.push({ field: "schedule.time", code: "CONFIG_INVALID", message: "\u65F6\u95F4\u683C\u5F0F\u5FC5\u987B\u662F HH:MM \u6216 HH:MM:SS" });
|
|
183
|
+
}
|
|
184
|
+
if (!isValidTimezone(schedule.timezone)) {
|
|
185
|
+
problems.push({ field: "schedule.timezone", code: "CONFIG_INVALID", message: "\u65F6\u533A\u5FC5\u987B\u662F IANA \u540D\u79F0\uFF08\u5982 Asia/Shanghai\uFF09" });
|
|
186
|
+
}
|
|
187
|
+
return problems;
|
|
188
|
+
}
|
|
189
|
+
function effectiveBaseURL(model) {
|
|
190
|
+
return model.baseURL !== "" ? model.baseURL : PROVIDER_BASE_URLS[model.provider] ?? "";
|
|
191
|
+
}
|
|
192
|
+
function toCoreConfig(settings, apiKey, baseURLOverride) {
|
|
193
|
+
const config = {
|
|
194
|
+
model: {
|
|
195
|
+
provider: settings.model.provider,
|
|
196
|
+
name: settings.model.model,
|
|
197
|
+
baseURL: baseURLOverride ?? effectiveBaseURL(settings.model),
|
|
198
|
+
apiKey,
|
|
199
|
+
temperature: settings.model.temperature,
|
|
200
|
+
maxTokens: settings.model.maxTokens,
|
|
201
|
+
allowDegraded: settings.model.allowDegraded
|
|
202
|
+
},
|
|
203
|
+
repo: {
|
|
204
|
+
path: settings.repo.path,
|
|
205
|
+
remote: settings.repo.remote,
|
|
206
|
+
baseBranch: settings.repo.baseBranch,
|
|
207
|
+
...settings.repo.authorName !== "" ? { authorName: settings.repo.authorName } : {},
|
|
208
|
+
...settings.repo.authorEmail !== "" ? { authorEmail: settings.repo.authorEmail } : {}
|
|
209
|
+
},
|
|
210
|
+
pr: {
|
|
211
|
+
enabled: settings.repo.commitMode === "branch" && settings.pr.enabled,
|
|
212
|
+
titleTemplate: settings.pr.titleTemplate,
|
|
213
|
+
labels: settings.pr.labels,
|
|
214
|
+
autoMerge: settings.pr.autoMerge
|
|
215
|
+
},
|
|
216
|
+
trending: {
|
|
217
|
+
languages: settings.trending.languages,
|
|
218
|
+
since: settings.trending.since,
|
|
219
|
+
perLanguage: settings.trending.topN
|
|
220
|
+
},
|
|
221
|
+
output: {
|
|
222
|
+
dir: settings.output.dir,
|
|
223
|
+
branchPrefix: "trending/",
|
|
224
|
+
retentionDays: 90
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
return config;
|
|
228
|
+
}
|
|
229
|
+
function toYaml(value, depth = 0) {
|
|
230
|
+
const pad = " ".repeat(depth);
|
|
231
|
+
if (Array.isArray(value)) return toYamlScalar(value);
|
|
232
|
+
if (typeof value === "object" && value !== null) {
|
|
233
|
+
const lines = [];
|
|
234
|
+
for (const [key, item] of Object.entries(value)) {
|
|
235
|
+
if (item === void 0) continue;
|
|
236
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
237
|
+
lines.push(`${pad}${key}:
|
|
238
|
+
${toYaml(item, depth + 1)}`);
|
|
239
|
+
} else {
|
|
240
|
+
lines.push(`${pad}${key}: ${toYamlScalar(item)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return lines.join("\n");
|
|
244
|
+
}
|
|
245
|
+
return `${pad}${toYamlScalar(value)}`;
|
|
246
|
+
}
|
|
247
|
+
function toYamlScalar(value) {
|
|
248
|
+
if (Array.isArray(value)) return `[${value.map((item) => toYamlScalar(item)).join(", ")}]`;
|
|
249
|
+
if (typeof value === "object" && value !== null) return JSON.stringify(value);
|
|
250
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
251
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "0";
|
|
252
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
253
|
+
return "null";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/paths.ts
|
|
257
|
+
var paths_exports = {};
|
|
258
|
+
__export(paths_exports, {
|
|
259
|
+
HISTORY_FILE: () => HISTORY_FILE,
|
|
260
|
+
PLUGIN_DIR_NAME: () => PLUGIN_DIR_NAME,
|
|
261
|
+
SETTINGS_FILE: () => SETTINGS_FILE,
|
|
262
|
+
STATE_FILE: () => STATE_FILE,
|
|
263
|
+
dataDir: () => dataDir,
|
|
264
|
+
dshHome: () => dshHome,
|
|
265
|
+
homeDir: () => homeDir,
|
|
266
|
+
runsDir: () => runsDir
|
|
267
|
+
});
|
|
268
|
+
import os from "os";
|
|
269
|
+
import path2 from "path";
|
|
270
|
+
var PLUGIN_DIR_NAME = "github-trending";
|
|
271
|
+
function homeDir(env = process.env) {
|
|
272
|
+
const raw = env.HOME?.trim();
|
|
273
|
+
return raw !== void 0 && raw !== "" ? path2.resolve(raw) : os.homedir();
|
|
274
|
+
}
|
|
275
|
+
function dshHome(env = process.env) {
|
|
276
|
+
const raw = env.DSH_HOME?.trim();
|
|
277
|
+
return raw !== void 0 && raw !== "" ? path2.resolve(raw) : path2.join(homeDir(env), ".dsh");
|
|
278
|
+
}
|
|
279
|
+
function dataDir(env = process.env) {
|
|
280
|
+
const raw = env.TRENDING_DIGEST_HOME?.trim();
|
|
281
|
+
if (raw !== void 0 && raw !== "") return path2.resolve(raw);
|
|
282
|
+
return path2.join(dshHome(env), PLUGIN_DIR_NAME);
|
|
283
|
+
}
|
|
284
|
+
function runsDir(env = process.env) {
|
|
285
|
+
return path2.join(dataDir(env), "runs");
|
|
286
|
+
}
|
|
287
|
+
var SETTINGS_FILE = "settings.json";
|
|
288
|
+
var STATE_FILE = "state.json";
|
|
289
|
+
var HISTORY_FILE = "history.json";
|
|
290
|
+
|
|
291
|
+
// src/state.ts
|
|
292
|
+
var state_exports = {};
|
|
293
|
+
__export(state_exports, {
|
|
294
|
+
HISTORY_LIMIT: () => HISTORY_LIMIT,
|
|
295
|
+
Store: () => Store,
|
|
296
|
+
newRunId: () => newRunId,
|
|
297
|
+
writeJsonAtomic: () => writeJsonAtomic
|
|
298
|
+
});
|
|
299
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync, appendFileSync, existsSync } from "fs";
|
|
300
|
+
import path3 from "path";
|
|
301
|
+
var HISTORY_LIMIT = 30;
|
|
302
|
+
function readJson(file, fallback) {
|
|
303
|
+
try {
|
|
304
|
+
const text = readFileSync(file, "utf8");
|
|
305
|
+
const value = JSON.parse(text);
|
|
306
|
+
if (typeof value === "object" && value !== null) return value;
|
|
307
|
+
return fallback;
|
|
308
|
+
} catch {
|
|
309
|
+
return fallback;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function ensureDir(dir) {
|
|
313
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
314
|
+
}
|
|
315
|
+
function writeJsonAtomic(file, value) {
|
|
316
|
+
ensureDir(path3.dirname(file));
|
|
317
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
318
|
+
writeFileSync(temp, `${JSON.stringify(value, null, 2)}
|
|
319
|
+
`, { mode: 384 });
|
|
320
|
+
renameSync(temp, file);
|
|
321
|
+
}
|
|
322
|
+
var Store = class {
|
|
323
|
+
dir;
|
|
324
|
+
env;
|
|
325
|
+
constructor(options = {}) {
|
|
326
|
+
this.env = options.env ?? process.env;
|
|
327
|
+
this.dir = options.dir ?? dataDir(this.env);
|
|
328
|
+
}
|
|
329
|
+
get settingsPath() {
|
|
330
|
+
return path3.join(this.dir, SETTINGS_FILE);
|
|
331
|
+
}
|
|
332
|
+
get statePath() {
|
|
333
|
+
return path3.join(this.dir, STATE_FILE);
|
|
334
|
+
}
|
|
335
|
+
get historyPath() {
|
|
336
|
+
return path3.join(this.dir, HISTORY_FILE);
|
|
337
|
+
}
|
|
338
|
+
get logsPath() {
|
|
339
|
+
return runsDir(this.env);
|
|
340
|
+
}
|
|
341
|
+
readSettings() {
|
|
342
|
+
return normalizeSettings(readJson(this.settingsPath, {}));
|
|
343
|
+
}
|
|
344
|
+
writeSettings(settings) {
|
|
345
|
+
writeJsonAtomic(this.settingsPath, settings);
|
|
346
|
+
}
|
|
347
|
+
readState() {
|
|
348
|
+
return readJson(this.statePath, {});
|
|
349
|
+
}
|
|
350
|
+
writeState(state) {
|
|
351
|
+
writeJsonAtomic(this.statePath, state);
|
|
352
|
+
}
|
|
353
|
+
readHistory() {
|
|
354
|
+
const file = readJson(this.historyPath, { records: [] });
|
|
355
|
+
return Array.isArray(file.records) ? file.records : [];
|
|
356
|
+
}
|
|
357
|
+
/** 记录一次运行:历史裁剪到 30 条,state 同步更新。 */
|
|
358
|
+
recordRun(record, next = {}) {
|
|
359
|
+
const records = [record, ...this.readHistory()].slice(0, HISTORY_LIMIT);
|
|
360
|
+
writeJsonAtomic(this.historyPath, { records });
|
|
361
|
+
const state = this.readState();
|
|
362
|
+
this.writeState({ ...state, ...next, lastResult: record });
|
|
363
|
+
}
|
|
364
|
+
newLogPath(id) {
|
|
365
|
+
ensureDir(this.logsPath);
|
|
366
|
+
return path3.join(this.logsPath, `${id}.log`);
|
|
367
|
+
}
|
|
368
|
+
appendLog(file, line) {
|
|
369
|
+
ensureDir(path3.dirname(file));
|
|
370
|
+
appendFileSync(file, `${line}
|
|
371
|
+
`, { mode: 384 });
|
|
372
|
+
}
|
|
373
|
+
hasLog(file) {
|
|
374
|
+
return existsSync(file);
|
|
375
|
+
}
|
|
376
|
+
readLog(file, maxBytes = 64 * 1024) {
|
|
377
|
+
try {
|
|
378
|
+
const text = readFileSync(file, "utf8");
|
|
379
|
+
return text.length > maxBytes ? text.slice(text.length - maxBytes) : text;
|
|
380
|
+
} catch {
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
function newRunId(now = /* @__PURE__ */ new Date()) {
|
|
386
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
|
|
387
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
388
|
+
return `${stamp}-${random}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/errors.ts
|
|
392
|
+
var errors_exports = {};
|
|
393
|
+
__export(errors_exports, {
|
|
394
|
+
PLUGIN_CODES: () => PLUGIN_CODES,
|
|
395
|
+
areaOf: () => areaOf,
|
|
396
|
+
classifyStderr: () => classifyStderr,
|
|
397
|
+
hintOf: () => hintOf,
|
|
398
|
+
isRetriable: () => isRetriable,
|
|
399
|
+
redact: () => redact
|
|
400
|
+
});
|
|
401
|
+
var PLUGIN_CODES = [
|
|
402
|
+
"CORE_UNAVAILABLE",
|
|
403
|
+
"CORE_INCOMPATIBLE",
|
|
404
|
+
"CORE_SPAWN_FAILED",
|
|
405
|
+
"CORE_TIMEOUT",
|
|
406
|
+
"CORE_CANCELLED",
|
|
407
|
+
"CORE_CONFIG_CHANNEL_FAILED",
|
|
408
|
+
"CORE_NOT_DOWNLOADED",
|
|
409
|
+
"CORE_CHECKSUM_MISMATCH",
|
|
410
|
+
"IPC_INVALID",
|
|
411
|
+
"MODEL_KEY_MISSING",
|
|
412
|
+
"MODEL_NOT_FOUND",
|
|
413
|
+
"MODEL_PROVIDER_UNSUPPORTED",
|
|
414
|
+
"GITHUB_AUTH",
|
|
415
|
+
"PUSH_FAILED",
|
|
416
|
+
"REPO_DIRTY"
|
|
417
|
+
];
|
|
418
|
+
var RETRIABLE = {
|
|
419
|
+
INTERNAL: true,
|
|
420
|
+
CONFIG_INVALID: false,
|
|
421
|
+
PAYLOAD_TOO_LARGE: false,
|
|
422
|
+
FETCH_FAILED: false,
|
|
423
|
+
FETCH_RATE_LIMITED: true,
|
|
424
|
+
PARSE_FAILED: false,
|
|
425
|
+
MODEL_UNAUTHORIZED: false,
|
|
426
|
+
MODEL_RATE_LIMITED: true,
|
|
427
|
+
MODEL_TIMEOUT: true,
|
|
428
|
+
MODEL_FAILED: true,
|
|
429
|
+
MODEL_BAD_OUTPUT: false,
|
|
430
|
+
GIT_NOT_INSTALLED: false,
|
|
431
|
+
REPO_NOT_GIT: false,
|
|
432
|
+
REPO_DIRTY: false,
|
|
433
|
+
GIT_OPERATION_FAILED: true,
|
|
434
|
+
PR_FAILED: true,
|
|
435
|
+
NOT_MERGEABLE: false,
|
|
436
|
+
CORE_UNAVAILABLE: false,
|
|
437
|
+
CORE_INCOMPATIBLE: false,
|
|
438
|
+
CORE_SPAWN_FAILED: true,
|
|
439
|
+
CORE_TIMEOUT: true,
|
|
440
|
+
CORE_CANCELLED: false,
|
|
441
|
+
CORE_CONFIG_CHANNEL_FAILED: true,
|
|
442
|
+
CORE_NOT_DOWNLOADED: false,
|
|
443
|
+
CORE_CHECKSUM_MISMATCH: true,
|
|
444
|
+
IPC_INVALID: false,
|
|
445
|
+
MODEL_KEY_MISSING: false,
|
|
446
|
+
MODEL_NOT_FOUND: false,
|
|
447
|
+
MODEL_PROVIDER_UNSUPPORTED: false,
|
|
448
|
+
GITHUB_AUTH: false,
|
|
449
|
+
PUSH_FAILED: true
|
|
450
|
+
};
|
|
451
|
+
function isRetriable(code) {
|
|
452
|
+
return RETRIABLE[code] === true;
|
|
453
|
+
}
|
|
454
|
+
function areaOf(code) {
|
|
455
|
+
if (code.startsWith("CORE_") || code === "IPC_INVALID") return "core";
|
|
456
|
+
if (code.startsWith("MODEL_")) return "model";
|
|
457
|
+
if (code.startsWith("REPO_") || code === "GIT_NOT_INSTALLED" || code === "GIT_OPERATION_FAILED") return "repo";
|
|
458
|
+
if (code === "GITHUB_AUTH") return "auth";
|
|
459
|
+
return "run";
|
|
460
|
+
}
|
|
461
|
+
function hintOf(code) {
|
|
462
|
+
switch (code) {
|
|
463
|
+
case "CORE_UNAVAILABLE":
|
|
464
|
+
return "\u6838\u5FC3\u4E8C\u8FDB\u5236\u6CA1\u627E\u5230\uFF1A\u786E\u8BA4\u63D2\u4EF6\u5B89\u88C5\u5B8C\u6574\uFF08\u542B\u5E73\u53F0\u5305\uFF09\uFF0C\u6216\u7528 TRENDING_DIGEST_CORE \u6307\u5411\u4E00\u4E2A\u53EF\u6267\u884C\u7684 trendradar\u3002";
|
|
465
|
+
case "CORE_INCOMPATIBLE":
|
|
466
|
+
return "\u6838\u5FC3\u534F\u8BAE\u7248\u672C\u4E0D\u5339\u914D\uFF1A\u5347\u7EA7\u63D2\u4EF6\uFF08\u6216\u5347\u7EA7\u6838\u5FC3\uFF09\uFF0C\u4E0D\u8981\u6DF7\u642D\u7248\u672C\u3002";
|
|
467
|
+
case "CORE_TIMEOUT":
|
|
468
|
+
return "\u6838\u5FC3\u8D85\u65F6\u672A\u8FD4\u56DE\uFF1A\u68C0\u67E5\u7F51\u7EDC\u4E0E\u6A21\u578B\u7AEF\u70B9\uFF1B\u8D85\u65F6\u540E\u4E0D\u4F1A\u63D0\u4EA4\u4EFB\u4F55\u72B6\u6001\u3002";
|
|
469
|
+
case "CORE_CANCELLED":
|
|
470
|
+
return "\u8FD0\u884C\u5DF2\u53D6\u6D88\uFF1A\u6838\u5FC3\u5DF2\u7EC8\u6B62\uFF0C\u672A\u63D0\u4EA4\u72B6\u6001\uFF0C\u4E5F\u672A\u7559\u534A\u6210\u54C1\u3002";
|
|
471
|
+
case "CORE_CONFIG_CHANNEL_FAILED":
|
|
472
|
+
return "\u914D\u7F6E\u901A\u9053\u5EFA\u7ACB\u5931\u8D25\uFF08FIFO \u4E0D\u53EF\u7528\uFF09\uFF1A\u6539\u7528\u4E34\u65F6\u6587\u4EF6\u6A21\u5F0F\u91CD\u8BD5\uFF0C\u6216\u68C0\u67E5\u76EE\u6807\u6587\u4EF6\u7CFB\u7EDF\u3002";
|
|
473
|
+
case "CORE_NOT_DOWNLOADED":
|
|
474
|
+
return "\u6838\u5FC3\u8FD8\u6CA1\u4E0B\u8F7D\uFF1A\u5728\u8BBE\u7F6E\u9875\u70B9\u300C\u4E0B\u8F7D\u6838\u5FC3\u300D\uFF08\u6216\u7528 TRENDING_DIGEST_CORE \u6307\u5411\u672C\u5730\u6838\u5FC3\uFF09\u3002\u9996\u6B21\u8FD0\u884C\u9700\u8981\u8054\u7F51\u3002";
|
|
475
|
+
case "CORE_CHECKSUM_MISMATCH":
|
|
476
|
+
return "\u6838\u5FC3\u4E8C\u8FDB\u5236\u6821\u9A8C\u5931\u8D25\u4E14\u5DF2\u4E22\u5F03\uFF1A\u7F51\u7EDC\u4EE3\u7406\u53EF\u80FD\u6539\u5199\u4E86\u5185\u5BB9\uFF0C\u91CD\u8BD5\u4E00\u6B21\uFF1B\u4ECD\u5931\u8D25\u8BF7\u624B\u52A8\u4E0B\u8F7D\u540E\u653E\u5230\u7F13\u5B58\u76EE\u5F55\u3002";
|
|
477
|
+
case "IPC_INVALID":
|
|
478
|
+
return "\u6838\u5FC3\u8F93\u51FA\u4E0D\u662F\u7EA6\u5B9A\u7684\u7ED3\u679C\u4FE1\u5C01\uFF1A\u5347\u7EA7\u63D2\u4EF6\u4E0E\u6838\u5FC3\u5230\u540C\u4E00\u7248\u672C\u3002";
|
|
479
|
+
case "MODEL_KEY_MISSING":
|
|
480
|
+
return "\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u5BC6\u94A5\uFF1A\u5728\u8BBE\u7F6E\u9875\u5FEB\u901F\u586B\u5145\u6216\u624B\u52A8\u586B\u5199\u3002";
|
|
481
|
+
case "MODEL_UNAUTHORIZED":
|
|
482
|
+
return "\u5BC6\u94A5\u88AB\u6A21\u578B\u7AEF\u70B9\u62D2\u7EDD\uFF1A\u91CD\u65B0\u9009\u62E9\u6A21\u578B\u6216\u66F4\u65B0\u5BC6\u94A5\u3002";
|
|
483
|
+
case "MODEL_NOT_FOUND":
|
|
484
|
+
return "\u7AEF\u70B9\u4E0D\u8BA4\u8BC6\u8FD9\u4E2A\u6A21\u578B ID\uFF1A\u6838\u5BF9\u6A21\u578B\u540D\uFF0C\u6216\u6362\u4E00\u4E2A\u6A21\u578B\u3002";
|
|
485
|
+
case "MODEL_PROVIDER_UNSUPPORTED":
|
|
486
|
+
return "\u5F53\u524D\u6838\u5FC3\u4E0D\u652F\u6301\u8FD9\u4E2A provider\uFF1A\u6309\u6838\u5FC3 version --json \u7684 supportedProviders \u9009\u62E9\u3002";
|
|
487
|
+
case "MODEL_RATE_LIMITED":
|
|
488
|
+
return "\u6A21\u578B\u9650\u6D41\uFF1A\u7A0D\u540E\u91CD\u8BD5\uFF1B\u7ED3\u679C\u672A\u5199\u5165\u4EFB\u4F55\u6587\u4EF6\u3002";
|
|
489
|
+
case "GIT_NOT_INSTALLED":
|
|
490
|
+
return "\u672C\u673A\u6CA1\u6709\u53EF\u7528\u7684 git\uFF1A\u5B89\u88C5 git \u2265 2.30 \u540E\u91CD\u8BD5\u3002";
|
|
491
|
+
case "REPO_NOT_GIT":
|
|
492
|
+
return "\u76EE\u6807\u76EE\u5F55\u4E0D\u662F git \u4ED3\u5E93\uFF1A\u63D2\u4EF6\u4E0D\u4F1A\u66FF\u4F60 git init\uFF0C\u8BF7\u5148\u5EFA\u5E93\u6216\u6362\u76EE\u5F55\u3002";
|
|
493
|
+
case "REPO_DIRTY":
|
|
494
|
+
return "\u4ED3\u5E93\u6709\u672A\u63D0\u4EA4\u6539\u52A8\uFF1A\u63D2\u4EF6\u4E0D stash\u3001\u4E0D reset\uFF0C\u8BF7\u5148\u5904\u7406\u3002";
|
|
495
|
+
case "GITHUB_AUTH":
|
|
496
|
+
return "GitHub \u8BA4\u8BC1\u5931\u6548\uFF1A\u91CD\u65B0\u8FDE\u63A5\u6216\u66F4\u65B0 PAT\uFF1B\u672C\u5730 commit \u4F1A\u4FDD\u7559\uFF0C\u53EF\u76F4\u63A5\u91CD\u8BD5\u3002";
|
|
497
|
+
case "NOT_MERGEABLE":
|
|
498
|
+
return "PR \u65E0\u6CD5\u81EA\u52A8\u5408\u5E76\uFF08\u5E38\u89C1\u4E8E\u5206\u652F\u4FDD\u62A4\uFF09\uFF1APR \u5DF2\u4FDD\u7559\uFF0C\u9700\u4EBA\u5DE5\u5904\u7406\u3002";
|
|
499
|
+
case "PUSH_FAILED":
|
|
500
|
+
return "\u63A8\u9001\u5931\u8D25\uFF1A\u672C\u5730 commit \u5DF2\u4FDD\u7559\uFF0C\u4FEE\u590D\u6743\u9650/\u7F51\u7EDC\u540E\u91CD\u8BD5\u5373\u53EF\u590D\u7528\u3002";
|
|
501
|
+
case "PR_FAILED":
|
|
502
|
+
return "\u521B\u5EFA PR \u5931\u8D25\uFF1A\u672C\u5730 commit \u4E0E\u5206\u652F\u5DF2\u4FDD\u7559\u3002";
|
|
503
|
+
case "FETCH_FAILED":
|
|
504
|
+
return "\u6293\u53D6\u5931\u8D25\uFF1A\u68C0\u67E5\u7F51\u7EDC\u6216\u4EE3\u7406\uFF1B\u6838\u5FC3\u4F1A\u5728\u5185\u90E8\u91CD\u8BD5\u4E00\u6B21\u3002";
|
|
505
|
+
case "FETCH_RATE_LIMITED":
|
|
506
|
+
return "GitHub \u533F\u540D\u9650\u6D41\uFF1A\u914D\u4E00\u4E2A GitHub token \u6216\u7A0D\u540E\u91CD\u8BD5\u3002";
|
|
507
|
+
default:
|
|
508
|
+
return "\u67E5\u770B\u6838\u5FC3\u65E5\u5FD7\uFF08\u6309\u8FD0\u884C id \u5F52\u6863\uFF09\u5B9A\u4F4D\u5931\u8D25\u539F\u56E0\u3002";
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
var CORE_CODES = /* @__PURE__ */ new Set([
|
|
512
|
+
"INTERNAL",
|
|
513
|
+
"CONFIG_INVALID",
|
|
514
|
+
"PAYLOAD_TOO_LARGE",
|
|
515
|
+
"FETCH_FAILED",
|
|
516
|
+
"FETCH_RATE_LIMITED",
|
|
517
|
+
"PARSE_FAILED",
|
|
518
|
+
"MODEL_UNAUTHORIZED",
|
|
519
|
+
"MODEL_RATE_LIMITED",
|
|
520
|
+
"MODEL_TIMEOUT",
|
|
521
|
+
"MODEL_FAILED",
|
|
522
|
+
"MODEL_BAD_OUTPUT",
|
|
523
|
+
"GIT_NOT_INSTALLED",
|
|
524
|
+
"REPO_NOT_GIT",
|
|
525
|
+
"REPO_DIRTY",
|
|
526
|
+
"GIT_OPERATION_FAILED",
|
|
527
|
+
"PR_FAILED",
|
|
528
|
+
"NOT_MERGEABLE"
|
|
529
|
+
]);
|
|
530
|
+
var TEXT_RULES = [
|
|
531
|
+
{ pattern: /\b401\b|unauthori[sz]ed|invalid api[ -]?key|incorrect api key|authentication (failed|error)/i, code: "MODEL_UNAUTHORIZED" },
|
|
532
|
+
{ pattern: /\b404\b|model[^\n]*not found|no such model/i, code: "MODEL_NOT_FOUND" },
|
|
533
|
+
{ pattern: /\b429\b|rate[ _-]?limit/i, code: "MODEL_RATE_LIMITED" },
|
|
534
|
+
{ pattern: /timeout|timed out|deadline exceeded|context deadline/i, code: "MODEL_TIMEOUT" },
|
|
535
|
+
{ pattern: /git not found|exec: "git"|git: command not found/i, code: "GIT_NOT_INSTALLED" },
|
|
536
|
+
{ pattern: /not a git (repository|repo)|does not have any commits|bad revision/i, code: "REPO_NOT_GIT" },
|
|
537
|
+
{ pattern: /working tree has tracked changes|dirty working tree/i, code: "REPO_DIRTY" },
|
|
538
|
+
{ pattern: /dial tcp|no such host|connection refused|fetch trending|get "https:/i, code: "FETCH_FAILED" },
|
|
539
|
+
{ pattern: /low level fetch|server misbehaving/i, code: "FETCH_FAILED" }
|
|
540
|
+
];
|
|
541
|
+
function classifyStderr(stderr) {
|
|
542
|
+
const text = stderr.trim();
|
|
543
|
+
const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
544
|
+
const explicit = /^(?:error:\s*)?([A-Z][A-Z0-9_]+):\s*(.*)$/;
|
|
545
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
546
|
+
const line = lines[index] ?? "";
|
|
547
|
+
const match = explicit.exec(line);
|
|
548
|
+
if (match !== null && CORE_CODES.has(match[1] ?? "")) {
|
|
549
|
+
const code = match[1];
|
|
550
|
+
return { code, message: (match[2] ?? "").trim() || line, retriable: isRetriable(code), inferred: false };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const message = lines[lines.length - 1] ?? "\u6838\u5FC3\u672A\u8F93\u51FA\u9519\u8BEF\u4FE1\u606F";
|
|
554
|
+
for (const rule of TEXT_RULES) {
|
|
555
|
+
if (rule.pattern.test(text)) {
|
|
556
|
+
return { code: rule.code, message, retriable: isRetriable(rule.code), inferred: true };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return { code: "INTERNAL", message, retriable: isRetriable("INTERNAL"), inferred: true };
|
|
560
|
+
}
|
|
561
|
+
var SECRET_PATTERNS = [
|
|
562
|
+
/sk-[A-Za-z0-9_-]{8,}/g,
|
|
563
|
+
/gh[pousr]_[A-Za-z0-9]{16,}/g,
|
|
564
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
565
|
+
/(authorization\s*:\s*bearer\s+)\S+/gi,
|
|
566
|
+
/("?(?:apiKey|api_key|token|access_token|secret)"?\s*[:=]\s*)"[^"]*"/gi
|
|
567
|
+
];
|
|
568
|
+
function redact(input, secrets = []) {
|
|
569
|
+
let output = input;
|
|
570
|
+
for (const secret of secrets) {
|
|
571
|
+
if (typeof secret !== "string" || secret.length < 8) continue;
|
|
572
|
+
output = output.split(secret).join("***");
|
|
573
|
+
}
|
|
574
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
575
|
+
output = output.replace(
|
|
576
|
+
pattern,
|
|
577
|
+
(match, prefix) => typeof prefix === "string" ? `${prefix}"***"` : "***"
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
return output;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/ipc.ts
|
|
584
|
+
var ipc_exports = {};
|
|
585
|
+
__export(ipc_exports, {
|
|
586
|
+
IPC_SCHEMA_VERSION: () => IPC_SCHEMA_VERSION,
|
|
587
|
+
IpcError: () => IpcError,
|
|
588
|
+
SUPPORTED_DIGEST_SCHEMA: () => SUPPORTED_DIGEST_SCHEMA,
|
|
589
|
+
isVersionInfo: () => isVersionInfo,
|
|
590
|
+
parseJsonObject: () => parseJsonObject,
|
|
591
|
+
parseRunResult: () => parseRunResult,
|
|
592
|
+
parseVersionInfo: () => parseVersionInfo
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// src/ipc.gen.ts
|
|
596
|
+
var IPC_SCHEMA_VERSION = 1;
|
|
597
|
+
|
|
598
|
+
// src/ipc.ts
|
|
599
|
+
var SUPPORTED_DIGEST_SCHEMA = 1;
|
|
600
|
+
var IpcError = class extends Error {
|
|
601
|
+
code;
|
|
602
|
+
constructor(code, message) {
|
|
603
|
+
super(message);
|
|
604
|
+
this.name = "IpcError";
|
|
605
|
+
this.code = code;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
function isRecord(value) {
|
|
609
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
610
|
+
}
|
|
611
|
+
function parseJsonObject(text) {
|
|
612
|
+
const trimmed = text.trim();
|
|
613
|
+
if (trimmed === "") throw new IpcError("IPC_INVALID", "\u6838\u5FC3 stdout \u4E3A\u7A7A");
|
|
614
|
+
let value;
|
|
615
|
+
try {
|
|
616
|
+
value = JSON.parse(trimmed);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
619
|
+
throw new IpcError("IPC_INVALID", `\u6838\u5FC3 stdout \u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${detail}`);
|
|
620
|
+
}
|
|
621
|
+
if (!isRecord(value)) throw new IpcError("IPC_INVALID", "\u6838\u5FC3 stdout \u4E0D\u662F JSON \u5BF9\u8C61");
|
|
622
|
+
return value;
|
|
623
|
+
}
|
|
624
|
+
function isVersionInfo(value) {
|
|
625
|
+
if (!isRecord(value)) return false;
|
|
626
|
+
return typeof value.version === "string" && typeof value.ipcSchema === "number" && typeof value.digestSchema === "number";
|
|
627
|
+
}
|
|
628
|
+
function parseVersionInfo(text) {
|
|
629
|
+
const value = parseJsonObject(text);
|
|
630
|
+
if (!isVersionInfo(value)) {
|
|
631
|
+
throw new IpcError("IPC_INVALID", "\u6838\u5FC3 version --json \u7F3A\u5C11 version/ipcSchema/digestSchema");
|
|
632
|
+
}
|
|
633
|
+
return value;
|
|
634
|
+
}
|
|
635
|
+
function normalizeFiles(value) {
|
|
636
|
+
if (!Array.isArray(value)) return [];
|
|
637
|
+
const files = [];
|
|
638
|
+
for (const item of value) {
|
|
639
|
+
if (!isRecord(item) || typeof item.path !== "string") continue;
|
|
640
|
+
files.push({
|
|
641
|
+
path: item.path,
|
|
642
|
+
bytes: typeof item.bytes === "number" ? item.bytes : 0,
|
|
643
|
+
sha256: typeof item.sha256 === "string" ? item.sha256 : ""
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return files;
|
|
647
|
+
}
|
|
648
|
+
function normalizeError(value) {
|
|
649
|
+
if (!isRecord(value)) return void 0;
|
|
650
|
+
if (typeof value.code !== "string" || typeof value.message !== "string") return void 0;
|
|
651
|
+
return {
|
|
652
|
+
code: value.code,
|
|
653
|
+
message: value.message,
|
|
654
|
+
retriable: value.retriable === true,
|
|
655
|
+
...isRecord(value.details) ? { details: value.details } : {}
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function parseRunResult(text, options) {
|
|
659
|
+
const value = parseJsonObject(text);
|
|
660
|
+
const contractShaped = value.ipcSchema === IPC_SCHEMA_VERSION && typeof value.ok === "boolean" && value.command === "run";
|
|
661
|
+
if (contractShaped) {
|
|
662
|
+
const result = value;
|
|
663
|
+
return {
|
|
664
|
+
ok: result.ok,
|
|
665
|
+
skipped: result.skipped === true,
|
|
666
|
+
dryRun: options.dryRun,
|
|
667
|
+
files: normalizeFiles(result.files),
|
|
668
|
+
contentHash: typeof value.contentHash === "string" ? value.contentHash : void 0,
|
|
669
|
+
branch: typeof value.branch === "string" ? value.branch : void 0,
|
|
670
|
+
commit: typeof value.commit === "string" ? value.commit : void 0,
|
|
671
|
+
prUrl: typeof value.prUrl === "string" ? value.prUrl : void 0,
|
|
672
|
+
error: normalizeError(result.error),
|
|
673
|
+
warnings: Array.isArray(result.warnings) ? result.warnings.filter((item) => typeof item === "string") : [],
|
|
674
|
+
...timing(options),
|
|
675
|
+
coreVersion: options.coreVersion
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (!Array.isArray(value.files)) {
|
|
679
|
+
throw new IpcError("IPC_INVALID", "\u6838\u5FC3 run --json \u65E2\u4E0D\u662F\u5951\u7EA6 RunResult\uFF0C\u4E5F\u4E0D\u542B files \u6570\u7EC4");
|
|
680
|
+
}
|
|
681
|
+
const digest = isRecord(value.digest) ? value.digest : void 0;
|
|
682
|
+
return {
|
|
683
|
+
ok: true,
|
|
684
|
+
skipped: value.skipped === true,
|
|
685
|
+
dryRun: options.dryRun,
|
|
686
|
+
files: normalizeFiles(value.files),
|
|
687
|
+
contentHash: typeof digest?.contentHash === "string" ? digest.contentHash : void 0,
|
|
688
|
+
branch: typeof value.branch === "string" ? value.branch : void 0,
|
|
689
|
+
prUrl: typeof value.prUrl === "string" ? value.prUrl : void 0,
|
|
690
|
+
warnings: [],
|
|
691
|
+
...timing(options),
|
|
692
|
+
coreVersion: options.coreVersion
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function timing(options) {
|
|
696
|
+
return { startedAt: options.startedAt, finishedAt: options.finishedAt, durationMs: options.durationMs };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/core/handshake.ts
|
|
700
|
+
var handshake_exports = {};
|
|
701
|
+
__export(handshake_exports, {
|
|
702
|
+
DEFAULT_PROVIDERS: () => DEFAULT_PROVIDERS,
|
|
703
|
+
handshake: () => handshake
|
|
704
|
+
});
|
|
705
|
+
var DEFAULT_PROVIDERS = ["deepseek", "openai-compatible"];
|
|
706
|
+
async function handshake(core, options = {}) {
|
|
707
|
+
let result;
|
|
708
|
+
try {
|
|
709
|
+
result = await execCapture(core.path, ["version", "--json"], {
|
|
710
|
+
timeoutMs: options.timeoutMs ?? 5e3,
|
|
711
|
+
signal: options.signal
|
|
712
|
+
});
|
|
713
|
+
} catch (error) {
|
|
714
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
715
|
+
throw new IpcError("CORE_SPAWN_FAILED", `\u65E0\u6CD5\u6267\u884C\u6838\u5FC3 ${core.path}\uFF1A${detail}`);
|
|
716
|
+
}
|
|
717
|
+
if (result.aborted || options.signal?.aborted === true) {
|
|
718
|
+
throw new IpcError("CORE_CANCELLED", "\u8FD0\u884C\u5DF2\u53D6\u6D88\uFF08\u63E1\u624B\u9636\u6BB5\uFF09\uFF1A\u6838\u5FC3\u8FDB\u7A0B\u5DF2\u7EC8\u6B62\uFF0C\u672A\u63D0\u4EA4\u4EFB\u4F55\u72B6\u6001");
|
|
719
|
+
}
|
|
720
|
+
if (result.timedOut) {
|
|
721
|
+
throw new IpcError("CORE_TIMEOUT", `\u6838\u5FC3 ${core.path} version --json \u8D85\u65F6\uFF085 s\uFF09\u672A\u8FD4\u56DE`);
|
|
722
|
+
}
|
|
723
|
+
if (result.code !== 0) {
|
|
724
|
+
const tail = result.stderr.trim().split("\n").slice(-3).join(" / ");
|
|
725
|
+
throw new IpcError("CORE_INCOMPATIBLE", `\u6838\u5FC3 version --json \u9000\u51FA\u7801 ${result.code ?? "null"}\uFF1A${tail}`);
|
|
726
|
+
}
|
|
727
|
+
let version;
|
|
728
|
+
try {
|
|
729
|
+
version = parseVersionInfo(result.stdout);
|
|
730
|
+
} catch (error) {
|
|
731
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
732
|
+
throw new IpcError("CORE_INCOMPATIBLE", `\u6838\u5FC3 version --json \u8F93\u51FA\u65E0\u6CD5\u8BC6\u522B\uFF1A${detail}`);
|
|
733
|
+
}
|
|
734
|
+
if (version.ipcSchema !== IPC_SCHEMA_VERSION || version.digestSchema !== SUPPORTED_DIGEST_SCHEMA) {
|
|
735
|
+
throw new IpcError(
|
|
736
|
+
"CORE_INCOMPATIBLE",
|
|
737
|
+
`\u6838\u5FC3\u534F\u8BAE\u4E0D\u5339\u914D\uFF1A\u6838\u5FC3 ipcSchema=${version.ipcSchema} digestSchema=${version.digestSchema}\uFF0C\u63D2\u4EF6\u652F\u6301 ipcSchema=${IPC_SCHEMA_VERSION} digestSchema=${SUPPORTED_DIGEST_SCHEMA}\u3002\u8BF7\u628A\u63D2\u4EF6\u4E0E\u6838\u5FC3\u5347\u7EA7\u5230\u540C\u4E00\u7248\u672C\uFF08\u63D2\u4EF6\u5E26\u5E73\u53F0\u5305\uFF0C\u5347\u7EA7\u63D2\u4EF6\u5373\u53EF\uFF09\u3002`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const supportedProviders = Array.isArray(version.supportedProviders) && version.supportedProviders.length > 0 ? version.supportedProviders : [...DEFAULT_PROVIDERS];
|
|
741
|
+
return { version, corePath: core.path, durationMs: result.durationMs, supportedProviders };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/core/release.ts
|
|
745
|
+
var release_exports = {};
|
|
746
|
+
__export(release_exports, {
|
|
747
|
+
assetName: () => assetName,
|
|
748
|
+
describeRelease: () => describeRelease,
|
|
749
|
+
ensureCore: () => ensureCore,
|
|
750
|
+
goTarget: () => goTarget,
|
|
751
|
+
readCoreRelease: () => readCoreRelease,
|
|
752
|
+
verifyCached: () => verifyCached
|
|
753
|
+
});
|
|
754
|
+
import { createHash } from "crypto";
|
|
755
|
+
import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync, statSync, unlinkSync } from "fs";
|
|
756
|
+
import { createRequire } from "module";
|
|
757
|
+
import { pipeline } from "stream/promises";
|
|
758
|
+
import { Readable } from "stream";
|
|
759
|
+
import path4 from "path";
|
|
760
|
+
var DOWNLOAD_TIMEOUT_MS = 18e4;
|
|
761
|
+
function requireJson() {
|
|
762
|
+
const require2 = createRequire(import.meta.url);
|
|
763
|
+
return require2("../package.json");
|
|
764
|
+
}
|
|
765
|
+
function readCoreRelease(pkg) {
|
|
766
|
+
const source = pkg ?? requireJson();
|
|
767
|
+
const repo = source.dsh?.core?.repo;
|
|
768
|
+
const version = source.dsh?.core?.version;
|
|
769
|
+
if (typeof repo !== "string" || !/^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
770
|
+
throw new IpcError("CORE_UNAVAILABLE", "package.json \u7F3A\u5C11 dsh.core.repo\uFF08owner/repo\uFF09\uFF0C\u65E0\u6CD5\u5B9A\u4F4D\u6838\u5FC3 Release");
|
|
771
|
+
}
|
|
772
|
+
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[\w.]+)?$/.test(version)) {
|
|
773
|
+
throw new IpcError("CORE_UNAVAILABLE", "package.json \u7F3A\u5C11 dsh.core.version\uFF08\u5F62\u5982 0.1.0\uFF09\uFF0C\u65E0\u6CD5\u5B9A\u4F4D\u6838\u5FC3 Release");
|
|
774
|
+
}
|
|
775
|
+
return { repo, version };
|
|
776
|
+
}
|
|
777
|
+
function goTarget(platform = process.platform, arch = process.arch) {
|
|
778
|
+
const goos = platform === "win32" ? "windows" : platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : platform;
|
|
779
|
+
const goarch = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : arch === "ia32" ? "386" : arch;
|
|
780
|
+
return { goos, goarch };
|
|
781
|
+
}
|
|
782
|
+
function assetName(platform = process.platform, arch = process.arch) {
|
|
783
|
+
const { goos, goarch } = goTarget(platform, arch);
|
|
784
|
+
const suffix = platform === "win32" ? ".exe" : "";
|
|
785
|
+
return `trendradar_${goos}_${goarch}${suffix}`;
|
|
786
|
+
}
|
|
787
|
+
function binaryName(platform = process.platform) {
|
|
788
|
+
return platform === "win32" ? "trendradar.exe" : "trendradar";
|
|
789
|
+
}
|
|
790
|
+
function describeRelease(release, options = {}) {
|
|
791
|
+
const env = options.env ?? process.env;
|
|
792
|
+
const platform = options.platform ?? process.platform;
|
|
793
|
+
const arch = options.arch ?? process.arch;
|
|
794
|
+
const { goos, goarch } = goTarget(platform, arch);
|
|
795
|
+
const asset = assetName(platform, arch);
|
|
796
|
+
const tag = `v${release.version}`;
|
|
797
|
+
const base = `https://github.com/${release.repo}/releases/download/${tag}`;
|
|
798
|
+
const cacheDir = path4.join(dataDir(env), "core", tag);
|
|
799
|
+
const binaryPath = path4.join(cacheDir, binaryName(platform));
|
|
800
|
+
return {
|
|
801
|
+
...release,
|
|
802
|
+
tag,
|
|
803
|
+
goos,
|
|
804
|
+
goarch,
|
|
805
|
+
asset,
|
|
806
|
+
url: `${base}/${asset}`,
|
|
807
|
+
checksumsUrl: `${base}/checksums.txt`,
|
|
808
|
+
cacheDir,
|
|
809
|
+
binaryPath,
|
|
810
|
+
cached: existsSync2(binaryPath)
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
function sha256File(file) {
|
|
814
|
+
return createHash("sha256").update(readFileSync2(file)).digest("hex");
|
|
815
|
+
}
|
|
816
|
+
function markerPath(binaryPath) {
|
|
817
|
+
return `${binaryPath}.sha256`;
|
|
818
|
+
}
|
|
819
|
+
function verifyCached(binaryPath) {
|
|
820
|
+
let info;
|
|
821
|
+
try {
|
|
822
|
+
info = statSync(binaryPath);
|
|
823
|
+
} catch {
|
|
824
|
+
return { ok: false, reason: "missing" };
|
|
825
|
+
}
|
|
826
|
+
if (!info.isFile()) return { ok: false, reason: "not-a-file" };
|
|
827
|
+
if (process.platform !== "win32" && (info.mode & 73) === 0) return { ok: false, reason: "not-executable" };
|
|
828
|
+
const marker = markerPath(binaryPath);
|
|
829
|
+
if (!existsSync2(marker)) return { ok: true };
|
|
830
|
+
const expected = readFileSync2(marker, "utf8").trim();
|
|
831
|
+
if (expected === "") return { ok: true };
|
|
832
|
+
try {
|
|
833
|
+
return sha256File(binaryPath) === expected ? { ok: true } : { ok: false, reason: "checksum-mismatch" };
|
|
834
|
+
} catch {
|
|
835
|
+
return { ok: false, reason: "unreadable" };
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function fetchOf(options) {
|
|
839
|
+
if (options.fetchImpl !== void 0) return options.fetchImpl;
|
|
840
|
+
if (typeof fetch !== "function") {
|
|
841
|
+
throw new IpcError("CORE_UNAVAILABLE", "\u5F53\u524D Node \u6CA1\u6709\u5168\u5C40 fetch\uFF08\u9700\u8981 Node \u2265 18\uFF09\uFF0C\u65E0\u6CD5\u4E0B\u8F7D\u6838\u5FC3");
|
|
842
|
+
}
|
|
843
|
+
return fetch;
|
|
844
|
+
}
|
|
845
|
+
async function fetchChecksum(descriptor, options) {
|
|
846
|
+
options.onProgress?.({ phase: "checksums" });
|
|
847
|
+
const response = await fetchOf(options)(descriptor.checksumsUrl, { signal: AbortSignal.timeout(3e4) }).catch((error) => {
|
|
848
|
+
throw new IpcError(
|
|
849
|
+
"CORE_UNAVAILABLE",
|
|
850
|
+
`\u65E0\u6CD5\u83B7\u53D6\u6838\u5FC3\u6821\u9A8C\u6587\u4EF6\uFF1A${descriptor.checksumsUrl}\uFF08${error instanceof Error ? error.message : String(error)}\uFF09\u3002\u82E5\u5728\u516C\u53F8\u7F51\u7EDC\u5185\uFF0C\u8BF7\u786E\u8BA4\u4EE3\u7406\u53EF\u7528\uFF1B\u4E5F\u53EF\u7528 TRENDING_DIGEST_CORE \u6307\u5411\u672C\u5730\u5DF2\u7F16\u8BD1\u7684\u6838\u5FC3\u3002`
|
|
851
|
+
);
|
|
852
|
+
});
|
|
853
|
+
if (!response.ok) {
|
|
854
|
+
throw new IpcError(
|
|
855
|
+
"CORE_UNAVAILABLE",
|
|
856
|
+
`\u6838\u5FC3 ${descriptor.tag} \u8FD8\u6CA1\u6709\u53D1\u5E03\uFF08${descriptor.checksumsUrl} \u8FD4\u56DE HTTP ${response.status}\uFF09\u3002\u53D1\u5E03\u987A\u5E8F\u662F\u5148\u5728 trendradar \u6253 tag \u5E76\u4E0A\u4F20 Release \u8D44\u4EA7\uFF0C\u63D2\u4EF6\u968F\u540E\u624D\u80FD\u4E0B\u8F7D\u3002`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const text = await response.text();
|
|
860
|
+
for (const line of text.split("\n")) {
|
|
861
|
+
const match = /^([0-9a-f]{64})\s+\*?(.+)$/.exec(line.trim());
|
|
862
|
+
if (match !== null && match[2] === descriptor.asset) return match[1];
|
|
863
|
+
}
|
|
864
|
+
throw new IpcError(
|
|
865
|
+
"CORE_UNAVAILABLE",
|
|
866
|
+
`checksums.txt \u91CC\u6CA1\u6709 ${descriptor.asset}\uFF08\u5F53\u524D\u5E73\u53F0 ${descriptor.goos}/${descriptor.goarch} \u53EF\u80FD\u4E0D\u5728\u53D1\u5E03\u77E9\u9635\u5185\uFF09`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
async function download(descriptor, options) {
|
|
870
|
+
const expected = await fetchChecksum(descriptor, options);
|
|
871
|
+
options.onProgress?.({ phase: "download" });
|
|
872
|
+
const response = await fetchOf(options)(descriptor.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) }).catch((error) => {
|
|
873
|
+
throw new IpcError(
|
|
874
|
+
"CORE_UNAVAILABLE",
|
|
875
|
+
`\u4E0B\u8F7D\u6838\u5FC3\u5931\u8D25\uFF1A${descriptor.url}\uFF08${error instanceof Error ? error.message : String(error)}\uFF09\u3002\u53EF\u624B\u52A8\u4E0B\u8F7D\u540E\u653E\u5230 ${descriptor.binaryPath}\uFF0C\u6216\u7528 TRENDING_DIGEST_CORE \u6307\u5B9A\u8DEF\u5F84\u3002`
|
|
876
|
+
);
|
|
877
|
+
});
|
|
878
|
+
if (!response.ok || response.body === null) {
|
|
879
|
+
throw new IpcError(
|
|
880
|
+
"CORE_UNAVAILABLE",
|
|
881
|
+
`\u4E0B\u8F7D\u6838\u5FC3\u5931\u8D25\uFF1A${descriptor.url} \u8FD4\u56DE HTTP ${response.status}\uFF08\u7248\u672C ${descriptor.tag} / ${descriptor.asset}\uFF09`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
mkdirSync2(descriptor.cacheDir, { recursive: true, mode: 448 });
|
|
885
|
+
const temp = `${descriptor.binaryPath}.${process.pid}.${Date.now()}.part`;
|
|
886
|
+
const hash = createHash("sha256");
|
|
887
|
+
const total = Number(response.headers.get("content-length") ?? "0") || void 0;
|
|
888
|
+
let bytes = 0;
|
|
889
|
+
try {
|
|
890
|
+
await pipeline(
|
|
891
|
+
Readable.fromWeb(response.body),
|
|
892
|
+
async function* (source) {
|
|
893
|
+
for await (const chunk of source) {
|
|
894
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
895
|
+
hash.update(buf);
|
|
896
|
+
bytes += buf.length;
|
|
897
|
+
yield buf;
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
createWriteStream(temp, { mode: 384 })
|
|
901
|
+
);
|
|
902
|
+
const actual = hash.digest("hex");
|
|
903
|
+
options.onProgress?.({ phase: "verify", bytes, total });
|
|
904
|
+
if (actual !== expected) {
|
|
905
|
+
throw new IpcError(
|
|
906
|
+
"CORE_CHECKSUM_MISMATCH",
|
|
907
|
+
`\u6838\u5FC3\u4E8C\u8FDB\u5236\u6821\u9A8C\u5931\u8D25\uFF1A\u671F\u671B ${expected}\uFF0C\u5B9E\u9645 ${actual}\uFF08${descriptor.url}\uFF09\u3002\u5DF2\u4E22\u5F03\u4E0B\u8F7D\u7ED3\u679C\u3002`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
chmodSync(temp, 493);
|
|
911
|
+
renameSync2(temp, descriptor.binaryPath);
|
|
912
|
+
const fs = await import("fs");
|
|
913
|
+
fs.writeFileSync(markerPath(descriptor.binaryPath), `${actual}
|
|
914
|
+
`, { mode: 384 });
|
|
915
|
+
return { path: descriptor.binaryPath, sha256: actual };
|
|
916
|
+
} catch (error) {
|
|
917
|
+
rmSync(temp, { force: true });
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
async function ensureCore(options = {}) {
|
|
922
|
+
const release = options.release ?? readCoreRelease();
|
|
923
|
+
const descriptor = describeRelease(release, options);
|
|
924
|
+
if (!options.force && existsSync2(descriptor.binaryPath)) {
|
|
925
|
+
const verified = verifyCached(descriptor.binaryPath);
|
|
926
|
+
if (verified.ok) {
|
|
927
|
+
return { path: descriptor.binaryPath, source: "cache", descriptor };
|
|
928
|
+
}
|
|
929
|
+
if (options.allowDownload === false) {
|
|
930
|
+
throw new IpcError(
|
|
931
|
+
"CORE_UNAVAILABLE",
|
|
932
|
+
`\u672C\u5730\u6838\u5FC3\u7F13\u5B58\u4E0D\u53EF\u7528\uFF08${verified.reason ?? "unknown"}\uFF09\uFF1A${descriptor.binaryPath}\u3002\u5220\u9664\u8BE5\u6587\u4EF6\u540E\u91CD\u8BD5\uFF0C\u6216\u5728\u8BBE\u7F6E\u9875\u70B9\u300C\u91CD\u65B0\u4E0B\u8F7D\u6838\u5FC3\u300D\u3002`
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
unlinkSync(descriptor.binaryPath);
|
|
937
|
+
} catch {
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (options.allowDownload === false) {
|
|
941
|
+
throw new IpcError(
|
|
942
|
+
"CORE_NOT_DOWNLOADED",
|
|
943
|
+
`\u6838\u5FC3 ${descriptor.tag} \u5C1A\u672A\u4E0B\u8F7D\uFF08\u7F13\u5B58\u76EE\u5F55 ${descriptor.cacheDir}\uFF09\u3002\u4E0B\u8F7D\u5730\u5740\uFF1A${descriptor.url}\uFF1B\u6216\u5728\u8BBE\u7F6E\u9875\u70B9\u300C\u4E0B\u8F7D\u6838\u5FC3\u300D\uFF0C\u6216\u8BBE\u7F6E TRENDING_DIGEST_CORE \u6307\u5411\u672C\u5730\u6838\u5FC3\u3002`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
const downloaded = await download(descriptor, options);
|
|
947
|
+
return { path: downloaded.path, source: "download", descriptor, sha256: downloaded.sha256 };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/core/locate.ts
|
|
951
|
+
var locate_exports = {};
|
|
952
|
+
__export(locate_exports, {
|
|
953
|
+
inspectRelease: () => inspectRelease,
|
|
954
|
+
locateCore: () => locateCore,
|
|
955
|
+
overridePath: () => overridePath
|
|
956
|
+
});
|
|
957
|
+
import { accessSync, constants, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
958
|
+
import path5 from "path";
|
|
959
|
+
function executable(file) {
|
|
960
|
+
try {
|
|
961
|
+
if (!statSync2(file).isFile()) return false;
|
|
962
|
+
accessSync(file, constants.X_OK);
|
|
963
|
+
return true;
|
|
964
|
+
} catch {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function overridePath(settings, env = process.env) {
|
|
969
|
+
const fromSettings = settings.corePath.trim();
|
|
970
|
+
if (fromSettings !== "") {
|
|
971
|
+
return { path: path5.isAbsolute(fromSettings) ? fromSettings : path5.resolve(fromSettings), source: "settings" };
|
|
972
|
+
}
|
|
973
|
+
const fromEnv2 = env.TRENDING_DIGEST_CORE?.trim() ?? "";
|
|
974
|
+
if (fromEnv2 !== "") return { path: path5.isAbsolute(fromEnv2) ? fromEnv2 : path5.resolve(fromEnv2), source: "env" };
|
|
975
|
+
return void 0;
|
|
976
|
+
}
|
|
977
|
+
async function locateCore(options) {
|
|
978
|
+
const env = options.env ?? process.env;
|
|
979
|
+
const override = overridePath(options.settings, env);
|
|
980
|
+
if (override !== void 0) {
|
|
981
|
+
if (!existsSync3(override.path)) {
|
|
982
|
+
throw new IpcError(
|
|
983
|
+
"CORE_UNAVAILABLE",
|
|
984
|
+
`${override.source === "settings" ? "\u8BBE\u7F6E\u91CC\u7684 corePath" : "TRENDING_DIGEST_CORE"} \u6307\u5411\u7684\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF1A${override.path}\u3002\u8BE5\u8986\u76D6\u53EA\u7528\u4E8E\u5F00\u53D1/\u6392\u969C\uFF0C\u6B63\u5E38\u4F7F\u7528\u8BF7\u6E05\u7A7A\u5B83\u3002`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
if (!executable(override.path)) {
|
|
988
|
+
throw new IpcError("CORE_UNAVAILABLE", `${override.path} \u4E0D\u53EF\u6267\u884C\uFF08\u7F3A\u6267\u884C\u4F4D\u6216\u4E0D\u662F\u6587\u4EF6\uFF09`);
|
|
989
|
+
}
|
|
990
|
+
return { path: override.path, source: override.source };
|
|
991
|
+
}
|
|
992
|
+
const result = await ensureCore({
|
|
993
|
+
...options.release === void 0 ? {} : { release: options.release },
|
|
994
|
+
env,
|
|
995
|
+
...options.platform === void 0 ? {} : { platform: options.platform },
|
|
996
|
+
...options.arch === void 0 ? {} : { arch: options.arch },
|
|
997
|
+
...options.allowDownload === void 0 ? {} : { allowDownload: options.allowDownload },
|
|
998
|
+
...options.forceDownload === void 0 ? {} : { force: options.forceDownload },
|
|
999
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
|
|
1000
|
+
});
|
|
1001
|
+
return {
|
|
1002
|
+
path: result.path,
|
|
1003
|
+
source: result.source,
|
|
1004
|
+
release: { repo: result.descriptor.repo, version: result.descriptor.version },
|
|
1005
|
+
tag: result.descriptor.tag,
|
|
1006
|
+
downloaded: result.source === "download",
|
|
1007
|
+
...result.sha256 === void 0 ? {} : { sha256: result.sha256 }
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function inspectRelease(settings, options = {}) {
|
|
1011
|
+
const env = options.env ?? process.env;
|
|
1012
|
+
const override = overridePath(settings, env);
|
|
1013
|
+
if (override !== void 0) return { override };
|
|
1014
|
+
try {
|
|
1015
|
+
const release = options.release ?? readCoreRelease();
|
|
1016
|
+
return {
|
|
1017
|
+
descriptor: describeRelease(release, {
|
|
1018
|
+
env,
|
|
1019
|
+
...options.platform === void 0 ? {} : { platform: options.platform },
|
|
1020
|
+
...options.arch === void 0 ? {} : { arch: options.arch }
|
|
1021
|
+
})
|
|
1022
|
+
};
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/core/channel.ts
|
|
1029
|
+
var channel_exports = {};
|
|
1030
|
+
__export(channel_exports, {
|
|
1031
|
+
WRITE_TIMEOUT_MS: () => WRITE_TIMEOUT_MS,
|
|
1032
|
+
openConfigChannel: () => openConfigChannel,
|
|
1033
|
+
removeChannelDir: () => removeChannelDir
|
|
1034
|
+
});
|
|
1035
|
+
import { spawn } from "child_process";
|
|
1036
|
+
import { constants as constants2 } from "fs";
|
|
1037
|
+
import { open, rm, stat, unlink, writeFile } from "fs/promises";
|
|
1038
|
+
import { mkdtempSync } from "fs";
|
|
1039
|
+
import os2 from "os";
|
|
1040
|
+
import path6 from "path";
|
|
1041
|
+
var WRITE_TIMEOUT_MS = 15e3;
|
|
1042
|
+
function tempDir() {
|
|
1043
|
+
return mkdtempSync(path6.join(os2.tmpdir(), "dsh-trending-"));
|
|
1044
|
+
}
|
|
1045
|
+
async function mkfifo(file) {
|
|
1046
|
+
await new Promise((resolve, reject) => {
|
|
1047
|
+
const child = spawn("mkfifo", ["-m", "600", file], { stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
|
|
1048
|
+
let stderr = "";
|
|
1049
|
+
child.stderr?.setEncoding("utf8");
|
|
1050
|
+
child.stderr?.on("data", (chunk) => {
|
|
1051
|
+
stderr += chunk;
|
|
1052
|
+
});
|
|
1053
|
+
child.on("error", reject);
|
|
1054
|
+
child.on("close", (code) => {
|
|
1055
|
+
if (code === 0) resolve();
|
|
1056
|
+
else reject(new Error(`mkfifo \u9000\u51FA\u7801 ${code ?? "null"}\uFF1A${stderr.trim()}`));
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
async function createFifoChannel(dir, writeTimeoutMs) {
|
|
1061
|
+
const file = path6.join(dir, "core.yml");
|
|
1062
|
+
await mkfifo(file);
|
|
1063
|
+
let disposed = false;
|
|
1064
|
+
return {
|
|
1065
|
+
kind: "fifo",
|
|
1066
|
+
path: file,
|
|
1067
|
+
/**
|
|
1068
|
+
* 用 `O_NONBLOCK` 轮询打开写端:没有读端时 open 会返回 ENXIO 而不是**永久阻塞**。
|
|
1069
|
+
* 阻塞式 open 会把 libuv 的请求挂在事件循环上,超时后进程都退不掉——这正是
|
|
1070
|
+
* 「先轮询、再写」的理由。配置只有几 KB,写进管道缓冲区不会出现 EAGAIN。
|
|
1071
|
+
*/
|
|
1072
|
+
async publish(content) {
|
|
1073
|
+
const deadline = Date.now() + writeTimeoutMs;
|
|
1074
|
+
let handle;
|
|
1075
|
+
for (; ; ) {
|
|
1076
|
+
try {
|
|
1077
|
+
handle = await open(file, constants2.O_WRONLY | constants2.O_NONBLOCK);
|
|
1078
|
+
break;
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
const code = error.code;
|
|
1081
|
+
if (code !== "ENXIO" && code !== "ENOENT") throw error;
|
|
1082
|
+
if (Date.now() >= deadline) {
|
|
1083
|
+
throw new Error(`FIFO \u5199\u8D85\u65F6\uFF08${writeTimeoutMs} ms\uFF09\uFF1A\u6838\u5FC3\u672A\u8BFB\u53D6\u914D\u7F6E`);
|
|
1084
|
+
}
|
|
1085
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
await handle.writeFile(content, "utf8");
|
|
1090
|
+
} finally {
|
|
1091
|
+
await handle.close();
|
|
1092
|
+
}
|
|
1093
|
+
},
|
|
1094
|
+
async dispose() {
|
|
1095
|
+
if (disposed) return;
|
|
1096
|
+
disposed = true;
|
|
1097
|
+
await unlink(file).catch(() => void 0);
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
async function createTmpfileChannel(dir) {
|
|
1102
|
+
const file = path6.join(dir, "core.yml");
|
|
1103
|
+
let disposed = false;
|
|
1104
|
+
return {
|
|
1105
|
+
kind: "tmpfile",
|
|
1106
|
+
path: file,
|
|
1107
|
+
async publish(content) {
|
|
1108
|
+
await writeFile(file, content, { mode: 384 });
|
|
1109
|
+
},
|
|
1110
|
+
async dispose() {
|
|
1111
|
+
if (disposed) return;
|
|
1112
|
+
disposed = true;
|
|
1113
|
+
await unlink(file).catch(() => void 0);
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
async function openConfigChannel(options = {}) {
|
|
1118
|
+
const dir = tempDir();
|
|
1119
|
+
const preferFifo = options.preferFifo ?? process.platform !== "win32";
|
|
1120
|
+
const writeTimeoutMs = options.writeTimeoutMs ?? WRITE_TIMEOUT_MS;
|
|
1121
|
+
if (preferFifo) {
|
|
1122
|
+
try {
|
|
1123
|
+
return await createFifoChannel(dir, writeTimeoutMs);
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return createTmpfileChannel(dir);
|
|
1128
|
+
}
|
|
1129
|
+
async function removeChannelDir(channel) {
|
|
1130
|
+
try {
|
|
1131
|
+
const info = await stat(channel.path);
|
|
1132
|
+
if (info.isFIFO()) return;
|
|
1133
|
+
} catch {
|
|
1134
|
+
}
|
|
1135
|
+
await rm(path6.dirname(channel.path), { recursive: true, force: true }).catch(() => void 0);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/core/run.ts
|
|
1139
|
+
var run_exports = {};
|
|
1140
|
+
__export(run_exports, {
|
|
1141
|
+
DEFAULT_RUN_TIMEOUT_MS: () => DEFAULT_RUN_TIMEOUT_MS,
|
|
1142
|
+
buildCoreArgs: () => buildCoreArgs,
|
|
1143
|
+
runCore: () => runCore
|
|
1144
|
+
});
|
|
1145
|
+
import { spawn as spawn2 } from "child_process";
|
|
1146
|
+
var DEFAULT_RUN_TIMEOUT_MS = 12e4;
|
|
1147
|
+
var MAX_STDOUT = 16 * 1024 * 1024;
|
|
1148
|
+
var MAX_STDERR = 2 * 1024 * 1024;
|
|
1149
|
+
var KILL_GRACE_MS = 3e3;
|
|
1150
|
+
function nowIso() {
|
|
1151
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1152
|
+
}
|
|
1153
|
+
function errorFrom(stderr, exitCode) {
|
|
1154
|
+
const classified = classifyStderr(stderr);
|
|
1155
|
+
return {
|
|
1156
|
+
code: classified.code,
|
|
1157
|
+
message: classified.message,
|
|
1158
|
+
retriable: classified.retriable,
|
|
1159
|
+
details: { inferred: classified.inferred, exitCode }
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function buildCoreArgs(channelPath, options) {
|
|
1163
|
+
const args = ["run", "--config", channelPath, "--json"];
|
|
1164
|
+
if (options.dryRun) args.push("--dry-run");
|
|
1165
|
+
if (options.date !== void 0 && options.date !== "") args.push("--date", options.date);
|
|
1166
|
+
return args;
|
|
1167
|
+
}
|
|
1168
|
+
async function runCore(options) {
|
|
1169
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
|
|
1170
|
+
const startedAt = nowIso();
|
|
1171
|
+
const started = Date.now();
|
|
1172
|
+
const channel = await openConfigChannel();
|
|
1173
|
+
const yaml = `${toYaml(toCoreConfig(options.settings, options.apiKey, options.baseURLOverride))}
|
|
1174
|
+
`;
|
|
1175
|
+
const secrets = [options.apiKey, options.token, ...options.secrets ?? []];
|
|
1176
|
+
const child = spawn2(options.core.path, buildCoreArgs(channel.path, { dryRun: options.dryRun, date: options.date }), {
|
|
1177
|
+
env: sanitizedEnv(process.env, { GITHUB_TOKEN: options.token, GH_TOKEN: options.token }),
|
|
1178
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1179
|
+
windowsHide: true
|
|
1180
|
+
});
|
|
1181
|
+
let stdout = "";
|
|
1182
|
+
let stdoutBytes = 0;
|
|
1183
|
+
let stderr = "";
|
|
1184
|
+
let timedOut = false;
|
|
1185
|
+
let aborted = false;
|
|
1186
|
+
let channelError;
|
|
1187
|
+
const publish = channel.publish(yaml).catch((error) => {
|
|
1188
|
+
channelError = error instanceof Error ? error : new Error(String(error));
|
|
1189
|
+
});
|
|
1190
|
+
const escalate = () => setTimeout(() => {
|
|
1191
|
+
try {
|
|
1192
|
+
child.kill("SIGKILL");
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
}, KILL_GRACE_MS);
|
|
1196
|
+
let escalations = [];
|
|
1197
|
+
const timer = setTimeout(() => {
|
|
1198
|
+
timedOut = true;
|
|
1199
|
+
try {
|
|
1200
|
+
child.kill("SIGTERM");
|
|
1201
|
+
} catch {
|
|
1202
|
+
}
|
|
1203
|
+
escalations.push(escalate());
|
|
1204
|
+
}, timeoutMs);
|
|
1205
|
+
const onAbort = () => {
|
|
1206
|
+
aborted = true;
|
|
1207
|
+
try {
|
|
1208
|
+
child.kill("SIGTERM");
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
escalations.push(escalate());
|
|
1212
|
+
};
|
|
1213
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1214
|
+
child.stdout?.setEncoding("utf8");
|
|
1215
|
+
child.stdout?.on("data", (chunk) => {
|
|
1216
|
+
stdoutBytes += Buffer.byteLength(chunk);
|
|
1217
|
+
if (stdoutBytes <= MAX_STDOUT) stdout += chunk;
|
|
1218
|
+
});
|
|
1219
|
+
let stderrBuffer = "";
|
|
1220
|
+
child.stderr?.setEncoding("utf8");
|
|
1221
|
+
child.stderr?.on("data", (chunk) => {
|
|
1222
|
+
if (stderr.length < MAX_STDERR) stderr += chunk;
|
|
1223
|
+
stderrBuffer += chunk;
|
|
1224
|
+
let index = stderrBuffer.indexOf("\n");
|
|
1225
|
+
while (index >= 0) {
|
|
1226
|
+
const line = redact(stderrBuffer.slice(0, index), secrets);
|
|
1227
|
+
stderrBuffer = stderrBuffer.slice(index + 1);
|
|
1228
|
+
if (line.trim() !== "") options.onStderr?.(line);
|
|
1229
|
+
index = stderrBuffer.indexOf("\n");
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
const closed = await new Promise((resolve, reject) => {
|
|
1233
|
+
child.on("error", reject);
|
|
1234
|
+
child.on("close", (code, signal) => resolve({ code, signal }));
|
|
1235
|
+
}).catch((error) => {
|
|
1236
|
+
throw new IpcError("CORE_SPAWN_FAILED", `\u65E0\u6CD5\u542F\u52A8\u6838\u5FC3 ${options.core.path}\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
1237
|
+
});
|
|
1238
|
+
clearTimeout(timer);
|
|
1239
|
+
for (const handle of escalations) clearTimeout(handle);
|
|
1240
|
+
escalations = [];
|
|
1241
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
1242
|
+
if (stderrBuffer.trim() !== "") options.onStderr?.(redact(stderrBuffer, secrets));
|
|
1243
|
+
await publish;
|
|
1244
|
+
await channel.dispose();
|
|
1245
|
+
await removeChannelDir(channel);
|
|
1246
|
+
const finishedAt = nowIso();
|
|
1247
|
+
const durationMs = Date.now() - started;
|
|
1248
|
+
const tail = stderr.trim().split("\n").slice(-20).join("\n");
|
|
1249
|
+
if (aborted) {
|
|
1250
|
+
throw new IpcError("CORE_CANCELLED", "\u8FD0\u884C\u5DF2\u53D6\u6D88\uFF1A\u6838\u5FC3\u8FDB\u7A0B\u5DF2\u7EC8\u6B62\uFF0C\u672A\u63D0\u4EA4\u4EFB\u4F55\u72B6\u6001");
|
|
1251
|
+
}
|
|
1252
|
+
if (timedOut) {
|
|
1253
|
+
throw new IpcError(
|
|
1254
|
+
"CORE_TIMEOUT",
|
|
1255
|
+
`\u6838\u5FC3\u8FD0\u884C\u8D85\u65F6\uFF08${Math.round(timeoutMs / 1e3)} s\uFF09\u5DF2\u7EC8\u6B62${tail === "" ? "" : `\uFF1B\u6700\u540E\u8F93\u51FA\uFF1A${tail}`}`
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
if (channelError !== void 0) {
|
|
1259
|
+
throw new IpcError("CORE_CONFIG_CHANNEL_FAILED", `\u914D\u7F6E\u901A\u9053\u5199\u5165\u5931\u8D25\uFF1A${channelError.message}`);
|
|
1260
|
+
}
|
|
1261
|
+
if (stdoutBytes > MAX_STDOUT) {
|
|
1262
|
+
throw new IpcError("PAYLOAD_TOO_LARGE", `\u6838\u5FC3\u8F93\u51FA\u8D85\u8FC7 ${MAX_STDOUT} \u5B57\u8282\u4E0A\u9650`);
|
|
1263
|
+
}
|
|
1264
|
+
const timing2 = { coreVersion: options.version.version, startedAt, finishedAt, durationMs, dryRun: options.dryRun };
|
|
1265
|
+
if (stdout.trim() === "") {
|
|
1266
|
+
if (closed.code === 0) {
|
|
1267
|
+
throw new IpcError("IPC_INVALID", "\u6838\u5FC3\u6B63\u5E38\u9000\u51FA\u4F46\u6CA1\u6709\u8F93\u51FA\u7ED3\u679C\u4FE1\u5C01");
|
|
1268
|
+
}
|
|
1269
|
+
return {
|
|
1270
|
+
outcome: { ok: false, skipped: false, files: [], warnings: [], error: errorFrom(stderr, closed.code), ...timing2 },
|
|
1271
|
+
channelKind: channel.kind,
|
|
1272
|
+
stderr,
|
|
1273
|
+
exitCode: closed.code
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
let outcome;
|
|
1277
|
+
try {
|
|
1278
|
+
outcome = parseRunResult(stdout, timing2);
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
if (closed.code !== 0) {
|
|
1281
|
+
return {
|
|
1282
|
+
outcome: { ok: false, skipped: false, files: [], warnings: [], error: errorFrom(stderr, closed.code), ...timing2 },
|
|
1283
|
+
channelKind: channel.kind,
|
|
1284
|
+
stderr,
|
|
1285
|
+
exitCode: closed.code
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
throw error;
|
|
1289
|
+
}
|
|
1290
|
+
if (closed.code !== 0) {
|
|
1291
|
+
return {
|
|
1292
|
+
outcome: { ...outcome, ok: false, error: errorFrom(stderr, closed.code) },
|
|
1293
|
+
channelKind: channel.kind,
|
|
1294
|
+
stderr,
|
|
1295
|
+
exitCode: closed.code
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
return { outcome, channelKind: channel.kind, stderr, exitCode: closed.code };
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/host/core.ts
|
|
1302
|
+
var core_exports = {};
|
|
1303
|
+
__export(core_exports, {
|
|
1304
|
+
detectCoexistence: () => detectCoexistence,
|
|
1305
|
+
findOnPath: () => findOnPath,
|
|
1306
|
+
inspectCore: () => inspectCore
|
|
1307
|
+
});
|
|
1308
|
+
import { accessSync as accessSync2, constants as constants3, statSync as statSync3 } from "fs";
|
|
1309
|
+
import path7 from "path";
|
|
1310
|
+
var CACHE_TTL_MS = 3e4;
|
|
1311
|
+
function executable2(file) {
|
|
1312
|
+
try {
|
|
1313
|
+
if (!statSync3(file).isFile()) return false;
|
|
1314
|
+
accessSync2(file, constants3.X_OK);
|
|
1315
|
+
return true;
|
|
1316
|
+
} catch {
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function findOnPath(command, env = process.env) {
|
|
1321
|
+
const raw = env.PATH ?? "";
|
|
1322
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
1323
|
+
for (const dir of raw.split(path7.delimiter)) {
|
|
1324
|
+
if (dir.trim() === "") continue;
|
|
1325
|
+
for (const ext of exts) {
|
|
1326
|
+
const candidate = path7.join(dir, `${command}${ext}`);
|
|
1327
|
+
if (executable2(candidate)) return candidate;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return void 0;
|
|
1331
|
+
}
|
|
1332
|
+
var cached;
|
|
1333
|
+
async function inspectCore(settings, options = {}) {
|
|
1334
|
+
const env = options.env ?? process.env;
|
|
1335
|
+
const key = `${settings.corePath}|${env.TRENDING_DIGEST_CORE ?? ""}|${process.platform}|${process.arch}`;
|
|
1336
|
+
if (!options.fresh && cached !== void 0 && cached.key === key && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
1337
|
+
return cached.status;
|
|
1338
|
+
}
|
|
1339
|
+
const view = inspectRelease(settings, { env });
|
|
1340
|
+
const release = view.descriptor === void 0 ? void 0 : {
|
|
1341
|
+
repo: view.descriptor.repo,
|
|
1342
|
+
tag: view.descriptor.tag,
|
|
1343
|
+
asset: view.descriptor.asset,
|
|
1344
|
+
url: view.descriptor.url,
|
|
1345
|
+
checksumsUrl: view.descriptor.checksumsUrl,
|
|
1346
|
+
cacheDir: view.descriptor.cacheDir,
|
|
1347
|
+
binaryPath: view.descriptor.binaryPath,
|
|
1348
|
+
cached: view.descriptor.cached
|
|
1349
|
+
};
|
|
1350
|
+
const base = {
|
|
1351
|
+
...release === void 0 ? {} : { release },
|
|
1352
|
+
...view.override === void 0 ? {} : { override: view.override },
|
|
1353
|
+
download: view.override !== void 0 ? { state: "override" } : release === void 0 ? { state: "missing", reason: view.error ?? "release \u672A\u914D\u7F6E" } : { state: release.cached ? "cached" : "missing" }
|
|
1354
|
+
};
|
|
1355
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1356
|
+
try {
|
|
1357
|
+
const core = await locateCore({ settings, env, allowDownload: false });
|
|
1358
|
+
const result = await handshake(core, { timeoutMs: 5e3 });
|
|
1359
|
+
const status = {
|
|
1360
|
+
...base,
|
|
1361
|
+
state: "ready",
|
|
1362
|
+
path: core.path,
|
|
1363
|
+
source: core.source,
|
|
1364
|
+
version: result.version.version,
|
|
1365
|
+
commit: result.version.commit,
|
|
1366
|
+
ipcSchema: result.version.ipcSchema,
|
|
1367
|
+
digestSchema: result.version.digestSchema,
|
|
1368
|
+
supportedProviders: result.supportedProviders,
|
|
1369
|
+
checkedAt,
|
|
1370
|
+
durationMs: result.durationMs
|
|
1371
|
+
};
|
|
1372
|
+
cached = { key, at: Date.now(), status };
|
|
1373
|
+
return status;
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
const code = error instanceof IpcError ? error.code : "CORE_UNAVAILABLE";
|
|
1376
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1377
|
+
const status = {
|
|
1378
|
+
...base,
|
|
1379
|
+
state: code === "CORE_INCOMPATIBLE" ? "incompatible" : "unavailable",
|
|
1380
|
+
code,
|
|
1381
|
+
message,
|
|
1382
|
+
supportedProviders: [],
|
|
1383
|
+
checkedAt
|
|
1384
|
+
};
|
|
1385
|
+
cached = { key, at: Date.now(), status };
|
|
1386
|
+
return status;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
var coexistCache;
|
|
1390
|
+
async function detectCoexistence(ownPath, options = {}) {
|
|
1391
|
+
const env = options.env ?? process.env;
|
|
1392
|
+
const found = findOnPath("trendradar", env);
|
|
1393
|
+
if (found === void 0) return { found: false };
|
|
1394
|
+
if (ownPath !== void 0 && path7.resolve(found) === path7.resolve(ownPath)) return { found: false };
|
|
1395
|
+
if (!options.fresh && coexistCache !== void 0 && coexistCache.key === found && Date.now() - coexistCache.at < CACHE_TTL_MS) {
|
|
1396
|
+
return coexistCache.status;
|
|
1397
|
+
}
|
|
1398
|
+
let status;
|
|
1399
|
+
try {
|
|
1400
|
+
const result = await execCapture(found, ["version", "--json"], { timeoutMs: 3e3 });
|
|
1401
|
+
if (result.code !== 0) {
|
|
1402
|
+
status = {
|
|
1403
|
+
found: true,
|
|
1404
|
+
path: found,
|
|
1405
|
+
message: "PATH \u4E0A\u7684 trendradar \u65E0\u6CD5\u6267\u884C version --json\uFF1B\u63D2\u4EF6\u4ECD\u4F7F\u7528\u81EA\u5E26\u6838\u5FC3\uFF0C\u4E0D\u5207\u6362\u3002"
|
|
1406
|
+
};
|
|
1407
|
+
} else {
|
|
1408
|
+
const info = JSON.parse(result.stdout);
|
|
1409
|
+
const compatible = info.ipcSchema === 1 && info.digestSchema === 1;
|
|
1410
|
+
status = {
|
|
1411
|
+
found: true,
|
|
1412
|
+
path: found,
|
|
1413
|
+
version: typeof info.version === "string" ? info.version : void 0,
|
|
1414
|
+
ipcSchema: typeof info.ipcSchema === "number" ? info.ipcSchema : void 0,
|
|
1415
|
+
digestSchema: typeof info.digestSchema === "number" ? info.digestSchema : void 0,
|
|
1416
|
+
compatible,
|
|
1417
|
+
message: compatible ? "PATH \u4E0A\u53E6\u6709\u4E00\u4EFD trendradar\uFF08\u534F\u8BAE\u517C\u5BB9\uFF09\u3002\u63D2\u4EF6\u4E0D\u63A5\u7BA1\u5B83\uFF0C\u4E5F\u4E0D\u4F1A\u4ECE PATH \u89E3\u6790\u6838\u5FC3\uFF1B\u6CE8\u610F\u522B\u8BA9\u4E24\u5957\u5B9A\u65F6\u540C\u65F6\u5F00\u542F\u3002" : "PATH \u4E0A\u7684 trendradar \u534F\u8BAE\u7248\u672C\u4E0E\u63D2\u4EF6\u4E0D\u540C\u3002\u63D2\u4EF6\u4ECD\u4F7F\u7528\u81EA\u5E26\u6838\u5FC3\uFF0C\u4E0D\u5207\u6362\uFF1B\u5EFA\u8BAE\u53EA\u7528\u5176\u4E2D\u4E00\u5957\u3002"
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
status = {
|
|
1422
|
+
found: true,
|
|
1423
|
+
path: found,
|
|
1424
|
+
message: `PATH \u4E0A\u7684 trendradar \u63A2\u6D4B\u5931\u8D25\uFF08${error instanceof Error ? error.message : String(error)}\uFF09\uFF1B\u63D2\u4EF6\u4ECD\u4F7F\u7528\u81EA\u5E26\u6838\u5FC3\u3002`
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
coexistCache = { key: found, at: Date.now(), status };
|
|
1428
|
+
return status;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/host/credentials.ts
|
|
1432
|
+
var credentials_exports = {};
|
|
1433
|
+
__export(credentials_exports, {
|
|
1434
|
+
credentialServices: () => credentialServices,
|
|
1435
|
+
describeRef: () => describeRef,
|
|
1436
|
+
resolveGithubToken: () => resolveGithubToken,
|
|
1437
|
+
resolveRef: () => resolveRef
|
|
1438
|
+
});
|
|
1439
|
+
function credentialServices(get) {
|
|
1440
|
+
const service = get("credentials");
|
|
1441
|
+
return service !== void 0 && typeof service?.resolve === "function" ? service : void 0;
|
|
1442
|
+
}
|
|
1443
|
+
function fromEnv(ref, env) {
|
|
1444
|
+
const raw = env[ref];
|
|
1445
|
+
return typeof raw === "string" && raw.trim() !== "" ? raw : void 0;
|
|
1446
|
+
}
|
|
1447
|
+
async function describeRef(services, ref, env = process.env) {
|
|
1448
|
+
if (ref.trim() === "") return { configured: false, writable: false };
|
|
1449
|
+
if (services?.describe !== void 0) {
|
|
1450
|
+
try {
|
|
1451
|
+
const described = await services.describe(ref);
|
|
1452
|
+
if (described?.configured === true) {
|
|
1453
|
+
return { configured: true, source: described.source, writable: described.writable === true };
|
|
1454
|
+
}
|
|
1455
|
+
} catch {
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const envValue = fromEnv(ref, env);
|
|
1459
|
+
return envValue === void 0 ? { configured: false, writable: services?.set !== void 0 } : { configured: true, source: "env", writable: false };
|
|
1460
|
+
}
|
|
1461
|
+
async function resolveRef(services, ref, env = process.env) {
|
|
1462
|
+
if (ref.trim() === "") return {};
|
|
1463
|
+
if (services?.resolve !== void 0) {
|
|
1464
|
+
try {
|
|
1465
|
+
const resolved = await services.resolve(ref);
|
|
1466
|
+
const value = resolved?.value;
|
|
1467
|
+
if (typeof value === "string" && value.trim() !== "") return { value, source: resolved?.source ?? "credentials" };
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
const envValue = fromEnv(ref, env);
|
|
1472
|
+
return envValue === void 0 ? {} : { value: envValue, source: "env" };
|
|
1473
|
+
}
|
|
1474
|
+
async function resolveGithubToken(services, authMode, tokenRef, deps = {}) {
|
|
1475
|
+
const env = deps.env ?? process.env;
|
|
1476
|
+
if (authMode === "gh") {
|
|
1477
|
+
const execGh = deps.execGh ?? defaultGhToken;
|
|
1478
|
+
try {
|
|
1479
|
+
const token = await execGh();
|
|
1480
|
+
if (token !== void 0 && token !== "") return { token, source: "gh" };
|
|
1481
|
+
} catch {
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
const resolved = await resolveRef(services, tokenRef, env);
|
|
1485
|
+
if (resolved.value !== void 0) return { token: resolved.value, source: resolved.source ?? "credentials" };
|
|
1486
|
+
return { source: "none" };
|
|
1487
|
+
}
|
|
1488
|
+
async function defaultGhToken() {
|
|
1489
|
+
const { execCapture: execCapture2 } = await import("./exec-33EQXGUA.js");
|
|
1490
|
+
const result = await execCapture2("gh", ["auth", "token"], { timeoutMs: 5e3 });
|
|
1491
|
+
if (result.code !== 0) return void 0;
|
|
1492
|
+
const token = result.stdout.trim();
|
|
1493
|
+
return token === "" ? void 0 : token;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/host/quickfill.ts
|
|
1497
|
+
var quickfill_exports = {};
|
|
1498
|
+
__export(quickfill_exports, {
|
|
1499
|
+
applyCandidate: () => applyCandidate,
|
|
1500
|
+
coreProviderFor: () => coreProviderFor,
|
|
1501
|
+
credentialServices: () => credentialServices,
|
|
1502
|
+
listProviderCandidates: () => listProviderCandidates,
|
|
1503
|
+
llmServices: () => llmServices,
|
|
1504
|
+
resolveModelKey: () => resolveModelKey
|
|
1505
|
+
});
|
|
1506
|
+
function llmServices(get) {
|
|
1507
|
+
const service = get("llm");
|
|
1508
|
+
return service !== void 0 && typeof service?.listProviders === "function" ? service : void 0;
|
|
1509
|
+
}
|
|
1510
|
+
function coreProviderFor(providerId) {
|
|
1511
|
+
return /deepseek/i.test(providerId) ? "deepseek" : "openai-compatible";
|
|
1512
|
+
}
|
|
1513
|
+
async function listModelsWithTimeout(llm, provider) {
|
|
1514
|
+
if (llm.listModels === void 0) return [];
|
|
1515
|
+
try {
|
|
1516
|
+
const models = await Promise.race([
|
|
1517
|
+
llm.listModels(provider),
|
|
1518
|
+
new Promise((_resolve, reject) => setTimeout(() => reject(new Error("listModels timeout")), 3e3))
|
|
1519
|
+
]);
|
|
1520
|
+
return models.filter((model) => typeof model?.id === "string" && model.id !== "").map((model) => ({ id: model.id, name: typeof model.name === "string" && model.name !== "" ? model.name : model.id }));
|
|
1521
|
+
} catch {
|
|
1522
|
+
return [];
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async function listProviderCandidates(options) {
|
|
1526
|
+
const { llm, credentials } = options;
|
|
1527
|
+
const env = options.env ?? process.env;
|
|
1528
|
+
if (llm === void 0) return [];
|
|
1529
|
+
const live = llm.listProviders?.() ?? [];
|
|
1530
|
+
const configurable = llm.listConfigurableProviders?.() ?? [];
|
|
1531
|
+
const declared = new Map(configurable.map((entry) => [entry.provider, entry]));
|
|
1532
|
+
const ids = [];
|
|
1533
|
+
for (const provider of live) if (!ids.includes(provider.id)) ids.push(provider.id);
|
|
1534
|
+
for (const entry of configurable) if (!ids.includes(entry.provider)) ids.push(entry.provider);
|
|
1535
|
+
const candidates = [];
|
|
1536
|
+
for (const id of ids) {
|
|
1537
|
+
const entry = declared.get(id);
|
|
1538
|
+
const name = live.find((provider) => provider.id === id)?.name ?? entry?.displayName ?? id;
|
|
1539
|
+
const envNames = [...entry?.nativeAuth?.env ?? []];
|
|
1540
|
+
const oauth = entry?.nativeAuth?.oauth === true;
|
|
1541
|
+
let ref;
|
|
1542
|
+
let source;
|
|
1543
|
+
let mode = "missing";
|
|
1544
|
+
for (const candidateRef of envNames) {
|
|
1545
|
+
const described = await describeRef(credentials, candidateRef, env);
|
|
1546
|
+
if (described.configured) {
|
|
1547
|
+
ref = candidateRef;
|
|
1548
|
+
source = described.source;
|
|
1549
|
+
mode = "direct";
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (mode !== "direct" && oauth) mode = "proxy";
|
|
1554
|
+
const models = await listModelsWithTimeout(llm, id);
|
|
1555
|
+
candidates.push({
|
|
1556
|
+
id,
|
|
1557
|
+
name,
|
|
1558
|
+
baseURL: entry?.defaultBaseURL ?? "",
|
|
1559
|
+
models,
|
|
1560
|
+
credential: {
|
|
1561
|
+
...ref === void 0 ? {} : { ref },
|
|
1562
|
+
env: envNames,
|
|
1563
|
+
configured: mode === "direct",
|
|
1564
|
+
oauth,
|
|
1565
|
+
mode,
|
|
1566
|
+
...source === void 0 ? {} : { source }
|
|
1567
|
+
},
|
|
1568
|
+
coreProvider: coreProviderFor(id)
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
return candidates;
|
|
1572
|
+
}
|
|
1573
|
+
function applyCandidate(settings, candidate, modelId) {
|
|
1574
|
+
const model = {
|
|
1575
|
+
...settings.model,
|
|
1576
|
+
provider: candidate.coreProvider,
|
|
1577
|
+
mode: candidate.credential.mode === "proxy" ? "proxy" : "direct",
|
|
1578
|
+
hostProvider: candidate.credential.mode === "proxy" ? candidate.id : "",
|
|
1579
|
+
baseURL: candidate.credential.mode === "proxy" ? "" : candidate.baseURL || PROVIDER_BASE_URLS[candidate.coreProvider] || "",
|
|
1580
|
+
model: modelId,
|
|
1581
|
+
apiKeyRef: candidate.credential.mode === "direct" && candidate.credential.ref !== void 0 ? candidate.credential.ref : settings.model.apiKeyRef
|
|
1582
|
+
};
|
|
1583
|
+
return { ...settings, model };
|
|
1584
|
+
}
|
|
1585
|
+
async function resolveModelKey(settings, credentials, env = process.env) {
|
|
1586
|
+
if (settings.model.mode === "proxy") return { mode: "proxy" };
|
|
1587
|
+
const resolved = await resolveRef(credentials, settings.model.apiKeyRef, env);
|
|
1588
|
+
return {
|
|
1589
|
+
mode: "direct",
|
|
1590
|
+
...resolved.value === void 0 ? {} : { apiKey: resolved.value },
|
|
1591
|
+
...resolved.source === void 0 ? {} : { source: resolved.source }
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/host/proxy.ts
|
|
1596
|
+
var proxy_exports = {};
|
|
1597
|
+
__export(proxy_exports, {
|
|
1598
|
+
foldMessages: () => foldMessages,
|
|
1599
|
+
startProxy: () => startProxy
|
|
1600
|
+
});
|
|
1601
|
+
import http from "http";
|
|
1602
|
+
import { randomBytes, timingSafeEqual } from "crypto";
|
|
1603
|
+
var MAX_BODY = 4 * 1024 * 1024;
|
|
1604
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
1605
|
+
function textOf(content) {
|
|
1606
|
+
if (typeof content === "string") return content;
|
|
1607
|
+
if (Array.isArray(content)) {
|
|
1608
|
+
return content.map((part) => {
|
|
1609
|
+
if (typeof part === "string") return part;
|
|
1610
|
+
if (typeof part === "object" && part !== null && part.type === "text") {
|
|
1611
|
+
const text = part.text;
|
|
1612
|
+
return typeof text === "string" ? text : "";
|
|
1613
|
+
}
|
|
1614
|
+
return "";
|
|
1615
|
+
}).filter((part) => part !== "").join("\n");
|
|
1616
|
+
}
|
|
1617
|
+
return "";
|
|
1618
|
+
}
|
|
1619
|
+
function foldMessages(messages) {
|
|
1620
|
+
const systemParts = [];
|
|
1621
|
+
const userParts = [];
|
|
1622
|
+
for (const message of messages) {
|
|
1623
|
+
const text = textOf(message.content);
|
|
1624
|
+
if (text === "") continue;
|
|
1625
|
+
if (message.role === "system" || message.role === "developer") systemParts.push(text);
|
|
1626
|
+
else userParts.push(text);
|
|
1627
|
+
}
|
|
1628
|
+
return { system: systemParts.join("\n\n"), user: userParts.join("\n\n") };
|
|
1629
|
+
}
|
|
1630
|
+
function sendJson(response, status, payload) {
|
|
1631
|
+
const body = JSON.stringify(payload);
|
|
1632
|
+
response.writeHead(status, {
|
|
1633
|
+
"content-type": "application/json; charset=utf-8",
|
|
1634
|
+
"cache-control": "no-store",
|
|
1635
|
+
"content-length": Buffer.byteLength(body)
|
|
1636
|
+
});
|
|
1637
|
+
response.end(body);
|
|
1638
|
+
}
|
|
1639
|
+
function fail(response, status, code, message) {
|
|
1640
|
+
sendJson(response, status, { error: { message, type: "invalid_request_error", code } });
|
|
1641
|
+
}
|
|
1642
|
+
async function readBody(request) {
|
|
1643
|
+
const chunks = [];
|
|
1644
|
+
let size = 0;
|
|
1645
|
+
for await (const chunk of request) {
|
|
1646
|
+
size += chunk.length;
|
|
1647
|
+
if (size > MAX_BODY) throw new Error("\u8BF7\u6C42\u4F53\u8D85\u8FC7 4 MB");
|
|
1648
|
+
chunks.push(chunk);
|
|
1649
|
+
}
|
|
1650
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1651
|
+
}
|
|
1652
|
+
async function startProxy(options) {
|
|
1653
|
+
const token = randomBytes(24).toString("hex");
|
|
1654
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1655
|
+
const handle = { requests: 0 };
|
|
1656
|
+
let closed = false;
|
|
1657
|
+
const server = http.createServer((request, response) => {
|
|
1658
|
+
void (async () => {
|
|
1659
|
+
try {
|
|
1660
|
+
if (request.method === "GET" && (request.url === "/healthz" || request.url === "/v1/models")) {
|
|
1661
|
+
if (!authorized(request, token)) return fail(response, 401, "unauthorized", "missing or bad proxy token");
|
|
1662
|
+
if (request.url === "/healthz") return sendJson(response, 200, { ok: true });
|
|
1663
|
+
return sendJson(response, 200, { object: "list", data: [{ id: options.model, object: "model", owned_by: options.provider }] });
|
|
1664
|
+
}
|
|
1665
|
+
if (request.method !== "POST" || request.url !== "/v1/chat/completions") {
|
|
1666
|
+
return fail(response, 404, "not_found", `unsupported route ${request.method ?? ""} ${request.url ?? ""}`);
|
|
1667
|
+
}
|
|
1668
|
+
if (!authorized(request, token)) return fail(response, 401, "unauthorized", "missing or bad proxy token");
|
|
1669
|
+
const raw = await readBody(request);
|
|
1670
|
+
let body;
|
|
1671
|
+
try {
|
|
1672
|
+
body = JSON.parse(raw);
|
|
1673
|
+
} catch {
|
|
1674
|
+
return fail(response, 400, "invalid_json", "request body is not JSON");
|
|
1675
|
+
}
|
|
1676
|
+
if (body.stream === true) {
|
|
1677
|
+
return fail(response, 400, "stream_unsupported", "\u56DE\u73AF\u4EE3\u7406\u53EA\u5B9E\u73B0\u975E\u6D41\u5F0F\u8865\u5168\uFF1B\u6838\u5FC3\u4E0D\u4F1A\u5F00\u542F\u6D41\u5F0F");
|
|
1678
|
+
}
|
|
1679
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
1680
|
+
if (messages.length === 0) return fail(response, 400, "invalid_request", "messages \u4E0D\u80FD\u4E3A\u7A7A");
|
|
1681
|
+
const { system, user } = foldMessages(messages);
|
|
1682
|
+
if (user === "") return fail(response, 400, "invalid_request", "messages \u91CC\u6CA1\u6709\u53EF\u7528\u7684\u6587\u672C\u5185\u5BB9");
|
|
1683
|
+
handle.requests += 1;
|
|
1684
|
+
const started = Date.now();
|
|
1685
|
+
let text = "";
|
|
1686
|
+
let usage;
|
|
1687
|
+
let failure;
|
|
1688
|
+
const controller = new AbortController();
|
|
1689
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1690
|
+
try {
|
|
1691
|
+
const stream = options.llm.stream({
|
|
1692
|
+
provider: options.provider,
|
|
1693
|
+
model: options.model,
|
|
1694
|
+
system: system === "" ? void 0 : system,
|
|
1695
|
+
messages: [{ role: "user", content: [{ type: "text", text: user }] }],
|
|
1696
|
+
temperature: typeof body.temperature === "number" ? body.temperature : options.temperature,
|
|
1697
|
+
maxTokens: typeof body.max_tokens === "number" ? body.max_tokens : typeof body.maxTokens === "number" ? body.maxTokens : options.maxTokens,
|
|
1698
|
+
signal: controller.signal
|
|
1699
|
+
});
|
|
1700
|
+
for await (const chunk of stream) {
|
|
1701
|
+
if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
1702
|
+
else if (chunk.type === "usage") usage = chunk.usage;
|
|
1703
|
+
else if (chunk.type === "finish") {
|
|
1704
|
+
const kind = chunk.reason?.kind;
|
|
1705
|
+
if (kind === "error" || kind === "aborted") failure = chunk.reason?.failure ?? { code: kind };
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
} finally {
|
|
1709
|
+
clearTimeout(timer);
|
|
1710
|
+
}
|
|
1711
|
+
if (failure !== void 0) {
|
|
1712
|
+
return fail(response, 502, failure.code ?? "llm_failed", failure.message ?? "\u5BBF\u4E3B\u6A21\u578B\u8C03\u7528\u5931\u8D25");
|
|
1713
|
+
}
|
|
1714
|
+
return sendJson(response, 200, {
|
|
1715
|
+
id: `chatcmpl-proxy-${Date.now().toString(36)}`,
|
|
1716
|
+
object: "chat.completion",
|
|
1717
|
+
created: Math.floor(Date.now() / 1e3),
|
|
1718
|
+
model: options.model,
|
|
1719
|
+
choices: [{ index: 0, message: { role: "assistant", content: text }, finish_reason: "stop" }],
|
|
1720
|
+
usage: {
|
|
1721
|
+
prompt_tokens: usage?.inputTokens ?? 0,
|
|
1722
|
+
completion_tokens: usage?.outputTokens ?? 0,
|
|
1723
|
+
total_tokens: usage?.totalTokens ?? (usage?.inputTokens ?? 0) + (usage?.outputTokens ?? 0)
|
|
1724
|
+
},
|
|
1725
|
+
proxy: { provider: options.provider, durationMs: Date.now() - started }
|
|
1726
|
+
});
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1729
|
+
if (!response.headersSent) fail(response, 500, "proxy_failed", message);
|
|
1730
|
+
else response.end();
|
|
1731
|
+
}
|
|
1732
|
+
})();
|
|
1733
|
+
});
|
|
1734
|
+
await new Promise((resolve, reject) => {
|
|
1735
|
+
server.once("error", reject);
|
|
1736
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1737
|
+
server.removeListener("error", reject);
|
|
1738
|
+
resolve();
|
|
1739
|
+
});
|
|
1740
|
+
});
|
|
1741
|
+
const address = server.address();
|
|
1742
|
+
if (typeof address !== "object" || address === null) {
|
|
1743
|
+
server.close();
|
|
1744
|
+
throw new Error("\u56DE\u73AF\u4EE3\u7406\u672A\u80FD\u62FF\u5230\u7AEF\u53E3");
|
|
1745
|
+
}
|
|
1746
|
+
handle.port = address.port;
|
|
1747
|
+
handle.url = `http://127.0.0.1:${address.port}/v1`;
|
|
1748
|
+
handle.token = token;
|
|
1749
|
+
handle.close = async () => {
|
|
1750
|
+
if (closed) return;
|
|
1751
|
+
closed = true;
|
|
1752
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
1753
|
+
};
|
|
1754
|
+
return handle;
|
|
1755
|
+
}
|
|
1756
|
+
function authorized(request, token) {
|
|
1757
|
+
const header = request.headers.authorization;
|
|
1758
|
+
if (typeof header !== "string") return false;
|
|
1759
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
1760
|
+
if (match === null) return false;
|
|
1761
|
+
const provided = Buffer.from(match[1] ?? "", "utf8");
|
|
1762
|
+
const expected = Buffer.from(token, "utf8");
|
|
1763
|
+
return provided.length === expected.length && timingSafeEqual(provided, expected);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
// src/host/runner.ts
|
|
1767
|
+
var runner_exports = {};
|
|
1768
|
+
__export(runner_exports, {
|
|
1769
|
+
Runner: () => Runner
|
|
1770
|
+
});
|
|
1771
|
+
import { existsSync as existsSync4, statSync as statSync4 } from "fs";
|
|
1772
|
+
import path8 from "path";
|
|
1773
|
+
function repoProblem(repoPath) {
|
|
1774
|
+
if (repoPath.trim() === "") return { code: "CONFIG_INVALID", message: "\u8FD8\u6CA1\u6709\u9009\u62E9\u5F52\u6863\u4ED3\u5E93" };
|
|
1775
|
+
if (!existsSync4(repoPath)) return { code: "REPO_NOT_GIT", message: `\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${repoPath}` };
|
|
1776
|
+
try {
|
|
1777
|
+
if (!statSync4(repoPath).isDirectory()) return { code: "REPO_NOT_GIT", message: `\u4E0D\u662F\u76EE\u5F55\uFF1A${repoPath}` };
|
|
1778
|
+
} catch {
|
|
1779
|
+
return { code: "REPO_NOT_GIT", message: `\u65E0\u6CD5\u8BBF\u95EE\uFF1A${repoPath}` };
|
|
1780
|
+
}
|
|
1781
|
+
if (!existsSync4(path8.join(repoPath, ".git"))) {
|
|
1782
|
+
return { code: "REPO_NOT_GIT", message: `\u4E0D\u662F git \u4ED3\u5E93\uFF1A${repoPath}\uFF08\u63D2\u4EF6\u4E0D\u4F1A\u66FF\u4F60 git init\uFF09` };
|
|
1783
|
+
}
|
|
1784
|
+
return void 0;
|
|
1785
|
+
}
|
|
1786
|
+
var Runner = class {
|
|
1787
|
+
active;
|
|
1788
|
+
deps;
|
|
1789
|
+
constructor(deps) {
|
|
1790
|
+
this.deps = deps;
|
|
1791
|
+
}
|
|
1792
|
+
get running() {
|
|
1793
|
+
return this.active === void 0 ? void 0 : { id: this.active.id, startedAt: this.active.startedAt };
|
|
1794
|
+
}
|
|
1795
|
+
cancel() {
|
|
1796
|
+
if (this.active === void 0) return false;
|
|
1797
|
+
this.active.controller.abort();
|
|
1798
|
+
return true;
|
|
1799
|
+
}
|
|
1800
|
+
async run(options) {
|
|
1801
|
+
const env = this.deps.env ?? process.env;
|
|
1802
|
+
const store = this.deps.store;
|
|
1803
|
+
const settings = store.readSettings();
|
|
1804
|
+
const credentials = credentialServices(this.deps.get);
|
|
1805
|
+
if (this.active !== void 0) {
|
|
1806
|
+
throw new IpcError("CONFIG_INVALID", `\u5DF2\u6709\u4E00\u6B21\u8FD0\u884C\u5728\u8FDB\u884C\u4E2D\uFF08${this.active.id}\uFF09\uFF0C\u8BF7\u5148\u7B49\u5F85\u6216\u53D6\u6D88`);
|
|
1807
|
+
}
|
|
1808
|
+
const problems = validateSettings(settings);
|
|
1809
|
+
if (problems.length > 0) {
|
|
1810
|
+
const first = problems[0];
|
|
1811
|
+
throw new IpcError(first?.code ?? "CONFIG_INVALID", `\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A${first?.message ?? "\u672A\u77E5\u95EE\u9898"}`);
|
|
1812
|
+
}
|
|
1813
|
+
const repoIssue = repoProblem(settings.repo.path);
|
|
1814
|
+
if (repoIssue !== void 0) throw new IpcError(repoIssue.code, repoIssue.message);
|
|
1815
|
+
const id = newRunId();
|
|
1816
|
+
const controller = new AbortController();
|
|
1817
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1818
|
+
this.active = { id, controller, startedAt };
|
|
1819
|
+
const logFile = store.newLogPath(id);
|
|
1820
|
+
const dryRun = options.dryRun === true;
|
|
1821
|
+
const startedMs = Date.now();
|
|
1822
|
+
let proxy;
|
|
1823
|
+
try {
|
|
1824
|
+
const core = await locateCore({ settings, env, allowDownload: settings.coreAutoDownload });
|
|
1825
|
+
if (core.downloaded === true) {
|
|
1826
|
+
store.appendLog(logFile, `[core] \u5DF2\u4E0B\u8F7D\u6838\u5FC3 ${core.tag ?? ""} \u2192 ${core.path}`);
|
|
1827
|
+
}
|
|
1828
|
+
const handshakeResult = await handshake(core, { signal: controller.signal });
|
|
1829
|
+
if (!handshakeResult.supportedProviders.includes(settings.model.provider)) {
|
|
1830
|
+
throw new IpcError(
|
|
1831
|
+
"MODEL_PROVIDER_UNSUPPORTED",
|
|
1832
|
+
`\u6838\u5FC3\u4E0D\u652F\u6301 provider\u300C${settings.model.provider}\u300D\uFF1B\u53EF\u7528\uFF1A${handshakeResult.supportedProviders.join(" / ")}`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
const modelKey = await resolveModelKey(settings, credentials, env);
|
|
1836
|
+
if (modelKey.mode === "direct" && (modelKey.apiKey === void 0 || modelKey.apiKey === "")) {
|
|
1837
|
+
throw new IpcError("MODEL_KEY_MISSING", `\u6CA1\u6709\u89E3\u6790\u5230\u6A21\u578B\u5BC6\u94A5\uFF08\u5F15\u7528 ${settings.model.apiKeyRef}\uFF09`);
|
|
1838
|
+
}
|
|
1839
|
+
let baseURLOverride;
|
|
1840
|
+
if (modelKey.mode === "proxy") {
|
|
1841
|
+
const llm = this.deps.get("llm");
|
|
1842
|
+
if (llm === void 0 || typeof llm.stream !== "function") {
|
|
1843
|
+
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");
|
|
1844
|
+
}
|
|
1845
|
+
proxy = await startProxy({
|
|
1846
|
+
llm,
|
|
1847
|
+
provider: settings.model.hostProvider !== "" ? settings.model.hostProvider : settings.model.provider,
|
|
1848
|
+
model: settings.model.model,
|
|
1849
|
+
temperature: settings.model.temperature,
|
|
1850
|
+
maxTokens: settings.model.maxTokens,
|
|
1851
|
+
timeoutMs: 12e4
|
|
1852
|
+
});
|
|
1853
|
+
baseURLOverride = proxy.url;
|
|
1854
|
+
store.appendLog(
|
|
1855
|
+
logFile,
|
|
1856
|
+
`[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`
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
const github = await resolveGithubToken(credentials, settings.github.authMode, settings.github.tokenRef, { env });
|
|
1860
|
+
if (github.token === void 0) {
|
|
1861
|
+
store.appendLog(logFile, "[github] \u672A\u627E\u5230 token\uFF1Apush/PR \u4F1A\u5931\u8D25\u5E76\u8FD4\u56DE GITHUB_AUTH\uFF08dryRun \u4E0D\u53D7\u5F71\u54CD\uFF09");
|
|
1862
|
+
}
|
|
1863
|
+
const result = await runCore({
|
|
1864
|
+
core,
|
|
1865
|
+
version: handshakeResult.version,
|
|
1866
|
+
settings,
|
|
1867
|
+
apiKey: modelKey.apiKey ?? "",
|
|
1868
|
+
...github.token === void 0 ? {} : { token: github.token },
|
|
1869
|
+
...options.date === void 0 ? {} : { date: options.date },
|
|
1870
|
+
dryRun,
|
|
1871
|
+
...baseURLOverride === void 0 ? {} : { baseURLOverride },
|
|
1872
|
+
signal: controller.signal,
|
|
1873
|
+
secrets: [modelKey.apiKey, github.token],
|
|
1874
|
+
onStderr: (line) => store.appendLog(logFile, line)
|
|
1875
|
+
});
|
|
1876
|
+
const outcome = result.outcome;
|
|
1877
|
+
const summary = this.summarize({
|
|
1878
|
+
id,
|
|
1879
|
+
outcome,
|
|
1880
|
+
dryRun,
|
|
1881
|
+
...options.date === void 0 ? {} : { date: options.date },
|
|
1882
|
+
logFile,
|
|
1883
|
+
channel: result.channelKind,
|
|
1884
|
+
proxy: proxy !== void 0,
|
|
1885
|
+
coreSource: core.source,
|
|
1886
|
+
...core.tag === void 0 ? {} : { coreTag: core.tag }
|
|
1887
|
+
});
|
|
1888
|
+
this.record(summary, settings);
|
|
1889
|
+
return summary;
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
const cancelled = controller.signal.aborted || error instanceof IpcError && error.code === "CORE_CANCELLED";
|
|
1892
|
+
const summary = this.failureSummary({
|
|
1893
|
+
id,
|
|
1894
|
+
error: cancelled && !(error instanceof IpcError && error.code === "CORE_CANCELLED") ? new IpcError("CORE_CANCELLED", "\u8FD0\u884C\u5DF2\u53D6\u6D88\uFF1A\u6838\u5FC3\u8FDB\u7A0B\u5DF2\u7EC8\u6B62\uFF0C\u672A\u63D0\u4EA4\u4EFB\u4F55\u72B6\u6001") : error,
|
|
1895
|
+
dryRun,
|
|
1896
|
+
...options.date === void 0 ? {} : { date: options.date },
|
|
1897
|
+
logFile,
|
|
1898
|
+
durationMs: Date.now() - startedMs
|
|
1899
|
+
});
|
|
1900
|
+
this.record(summary, settings);
|
|
1901
|
+
return summary;
|
|
1902
|
+
} finally {
|
|
1903
|
+
if (proxy !== void 0) {
|
|
1904
|
+
const port = proxy.port;
|
|
1905
|
+
await proxy.close().catch(() => void 0);
|
|
1906
|
+
this.deps.store.appendLog(logFile, `[proxy] \u5DF2\u5173\u95ED 127.0.0.1:${port}`);
|
|
1907
|
+
}
|
|
1908
|
+
this.active = void 0;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
summarize(input) {
|
|
1912
|
+
const { outcome } = input;
|
|
1913
|
+
const error = outcome.error === void 0 ? void 0 : {
|
|
1914
|
+
code: outcome.error.code,
|
|
1915
|
+
message: redact(outcome.error.message),
|
|
1916
|
+
hint: hintOf(outcome.error.code),
|
|
1917
|
+
area: areaOf(outcome.error.code),
|
|
1918
|
+
retriable: outcome.error.retriable,
|
|
1919
|
+
inferred: outcome.error.details?.inferred === true
|
|
1920
|
+
};
|
|
1921
|
+
return {
|
|
1922
|
+
id: input.id,
|
|
1923
|
+
ok: outcome.ok,
|
|
1924
|
+
skipped: outcome.skipped,
|
|
1925
|
+
dryRun: input.dryRun,
|
|
1926
|
+
...input.date === void 0 ? {} : { date: input.date },
|
|
1927
|
+
durationMs: outcome.durationMs,
|
|
1928
|
+
coreVersion: outcome.coreVersion,
|
|
1929
|
+
files: outcome.files.length,
|
|
1930
|
+
...outcome.contentHash === void 0 ? {} : { contentHash: outcome.contentHash },
|
|
1931
|
+
...outcome.branch === void 0 ? {} : { branch: outcome.branch },
|
|
1932
|
+
...outcome.prUrl === void 0 ? {} : { prUrl: outcome.prUrl },
|
|
1933
|
+
...error === void 0 ? {} : { error },
|
|
1934
|
+
warnings: outcome.warnings,
|
|
1935
|
+
logFile: input.logFile,
|
|
1936
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
1937
|
+
proxy: input.proxy,
|
|
1938
|
+
...input.coreSource === void 0 ? {} : { coreSource: input.coreSource },
|
|
1939
|
+
...input.coreTag === void 0 ? {} : { coreTag: input.coreTag }
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
failureSummary(input) {
|
|
1943
|
+
const code = input.error instanceof IpcError ? input.error.code : "INTERNAL";
|
|
1944
|
+
const message = redact(input.error instanceof Error ? input.error.message : String(input.error));
|
|
1945
|
+
return {
|
|
1946
|
+
id: input.id,
|
|
1947
|
+
ok: false,
|
|
1948
|
+
skipped: false,
|
|
1949
|
+
dryRun: input.dryRun,
|
|
1950
|
+
...input.date === void 0 ? {} : { date: input.date },
|
|
1951
|
+
durationMs: input.durationMs ?? 0,
|
|
1952
|
+
coreVersion: "",
|
|
1953
|
+
files: 0,
|
|
1954
|
+
error: { code, message, hint: hintOf(code), area: areaOf(code), retriable: false },
|
|
1955
|
+
warnings: [],
|
|
1956
|
+
logFile: input.logFile
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
/** 写历史与状态;同一天的 skipped 运行不覆盖 lastDigest。 */
|
|
1960
|
+
record(summary, settings) {
|
|
1961
|
+
const store = this.deps.store;
|
|
1962
|
+
const record = {
|
|
1963
|
+
id: summary.id,
|
|
1964
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1965
|
+
kind: summary.dryRun ? "dry-run" : "run",
|
|
1966
|
+
ok: summary.ok,
|
|
1967
|
+
skipped: summary.skipped,
|
|
1968
|
+
...summary.error === void 0 ? {} : { errorCode: summary.error.code, errorMessage: summary.error.message },
|
|
1969
|
+
durationMs: summary.durationMs,
|
|
1970
|
+
files: summary.files,
|
|
1971
|
+
...summary.contentHash === void 0 ? {} : { contentHash: summary.contentHash },
|
|
1972
|
+
...summary.branch === void 0 ? {} : { branch: summary.branch },
|
|
1973
|
+
...summary.prUrl === void 0 ? {} : { prUrl: summary.prUrl },
|
|
1974
|
+
coreVersion: summary.coreVersion,
|
|
1975
|
+
model: { provider: settings.model.provider, model: settings.model.model },
|
|
1976
|
+
languages: settings.trending.languages,
|
|
1977
|
+
warnings: summary.warnings,
|
|
1978
|
+
logFile: summary.logFile
|
|
1979
|
+
};
|
|
1980
|
+
const state = store.readState();
|
|
1981
|
+
const next = {
|
|
1982
|
+
lastRunAt: record.at,
|
|
1983
|
+
...summary.ok && summary.prUrl !== void 0 ? { pendingPr: { url: summary.prUrl, createdAt: record.at } } : {},
|
|
1984
|
+
...summary.ok && !summary.skipped && summary.contentHash !== void 0 ? {
|
|
1985
|
+
lastDigest: {
|
|
1986
|
+
date: summary.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1987
|
+
contentHash: summary.contentHash,
|
|
1988
|
+
coreVersion: summary.coreVersion
|
|
1989
|
+
},
|
|
1990
|
+
...state.pendingPr !== void 0 && summary.prUrl === void 0 ? { pendingPr: state.pendingPr } : {}
|
|
1991
|
+
} : {}
|
|
1992
|
+
};
|
|
1993
|
+
store.recordRun(record, next);
|
|
1994
|
+
this.deps.onFinished?.(summary);
|
|
1995
|
+
}
|
|
1996
|
+
/** 给设置页/工具用的「核心现在什么状态」。 */
|
|
1997
|
+
async coreStatus(fresh = false) {
|
|
1998
|
+
const settings = this.deps.store.readSettings();
|
|
1999
|
+
return inspectCore(settings, { env: this.deps.env ?? process.env, fresh });
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
// src/host/schedule.ts
|
|
2004
|
+
var schedule_exports = {};
|
|
2005
|
+
__export(schedule_exports, {
|
|
2006
|
+
Scheduler: () => Scheduler,
|
|
2007
|
+
isDue: () => isDue,
|
|
2008
|
+
nextOccurrence: () => nextOccurrence,
|
|
2009
|
+
parseClock: () => parseClock,
|
|
2010
|
+
zonedDateString: () => zonedDateString,
|
|
2011
|
+
zonedTimeToUtc: () => zonedTimeToUtc
|
|
2012
|
+
});
|
|
2013
|
+
var MINUTE = 6e4;
|
|
2014
|
+
var CATCH_UP_MINUTES = 6 * 60;
|
|
2015
|
+
function partsOf(date, timezone) {
|
|
2016
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
2017
|
+
timeZone: timezone,
|
|
2018
|
+
hourCycle: "h23",
|
|
2019
|
+
year: "numeric",
|
|
2020
|
+
month: "2-digit",
|
|
2021
|
+
day: "2-digit",
|
|
2022
|
+
hour: "2-digit",
|
|
2023
|
+
minute: "2-digit",
|
|
2024
|
+
second: "2-digit"
|
|
2025
|
+
});
|
|
2026
|
+
const map = /* @__PURE__ */ new Map();
|
|
2027
|
+
for (const part of formatter.formatToParts(date)) {
|
|
2028
|
+
if (part.type !== "literal") map.set(part.type, part.value);
|
|
2029
|
+
}
|
|
2030
|
+
return {
|
|
2031
|
+
year: Number(map.get("year")),
|
|
2032
|
+
month: Number(map.get("month")),
|
|
2033
|
+
day: Number(map.get("day")),
|
|
2034
|
+
hour: Number(map.get("hour")),
|
|
2035
|
+
minute: Number(map.get("minute")),
|
|
2036
|
+
second: Number(map.get("second"))
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
function offsetMs(date, timezone) {
|
|
2040
|
+
const parts = partsOf(date, timezone);
|
|
2041
|
+
const asUtc = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
2042
|
+
return asUtc - Math.floor(date.getTime() / 1e3) * 1e3;
|
|
2043
|
+
}
|
|
2044
|
+
function zonedTimeToUtc(year, month, day, hour, minute, second, timezone) {
|
|
2045
|
+
const guess = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
2046
|
+
const first = new Date(guess - offsetMs(new Date(guess), timezone));
|
|
2047
|
+
return new Date(guess - offsetMs(first, timezone));
|
|
2048
|
+
}
|
|
2049
|
+
function parseClock(time) {
|
|
2050
|
+
const match = /^([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/.exec(time.trim());
|
|
2051
|
+
if (match === null) throw new Error(`\u975E\u6CD5\u65F6\u95F4\uFF1A${time}`);
|
|
2052
|
+
return {
|
|
2053
|
+
hour: Number(match[1]),
|
|
2054
|
+
minute: Number(match[2]),
|
|
2055
|
+
second: Number(match[3] ?? "0")
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
function zonedDateString(date, timezone) {
|
|
2059
|
+
const parts = partsOf(date, timezone);
|
|
2060
|
+
return `${String(parts.year).padStart(4, "0")}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}`;
|
|
2061
|
+
}
|
|
2062
|
+
function nextOccurrence(now, schedule) {
|
|
2063
|
+
const { hour, minute, second } = parseClock(schedule.time);
|
|
2064
|
+
const today = partsOf(now, schedule.timezone);
|
|
2065
|
+
const candidate = zonedTimeToUtc(today.year, today.month, today.day, hour, minute, second, schedule.timezone);
|
|
2066
|
+
if (candidate.getTime() > now.getTime()) return candidate;
|
|
2067
|
+
const tomorrow = new Date(Date.UTC(today.year, today.month - 1, today.day) + 864e5);
|
|
2068
|
+
return zonedTimeToUtc(
|
|
2069
|
+
tomorrow.getUTCFullYear(),
|
|
2070
|
+
tomorrow.getUTCMonth() + 1,
|
|
2071
|
+
tomorrow.getUTCDate(),
|
|
2072
|
+
hour,
|
|
2073
|
+
minute,
|
|
2074
|
+
second,
|
|
2075
|
+
schedule.timezone
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
function isDue(now, schedule) {
|
|
2079
|
+
const { hour, minute } = parseClock(schedule.time);
|
|
2080
|
+
const parts = partsOf(now, schedule.timezone);
|
|
2081
|
+
const nowMinutes = parts.hour * 60 + parts.minute;
|
|
2082
|
+
const targetMinutes = hour * 60 + minute;
|
|
2083
|
+
const late = nowMinutes - targetMinutes;
|
|
2084
|
+
return late >= 0 && late <= CATCH_UP_MINUTES;
|
|
2085
|
+
}
|
|
2086
|
+
var Scheduler = class {
|
|
2087
|
+
deps;
|
|
2088
|
+
timerHandle;
|
|
2089
|
+
stopped = false;
|
|
2090
|
+
lastFiredFor;
|
|
2091
|
+
constructor(deps) {
|
|
2092
|
+
this.deps = deps;
|
|
2093
|
+
}
|
|
2094
|
+
start() {
|
|
2095
|
+
this.stopped = false;
|
|
2096
|
+
const timer = this.deps.getTimer();
|
|
2097
|
+
const tick = () => {
|
|
2098
|
+
void this.tick();
|
|
2099
|
+
};
|
|
2100
|
+
if (timer !== void 0 && typeof timer.interval === "function") {
|
|
2101
|
+
this.timerHandle = timer.interval(tick, MINUTE);
|
|
2102
|
+
} else {
|
|
2103
|
+
const handle = setInterval(tick, MINUTE);
|
|
2104
|
+
handle.unref?.();
|
|
2105
|
+
this.timerHandle = handle;
|
|
2106
|
+
}
|
|
2107
|
+
setTimeout(tick, 5e3).unref?.();
|
|
2108
|
+
}
|
|
2109
|
+
stop() {
|
|
2110
|
+
this.stopped = true;
|
|
2111
|
+
const handle = this.timerHandle;
|
|
2112
|
+
if (typeof handle === "object" && handle !== null && typeof handle.clear === "function") handle.clear();
|
|
2113
|
+
else if (typeof handle === "number" || handle !== void 0) clearInterval(handle);
|
|
2114
|
+
this.timerHandle = void 0;
|
|
2115
|
+
}
|
|
2116
|
+
/** 公开给测试:判断此刻是否该跑,并按需触发一次。 */
|
|
2117
|
+
async tick(now = /* @__PURE__ */ new Date()) {
|
|
2118
|
+
if (this.stopped) return "skipped";
|
|
2119
|
+
const schedule = this.readSchedule();
|
|
2120
|
+
if (!schedule.enabled) return "disabled";
|
|
2121
|
+
if (!isDue(now, schedule)) return "skipped";
|
|
2122
|
+
const today = zonedDateString(now, schedule.timezone);
|
|
2123
|
+
const state = this.deps.store.readState();
|
|
2124
|
+
const lastDate = state.lastRunAt === void 0 ? void 0 : zonedDateString(new Date(state.lastRunAt), schedule.timezone);
|
|
2125
|
+
if (lastDate === today) return "skipped";
|
|
2126
|
+
if (this.lastFiredFor === today) return "skipped";
|
|
2127
|
+
if (this.deps.isRunning()) return "busy";
|
|
2128
|
+
this.lastFiredFor = today;
|
|
2129
|
+
try {
|
|
2130
|
+
await this.deps.run("schedule");
|
|
2131
|
+
return "ran";
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
this.deps.onError?.(error);
|
|
2134
|
+
return "skipped";
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
readSchedule() {
|
|
2138
|
+
const settings = this.deps.store.readSettings();
|
|
2139
|
+
return {
|
|
2140
|
+
enabled: settings.schedule.enabled,
|
|
2141
|
+
time: settings.schedule.time,
|
|
2142
|
+
timezone: settings.schedule.timezone
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
/** 下一次运行(禁用时为 undefined)。 */
|
|
2146
|
+
next() {
|
|
2147
|
+
const schedule = this.readSchedule();
|
|
2148
|
+
if (!schedule.enabled) return void 0;
|
|
2149
|
+
try {
|
|
2150
|
+
return nextOccurrence(/* @__PURE__ */ new Date(), schedule);
|
|
2151
|
+
} catch {
|
|
2152
|
+
return void 0;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
describe(state, now = /* @__PURE__ */ new Date()) {
|
|
2156
|
+
const schedule = this.readSchedule();
|
|
2157
|
+
const next = schedule.enabled ? this.next() : void 0;
|
|
2158
|
+
return {
|
|
2159
|
+
enabled: schedule.enabled,
|
|
2160
|
+
time: schedule.time,
|
|
2161
|
+
timezone: schedule.timezone,
|
|
2162
|
+
...next === void 0 ? {} : { nextRunAt: next.toISOString() },
|
|
2163
|
+
...state.lastRunAt === void 0 ? {} : { lastRunDate: zonedDateString(new Date(state.lastRunAt), schedule.timezone) },
|
|
2164
|
+
catchUpMinutes: CATCH_UP_MINUTES,
|
|
2165
|
+
source: "timer"
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
};
|
|
2169
|
+
|
|
2170
|
+
// src/host/import-cli.ts
|
|
2171
|
+
var import_cli_exports = {};
|
|
2172
|
+
__export(import_cli_exports, {
|
|
2173
|
+
cliConfigCandidates: () => cliConfigCandidates,
|
|
2174
|
+
importFirstAvailable: () => importFirstAvailable,
|
|
2175
|
+
importFromCli: () => importFromCli,
|
|
2176
|
+
parseSimpleYaml: () => parseSimpleYaml
|
|
2177
|
+
});
|
|
2178
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2179
|
+
import os3 from "os";
|
|
2180
|
+
import path9 from "path";
|
|
2181
|
+
function cliConfigCandidates(env = process.env, home = os3.homedir()) {
|
|
2182
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
2183
|
+
const base = xdg !== void 0 && xdg !== "" ? path9.resolve(xdg) : path9.join(home, ".config");
|
|
2184
|
+
return [
|
|
2185
|
+
env.TRENDradar_CONFIG?.trim() ?? "",
|
|
2186
|
+
path9.join(base, "trendradar", "config.yaml"),
|
|
2187
|
+
path9.join(home, ".trendradar", "config.yaml")
|
|
2188
|
+
].filter((item) => item !== "");
|
|
2189
|
+
}
|
|
2190
|
+
function unquote(raw) {
|
|
2191
|
+
const value = raw.trim();
|
|
2192
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
|
|
2193
|
+
try {
|
|
2194
|
+
return JSON.parse(value);
|
|
2195
|
+
} catch {
|
|
2196
|
+
return value.slice(1, -1);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) return value.slice(1, -1);
|
|
2200
|
+
return value;
|
|
2201
|
+
}
|
|
2202
|
+
function scalar(raw) {
|
|
2203
|
+
const value = raw.trim();
|
|
2204
|
+
if (value === "") return "";
|
|
2205
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
2206
|
+
const inner = value.slice(1, -1).trim();
|
|
2207
|
+
if (inner === "") return [];
|
|
2208
|
+
return inner.split(",").map((item) => unquote(item));
|
|
2209
|
+
}
|
|
2210
|
+
if (value === "true") return true;
|
|
2211
|
+
if (value === "false") return false;
|
|
2212
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
2213
|
+
return unquote(value);
|
|
2214
|
+
}
|
|
2215
|
+
function parseSimpleYaml(text) {
|
|
2216
|
+
const root = {};
|
|
2217
|
+
let current;
|
|
2218
|
+
let currentIndent = -1;
|
|
2219
|
+
for (const rawLine of text.split("\n")) {
|
|
2220
|
+
const line = rawLine.replace(/\r$/, "");
|
|
2221
|
+
const trimmed = line.trim();
|
|
2222
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
2223
|
+
const indent = line.length - line.trimStart().length;
|
|
2224
|
+
const match = /^([^:#]+):(.*)$/.exec(trimmed);
|
|
2225
|
+
if (match === null) throw new Error(`\u65E0\u6CD5\u89E3\u6790\u7684 YAML \u884C\uFF1A${trimmed}`);
|
|
2226
|
+
const key = match[1]?.trim() ?? "";
|
|
2227
|
+
const rest = match[2] ?? "";
|
|
2228
|
+
if (rest.trim() === "") {
|
|
2229
|
+
const child = {};
|
|
2230
|
+
root[key] = child;
|
|
2231
|
+
current = child;
|
|
2232
|
+
currentIndent = indent;
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
if (indent === 0) {
|
|
2236
|
+
root[key] = scalar(rest);
|
|
2237
|
+
current = void 0;
|
|
2238
|
+
currentIndent = -1;
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
if (current === void 0 || indent <= currentIndent) {
|
|
2242
|
+
root[key] = scalar(rest);
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
current[key] = scalar(rest);
|
|
2246
|
+
}
|
|
2247
|
+
return root;
|
|
2248
|
+
}
|
|
2249
|
+
function asRecord(value) {
|
|
2250
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
2251
|
+
}
|
|
2252
|
+
function asString(value) {
|
|
2253
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
2254
|
+
}
|
|
2255
|
+
function asNumber(value) {
|
|
2256
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2257
|
+
}
|
|
2258
|
+
function asBool(value) {
|
|
2259
|
+
return typeof value === "boolean" ? value : void 0;
|
|
2260
|
+
}
|
|
2261
|
+
function asStrings(value) {
|
|
2262
|
+
return Array.isArray(value) && value.length > 0 ? value.filter((item) => typeof item === "string") : void 0;
|
|
2263
|
+
}
|
|
2264
|
+
function isSecretKey(key) {
|
|
2265
|
+
return /(apikey|api_key|token|secret|password|passwd|credential)/i.test(key);
|
|
2266
|
+
}
|
|
2267
|
+
function importFromCli(filePath, base = DEFAULT_SETTINGS) {
|
|
2268
|
+
let text;
|
|
2269
|
+
try {
|
|
2270
|
+
text = readFileSync3(filePath, "utf8");
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
return {
|
|
2273
|
+
ok: false,
|
|
2274
|
+
source: filePath,
|
|
2275
|
+
imported: [],
|
|
2276
|
+
skippedSecrets: [],
|
|
2277
|
+
problems: [`\u8BFB\u53D6\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`]
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
let parsed;
|
|
2281
|
+
try {
|
|
2282
|
+
parsed = parseSimpleYaml(text);
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
return {
|
|
2285
|
+
ok: false,
|
|
2286
|
+
source: filePath,
|
|
2287
|
+
imported: [],
|
|
2288
|
+
skippedSecrets: [],
|
|
2289
|
+
problems: [`\u89E3\u6790\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`]
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
const imported = [];
|
|
2293
|
+
const skippedSecrets = [];
|
|
2294
|
+
const model = asRecord(parsed.model);
|
|
2295
|
+
const repo = asRecord(parsed.repo);
|
|
2296
|
+
const pr = asRecord(parsed.pr);
|
|
2297
|
+
const trending = asRecord(parsed.trending);
|
|
2298
|
+
const output = asRecord(parsed.output);
|
|
2299
|
+
const schedule = asRecord(parsed.schedule);
|
|
2300
|
+
const patch = JSON.parse(JSON.stringify(base));
|
|
2301
|
+
const take = (keys, key, value, apply) => {
|
|
2302
|
+
if (value === void 0) return;
|
|
2303
|
+
apply(value);
|
|
2304
|
+
keys.push(key);
|
|
2305
|
+
};
|
|
2306
|
+
take(imported, "model.provider", asString(model.provider), (value) => {
|
|
2307
|
+
patch.model.provider = value;
|
|
2308
|
+
});
|
|
2309
|
+
take(imported, "model.model", asString(model.name), (value) => {
|
|
2310
|
+
patch.model.model = value;
|
|
2311
|
+
});
|
|
2312
|
+
take(imported, "model.baseURL", asString(model.baseURL), (value) => {
|
|
2313
|
+
patch.model.baseURL = value;
|
|
2314
|
+
});
|
|
2315
|
+
take(imported, "model.temperature", asNumber(model.temperature), (value) => {
|
|
2316
|
+
patch.model.temperature = value;
|
|
2317
|
+
});
|
|
2318
|
+
take(imported, "model.maxTokens", asNumber(model.maxTokens), (value) => {
|
|
2319
|
+
patch.model.maxTokens = value;
|
|
2320
|
+
});
|
|
2321
|
+
take(imported, "model.allowDegraded", asBool(model.allowDegraded), (value) => {
|
|
2322
|
+
patch.model.allowDegraded = value;
|
|
2323
|
+
});
|
|
2324
|
+
take(imported, "repo.path", asString(repo.path), (value) => {
|
|
2325
|
+
patch.repo.path = value;
|
|
2326
|
+
});
|
|
2327
|
+
take(imported, "repo.remote", asString(repo.remote), (value) => {
|
|
2328
|
+
patch.repo.remote = value;
|
|
2329
|
+
});
|
|
2330
|
+
take(imported, "repo.baseBranch", asString(repo.baseBranch), (value) => {
|
|
2331
|
+
patch.repo.baseBranch = value;
|
|
2332
|
+
});
|
|
2333
|
+
take(imported, "repo.authorName", asString(repo.authorName), (value) => {
|
|
2334
|
+
patch.repo.authorName = value;
|
|
2335
|
+
});
|
|
2336
|
+
take(imported, "repo.authorEmail", asString(repo.authorEmail), (value) => {
|
|
2337
|
+
patch.repo.authorEmail = value;
|
|
2338
|
+
});
|
|
2339
|
+
take(imported, "pr.enabled", asBool(pr.enabled), (value) => {
|
|
2340
|
+
patch.pr.enabled = value;
|
|
2341
|
+
});
|
|
2342
|
+
take(imported, "pr.titleTemplate", asString(pr.titleTemplate), (value) => {
|
|
2343
|
+
patch.pr.titleTemplate = value;
|
|
2344
|
+
});
|
|
2345
|
+
take(imported, "pr.autoMerge", asBool(pr.autoMerge), (value) => {
|
|
2346
|
+
patch.pr.autoMerge = value;
|
|
2347
|
+
});
|
|
2348
|
+
take(imported, "pr.labels", asStrings(pr.labels), (value) => {
|
|
2349
|
+
patch.pr.labels = value;
|
|
2350
|
+
});
|
|
2351
|
+
take(imported, "trending.languages", asStrings(trending.languages), (value) => {
|
|
2352
|
+
patch.trending.languages = value;
|
|
2353
|
+
});
|
|
2354
|
+
take(imported, "trending.since", asString(trending.since), (value) => {
|
|
2355
|
+
patch.trending.since = value === "weekly" ? "weekly" : "daily";
|
|
2356
|
+
});
|
|
2357
|
+
take(imported, "trending.perLanguage", asNumber(trending.perLanguage), (value) => {
|
|
2358
|
+
patch.trending.topN = Math.round(value);
|
|
2359
|
+
});
|
|
2360
|
+
take(imported, "output.dir", asString(output.dir), (value) => {
|
|
2361
|
+
patch.output.dir = value;
|
|
2362
|
+
});
|
|
2363
|
+
for (const [section, values] of Object.entries({ model, repo, pr, trending, output, schedule })) {
|
|
2364
|
+
for (const [key, value] of Object.entries(values)) {
|
|
2365
|
+
if (isSecretKey(key) && value !== void 0 && value !== "") skippedSecrets.push(`${section}.${key}`);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
if (patch.model.baseURL === "") {
|
|
2369
|
+
const fallback = PROVIDER_BASE_URLS[patch.model.provider];
|
|
2370
|
+
if (fallback !== void 0) patch.model.baseURL = fallback;
|
|
2371
|
+
}
|
|
2372
|
+
const problems = [];
|
|
2373
|
+
if (asString(schedule.cron) !== void 0) {
|
|
2374
|
+
problems.push("CLI \u7684 schedule.cron \u65E0\u6CD5\u6620\u5C04\u6210\u63D2\u4EF6\u7684\u6BCF\u65E5\u65F6\u95F4\uFF0C\u672A\u5BFC\u5165\uFF08\u8BF7\u5728\u8BBE\u7F6E\u9875\u91CD\u8BBE\u65F6\u95F4\uFF09\u3002");
|
|
2375
|
+
}
|
|
2376
|
+
if (skippedSecrets.length > 0) {
|
|
2377
|
+
problems.push(`\u5BC6\u94A5\u7C7B\u5B57\u6BB5\u4E0D\u5BFC\u5165\uFF1A${skippedSecrets.join("\u3001")}\uFF08\u8BF7\u5728\u8BBE\u7F6E\u9875\u624B\u52A8\u586B\u5199\u6216\u6CBF\u7528 DSH \u51ED\u636E\uFF09\u3002`);
|
|
2378
|
+
}
|
|
2379
|
+
return {
|
|
2380
|
+
ok: imported.length > 0,
|
|
2381
|
+
source: filePath,
|
|
2382
|
+
settings: normalizeSettings(patch),
|
|
2383
|
+
imported,
|
|
2384
|
+
skippedSecrets,
|
|
2385
|
+
problems
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
function importFirstAvailable(env = process.env, base) {
|
|
2389
|
+
const candidates = cliConfigCandidates(env);
|
|
2390
|
+
for (const candidate of candidates) {
|
|
2391
|
+
const result = importFromCli(candidate, base);
|
|
2392
|
+
if (result.ok || result.problems.some((problem) => problem.startsWith("\u89E3\u6790\u5931\u8D25"))) return result;
|
|
2393
|
+
}
|
|
2394
|
+
return {
|
|
2395
|
+
ok: false,
|
|
2396
|
+
imported: [],
|
|
2397
|
+
skippedSecrets: [],
|
|
2398
|
+
problems: [`\u6CA1\u6709\u627E\u5230\u672C\u673A CLI \u914D\u7F6E\uFF08\u627E\u8FC7\uFF1A${candidates.join("\u3001")}\uFF09`]
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// src/host/preflight.ts
|
|
2403
|
+
var preflight_exports = {};
|
|
2404
|
+
__export(preflight_exports, {
|
|
2405
|
+
preflightModel: () => preflightModel
|
|
2406
|
+
});
|
|
2407
|
+
var TIMEOUT_MS = 15e3;
|
|
2408
|
+
async function preflightModel(options) {
|
|
2409
|
+
const started = Date.now();
|
|
2410
|
+
const settings = options.settings;
|
|
2411
|
+
const key = await resolveModelKey(settings, options.credentials, options.env ?? process.env);
|
|
2412
|
+
if (key.mode === "proxy") {
|
|
2413
|
+
const llm = options.llm;
|
|
2414
|
+
if (llm === void 0 || typeof llm.stream !== "function") {
|
|
2415
|
+
return {
|
|
2416
|
+
ok: false,
|
|
2417
|
+
code: "MODEL_KEY_MISSING",
|
|
2418
|
+
message: "\u8BE5\u6A21\u578B\u9700\u8981\u56DE\u73AF\u4EE3\u7406\uFF0C\u4F46\u5BBF\u4E3B\u6CA1\u6709\u53EF\u7528\u7684 ctx.llm \u670D\u52A1",
|
|
2419
|
+
hint: hintOf("MODEL_KEY_MISSING"),
|
|
2420
|
+
area: areaOf("MODEL_KEY_MISSING"),
|
|
2421
|
+
durationMs: Date.now() - started,
|
|
2422
|
+
mode: "proxy"
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
const provider = settings.model.hostProvider !== "" ? settings.model.hostProvider : settings.model.provider;
|
|
2426
|
+
const controller2 = new AbortController();
|
|
2427
|
+
const timer2 = setTimeout(() => controller2.abort(), TIMEOUT_MS);
|
|
2428
|
+
try {
|
|
2429
|
+
const stream = llm.stream({
|
|
2430
|
+
provider,
|
|
2431
|
+
model: settings.model.model,
|
|
2432
|
+
messages: [{ role: "user", content: [{ type: "text", text: "ping" }] }],
|
|
2433
|
+
maxTokens: 8,
|
|
2434
|
+
signal: controller2.signal
|
|
2435
|
+
});
|
|
2436
|
+
let failure;
|
|
2437
|
+
for await (const chunk of stream) {
|
|
2438
|
+
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
2439
|
+
failure = chunk.reason.failure;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
if (failure !== void 0) {
|
|
2443
|
+
const code = failure.code === "AUTH" ? "MODEL_UNAUTHORIZED" : failure.code === "RATE_LIMIT" ? "MODEL_RATE_LIMITED" : "MODEL_FAILED";
|
|
2444
|
+
return {
|
|
2445
|
+
ok: false,
|
|
2446
|
+
code,
|
|
2447
|
+
message: failure.message ?? failure.code ?? "\u5BBF\u4E3B\u6A21\u578B\u8C03\u7528\u5931\u8D25",
|
|
2448
|
+
hint: hintOf(code),
|
|
2449
|
+
area: areaOf(code),
|
|
2450
|
+
durationMs: Date.now() - started,
|
|
2451
|
+
mode: "proxy"
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
return { ok: true, durationMs: Date.now() - started, mode: "proxy", endpoint: `\u5BBF\u4E3B provider ${provider}` };
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
return {
|
|
2457
|
+
ok: false,
|
|
2458
|
+
code: "MODEL_FAILED",
|
|
2459
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2460
|
+
hint: hintOf("MODEL_FAILED"),
|
|
2461
|
+
area: areaOf("MODEL_FAILED"),
|
|
2462
|
+
durationMs: Date.now() - started,
|
|
2463
|
+
mode: "proxy"
|
|
2464
|
+
};
|
|
2465
|
+
} finally {
|
|
2466
|
+
clearTimeout(timer2);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
if (key.apiKey === void 0 || key.apiKey === "") {
|
|
2470
|
+
return {
|
|
2471
|
+
ok: false,
|
|
2472
|
+
code: "MODEL_KEY_MISSING",
|
|
2473
|
+
message: `\u6CA1\u6709\u89E3\u6790\u5230\u6A21\u578B\u5BC6\u94A5\uFF08\u5F15\u7528 ${settings.model.apiKeyRef}\uFF09`,
|
|
2474
|
+
hint: hintOf("MODEL_KEY_MISSING"),
|
|
2475
|
+
area: areaOf("MODEL_KEY_MISSING"),
|
|
2476
|
+
durationMs: Date.now() - started,
|
|
2477
|
+
mode: "direct"
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
const baseURL = effectiveBaseURL(settings.model).replace(/\/+$/, "");
|
|
2481
|
+
const endpoint = `${baseURL}/chat/completions`;
|
|
2482
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
2483
|
+
const controller = new AbortController();
|
|
2484
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
2485
|
+
try {
|
|
2486
|
+
const response = await doFetch(endpoint, {
|
|
2487
|
+
method: "POST",
|
|
2488
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${key.apiKey}` },
|
|
2489
|
+
body: JSON.stringify({
|
|
2490
|
+
model: settings.model.model,
|
|
2491
|
+
messages: [{ role: "user", content: "ping" }],
|
|
2492
|
+
max_tokens: 8,
|
|
2493
|
+
temperature: 0
|
|
2494
|
+
}),
|
|
2495
|
+
signal: controller.signal
|
|
2496
|
+
});
|
|
2497
|
+
if (response.ok) return { ok: true, durationMs: Date.now() - started, mode: "direct", endpoint };
|
|
2498
|
+
const text = await response.text().catch(() => "");
|
|
2499
|
+
const snippet = text.slice(0, 400);
|
|
2500
|
+
const code = response.status === 401 || response.status === 403 ? "MODEL_UNAUTHORIZED" : response.status === 404 || /model[^\n]*not found|not found[^\n]*model/i.test(snippet) ? "MODEL_NOT_FOUND" : response.status === 429 ? "MODEL_RATE_LIMITED" : "MODEL_FAILED";
|
|
2501
|
+
return {
|
|
2502
|
+
ok: response.status === 429,
|
|
2503
|
+
code,
|
|
2504
|
+
message: `HTTP ${response.status}\uFF1A${snippet === "" ? response.statusText : snippet}`,
|
|
2505
|
+
hint: hintOf(code),
|
|
2506
|
+
area: areaOf(code),
|
|
2507
|
+
durationMs: Date.now() - started,
|
|
2508
|
+
mode: "direct",
|
|
2509
|
+
endpoint
|
|
2510
|
+
};
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
2513
|
+
const code = aborted ? "MODEL_TIMEOUT" : "MODEL_FAILED";
|
|
2514
|
+
return {
|
|
2515
|
+
ok: false,
|
|
2516
|
+
code,
|
|
2517
|
+
message: aborted ? `\u9884\u68C0\u8D85\u65F6\uFF08${TIMEOUT_MS / 1e3} s\uFF09` : error instanceof Error ? error.message : String(error),
|
|
2518
|
+
hint: hintOf(code),
|
|
2519
|
+
area: areaOf(code),
|
|
2520
|
+
durationMs: Date.now() - started,
|
|
2521
|
+
mode: "direct",
|
|
2522
|
+
endpoint
|
|
2523
|
+
};
|
|
2524
|
+
} finally {
|
|
2525
|
+
clearTimeout(timer);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
// src/host/routes.ts
|
|
2530
|
+
var routes_exports = {};
|
|
2531
|
+
__export(routes_exports, {
|
|
2532
|
+
HEADER: () => HEADER,
|
|
2533
|
+
ROUTE_PREFIX: () => ROUTE_PREFIX,
|
|
2534
|
+
createRoutes: () => createRoutes
|
|
2535
|
+
});
|
|
2536
|
+
var ROUTE_PREFIX = "/api/morehao/github-trending";
|
|
2537
|
+
var HEADER = "x-dsh-github-trending";
|
|
2538
|
+
var MAX_BODY2 = 512 * 1024;
|
|
2539
|
+
function sendJson2(response, status, payload) {
|
|
2540
|
+
const body = JSON.stringify(payload);
|
|
2541
|
+
response.writeHead(status, {
|
|
2542
|
+
"content-type": "application/json; charset=utf-8",
|
|
2543
|
+
"cache-control": "no-store",
|
|
2544
|
+
"content-length": Buffer.byteLength(body)
|
|
2545
|
+
});
|
|
2546
|
+
response.end(body);
|
|
2547
|
+
}
|
|
2548
|
+
async function readBody2(request) {
|
|
2549
|
+
const chunks = [];
|
|
2550
|
+
let size = 0;
|
|
2551
|
+
for await (const chunk of request) {
|
|
2552
|
+
size += chunk.length;
|
|
2553
|
+
if (size > MAX_BODY2) throw new IpcError("PAYLOAD_TOO_LARGE", "\u8BF7\u6C42\u4F53\u8FC7\u5927");
|
|
2554
|
+
chunks.push(chunk);
|
|
2555
|
+
}
|
|
2556
|
+
if (chunks.length === 0) return {};
|
|
2557
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
2558
|
+
if (text === "") return {};
|
|
2559
|
+
try {
|
|
2560
|
+
const value = JSON.parse(text);
|
|
2561
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2562
|
+
throw new Error("body must be a JSON object");
|
|
2563
|
+
}
|
|
2564
|
+
return value;
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
throw new IpcError("CONFIG_INVALID", `\u8BF7\u6C42\u4F53\u4E0D\u662F JSON \u5BF9\u8C61\uFF1A${error instanceof Error ? error.message : String(error)}`);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
function createRoutes(deps) {
|
|
2570
|
+
return async function handler(request, response) {
|
|
2571
|
+
if (request.headers[HEADER] !== "1") {
|
|
2572
|
+
sendJson2(response, 403, { ok: false, code: "FORBIDDEN", message: `\u7F3A\u5C11 ${HEADER}: 1 \u5934` });
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
2576
|
+
const pathname = url.pathname.startsWith(ROUTE_PREFIX) ? url.pathname.slice(ROUTE_PREFIX.length) || "/" : url.pathname;
|
|
2577
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
2578
|
+
try {
|
|
2579
|
+
const result = await dispatch(method, pathname, url, request, deps);
|
|
2580
|
+
if (!response.writableEnded) sendJson2(response, 200, { ok: true, ...result });
|
|
2581
|
+
} catch (error) {
|
|
2582
|
+
if (response.writableEnded) return;
|
|
2583
|
+
const code = error instanceof IpcError ? error.code : "INTERNAL";
|
|
2584
|
+
const message = redact(error instanceof Error ? error.message : String(error));
|
|
2585
|
+
deps.logger?.("route.error", { pathname, method, code, message });
|
|
2586
|
+
sendJson2(response, code === "CONFIG_INVALID" ? 400 : 500, { ok: false, code, message });
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
async function dispatch(method, pathname, url, request, deps) {
|
|
2591
|
+
const env = deps.env ?? process.env;
|
|
2592
|
+
const settings = deps.store.readSettings();
|
|
2593
|
+
if (method === "GET" && pathname === "/state") return statePayload(deps, settings);
|
|
2594
|
+
if (method === "GET" && pathname === "/core") {
|
|
2595
|
+
const core = await inspectCore(settings, { env, fresh: url.searchParams.get("fresh") === "1" });
|
|
2596
|
+
const coexistence = await detectCoexistence(core.path, { env });
|
|
2597
|
+
return { core, coexistence };
|
|
2598
|
+
}
|
|
2599
|
+
if (method === "POST" && pathname === "/core/download") {
|
|
2600
|
+
const core = await locateCore({ settings, env, forceDownload: true });
|
|
2601
|
+
const status = await inspectCore(settings, { env, fresh: true });
|
|
2602
|
+
return { downloaded: true, path: core.path, tag: core.tag ?? null, core: status };
|
|
2603
|
+
}
|
|
2604
|
+
if (method === "POST" && pathname === "/config") {
|
|
2605
|
+
const body = await readBody2(request);
|
|
2606
|
+
const next = normalizeSettings(body.settings ?? body);
|
|
2607
|
+
const core = await inspectCore({ ...next, corePath: settings.corePath }, { env });
|
|
2608
|
+
const providers = core.supportedProviders.length > 0 ? core.supportedProviders : void 0;
|
|
2609
|
+
const problems = validateSettings(next, providers);
|
|
2610
|
+
if (problems.length > 0) {
|
|
2611
|
+
return { saved: false, problems, settings: next };
|
|
2612
|
+
}
|
|
2613
|
+
deps.store.writeSettings(next);
|
|
2614
|
+
return { saved: true, settings: next, problems: [] };
|
|
2615
|
+
}
|
|
2616
|
+
if (method === "POST" && pathname === "/preflight") {
|
|
2617
|
+
const body = await readBody2(request);
|
|
2618
|
+
const candidate = body.settings === void 0 ? settings : normalizeSettings(body.settings);
|
|
2619
|
+
const result = await preflightModel({
|
|
2620
|
+
settings: candidate,
|
|
2621
|
+
credentials: credentialServices(deps.get),
|
|
2622
|
+
llm: llmServices(deps.get),
|
|
2623
|
+
env
|
|
2624
|
+
});
|
|
2625
|
+
return { preflight: result };
|
|
2626
|
+
}
|
|
2627
|
+
if (method === "POST" && pathname === "/run") {
|
|
2628
|
+
const body = await readBody2(request);
|
|
2629
|
+
const summary = await deps.runner.run({
|
|
2630
|
+
dryRun: body.dryRun === true,
|
|
2631
|
+
...typeof body.date === "string" && body.date !== "" ? { date: body.date } : {},
|
|
2632
|
+
trigger: "manual"
|
|
2633
|
+
});
|
|
2634
|
+
return { summary };
|
|
2635
|
+
}
|
|
2636
|
+
if (method === "POST" && pathname === "/run/cancel") {
|
|
2637
|
+
return { cancelled: deps.runner.cancel() };
|
|
2638
|
+
}
|
|
2639
|
+
if (method === "GET" && pathname === "/quickfill") {
|
|
2640
|
+
const candidates = await listProviderCandidates({
|
|
2641
|
+
llm: llmServices(deps.get),
|
|
2642
|
+
credentials: credentialServices(deps.get),
|
|
2643
|
+
env
|
|
2644
|
+
});
|
|
2645
|
+
return { providers: candidates };
|
|
2646
|
+
}
|
|
2647
|
+
if (method === "POST" && pathname === "/quickfill") {
|
|
2648
|
+
const body = await readBody2(request);
|
|
2649
|
+
const providerId = typeof body.providerId === "string" ? body.providerId : "";
|
|
2650
|
+
const modelId = typeof body.modelId === "string" ? body.modelId : "";
|
|
2651
|
+
const candidates = await listProviderCandidates({
|
|
2652
|
+
llm: llmServices(deps.get),
|
|
2653
|
+
credentials: credentialServices(deps.get),
|
|
2654
|
+
env
|
|
2655
|
+
});
|
|
2656
|
+
const candidate = candidates.find((item) => item.id === providerId);
|
|
2657
|
+
if (candidate === void 0) throw new IpcError("CONFIG_INVALID", `\u5BBF\u4E3B\u6CA1\u6709\u8FD9\u4E2A provider\uFF1A${providerId}`);
|
|
2658
|
+
if (modelId === "") throw new IpcError("CONFIG_INVALID", "\u5FC5\u987B\u9009\u62E9\u4E00\u4E2A\u6A21\u578B");
|
|
2659
|
+
const next = applyCandidate(settings, candidate, modelId);
|
|
2660
|
+
deps.store.writeSettings(next);
|
|
2661
|
+
return { saved: true, settings: next, applied: { provider: candidate.id, model: modelId, mode: candidate.credential.mode } };
|
|
2662
|
+
}
|
|
2663
|
+
if (method === "POST" && pathname === "/import") {
|
|
2664
|
+
const result = importFirstAvailable(env, settings);
|
|
2665
|
+
if (result.ok && result.settings !== void 0) deps.store.writeSettings(result.settings);
|
|
2666
|
+
return { import: { ...result, settings: result.settings === void 0 ? void 0 : result.settings } };
|
|
2667
|
+
}
|
|
2668
|
+
if (method === "GET" && pathname === "/log") {
|
|
2669
|
+
const id = url.searchParams.get("id") ?? "";
|
|
2670
|
+
if (!/^[A-Za-z0-9-]+$/.test(id)) throw new IpcError("CONFIG_INVALID", "\u975E\u6CD5\u7684\u8FD0\u884C id");
|
|
2671
|
+
const file = deps.store.newLogPath(id);
|
|
2672
|
+
const text = deps.store.readLog(file);
|
|
2673
|
+
return { id, exists: text !== "", text };
|
|
2674
|
+
}
|
|
2675
|
+
throw new IpcError("NOT_FOUND", `\u672A\u5904\u7406\u7684\u8DEF\u7531\uFF1A${method} ${pathname}`);
|
|
2676
|
+
}
|
|
2677
|
+
async function statePayload(deps, settings) {
|
|
2678
|
+
const env = deps.env ?? process.env;
|
|
2679
|
+
const credentials = credentialServices(deps.get);
|
|
2680
|
+
const state = deps.store.readState();
|
|
2681
|
+
const history = deps.store.readHistory().slice(0, 10);
|
|
2682
|
+
const core = await inspectCore(settings, { env });
|
|
2683
|
+
const coexistence = await detectCoexistence(core.path, { env });
|
|
2684
|
+
const modelCredential = await describeRef(credentials, settings.model.apiKeyRef, env);
|
|
2685
|
+
const github = await resolveGithubToken(credentials, settings.github.authMode, settings.github.tokenRef, { env });
|
|
2686
|
+
return {
|
|
2687
|
+
plugin: { name: "@morehao/dsh-github-trending", version: deps.version },
|
|
2688
|
+
settings,
|
|
2689
|
+
core,
|
|
2690
|
+
coexistence,
|
|
2691
|
+
schedule: deps.scheduler.describe(state),
|
|
2692
|
+
status: {
|
|
2693
|
+
running: deps.runner.running ?? null,
|
|
2694
|
+
lastRunAt: state.lastRunAt ?? null,
|
|
2695
|
+
lastDigest: state.lastDigest ?? null,
|
|
2696
|
+
lastResult: state.lastResult ?? null,
|
|
2697
|
+
pendingPr: state.pendingPr ?? null,
|
|
2698
|
+
history
|
|
2699
|
+
},
|
|
2700
|
+
model: {
|
|
2701
|
+
mode: settings.model.mode,
|
|
2702
|
+
credential: {
|
|
2703
|
+
ref: settings.model.apiKeyRef,
|
|
2704
|
+
configured: modelCredential.configured,
|
|
2705
|
+
source: modelCredential.source ?? null,
|
|
2706
|
+
writable: modelCredential.writable
|
|
2707
|
+
},
|
|
2708
|
+
effectiveBaseURL: settings.model.baseURL
|
|
2709
|
+
},
|
|
2710
|
+
github: {
|
|
2711
|
+
authMode: settings.github.authMode,
|
|
2712
|
+
configured: github.token !== void 0,
|
|
2713
|
+
source: github.source
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// src/tools.ts
|
|
2719
|
+
var tools_exports = {};
|
|
2720
|
+
__export(tools_exports, {
|
|
2721
|
+
createTrendingHistoryTool: () => createTrendingHistoryTool,
|
|
2722
|
+
createTrendingRunTool: () => createTrendingRunTool
|
|
2723
|
+
});
|
|
2724
|
+
var RUN_OUTPUT_SCHEMA = {
|
|
2725
|
+
type: "object",
|
|
2726
|
+
additionalProperties: true,
|
|
2727
|
+
properties: {
|
|
2728
|
+
id: { type: "string" },
|
|
2729
|
+
ok: { type: "boolean" },
|
|
2730
|
+
skipped: { type: "boolean" },
|
|
2731
|
+
dryRun: { type: "boolean" },
|
|
2732
|
+
durationMs: { type: "number" },
|
|
2733
|
+
coreVersion: { type: "string" },
|
|
2734
|
+
files: { type: "number" },
|
|
2735
|
+
contentHash: { type: "string" },
|
|
2736
|
+
branch: { type: "string" },
|
|
2737
|
+
prUrl: { type: "string" },
|
|
2738
|
+
errorCode: { type: "string" },
|
|
2739
|
+
errorMessage: { type: "string" },
|
|
2740
|
+
errorHint: { type: "string" },
|
|
2741
|
+
warnings: { type: "array", items: { type: "string" } },
|
|
2742
|
+
markdown: { type: "string" }
|
|
2743
|
+
},
|
|
2744
|
+
required: ["ok", "markdown"]
|
|
2745
|
+
};
|
|
2746
|
+
var HISTORY_OUTPUT_SCHEMA = {
|
|
2747
|
+
type: "object",
|
|
2748
|
+
additionalProperties: true,
|
|
2749
|
+
properties: {
|
|
2750
|
+
lastRunAt: { type: "string" },
|
|
2751
|
+
nextRunAt: { type: "string" },
|
|
2752
|
+
coreVersion: { type: "string" },
|
|
2753
|
+
model: { type: "string" },
|
|
2754
|
+
provider: { type: "string" },
|
|
2755
|
+
pendingPr: { type: "string" },
|
|
2756
|
+
records: {
|
|
2757
|
+
type: "array",
|
|
2758
|
+
items: {
|
|
2759
|
+
type: "object",
|
|
2760
|
+
additionalProperties: true,
|
|
2761
|
+
properties: {
|
|
2762
|
+
at: { type: "string" },
|
|
2763
|
+
ok: { type: "boolean" },
|
|
2764
|
+
skipped: { type: "boolean" },
|
|
2765
|
+
errorCode: { type: "string" },
|
|
2766
|
+
durationMs: { type: "number" },
|
|
2767
|
+
files: { type: "number" },
|
|
2768
|
+
branch: { type: "string" },
|
|
2769
|
+
prUrl: { type: "string" },
|
|
2770
|
+
coreVersion: { type: "string" }
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
},
|
|
2774
|
+
markdown: { type: "string" }
|
|
2775
|
+
},
|
|
2776
|
+
required: ["markdown"]
|
|
2777
|
+
};
|
|
2778
|
+
function formatDuration(ms) {
|
|
2779
|
+
if (ms <= 0) return "\u2014";
|
|
2780
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
2781
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
2782
|
+
}
|
|
2783
|
+
function describeRecord(record) {
|
|
2784
|
+
const when = record.at.replace("T", " ").slice(0, 19);
|
|
2785
|
+
const state = record.skipped ? "\u65E0\u53D8\u66F4" : record.ok ? "\u6210\u529F" : `\u5931\u8D25 ${record.errorCode ?? ""}`.trim();
|
|
2786
|
+
const tail = [record.branch, record.prUrl].filter((item) => typeof item === "string" && item !== "").join(" \xB7 ");
|
|
2787
|
+
return `\xB7 ${when} ${state} \xB7 ${record.files} \u4E2A\u6587\u4EF6 \xB7 ${formatDuration(record.durationMs)}${tail === "" ? "" : ` \xB7 ${tail}`}`;
|
|
2788
|
+
}
|
|
2789
|
+
function createTrendingRunTool(runner) {
|
|
2790
|
+
return {
|
|
2791
|
+
name: "trending_run",
|
|
2792
|
+
description: "\u6267\u884C\u4E00\u6B21 GitHub \u8D8B\u52BF\u65E5\u62A5\uFF1A\u5B9A\u4F4D\u5E76\u63E1\u624B\u672C\u5730\u6838\u5FC3\uFF0Cspawn \u4E00\u6B21 run\uFF0C\u8FD4\u56DE\u7ED3\u6784\u5316\u6458\u8981\uFF08\u6587\u4EF6\u6570\u3001commit\u3001PR\u3001\u9519\u8BEF\u7801\uFF09\u3002dryRun=true \u53EA\u6293\u53D6\u4E0E\u6E32\u67D3\u3001\u4E0D\u5199\u6587\u4EF6\u4E5F\u4E0D\u63D0\u4EA4\u3002\u540C\u65E5\u540C\u5185\u5BB9\u91CD\u8DD1\u4F1A\u8FD4\u56DE skipped=true\uFF08\u96F6\u65B0\u589E commit\uFF09\u3002",
|
|
2793
|
+
parameters: {
|
|
2794
|
+
dryRun: { type: "boolean", description: "\u53EA\u8BD5\u8DD1\uFF1A\u4E0D\u5199\u6587\u4EF6\u3001\u4E0D\u63D0\u4EA4\u3001\u4E0D\u5F00 PR" },
|
|
2795
|
+
date: { type: "string", description: "\u6307\u5B9A\u65E5\u671F\uFF08YYYY-MM-DD\uFF09\uFF1B\u7701\u7565\u5219\u7528\u4ECA\u5929" }
|
|
2796
|
+
},
|
|
2797
|
+
output: {
|
|
2798
|
+
schema: RUN_OUTPUT_SCHEMA,
|
|
2799
|
+
render: (_args, value) => [{ type: "text", text: typeof value?.markdown === "string" ? value.markdown : "" }]
|
|
2800
|
+
},
|
|
2801
|
+
async execute(args = {}, exec = {}) {
|
|
2802
|
+
void exec;
|
|
2803
|
+
const summary = await runner.run({
|
|
2804
|
+
dryRun: args.dryRun === true,
|
|
2805
|
+
...typeof args.date === "string" && args.date !== "" ? { date: args.date } : {},
|
|
2806
|
+
trigger: "tool"
|
|
2807
|
+
});
|
|
2808
|
+
const lines = [];
|
|
2809
|
+
const head = summary.ok ? summary.skipped ? "\u65E0\u53D8\u66F4\uFF08skipped\uFF09" : summary.dryRun ? "\u8BD5\u8DD1\u5B8C\u6210" : "\u8FD0\u884C\u6210\u529F" : `\u8FD0\u884C\u5931\u8D25\uFF1A${summary.error?.code ?? "INTERNAL"}`;
|
|
2810
|
+
lines.push(`${summary.ok ? "\u2713" : "\u2717"} ${head}`);
|
|
2811
|
+
lines.push(`\xB7 \u6838\u5FC3 ${summary.coreVersion || "(\u672A\u77E5)"} \xB7 ${formatDuration(summary.durationMs)} \xB7 ${summary.files} \u4E2A\u6587\u4EF6`);
|
|
2812
|
+
if (summary.contentHash !== void 0) lines.push(`\xB7 contentHash ${summary.contentHash.slice(0, 12)}`);
|
|
2813
|
+
if (summary.branch !== void 0) lines.push(`\xB7 \u5206\u652F ${summary.branch}`);
|
|
2814
|
+
if (summary.prUrl !== void 0) lines.push(`\xB7 PR ${summary.prUrl}`);
|
|
2815
|
+
if (summary.error !== void 0) {
|
|
2816
|
+
lines.push(`\xB7 ${summary.error.message}`);
|
|
2817
|
+
lines.push(`\xB7 \u4E0B\u4E00\u6B65\uFF1A${summary.error.hint}`);
|
|
2818
|
+
}
|
|
2819
|
+
if (summary.dryRun) lines.push("\xB7 \u8FD9\u662F\u8BD5\u8DD1\uFF0C\u6CA1\u6709\u5199\u6587\u4EF6\u3001\u6CA1\u6709\u63D0\u4EA4");
|
|
2820
|
+
return {
|
|
2821
|
+
id: summary.id,
|
|
2822
|
+
ok: summary.ok,
|
|
2823
|
+
skipped: summary.skipped,
|
|
2824
|
+
dryRun: summary.dryRun,
|
|
2825
|
+
durationMs: summary.durationMs,
|
|
2826
|
+
coreVersion: summary.coreVersion,
|
|
2827
|
+
files: summary.files,
|
|
2828
|
+
...summary.contentHash === void 0 ? {} : { contentHash: summary.contentHash },
|
|
2829
|
+
...summary.branch === void 0 ? {} : { branch: summary.branch },
|
|
2830
|
+
...summary.prUrl === void 0 ? {} : { prUrl: summary.prUrl },
|
|
2831
|
+
...summary.error === void 0 ? {} : { errorCode: summary.error.code, errorMessage: summary.error.message, errorHint: summary.error.hint },
|
|
2832
|
+
warnings: summary.warnings,
|
|
2833
|
+
markdown: lines.join("\n")
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
function createTrendingHistoryTool(store, scheduler) {
|
|
2839
|
+
return {
|
|
2840
|
+
name: "trending_history",
|
|
2841
|
+
description: "\u67E5\u770B GitHub \u8D8B\u52BF\u65E5\u62A5\u7684\u8FD0\u884C\u72B6\u6001\uFF1A\u4E0A\u6B21/\u4E0B\u6B21\u8FD0\u884C\u3001\u6700\u8FD1 N \u6761\u8BB0\u5F55\uFF08\u7ED3\u679C\u3001\u8017\u65F6\u3001commit/PR\u3001\u9519\u8BEF\u7801\uFF09\u3001\u6838\u5FC3\u7248\u672C\u3001\u5F53\u524D\u6A21\u578B\u4E0E\u672A\u5408\u5E76 PR\u3002",
|
|
2842
|
+
parameters: {
|
|
2843
|
+
limit: { type: "number", description: "\u8FD4\u56DE\u6700\u8FD1\u591A\u5C11\u6761\u8BB0\u5F55\uFF08\u9ED8\u8BA4 10\uFF0C\u6700\u591A 30\uFF09" }
|
|
2844
|
+
},
|
|
2845
|
+
output: {
|
|
2846
|
+
schema: HISTORY_OUTPUT_SCHEMA,
|
|
2847
|
+
render: (_args, value) => [{ type: "text", text: typeof value?.markdown === "string" ? value.markdown : "" }]
|
|
2848
|
+
},
|
|
2849
|
+
async execute(args = {}) {
|
|
2850
|
+
const limit = Math.min(Math.max(Math.round(Number(args.limit ?? 10)) || 10, 1), 30);
|
|
2851
|
+
const settings = store.readSettings();
|
|
2852
|
+
const state = store.readState();
|
|
2853
|
+
const records = store.readHistory().slice(0, limit);
|
|
2854
|
+
const schedule = scheduler.describe(state);
|
|
2855
|
+
const lines = [];
|
|
2856
|
+
lines.push(`\u4E0A\u6B21\u8FD0\u884C\uFF1A${state.lastRunAt === void 0 ? "\u4ECE\u672A" : state.lastRunAt.replace("T", " ").slice(0, 19)}`);
|
|
2857
|
+
lines.push(`\u4E0B\u6B21\u8FD0\u884C\uFF1A${schedule.nextRunAt === void 0 ? "\u672A\u542F\u7528" : `${schedule.nextRunAt.replace("T", " ").slice(0, 16)} UTC\uFF08${schedule.time} ${schedule.timezone}\uFF09`}`);
|
|
2858
|
+
lines.push(`\u6838\u5FC3\u7248\u672C\uFF1A${state.lastResult?.coreVersion ?? "(\u5C1A\u672A\u8FD0\u884C)"}`);
|
|
2859
|
+
lines.push(`\u6A21\u578B\uFF1A${settings.model.provider} / ${settings.model.model || "(\u672A\u586B)"} \xB7 ${settings.model.mode === "proxy" ? "\u56DE\u73AF\u4EE3\u7406" : `\u51ED\u636E ${settings.model.apiKeyRef}`}`);
|
|
2860
|
+
if (state.pendingPr !== void 0) lines.push(`\u672A\u5408\u5E76 PR\uFF1A${state.pendingPr.url}`);
|
|
2861
|
+
if (state.lastDigest !== void 0) {
|
|
2862
|
+
lines.push(`\u4E0A\u6B21\u5185\u5BB9\uFF1A${state.lastDigest.date} \xB7 ${state.lastDigest.contentHash.slice(0, 12)} \xB7 \u6838\u5FC3 ${state.lastDigest.coreVersion}`);
|
|
2863
|
+
}
|
|
2864
|
+
lines.push(`\u6700\u8FD1 ${records.length} \u6B21\uFF1A`);
|
|
2865
|
+
if (records.length === 0) lines.push("\xB7 \u8FD8\u6CA1\u6709\u8FD0\u884C\u8BB0\u5F55");
|
|
2866
|
+
for (const record of records) lines.push(describeRecord(record));
|
|
2867
|
+
return {
|
|
2868
|
+
...state.lastRunAt === void 0 ? {} : { lastRunAt: state.lastRunAt },
|
|
2869
|
+
...schedule.nextRunAt === void 0 ? {} : { nextRunAt: schedule.nextRunAt },
|
|
2870
|
+
coreVersion: state.lastResult?.coreVersion ?? "",
|
|
2871
|
+
model: settings.model.model,
|
|
2872
|
+
provider: settings.model.provider,
|
|
2873
|
+
...state.pendingPr === void 0 ? {} : { pendingPr: state.pendingPr.url },
|
|
2874
|
+
records,
|
|
2875
|
+
markdown: lines.join("\n")
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
export {
|
|
2882
|
+
config_exports,
|
|
2883
|
+
paths_exports,
|
|
2884
|
+
Store,
|
|
2885
|
+
state_exports,
|
|
2886
|
+
errors_exports,
|
|
2887
|
+
ipc_exports,
|
|
2888
|
+
handshake_exports,
|
|
2889
|
+
release_exports,
|
|
2890
|
+
locate_exports,
|
|
2891
|
+
channel_exports,
|
|
2892
|
+
run_exports,
|
|
2893
|
+
core_exports,
|
|
2894
|
+
credentials_exports,
|
|
2895
|
+
quickfill_exports,
|
|
2896
|
+
proxy_exports,
|
|
2897
|
+
Runner,
|
|
2898
|
+
runner_exports,
|
|
2899
|
+
Scheduler,
|
|
2900
|
+
schedule_exports,
|
|
2901
|
+
import_cli_exports,
|
|
2902
|
+
preflight_exports,
|
|
2903
|
+
ROUTE_PREFIX,
|
|
2904
|
+
createRoutes,
|
|
2905
|
+
routes_exports,
|
|
2906
|
+
createTrendingRunTool,
|
|
2907
|
+
createTrendingHistoryTool,
|
|
2908
|
+
tools_exports
|
|
2909
|
+
};
|