@trim21/personal-pi-extensions 0.0.358 → 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.358",
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": [
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
  }),