@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.357",
3
+ "version": "0.0.359",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -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() {
@@ -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({ path: file, version, mode: diagnostics, after }),
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
- if (!contentType.includes("text/html") && !contentType.includes("text/plain")) {
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 html = "";
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
- html += decoder.decode(chunk.value, { stream: true });
144
- if (Buffer.byteLength(html, "utf8") > MAX_BYTES) {
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
- html += decoder.decode();
149
+ body += decoder.decode();
149
150
  }
150
151
 
151
- return extractMarkdown(html, response.url);
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 extract the main content as markdown. SSRF-protected: refuses " +
144
- "private/internal addresses. Only http/https HTML pages are supported.",
145
- promptSnippet: "Fetch a web page and extract its content",
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
  }),