@d5render/cli 0.1.56 → 0.1.58

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/bin/d5cli CHANGED
@@ -1,115 +1,61 @@
1
1
  #!/usr/bin/env node
2
+ import { argv, env, platform } from "node:process";
2
3
  import { execSync, spawn } from "node:child_process";
3
- import { createConnection } from "node:net";
4
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
- import { argv, env, platform } from "node:process";
7
6
  import { fileURLToPath } from "node:url";
8
7
 
9
- //#region copilot/server/config.ts
10
- const name$1 = "d5_mcp_review_builtin";
8
+ //#region package.json
9
+ var name$1 = "@d5render/cli";
10
+
11
+ //#endregion
12
+ //#region packages/env.ts
13
+ const name = "d5_mcp_review_builtin";
11
14
  const report = "report";
12
15
  const getHash = "hash";
13
- const file = "bin/copilot.js";
16
+ const file = "bin/mcpServer.js";
14
17
  const RUNTIME_CWD = join(dirname(fileURLToPath(import.meta.url)), "../");
15
18
  const serveFile = join(RUNTIME_CWD, file);
16
19
  const envJson = buildEnv();
17
- const envUsed = {
18
- CI_SERVER_URL: toEnv("CI_SERVER_URL"),
19
- CI_PROJECT_PATH: toEnv("CI_PROJECT_PATH"),
20
- CI_PROJECT_ID: toEnv("CI_PROJECT_ID"),
21
- CI_PROJECT_NAME: toEnv("CI_PROJECT_NAME"),
22
- CI_COMMIT_SHA: toEnv("CI_COMMIT_SHA"),
23
- CI_COMMIT_BEFORE_SHA: toEnv("CI_COMMIT_BEFORE_SHA"),
24
- CI_MERGE_REQUEST_IID: toEnv("CI_MERGE_REQUEST_IID"),
25
- CI_MERGE_REQUEST_TITLE: toEnv("CI_MERGE_REQUEST_TITLE"),
26
- CI_MERGE_REQUEST_DESCRIPTION: toEnv("CI_MERGE_REQUEST_DESCRIPTION"),
27
- JIRA_BASE_URL: toEnv("JIRA_BASE_URL", "http://jira.d5techs.com.cn"),
28
- DINGTALK_WEBHOOK: toEnv("DINGTALK_WEBHOOK"),
29
- GITLAB_TOKEN: toEnv("GITLAB_TOKEN"),
30
- JIRA_PAT: toEnv("JIRA_PAT"),
31
- JIRA_COOKIE: toEnv("JIRA_COOKIE"),
32
- JIRA_USERNAME: toEnv("JIRA_USERNAME"),
33
- JIRA_PASSWORD: toEnv("JIRA_PASSWORD")
34
- };
35
- const tools = [
36
- "--additional-mcp-config",
37
- JSON.stringify({ mcpServers: { [name$1]: {
38
- type: "local",
39
- command: "node",
40
- args: [serveFile, `--customizenv=${JSON.stringify(envUsed)}`],
41
- tools: ["*"]
42
- } } }),
43
- "--allow-all-paths",
44
- "--allow-all-tools",
45
- "--deny-tool",
46
- "write",
47
- "--deny-tool",
48
- "github-mcp-server"
20
+ const envConfigKeys = [
21
+ "CI_SERVER_URL",
22
+ "CI_PROJECT_PATH",
23
+ "CI_PROJECT_ID",
24
+ "CI_PROJECT_NAME",
25
+ "CI_COMMIT_SHA",
26
+ "CI_COMMIT_BEFORE_SHA",
27
+ "CI_MERGE_REQUEST_IID",
28
+ "CI_MERGE_REQUEST_TITLE",
29
+ "CI_MERGE_REQUEST_DESCRIPTION",
30
+ "JIRA_BASE_URL",
31
+ "DINGTALK_WEBHOOK",
32
+ "GITLAB_TOKEN",
33
+ "JIRA_PAT",
34
+ "JIRA_COOKIE",
35
+ "JIRA_USERNAME",
36
+ "JIRA_PASSWORD",
37
+ "TOKEN_USAGE"
49
38
  ];
39
+ const envUsed = Object.defineProperty({}, "JIRA_BASE_URL", {
40
+ get: () => toEnv("CI_SERVER_URL", "http://jira.d5techs.com.cn"),
41
+ enumerable: true,
42
+ configurable: true
43
+ });
44
+ envConfigKeys.forEach((key) => Object.defineProperty(envUsed, key, {
45
+ get: () => toEnv(key),
46
+ enumerable: true,
47
+ configurable: true
48
+ }));
50
49
  function toEnv(key, defaultValue) {
51
50
  return envJson[key] || process.env[key] || defaultValue;
52
51
  }
53
52
  function buildEnv() {
54
53
  const envArg = argv.find((arg) => arg.startsWith("--customizenv="));
55
- let envJson$1 = {};
56
- if (envArg) envJson$1 = JSON.parse(envArg.replace("--customizenv=", ""));
57
- return envJson$1;
58
- }
59
-
60
- //#endregion
61
- //#region copilot/bin/install.ts
62
- function install() {
63
- if (!env.CI) return;
64
- console.log("install copilot...");
65
- let success = false;
66
- let local = "";
67
- try {
68
- local = execSync("npm list @github/copilot -g --depth=0 --json").toString();
69
- } catch {
70
- local = "{}";
71
- }
72
- try {
73
- const localInfo = JSON.parse(local);
74
- const localVersion = localInfo.dependencies ? localInfo.dependencies?.["@github/copilot"]?.version : void 0;
75
- if (!localVersion) {
76
- installCopilot();
77
- success = true;
78
- console.log("install copilot success");
79
- return;
80
- }
81
- if (localVersion !== execSync("npm view @github/copilot version --registry=https://registry.npmmirror.com").toString().trim()) {
82
- execSync("npm uninstall -g @github/copilot --force");
83
- installCopilot();
84
- success = true;
85
- console.log("update copilot success");
86
- } else {
87
- success = true;
88
- console.log("copilot exists and is up-to-date");
89
- }
90
- } catch (error) {
91
- console.error(error);
92
- }
93
- if (!success) try {
94
- console.warn("try to reinstall copilot...");
95
- installCopilot();
96
- success = true;
97
- } catch (error) {
98
- console.error(error);
99
- }
100
- if (success) return;
101
- console.warn("try to reinstall copilot...");
102
- installCopilot();
103
- }
104
- function installCopilot() {
105
- execSync("npm install -g @github/copilot --registry=https://registry.npmmirror.com", { stdio: "inherit" });
54
+ let envJson = {};
55
+ if (envArg) envJson = JSON.parse(envArg.replace("--customizenv=", ""));
56
+ return envJson;
106
57
  }
107
58
 
108
- //#endregion
109
- //#region package.json
110
- var name = "@d5render/cli";
111
- var version = "0.1.56";
112
-
113
59
  //#endregion
114
60
  //#region packages/gitlab/url.ts
115
61
  function buildHeaders() {
@@ -154,8 +100,8 @@ const commits = () => {
154
100
  //#endregion
155
101
  //#region packages/message/sendding.ts
156
102
  async function sendding(title, text) {
157
- if (!envUsed.DINGTALK_WEBHOOK) throw new Error("non DINGTALK_WEBHOOK");
158
103
  let res = new Response();
104
+ if (!envUsed.DINGTALK_WEBHOOK) throw res;
159
105
  for (const url of envUsed.DINGTALK_WEBHOOK.split(",")) res = await fetch(url, {
160
106
  method: "POST",
161
107
  headers: { "Content-Type": "application/json" },
@@ -175,60 +121,46 @@ async function sendding(title, text) {
175
121
  }
176
122
 
177
123
  //#endregion
178
- //#region copilot/bin/utils.ts
179
- const NAME = name.replaceAll("/", "_");
180
- const VERSION = version;
181
- const TEMP = env.CI_PROJECT_DIR + "." + NAME;
182
- const dingding = async (...args) => {
183
- try {
184
- const msg = await (await sendding(...args)).text();
185
- console.log(msg);
186
- } catch (error) {
187
- console.error(error);
188
- }
189
- };
190
- async function deploy() {
191
- if (!env.CI) return;
124
+ //#region review/helper.ts
125
+ const NAME = name$1.replaceAll("/", "_");
126
+ const TEMP = join(env.CI_PROJECT_DIR || "", `../../.${NAME}`);
127
+ const common_review_prompt = `Load code-review skills, then call the mcp tool '${name}-${getHash}' to load code-review commits.
128
+ Then use chinese(中文) to call the mcp tool '${name}-${report}', only main agent can call the tool '${name}-${report}', **prevent** subagent from calling tool '${name}-${report}`;
129
+ async function changelog() {
130
+ const changelog = readFileSync(join(RUNTIME_CWD, "README.md"), "utf8");
131
+ const cachepath = join(TEMP, "CHANGELOG_" + (env.CI_RUNNER_ID ?? env.CI_PROJECT_ID ?? "0"));
132
+ if (changelog === (existsSync(cachepath) ? readFileSync(cachepath, "utf8") : "")) return;
133
+ if (!existsSync(TEMP)) mkdirSync(TEMP, { recursive: true });
134
+ writeFileSync(cachepath, changelog, "utf8");
135
+ console.log("updated CHANGELOG cache.");
136
+ let matched = changelog.match(/CHANGELOG[\s\S]*?(#+[^\n]*\n+([\s\S]*?))(?=#+|$)/)?.[1] ?? "";
137
+ const matcheds = matched.split("\n");
138
+ matcheds.shift();
139
+ matched = matcheds.join("\n").trim();
140
+ if (matched) await dingding("NOTICE", `skills 更新\n\n\n${matched}\n\n\n历史请参考 [线上文档内容.skills文件夹](https://www.npmjs.com/package/@d5render/cli)`);
141
+ }
142
+ function deploySkills(SKILLS_DIR = ".copilot/skills") {
192
143
  const HOME = env.USERPROFILE ?? env.HOME ?? env.HOMEPATH;
193
144
  if (!HOME) throw new Error("cannot find `USERPROFILE` directory");
194
- const changelog = readFileSync(join(RUNTIME_CWD, "CHANGELOG.md"), "utf8");
195
- const cachepath = join(TEMP, "CHANGELOG-" + env.CI_RUNNER_ID || "0");
196
- let cache = "";
197
- if (existsSync(cachepath)) cache = readFileSync(cachepath, "utf8");
198
- else if (!existsSync(TEMP)) mkdirSync(TEMP, { recursive: true });
199
- if (changelog !== cache) {
200
- writeFileSync(cachepath, changelog, "utf8");
201
- console.log("updated CHANGELOG cache.");
202
- await dingding("NOTICE", `code-review/SKILL.md 更新\n\n细节请参考 [线上文档内容.skills](https://www.npmjs.com/package/@d5render/cli?activeTab=code)`);
203
- }
204
- const config = join(HOME, ".copilot/config.json"), dir = join(HOME, ".copilot/skills/code-review");
205
- console.log("deploy...");
206
- if (existsSync(config)) {
207
- rmSync(config);
208
- console.log("removed config cache.");
209
- }
210
- if (existsSync(dir)) rmSync(dir, {
211
- recursive: true,
212
- force: true
213
- });
145
+ const dir = join(HOME, `${SKILLS_DIR}/code-review`);
214
146
  mkdirSync(dir, { recursive: true });
147
+ console.log("deploy code-review skills...");
215
148
  const skillRoot = join(RUNTIME_CWD, ".skills/code-review");
216
- readdirSync(skillRoot).forEach((skill) => copyFileSync(join(skillRoot, skill), join(dir, skill)));
149
+ readdirSync(skillRoot).forEach((file) => copyFileSync(join(skillRoot, file), join(dir, file)));
217
150
  const instructionsRoot = join(RUNTIME_CWD, ".github/instructions");
218
151
  readdirSync(instructionsRoot).forEach((instruction) => copyFileSync(join(instructionsRoot, instruction), join(dir, instruction)));
219
- console.log("to new skill.");
220
152
  }
221
153
  async function need() {
222
154
  if (!env.CI) return true;
223
155
  const { CI_MERGE_REQUEST_IID, CI_COMMIT_SHA } = env;
224
- const file$1 = join(TEMP, "CODEREVIEW");
156
+ const file = join(TEMP, "CODEREVIEW_" + (env.CI_PROJECT_ID ?? "0"));
225
157
  if (CI_MERGE_REQUEST_IID) {
226
- let appended = `${existsSync(file$1) ? readFileSync(file$1, "utf8") : ""}\n${CI_MERGE_REQUEST_IID}`.split("\n");
158
+ let appended = `${existsSync(file) ? readFileSync(file, "utf8") : ""}\n${CI_MERGE_REQUEST_IID}`.split("\n");
227
159
  const max = 1e4;
228
160
  if (appended.length > max) appended = appended.slice(-max);
229
161
  if (!existsSync(TEMP)) mkdirSync(TEMP, { recursive: true });
230
- writeFileSync(file$1, appended.join("\n"), "utf8");
231
- console.log("merge pipeline, recorded IID:", CI_MERGE_REQUEST_IID, "to:", file$1);
162
+ writeFileSync(file, appended.join("\n"), "utf8");
163
+ console.log("merge pipeline, recorded IID:", CI_MERGE_REQUEST_IID, "to:", file);
232
164
  return true;
233
165
  }
234
166
  if (!CI_COMMIT_SHA) return true;
@@ -238,8 +170,8 @@ async function need() {
238
170
  encoding: "utf8"
239
171
  }).toString().match(/See merge request[\s\S]+!(\d+)/);
240
172
  if (!match) return true;
241
- const iid = match[1];
242
- const yes = (existsSync(file$1) ? readFileSync(file$1, "utf8") : "").split("\n").includes(iid);
173
+ const [, iid] = match;
174
+ const yes = (existsSync(file) ? readFileSync(file, "utf8") : "").split("\n").includes(iid);
243
175
  if (yes) {
244
176
  console.warn(`Merge Request !${iid} has been AI reviewed before.`);
245
177
  const mergeURL = visitPipeline(iid).url;
@@ -255,32 +187,135 @@ async function need() {
255
187
  }
256
188
  return !yes;
257
189
  }
190
+ const dingding = async (...args) => {
191
+ try {
192
+ const msg = await (await sendding(...args)).text();
193
+ console.log(msg);
194
+ } catch (error) {
195
+ console.warn(error);
196
+ }
197
+ };
258
198
 
259
199
  //#endregion
260
- //#region copilot/bin/index.ts
261
- codereview().catch(async (error) => {
262
- await dingding("CRITICAL", "CI ERROR: 未知错误,请自行检查日志");
263
- throw error;
264
- });
265
- async function codereview() {
266
- await deploy();
267
- if (!await need()) {
268
- console.log("重复提交,进程跳过");
200
+ //#region review/token.copilot.ts
201
+ const GITHUB_API = "https://api.github.com";
202
+ const COPILOT_HEADERS = {
203
+ "User-Agent": "GitHubCopilotChat/0.35.0",
204
+ "Editor-Version": "vscode/1.107.0",
205
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
206
+ "Copilot-Integration-Id": "vscode-chat"
207
+ };
208
+ function getOAuthToken() {
209
+ const token = env.GITHUB_COPILOT_TOKEN || env.COPILOT_TOKEN || env.GITHUB_TOKEN || env.GH_TOKEN;
210
+ if (!token) throw new Error("未找到 GitHub Token,请设置 GITHUB_COPILOT_TOKEN / GITHUB_TOKEN / GH_TOKEN 环境变量");
211
+ return token;
212
+ }
213
+ /**
214
+ * 将 OAuth token 换成 Copilot session token
215
+ * 新版 OpenCode 官方 OAuth 接入需要此步骤才能访问 /copilot_internal/* API
216
+ */
217
+ async function exchangeForCopilotToken(oauthToken) {
218
+ try {
219
+ const res = await fetch(`${GITHUB_API}/copilot_internal/v2/token`, { headers: {
220
+ Accept: "application/json",
221
+ Authorization: `Bearer ${oauthToken}`,
222
+ ...COPILOT_HEADERS
223
+ } });
224
+ if (!res.ok) return void 0;
225
+ return (await res.json()).token || void 0;
226
+ } catch {
269
227
  return;
270
228
  }
271
- install();
272
- const prompt = `Load skills, then call the mcp tool '${name$1}-${getHash}' to load code-review commits, if the task encounters an error, throw that.
273
- Otherwise, use chinese as default language to call the mcp tool '${name$1}-${report}'`;
274
- const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy || "";
275
- const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy || "";
276
- await logProxyStatus(httpProxy, httpsProxy);
277
- const copilot = spawn("node", [
278
- findCopilopt(),
229
+ }
230
+ async function fetchInternalUsage() {
231
+ const oauthToken = getOAuthToken();
232
+ const directRes = await fetch(`${GITHUB_API}/copilot_internal/user`, { headers: {
233
+ Accept: "application/json",
234
+ Authorization: `token ${oauthToken}`,
235
+ ...COPILOT_HEADERS
236
+ } });
237
+ if (directRes.ok) return directRes.json();
238
+ const copilotToken = await exchangeForCopilotToken(oauthToken);
239
+ if (!copilotToken) throw new Error([
240
+ "Copilot OAuth token 无法访问配额 API。",
241
+ "请创建 fine-grained PAT(https://github.com/settings/tokens?type=beta)",
242
+ "并在 Account permissions 中设置 Plan = Read-only,",
243
+ "然后通过 GITHUB_PAT 环境变量传入。"
244
+ ].join("\n"));
245
+ const res = await fetch(`${GITHUB_API}/copilot_internal/user`, { headers: {
246
+ Accept: "application/json",
247
+ Authorization: `Bearer ${copilotToken}`,
248
+ ...COPILOT_HEADERS
249
+ } });
250
+ if (!res.ok) {
251
+ const text = await res.text();
252
+ throw new Error(`查询 Copilot 内部 API 失败: ${res.status} ${text}`);
253
+ }
254
+ return res.json();
255
+ }
256
+ function formatInternalResult(data) {
257
+ const premium = data.quota_snapshots.premium_interactions;
258
+ return premium.entitlement - premium.remaining;
259
+ }
260
+ /**
261
+ * 查询 Copilot Premium Request 使用量
262
+ *
263
+ * 优先级:
264
+ * 1. 若设置了 GITHUB_PAT(fine-grained PAT)→ 使用 Public Billing API,支持模型明细
265
+ * 2. 否则使用 Internal API(GITHUB_COPILOT_TOKEN OAuth token),返回配额百分比和重置时间
266
+ */
267
+ async function getCopilotUsage(options = {}) {
268
+ try {
269
+ return formatInternalResult(await fetchInternalUsage());
270
+ } catch {
271
+ return "使用量未知";
272
+ }
273
+ }
274
+
275
+ //#endregion
276
+ //#region review/copilot/deploy.ts
277
+ const config = join(dirname(fileURLToPath(import.meta.url)), "copilot-mcp.json");
278
+ const tools = [
279
+ "--additional-mcp-config",
280
+ `"@${config}"`,
281
+ "--model",
282
+ "claude-opus-4.6",
283
+ "--allow-all-paths",
284
+ "--allow-all-tools",
285
+ "--enable-all-github-mcp-tools",
286
+ "--deny-tool",
287
+ "write",
288
+ "--stream",
289
+ "off"
290
+ ];
291
+ async function deploy() {
292
+ if (!env.CI) return;
293
+ await changelog();
294
+ execSync("npm i -g @github/copilot@latest --registry=https://registry.npmmirror.com", { stdio: "inherit" });
295
+ deploySkills();
296
+ const token = await getCopilotUsage();
297
+ env["TOKEN_USAGE"] = String(token);
298
+ writeFileSync(config, JSON.stringify({ mcpServers: { [name]: {
299
+ type: "local",
300
+ command: "node",
301
+ args: [serveFile, `--customizenv=${JSON.stringify(envUsed)}`],
302
+ tools: ["*"]
303
+ } } }, void 0, 2), "utf8");
304
+ }
305
+
306
+ //#endregion
307
+ //#region review/copilot/index.ts
308
+ const bind = "copilot";
309
+ async function cli() {
310
+ await deploy();
311
+ const httpProxy = env.HTTP_PROXY || env.http_proxy || "";
312
+ const httpsProxy = env.HTTPS_PROXY || env.https_proxy || "";
313
+ if (httpProxy) env.HTTP_PROXY = httpProxy;
314
+ if (httpsProxy) env.HTTPS_PROXY = httpsProxy;
315
+ const child = spawn(bind, [
279
316
  ...tools,
280
- "--stream",
281
- "off",
282
317
  "-p",
283
- prompt
318
+ `"${common_review_prompt.replace(/"/g, "\\\"")}"`
284
319
  ], {
285
320
  cwd: env.CI_PROJECT_DIR,
286
321
  stdio: [
@@ -288,169 +323,30 @@ Otherwise, use chinese as default language to call the mcp tool '${name$1}-${rep
288
323
  "pipe",
289
324
  "pipe"
290
325
  ],
291
- ...platform === "win32" && { windowsHide: true },
292
- env: {
293
- ...process.env,
294
- HTTP_PROXY: httpProxy,
295
- HTTPS_PROXY: httpsProxy
296
- }
297
- });
298
- copilot.stdout.on("data", (chunk) => console.log(String(chunk)));
299
- copilot.stderr.on("data", (chunk) => console.error(String(chunk)));
300
- return new Promise((res, rej) => {
301
- copilot.on("close", (code) => res());
326
+ ...platform === "win32" && { shell: true }
302
327
  });
328
+ child.stdout.on("data", (chunk) => console.log(String(chunk)));
329
+ child.stderr.on("data", (chunk) => console.error(String(chunk)));
330
+ return new Promise((res, rej) => child.on("close", (code) => getCopilotUsage().then((res) => console.log("本次Token积累使用量:", res)).catch(() => {}).finally(() => {
331
+ if (code === 0) res();
332
+ else rej(/* @__PURE__ */ new Error(`${bind} exited with code ${code}`));
333
+ })));
303
334
  }
304
- /** 代理排查:环境变量 + 检测代理是否在运行、子进程是否会走代理 */
305
- async function logProxyStatus(httpProxy, httpsProxy) {
306
- const tag = "[proxy]";
307
- console.log(`${tag} -------- 代理排查 --------`);
308
- const proxyKeys = Object.keys(process.env).filter((k) => /proxy/i.test(k));
309
- if (proxyKeys.length > 0) console.log(`${tag} 当前进程里含 proxy 的环境变量名: ${proxyKeys.join(", ")}`);
310
- else console.log(`${tag} 当前进程里没有任何含 proxy 的环境变量(GitLab 未传入或未生效)`);
311
- console.log(`${tag} 当前进程 env: HTTP_PROXY = ${process.env.HTTP_PROXY ?? "(未设置)"}, http_proxy = ${process.env.http_proxy ?? "(未设置)"}`);
312
- console.log(`${tag} 当前进程 env: HTTPS_PROXY = ${process.env.HTTPS_PROXY ?? "(未设置)"}, https_proxy = ${process.env.https_proxy ?? "(未设置)"}`);
313
- console.log(`${tag} 传给子进程: HTTP_PROXY = ${httpProxy || "(空)"}, HTTPS_PROXY = ${httpsProxy || "(空)"}`);
314
- const proxyUrl = httpsProxy || httpProxy;
315
- if (!proxyUrl) {
316
- console.log(`${tag} 结论: 未设置代理,子进程将直连外网,不会经代理`);
317
- await logGitHubReachable(tag, false);
318
- console.log(`${tag} ------------------------`);
319
- return;
320
- }
321
- let host;
322
- let port;
323
- try {
324
- const u = new URL(proxyUrl);
325
- host = u.hostname || "127.0.0.1";
326
- port = u.port ? parseInt(u.port, 10) : 7890;
327
- } catch {
328
- console.log(`${tag} 结论: 代理 URL 解析失败,子进程可能无法正确使用代理: ${proxyUrl}`);
329
- console.log(`${tag} ------------------------`);
330
- return;
331
- }
332
- const reachable = await checkPortReachable(host, port);
333
- if (reachable) {
334
- console.log(`${tag} 代理检测: ${host}:${port} 可连接,代理服务应在运行`);
335
- logPortListener(port, tag);
336
- } else {
337
- console.log(`${tag} 代理检测: ${host}:${port} 无法连接,可能代理未启动或地址/端口错误`);
338
- logPortListener(port, tag);
339
- }
340
- await logGitHubReachable(tag, !!proxyUrl);
341
- if (reachable) console.log(`${tag} 结论: 子进程已继承代理环境变量,请求应能通过代理发出`);
342
- else console.log(`${tag} 结论: 子进程虽有代理变量,但代理不可达,请求可能失败或直连`);
343
- console.log(`${tag} ------------------------`);
344
- }
345
- function checkPortReachable(host, port, timeoutMs = 3e3) {
346
- return new Promise((resolve) => {
347
- const socket = createConnection(port, host, () => {
348
- socket.destroy();
349
- resolve(true);
350
- });
351
- socket.setTimeout(timeoutMs);
352
- socket.on("timeout", () => {
353
- socket.destroy();
354
- resolve(false);
355
- });
356
- socket.on("error", () => resolve(false));
357
- });
358
- }
359
- /** 打印指定端口上的监听进程(如 7890 上是 mihomo) */
360
- function logPortListener(port, tag) {
335
+
336
+ //#endregion
337
+ //#region review/index.ts
338
+ async function codereview() {
361
339
  try {
362
- if (platform === "win32") {
363
- const out = execSync(`netstat -ano | findstr :${port}`, {
364
- encoding: "utf8",
365
- maxBuffer: 65536
366
- });
367
- console.log(`${tag} 端口 ${port} 监听情况 (netstat):\n${out.trim().split("\n").slice(0, 10).join("\n")}`);
368
- return;
369
- }
370
- const lsof = execSync(`lsof -i :${port} 2>/dev/null`, {
371
- encoding: "utf8",
372
- maxBuffer: 65536
373
- }).trim();
374
- if (lsof) {
375
- console.log(`${tag} 端口 ${port} 监听进程 (lsof):\n${lsof}`);
340
+ if (!await need()) {
341
+ console.log("重复提交,进程跳过");
376
342
  return;
377
343
  }
378
- } catch {}
379
- try {
380
- const ss = execSync(`ss -tlnp 2>/dev/null | grep :${port}`, {
381
- encoding: "utf8",
382
- maxBuffer: 65536
383
- }).trim();
384
- if (ss) console.log(`${tag} 端口 ${port} 监听进程 (ss):\n${ss}`);
385
- else console.log(`${tag} 端口 ${port}: 无法获取监听进程 (无 lsof/ss 或无权限)`);
386
- } catch {
387
- console.log(`${tag} 端口 ${port}: 无法获取监听进程 (无 lsof/ss 或无权限)`);
388
- }
389
- }
390
- /** 检测 GitHub 是否可访问(Node 内置 fetch 不走代理,此处为直连可达性) */
391
- async function logGitHubReachable(tag, proxySet) {
392
- const url = "https://api.github.com";
393
- const note = proxySet ? " (当前进程已设代理,但 Node fetch 不经过代理,此处为直连)" : " (直连)";
394
- try {
395
- const ac = new AbortController();
396
- const t = setTimeout(() => ac.abort(), 1e4);
397
- const res = await fetch(url, { signal: ac.signal });
398
- clearTimeout(t);
399
- const ok = res.ok || res.status === 403;
400
- console.log(`${tag} GitHub 连通性${note}: ${ok ? "可调通" : "异常"} status=${res.status}`);
401
- } catch (e) {
402
- const msg = e instanceof Error ? e.message : String(e);
403
- console.log(`${tag} GitHub 连通性${note}: 调不通 - ${msg}`);
404
- }
405
- }
406
- function findCopilopt() {
407
- let copilot = "";
408
- try {
409
- copilot = execSync("npm list @github/copilot -g -p").toString().trim();
410
- } catch {}
411
- if (!copilot) {
412
- const first = platform === "win32" ? win : linux;
413
- const second = platform === "win32" ? linux : win;
414
- copilot = first();
415
- if (!copilot) copilot = second();
416
- if (!copilot) throw new Error("没找到安装的包");
417
- }
418
- const pkg = join(copilot, "package.json");
419
- if (!existsSync(pkg)) throw new Error("安装的包找不到正确版本 " + pkg);
420
- const copilotPackage = JSON.parse(readFileSync(pkg, "utf8"));
421
- const binPath = typeof copilotPackage.bin === "string" ? copilotPackage.bin : copilotPackage.bin?.copilot || copilotPackage.bin?.["@github/copilot"];
422
- if (!binPath) throw new Error("non copilot executable found");
423
- const copilotVersion = copilotPackage.version || "unknown";
424
- const copilotPath = join(copilot, binPath);
425
- console.log(`${NAME} server:
426
- version: ${VERSION}
427
- path: ${serveFile}
428
- copilot:
429
- version: ${copilotVersion}
430
- path: ${copilotPath}`);
431
- return copilotPath;
432
- }
433
- function win() {
434
- const pathEnv = env.PATH || env.Path || "";
435
- const pathSeparator = platform === "win32" ? ";" : ":";
436
- const npm = pathEnv.split(pathSeparator).find((p) => p.includes("npm"));
437
- if (!npm) return "";
438
- const fallbackPath = join(npm, "node_modules", "@github/copilot");
439
- if (existsSync(join(fallbackPath, "package.json"))) return fallbackPath;
440
- return "";
441
- }
442
- function linux() {
443
- let cached = env.NVM_BIN;
444
- if (!cached) {
445
- const pathEnv = env.PATH || env.Path || "";
446
- const pathSeparator = platform === "win32" ? ";" : ":";
447
- const npm = pathEnv.split(pathSeparator).find((p) => p.includes(".nvm"));
448
- if (npm) cached = npm;
344
+ await cli();
345
+ } catch (error) {
346
+ await dingding("CRITICAL", "CI ERROR: 未知错误,请自行检查日志");
347
+ throw error;
449
348
  }
450
- if (!cached) return "";
451
- const fallbackPath = join(cached, "..", "lib", "node_modules", "@github/copilot");
452
- if (existsSync(join(fallbackPath, "package.json"))) return fallbackPath;
453
- return "";
454
349
  }
350
+ if (argv.includes("codereview")) codereview();
455
351
 
456
352
  //#endregion