@trim21/personal-pi-extensions 0.0.357 → 0.0.359
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/lib/lsp/client.ts +38 -4
- package/src/lib/lsp/lsp.ts +7 -1
- package/src/web/fetch.ts +21 -6
- package/src/web/index.ts +4 -3
package/package.json
CHANGED
package/src/lib/lsp/client.ts
CHANGED
|
@@ -99,6 +99,7 @@ export interface LspClient {
|
|
|
99
99
|
version: number;
|
|
100
100
|
mode?: "document" | "full";
|
|
101
101
|
after?: number;
|
|
102
|
+
signal?: AbortSignal;
|
|
102
103
|
}): Promise<void>;
|
|
103
104
|
shutdown(): Promise<void>;
|
|
104
105
|
}
|
|
@@ -168,6 +169,11 @@ async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
173
|
+
|
|
174
|
+
/** pull 诊断失败后的重试间隔。 */
|
|
175
|
+
const PULL_RETRY_INTERVAL_MS = 100;
|
|
176
|
+
|
|
171
177
|
function stopProcess(process: LspServerHandle["process"]): Promise<void> {
|
|
172
178
|
if (process.exitCode !== null) return Promise.resolve();
|
|
173
179
|
try {
|
|
@@ -201,6 +207,14 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
201
207
|
new StreamMessageWriter(input.server.process.stdin),
|
|
202
208
|
);
|
|
203
209
|
input.server.process.stderr?.resume();
|
|
210
|
+
/** 连接或服务器进程已关闭;pull 重试循环以此终止,避免无界等待。 */
|
|
211
|
+
let connectionClosed = false;
|
|
212
|
+
input.server.process.once("exit", () => {
|
|
213
|
+
connectionClosed = true;
|
|
214
|
+
});
|
|
215
|
+
connection.onDispose(() => {
|
|
216
|
+
connectionClosed = true;
|
|
217
|
+
});
|
|
204
218
|
|
|
205
219
|
// ── 连接状态 ────────────────────────────────────────────────────────────────
|
|
206
220
|
|
|
@@ -589,8 +603,20 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
589
603
|
path: string;
|
|
590
604
|
version: number;
|
|
591
605
|
after?: number;
|
|
606
|
+
signal?: AbortSignal;
|
|
592
607
|
}): Promise<void> {
|
|
593
608
|
const startedAt = request.after ?? Date.now();
|
|
609
|
+
// 支持 pull 的服务器:push 已被忽略,pull 是唯一通道。无窗口截断——
|
|
610
|
+
// 一直重试直到拿到当前文档结果(matched),确保诊断不因服务器重算慢而丢失。
|
|
611
|
+
if (supportsPullDiagnostics()) {
|
|
612
|
+
while (!connectionClosed && !request.signal?.aborted) {
|
|
613
|
+
const result = await requestDocumentDiagnostics(request.path);
|
|
614
|
+
if (result.matched) return;
|
|
615
|
+
await sleep(PULL_RETRY_INTERVAL_MS);
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
594
620
|
const pushWait = waitForFreshPush({
|
|
595
621
|
path: request.path,
|
|
596
622
|
version: request.version,
|
|
@@ -601,9 +627,6 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
601
627
|
while (Date.now() - startedAt < diagnosticsDocumentWaitTimeoutMs) {
|
|
602
628
|
const result = await requestDocumentDiagnostics(request.path);
|
|
603
629
|
if (result.matched) return;
|
|
604
|
-
// 支持 pull 的服务器:pull 未匹配(失败/超时)即返回,不依赖 push 兜底
|
|
605
|
-
// (push 已被忽略);纯 push 服务器继续走下面的 push 等待。
|
|
606
|
-
if (supportsPullDiagnostics()) return;
|
|
607
630
|
const remaining = diagnosticsDocumentWaitTimeoutMs - (Date.now() - startedAt);
|
|
608
631
|
if (remaining <= 0) return;
|
|
609
632
|
const next = await Promise.race([
|
|
@@ -620,8 +643,18 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
620
643
|
path: string;
|
|
621
644
|
version: number;
|
|
622
645
|
after?: number;
|
|
646
|
+
signal?: AbortSignal;
|
|
623
647
|
}): Promise<void> {
|
|
624
648
|
const startedAt = request.after ?? Date.now();
|
|
649
|
+
if (supportsPullDiagnostics()) {
|
|
650
|
+
while (!connectionClosed && !request.signal?.aborted) {
|
|
651
|
+
const result = await requestFullDiagnostics(request.path);
|
|
652
|
+
if (result.handled || result.matched) return;
|
|
653
|
+
await sleep(PULL_RETRY_INTERVAL_MS);
|
|
654
|
+
}
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
625
658
|
const pushWait = waitForFreshPush({
|
|
626
659
|
path: request.path,
|
|
627
660
|
version: request.version,
|
|
@@ -632,7 +665,6 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
632
665
|
while (Date.now() - startedAt < diagnosticsFullWaitTimeoutMs) {
|
|
633
666
|
const result = await requestFullDiagnostics(request.path);
|
|
634
667
|
if (result.handled || result.matched) return;
|
|
635
|
-
if (supportsPullDiagnostics()) return;
|
|
636
668
|
const remaining = diagnosticsFullWaitTimeoutMs - (Date.now() - startedAt);
|
|
637
669
|
if (remaining <= 0) return;
|
|
638
670
|
const next = await Promise.race([
|
|
@@ -724,6 +756,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
724
756
|
path: normalizedPath,
|
|
725
757
|
version: request.version,
|
|
726
758
|
after: request.after,
|
|
759
|
+
signal: request.signal,
|
|
727
760
|
});
|
|
728
761
|
return;
|
|
729
762
|
}
|
|
@@ -731,6 +764,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
731
764
|
path: normalizedPath,
|
|
732
765
|
version: request.version,
|
|
733
766
|
after: request.after,
|
|
767
|
+
signal: request.signal,
|
|
734
768
|
});
|
|
735
769
|
},
|
|
736
770
|
async shutdown() {
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -348,7 +348,13 @@ export function createLspService(
|
|
|
348
348
|
const version = await client.notify.open({ path: file });
|
|
349
349
|
if (!diagnostics) return;
|
|
350
350
|
await abortable(
|
|
351
|
-
client.waitForDiagnostics({
|
|
351
|
+
client.waitForDiagnostics({
|
|
352
|
+
path: file,
|
|
353
|
+
version,
|
|
354
|
+
mode: diagnostics,
|
|
355
|
+
after,
|
|
356
|
+
signal: options?.signal,
|
|
357
|
+
}),
|
|
352
358
|
options?.signal,
|
|
353
359
|
);
|
|
354
360
|
}),
|
package/src/web/fetch.ts
CHANGED
|
@@ -124,7 +124,8 @@ export async function fetchPage(url: string, signal?: AbortSignal): Promise<Fetc
|
|
|
124
124
|
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
125
125
|
}
|
|
126
126
|
const contentType = response.headers.get("content-type") ?? "";
|
|
127
|
-
|
|
127
|
+
const category = classifyContentType(contentType);
|
|
128
|
+
if (category === null) {
|
|
128
129
|
throw new Error(`不支持的内容类型: ${contentType || "unknown"}`);
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -133,22 +134,36 @@ export async function fetchPage(url: string, signal?: AbortSignal): Promise<Fetc
|
|
|
133
134
|
throw new Error(`页面过大 (${declaredLength} bytes),上限 ${MAX_BYTES}`);
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
let
|
|
137
|
+
let body = "";
|
|
137
138
|
if (response.body) {
|
|
138
139
|
const reader = response.body.getReader();
|
|
139
140
|
const decoder = new TextDecoder();
|
|
140
141
|
for (;;) {
|
|
141
142
|
const chunk = (await reader.read()) as { done: boolean; value: Uint8Array };
|
|
142
143
|
if (chunk.done) break;
|
|
143
|
-
|
|
144
|
-
if (Buffer.byteLength(
|
|
144
|
+
body += decoder.decode(chunk.value, { stream: true });
|
|
145
|
+
if (Buffer.byteLength(body, "utf8") > MAX_BYTES) {
|
|
145
146
|
throw new Error(`页面过大,上限 ${MAX_BYTES} bytes`);
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
+
body += decoder.decode();
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
|
|
152
|
+
if (category === "html") {
|
|
153
|
+
return extractMarkdown(body, response.url);
|
|
154
|
+
}
|
|
155
|
+
// JSON / XML / text/*:原样返回
|
|
156
|
+
return { url: response.url, title: response.url, markdown: body.trim() };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 按 mime 主体分类响应;html 走 readability,其余文本类原样返回 */
|
|
160
|
+
function classifyContentType(contentType: string): "html" | "text" | null {
|
|
161
|
+
const mime = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
162
|
+
if (mime === "text/html" || mime === "application/xhtml+xml") return "html";
|
|
163
|
+
if (mime.startsWith("text/")) return "text";
|
|
164
|
+
if (mime === "application/json" || mime.endsWith("+json")) return "text";
|
|
165
|
+
if (mime === "application/xml" || mime.endsWith("+xml")) return "text";
|
|
166
|
+
return null;
|
|
152
167
|
}
|
|
153
168
|
|
|
154
169
|
/** 提取用到的 document 最小接口(linkedom 类型是 any,显式标注避免 unsafe) */
|
package/src/web/index.ts
CHANGED
|
@@ -140,9 +140,10 @@ export default function webTools(pi: ExtensionAPI) {
|
|
|
140
140
|
name: "web_fetch",
|
|
141
141
|
label: "Web Fetch",
|
|
142
142
|
description:
|
|
143
|
-
"Fetch a URL and
|
|
144
|
-
"
|
|
145
|
-
|
|
143
|
+
"Fetch a URL and return its content as markdown (HTML pages) or raw text " +
|
|
144
|
+
"(JSON/XML/plain-text API responses). SSRF-protected: refuses private/internal " +
|
|
145
|
+
"addresses.",
|
|
146
|
+
promptSnippet: "Fetch a web page or API response",
|
|
146
147
|
parameters: Type.Object({
|
|
147
148
|
url: Type.String({ description: "The URL to fetch" }),
|
|
148
149
|
}),
|