@trim21/personal-pi-extensions 0.1.634 → 0.1.636
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/package.json +1 -1
- package/src/gh/base.ts +14 -141
- package/src/gh/tools/watch-run.ts +2 -1
- package/src/lib/egress.ts +29 -0
- package/src/lib/gh-process.ts +151 -0
- package/src/lib/github.ts +4 -35
- package/src/web/fetch.ts +5 -7
- package/src/web/search.ts +2 -1
package/package.json
CHANGED
package/src/gh/base.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared infrastructure of the GitHub read-only tools:
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Shared infrastructure of the GitHub read-only tools: result shaping, the
|
|
3
|
+
* octokit-backed client, and the checks-wait pipeline (running a `gh` process
|
|
4
|
+
* itself lives in `lib/gh-process.ts`, which also owns the proxy env shared by
|
|
5
|
+
* the subprocess and octokit). Each tool lives in `tools/<tool-name>.ts`;
|
|
6
|
+
* anything used by more than one tool belongs here.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import { spawn } from "node:child_process";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { delimiter, join } from "node:path";
|
|
11
11
|
|
|
12
12
|
import { Type } from "typebox";
|
|
13
13
|
import { Value } from "typebox/value";
|
|
14
14
|
|
|
15
|
+
import { egress } from "../lib/egress.js";
|
|
16
|
+
import { type GhResult, runGh } from "../lib/gh-process.js";
|
|
15
17
|
import {
|
|
16
18
|
type ActionJob,
|
|
17
19
|
type CheckRun,
|
|
@@ -19,17 +21,11 @@ import {
|
|
|
19
21
|
createGithubChecks,
|
|
20
22
|
createGithubSearch,
|
|
21
23
|
type GithubChecksClient,
|
|
24
|
+
type GithubClientOptions,
|
|
22
25
|
type GithubSearch,
|
|
23
26
|
renderHits,
|
|
24
27
|
} from "../lib/github.js";
|
|
25
28
|
import { type ToolPendant } from "../lib/pendant.js";
|
|
26
|
-
import { createHttpProxy } from "../lib/proxy.js";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 代理配置(~/.pi/agent/proxy.json,回退到 HTTP(S)_PROXY 环境变量)在本模块内共享:
|
|
30
|
-
* `gh` 子进程与 octokit 请求都从这里取,配置只在首次使用时读一次。
|
|
31
|
-
*/
|
|
32
|
-
export const httpProxy = createHttpProxy();
|
|
33
29
|
|
|
34
30
|
/** A tool result: what the model sees plus the structured details payload. */
|
|
35
31
|
export interface ToolResult {
|
|
@@ -46,18 +42,6 @@ export interface ToolCall<Params> {
|
|
|
46
42
|
onUpdate?: (update: ToolResult) => void;
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
export interface GhResult {
|
|
50
|
-
stdout: string;
|
|
51
|
-
stderr: string;
|
|
52
|
-
code: number;
|
|
53
|
-
killed: boolean;
|
|
54
|
-
combined: string;
|
|
55
|
-
/** Why the process was killed, when `killed` is true. */
|
|
56
|
-
reason?: "timeout" | "abort";
|
|
57
|
-
/** When the process could not be started at all (e.g. `gh` not found in PATH). */
|
|
58
|
-
spawnError?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
45
|
/**
|
|
62
46
|
* Check whether the `gh` CLI is on the system, scanning PATH like
|
|
63
47
|
* `findDefaultBwrap`. The extension registers no tools when `gh` is missing, so
|
|
@@ -78,120 +62,6 @@ export function isGhAvailable(): boolean {
|
|
|
78
62
|
return false;
|
|
79
63
|
}
|
|
80
64
|
|
|
81
|
-
export function runGh(
|
|
82
|
-
args: string[],
|
|
83
|
-
ctx: {
|
|
84
|
-
cwd?: string;
|
|
85
|
-
signal?: AbortSignal;
|
|
86
|
-
timeout?: number;
|
|
87
|
-
/** 追加到子进程环境变量(覆盖进程环境与代理配置),供测试或调用方定制。 */
|
|
88
|
-
env?: NodeJS.ProcessEnv;
|
|
89
|
-
},
|
|
90
|
-
): Promise<GhResult> {
|
|
91
|
-
return new Promise((resolve) => {
|
|
92
|
-
const proc = spawn("gh", args, {
|
|
93
|
-
cwd: ctx.cwd,
|
|
94
|
-
shell: false,
|
|
95
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
96
|
-
// gh 是 Go 程序,只认环境变量形式的代理配置;ctx.env 最后合并,调用方可覆盖。
|
|
97
|
-
env: { ...process.env, ...httpProxy.env, ...ctx.env, GH_PAGER: "cat" },
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
let stdout = "";
|
|
101
|
-
let stderr = "";
|
|
102
|
-
const combined: string[] = [];
|
|
103
|
-
let killed = false;
|
|
104
|
-
let killReason: "timeout" | "abort" | undefined;
|
|
105
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
106
|
-
let onAbort: (() => void) | undefined;
|
|
107
|
-
|
|
108
|
-
const killProcess = (reason: "timeout" | "abort") => {
|
|
109
|
-
if (killed) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
killed = true;
|
|
114
|
-
killReason = reason;
|
|
115
|
-
proc.kill("SIGTERM");
|
|
116
|
-
setTimeout(() => {
|
|
117
|
-
if (!proc.killed) {
|
|
118
|
-
proc.kill("SIGKILL");
|
|
119
|
-
}
|
|
120
|
-
}, 5000);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
if (ctx.signal) {
|
|
124
|
-
onAbort = () => killProcess("abort");
|
|
125
|
-
if (ctx.signal.aborted) {
|
|
126
|
-
killProcess("abort");
|
|
127
|
-
} else {
|
|
128
|
-
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Default timeout: 10 minutes. Long operations like downloading a CI job's
|
|
133
|
-
// full log routinely take well over 30s, so a short default would kill them
|
|
134
|
-
// mid-transfer; combined with `code ?? 0` that would silently cache a
|
|
135
|
-
// truncated log as success. A killed process must never look successful.
|
|
136
|
-
const timeout = ctx.timeout ?? 600_000;
|
|
137
|
-
if (timeout > 0) {
|
|
138
|
-
timeoutId = setTimeout(() => killProcess("timeout"), timeout);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
proc.stdout.on("data", (data: Buffer) => {
|
|
142
|
-
const text = data.toString();
|
|
143
|
-
stdout += text;
|
|
144
|
-
combined.push(text);
|
|
145
|
-
});
|
|
146
|
-
proc.stderr.on("data", (data: Buffer) => {
|
|
147
|
-
const text = data.toString();
|
|
148
|
-
stderr += text;
|
|
149
|
-
combined.push(text);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
proc.on("close", (code) => {
|
|
153
|
-
if (timeoutId) {
|
|
154
|
-
clearTimeout(timeoutId);
|
|
155
|
-
}
|
|
156
|
-
if (onAbort && ctx.signal) {
|
|
157
|
-
ctx.signal.removeEventListener("abort", onAbort);
|
|
158
|
-
}
|
|
159
|
-
resolve({
|
|
160
|
-
stdout,
|
|
161
|
-
stderr,
|
|
162
|
-
// When killed by a signal the close event's code is null — including
|
|
163
|
-
// kills we did not initiate. Report failure instead of pretending it
|
|
164
|
-
// succeeded. -1 is a sentinel for "did not exit normally" — distinct
|
|
165
|
-
// from a real gh failure exit code (1), which is always in 0-255.
|
|
166
|
-
code: code ?? -1,
|
|
167
|
-
killed,
|
|
168
|
-
combined: combined.join(""),
|
|
169
|
-
reason: killReason,
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
proc.on("error", (err: Error) => {
|
|
174
|
-
if (timeoutId) {
|
|
175
|
-
clearTimeout(timeoutId);
|
|
176
|
-
}
|
|
177
|
-
if (onAbort && ctx.signal) {
|
|
178
|
-
ctx.signal.removeEventListener("abort", onAbort);
|
|
179
|
-
}
|
|
180
|
-
// spawn 失败(如 gh 不在 PATH → ENOENT、cwd 不存在)时进程从未启动,
|
|
181
|
-
// 没有任何 stdout/stderr;把底层错误带上,否则会退化成无信息的 "exit code 1"。
|
|
182
|
-
resolve({
|
|
183
|
-
stdout,
|
|
184
|
-
stderr,
|
|
185
|
-
code: 1,
|
|
186
|
-
killed,
|
|
187
|
-
combined: combined.join(""),
|
|
188
|
-
reason: killReason,
|
|
189
|
-
spawnError: err.message,
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
65
|
/**
|
|
196
66
|
* Error thrown by `ghExec` when the `gh` invocation exits non-zero.
|
|
197
67
|
* The message carries the toolcall input (JSON) wrapped in `<input>` markers,
|
|
@@ -474,10 +344,13 @@ export class GhClient {
|
|
|
474
344
|
readonly search: GithubSearch;
|
|
475
345
|
readonly checks: GithubChecksClient;
|
|
476
346
|
|
|
477
|
-
constructor(
|
|
347
|
+
constructor(
|
|
348
|
+
fetchImpl: typeof globalThis.fetch = egress.fetch,
|
|
349
|
+
options: Pick<GithubClientOptions, "token"> = {},
|
|
350
|
+
) {
|
|
478
351
|
this.fetch = fetchImpl;
|
|
479
|
-
this.search = createGithubSearch({ fetch: fetchImpl });
|
|
480
|
-
this.checks = createGithubChecks({ fetch: fetchImpl });
|
|
352
|
+
this.search = createGithubSearch({ fetch: fetchImpl, ...options });
|
|
353
|
+
this.checks = createGithubChecks({ fetch: fetchImpl, ...options });
|
|
481
354
|
}
|
|
482
355
|
}
|
|
483
356
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { runGh } from "../../lib/gh-process.js";
|
|
5
|
+
import { type GhClient, repoArgs, subtitlePendant } from "../base.js";
|
|
5
6
|
|
|
6
7
|
export function addWatchRunTool(_gh: GhClient, pi: ExtensionAPI) {
|
|
7
8
|
pi.registerTool({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 出网入口:代理配置的解析、dispatcher 与 NO_PROXY 分流仍在 `src/lib/proxy.ts`(代理
|
|
3
|
+
* 适配器),本模块只把它收敛成一处,让所有出网调用方从同一处取 `fetch` 与子进程代理
|
|
4
|
+
* 环境变量,并保持「配置写错即加载失败」的行为——不静默直连。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHttpProxy, type HttpProxySettings } from "./proxy.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 出网的单一入口:需要的调用方从这里取请求用的 fetch 与子进程代理环境变量,
|
|
11
|
+
* 不必各自判断「该用代理 fetch 还是全局 fetch」。
|
|
12
|
+
*/
|
|
13
|
+
export interface Egress {
|
|
14
|
+
/** 生效的代理设置,供诊断与展示。 */
|
|
15
|
+
readonly settings: HttpProxySettings;
|
|
16
|
+
/** 请求用的 fetch:undici 的 fetch,配置了代理时自动走代理(NO_PROXY 由 dispatcher 处理)。 */
|
|
17
|
+
readonly fetch: typeof globalThis.fetch;
|
|
18
|
+
/** 要注入子进程的代理环境变量;未配置代理时为空对象。 */
|
|
19
|
+
readonly env: NodeJS.ProcessEnv;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 组装出网层(读一次代理配置;配置写错直接抛)。 */
|
|
23
|
+
export function createEgress(): Egress {
|
|
24
|
+
const proxy = createHttpProxy();
|
|
25
|
+
return { settings: proxy.settings, fetch: proxy.fetch, env: proxy.env };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 共享出网实例:进程内所有出网调用方共用(代理配置在扩展加载时读一次)。 */
|
|
29
|
+
export const egress: Egress = createEgress();
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gh` subprocess adapter: start a `gh` process, collect its output, apply the
|
|
3
|
+
* shared timeout / kill semantics, and hand it the proxy env from the shared
|
|
4
|
+
* egress layer. It lives in `lib/` so both the gh tool layer (`src/gh/`) and
|
|
5
|
+
* the octokit clients (`lib/github.ts`) can depend on it without a cycle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
import { egress } from "./egress.js";
|
|
11
|
+
|
|
12
|
+
export interface GhResult {
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
code: number;
|
|
16
|
+
killed: boolean;
|
|
17
|
+
combined: string;
|
|
18
|
+
/** Why the process was killed, when `killed` is true. */
|
|
19
|
+
reason?: "timeout" | "abort";
|
|
20
|
+
/** When the process could not be started at all (e.g. `gh` not found in PATH). */
|
|
21
|
+
spawnError?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GhRunContext {
|
|
25
|
+
cwd?: string;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
timeout?: number;
|
|
28
|
+
/** 追加到子进程环境变量(覆盖进程环境与代理配置),供测试或调用方定制。 */
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function runGh(args: string[], ctx: GhRunContext): Promise<GhResult> {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const proc = spawn("gh", args, {
|
|
35
|
+
cwd: ctx.cwd,
|
|
36
|
+
shell: false,
|
|
37
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
+
// gh 是 Go 程序,只认环境变量形式的代理配置;ctx.env 最后合并,调用方可覆盖。
|
|
39
|
+
env: { ...process.env, ...egress.env, ...ctx.env, GH_PAGER: "cat" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
let stdout = "";
|
|
43
|
+
let stderr = "";
|
|
44
|
+
const combined: string[] = [];
|
|
45
|
+
let killed = false;
|
|
46
|
+
let killReason: "timeout" | "abort" | undefined;
|
|
47
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
48
|
+
let onAbort: (() => void) | undefined;
|
|
49
|
+
|
|
50
|
+
const killProcess = (reason: "timeout" | "abort") => {
|
|
51
|
+
if (killed) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
killed = true;
|
|
56
|
+
killReason = reason;
|
|
57
|
+
proc.kill("SIGTERM");
|
|
58
|
+
setTimeout(() => {
|
|
59
|
+
if (!proc.killed) {
|
|
60
|
+
proc.kill("SIGKILL");
|
|
61
|
+
}
|
|
62
|
+
}, 5000);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (ctx.signal) {
|
|
66
|
+
onAbort = () => killProcess("abort");
|
|
67
|
+
if (ctx.signal.aborted) {
|
|
68
|
+
killProcess("abort");
|
|
69
|
+
} else {
|
|
70
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Default timeout: 10 minutes. Long operations like downloading a CI job's
|
|
75
|
+
// full log routinely take well over 30s, so a short default would kill them
|
|
76
|
+
// mid-transfer; combined with `code ?? 0` that would silently cache a
|
|
77
|
+
// truncated log as success. A killed process must never look successful.
|
|
78
|
+
const timeout = ctx.timeout ?? 600_000;
|
|
79
|
+
if (timeout > 0) {
|
|
80
|
+
timeoutId = setTimeout(() => killProcess("timeout"), timeout);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
84
|
+
const text = data.toString();
|
|
85
|
+
stdout += text;
|
|
86
|
+
combined.push(text);
|
|
87
|
+
});
|
|
88
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
89
|
+
const text = data.toString();
|
|
90
|
+
stderr += text;
|
|
91
|
+
combined.push(text);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
proc.on("close", (code) => {
|
|
95
|
+
if (timeoutId) {
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
}
|
|
98
|
+
if (onAbort && ctx.signal) {
|
|
99
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
100
|
+
}
|
|
101
|
+
resolve({
|
|
102
|
+
stdout,
|
|
103
|
+
stderr,
|
|
104
|
+
// When killed by a signal the close event's code is null — including
|
|
105
|
+
// kills we did not initiate. Report failure instead of pretending it
|
|
106
|
+
// succeeded. -1 is a sentinel for "did not exit normally" — distinct
|
|
107
|
+
// from a real gh failure exit code (1), which is always in 0-255.
|
|
108
|
+
code: code ?? -1,
|
|
109
|
+
killed,
|
|
110
|
+
combined: combined.join(""),
|
|
111
|
+
reason: killReason,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
proc.on("error", (err: Error) => {
|
|
116
|
+
if (timeoutId) {
|
|
117
|
+
clearTimeout(timeoutId);
|
|
118
|
+
}
|
|
119
|
+
if (onAbort && ctx.signal) {
|
|
120
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
121
|
+
}
|
|
122
|
+
// spawn 失败(如 gh 不在 PATH → ENOENT、cwd 不存在)时进程从未启动,
|
|
123
|
+
// 没有任何 stdout/stderr;把底层错误带上,否则会退化成无信息的 "exit code 1"。
|
|
124
|
+
resolve({
|
|
125
|
+
stdout,
|
|
126
|
+
stderr,
|
|
127
|
+
code: 1,
|
|
128
|
+
killed,
|
|
129
|
+
combined: combined.join(""),
|
|
130
|
+
reason: killReason,
|
|
131
|
+
spawnError: err.message,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Read the logged-in token from the system `gh` CLI (`gh auth token`). */
|
|
138
|
+
export async function ghAuthToken(): Promise<string> {
|
|
139
|
+
const result = await runGh(["auth", "token"], { timeout: 10_000 });
|
|
140
|
+
if (result.spawnError) {
|
|
141
|
+
throw new Error(`failed to start gh: ${result.spawnError}`);
|
|
142
|
+
}
|
|
143
|
+
const token = result.stdout.trim();
|
|
144
|
+
if (token && result.code === 0) {
|
|
145
|
+
return token;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(
|
|
148
|
+
result.stderr.trim() ||
|
|
149
|
+
`gh auth token exited with code ${result.code} — run "gh auth login" first`,
|
|
150
|
+
);
|
|
151
|
+
}
|
package/src/lib/github.ts
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
* rendered state is derived from `pull_request.merged_at`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { spawn } from "node:child_process";
|
|
12
|
-
|
|
13
11
|
import { Octokit } from "octokit";
|
|
14
12
|
import { type Static, Type } from "typebox";
|
|
15
13
|
|
|
14
|
+
import { ghAuthToken } from "./gh-process.js";
|
|
16
15
|
import { parseWithSchema } from "./parse-with-schema.js";
|
|
17
16
|
|
|
18
17
|
export type SearchKind = "issue" | "pr";
|
|
@@ -218,38 +217,6 @@ function normalize(raw: SearchItem): SearchHit {
|
|
|
218
217
|
};
|
|
219
218
|
}
|
|
220
219
|
|
|
221
|
-
function ghAuthToken(): Promise<string> {
|
|
222
|
-
return new Promise((resolve, reject) => {
|
|
223
|
-
const proc = spawn("gh", ["auth", "token"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
224
|
-
let stdout = "";
|
|
225
|
-
let stderr = "";
|
|
226
|
-
const timer = setTimeout(() => proc.kill("SIGTERM"), 10_000);
|
|
227
|
-
proc.stdout.on("data", (d: Buffer) => {
|
|
228
|
-
stdout += String(d);
|
|
229
|
-
});
|
|
230
|
-
proc.stderr.on("data", (d: Buffer) => {
|
|
231
|
-
stderr += String(d);
|
|
232
|
-
});
|
|
233
|
-
proc.on("error", (err) => {
|
|
234
|
-
clearTimeout(timer);
|
|
235
|
-
reject(new Error(`failed to start gh: ${err.message}`));
|
|
236
|
-
});
|
|
237
|
-
proc.on("close", (code) => {
|
|
238
|
-
clearTimeout(timer);
|
|
239
|
-
const token = stdout.trim();
|
|
240
|
-
if (code === 0 && token) {
|
|
241
|
-
resolve(token);
|
|
242
|
-
} else {
|
|
243
|
-
reject(
|
|
244
|
-
new Error(
|
|
245
|
-
stderr.trim() || `gh auth token exited with code ${code} — run "gh auth login" first`,
|
|
246
|
-
),
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
220
|
/**
|
|
254
221
|
* Error thrown when the GitHub search API rejects the request. Carries the
|
|
255
222
|
* original toolcall params so the model can see the exact input.
|
|
@@ -291,6 +258,8 @@ export interface GithubClientOptions {
|
|
|
291
258
|
* proxy dispatcher attached. Defaults to the global fetch.
|
|
292
259
|
*/
|
|
293
260
|
fetch?: typeof globalThis.fetch;
|
|
261
|
+
/** gh token provider;缺省读系统 `gh auth token`(见 lib/gh-process.ts)。 */
|
|
262
|
+
token?: () => Promise<string>;
|
|
294
263
|
}
|
|
295
264
|
|
|
296
265
|
/**
|
|
@@ -304,7 +273,7 @@ function createGithubApi(options: GithubClientOptions = {}): GithubApi {
|
|
|
304
273
|
|
|
305
274
|
async function getClient(): Promise<Octokit> {
|
|
306
275
|
client ??= new Octokit({
|
|
307
|
-
auth: await ghAuthToken(),
|
|
276
|
+
auth: await (options.token ?? ghAuthToken)(),
|
|
308
277
|
...(options.fetch && { request: { fetch: options.fetch } }),
|
|
309
278
|
});
|
|
310
279
|
return client;
|
package/src/web/fetch.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* SSRF 防护:DNS 预解析 + 拒绝私有/保留地址 + 每跳重定向重新校验,
|
|
5
5
|
* 防止把 agent 变成内网探测口。正文提取用 readability 主内容算法。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* 出网走共享出网层 `src/lib/egress.ts`(代理配置 ~/.pi/agent/proxy.json,回退 HTTPS_PROXY
|
|
8
|
+
* 等环境变量):Node 的全局 fetch 不认代理环境变量,GitHub 的用户附件、release 资产这类
|
|
9
|
+
* 只在代理可达的 host 上,必须从这里挂出去,否则沙箱内一律 fetch failed。
|
|
10
10
|
*
|
|
11
11
|
* 本文件是独立扩展入口(见 package.json 的 pi.extensions),可在配置里单独禁用。
|
|
12
12
|
*/
|
|
@@ -21,13 +21,11 @@ import { parseHTML } from "linkedom";
|
|
|
21
21
|
import TurndownService from "turndown";
|
|
22
22
|
import { Type } from "typebox";
|
|
23
23
|
|
|
24
|
+
import { egress } from "../lib/egress.js";
|
|
24
25
|
import { resolvePathArg } from "../lib/path.js";
|
|
25
|
-
import { createHttpProxy } from "../lib/proxy.js";
|
|
26
26
|
import { createRequestPolicy } from "../lib/request-policy.js";
|
|
27
27
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
28
28
|
|
|
29
|
-
const httpProxy = createHttpProxy();
|
|
30
|
-
|
|
31
29
|
const MAX_REDIRECTS = 5;
|
|
32
30
|
const TIMEOUT_MS = 30_000;
|
|
33
31
|
const MAX_BYTES = 5 * 1024 * 1024;
|
|
@@ -143,7 +141,7 @@ interface SavedFile {
|
|
|
143
141
|
async function fetchWithRedirects(
|
|
144
142
|
url: URL,
|
|
145
143
|
signal: AbortSignal | undefined,
|
|
146
|
-
fetchFn: typeof fetch =
|
|
144
|
+
fetchFn: typeof fetch = egress.fetch,
|
|
147
145
|
): Promise<{ response: Response; url: URL }> {
|
|
148
146
|
let current = url;
|
|
149
147
|
for (let redirects = 0; ; redirects++) {
|
package/src/web/search.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
import { Value } from "typebox/value";
|
|
13
13
|
|
|
14
|
+
import { egress } from "../lib/egress.js";
|
|
14
15
|
import { loadSearch1ApiKey } from "./config.js";
|
|
15
16
|
|
|
16
17
|
const SEARCH_URL = "https://api.search1api.com/search";
|
|
@@ -150,7 +151,7 @@ export async function searchWeb(
|
|
|
150
151
|
apiKey: string,
|
|
151
152
|
options: SearchOptions = {},
|
|
152
153
|
): Promise<SearchResult> {
|
|
153
|
-
const response = await fetch(SEARCH_URL, {
|
|
154
|
+
const response = await egress.fetch(SEARCH_URL, {
|
|
154
155
|
method: "POST",
|
|
155
156
|
headers: {
|
|
156
157
|
Authorization: `Bearer ${apiKey}`,
|