@dream-craft/canvas-agent 0.6.7 → 0.6.8
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/dist/server/http.js +56 -2
- package/package.json +1 -1
package/dist/server/http.js
CHANGED
|
@@ -10,6 +10,28 @@ import { DEFAULT_PORT, ensureSiteWorkspace, loadConfig, saveConfig, updateSiteWo
|
|
|
10
10
|
import { logger } from "../utils/logger.js";
|
|
11
11
|
import { checkVersions } from "../version-check.js";
|
|
12
12
|
import { SkillStore, SkillStoreError } from "../skills/store.js";
|
|
13
|
+
/** 远程图片转存的体积与耗时上限(防止本地服务被大文件拖垮)。 */
|
|
14
|
+
const REMOTE_IMAGE_MAX_BYTES = 20 * 1024 * 1024;
|
|
15
|
+
const REMOTE_IMAGE_TIMEOUT_MS = 30_000;
|
|
16
|
+
/** 拒绝把转存端点当作访问内网/回环地址的跳板(SSRF 防护)。 */
|
|
17
|
+
function isPrivateHost(hostname) {
|
|
18
|
+
const host = hostname.toLowerCase();
|
|
19
|
+
if (host === "localhost" || host === "::1" || host.endsWith(".localhost") || host.endsWith(".local"))
|
|
20
|
+
return true;
|
|
21
|
+
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host))
|
|
22
|
+
return true;
|
|
23
|
+
const matched = /^172\.(\d{1,3})\./.exec(host);
|
|
24
|
+
if (matched) {
|
|
25
|
+
const second = Number(matched[1]);
|
|
26
|
+
if (second >= 16 && second <= 31)
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (/^0\./.test(host))
|
|
30
|
+
return true;
|
|
31
|
+
if (host.includes(":"))
|
|
32
|
+
return true; // IPv6 字面量(含回环与链路本地)一律拒绝
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
13
35
|
/** 启动仅监听本机的 Canvas Agent HTTP 服务。 */
|
|
14
36
|
export function startHttpServer() {
|
|
15
37
|
const config = loadConfig(true);
|
|
@@ -176,13 +198,45 @@ export function startHttpServer() {
|
|
|
176
198
|
app.post("/agent/local-image", route(async (req, res) => {
|
|
177
199
|
const filePath = String(req.body?.path || "");
|
|
178
200
|
if (!path.isAbsolute(filePath) || !/\.(?:avif|gif|jpe?g|png|webp)$/i.test(filePath))
|
|
179
|
-
return res.status(400).json({ ok: false, error: "图片路径无效" });
|
|
201
|
+
return void res.status(400).json({ ok: false, error: "图片路径无效" });
|
|
180
202
|
const file = await stat(filePath);
|
|
181
203
|
if (!file.isFile())
|
|
182
|
-
return res.status(400).json({ ok: false, error: "图片文件无效" });
|
|
204
|
+
return void res.status(400).json({ ok: false, error: "图片文件无效" });
|
|
183
205
|
res.setHeader("Cache-Control", "no-store");
|
|
184
206
|
res.type(path.extname(filePath)).send(await readFile(filePath));
|
|
185
207
|
}));
|
|
208
|
+
// 远程图片转存:浏览器直连上游图片常被 CORS 拦下,<img> 能显示但取不到字节,产物无法持久化。
|
|
209
|
+
// 这里是本地服务端,取字节无跨域限制——不参与任何模型请求转发,模型请求仍直连上游。
|
|
210
|
+
app.post("/agent/fetch-image", route(async (req, res) => {
|
|
211
|
+
const raw = String(req.body?.url || "").trim();
|
|
212
|
+
if (!/^https?:\/\//i.test(raw))
|
|
213
|
+
return void res.status(400).json({ ok: false, error: "仅支持 http/https 图片地址" });
|
|
214
|
+
const target = new URL(raw);
|
|
215
|
+
if (isPrivateHost(target.hostname))
|
|
216
|
+
return void res.status(400).json({ ok: false, error: "拒绝访问内网地址" });
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
const timer = setTimeout(() => controller.abort(), REMOTE_IMAGE_TIMEOUT_MS);
|
|
219
|
+
try {
|
|
220
|
+
const response = await fetch(target, { signal: controller.signal, redirect: "follow" });
|
|
221
|
+
if (!response.ok)
|
|
222
|
+
return void res.status(502).json({ ok: false, error: `上游返回 ${response.status}` });
|
|
223
|
+
const contentType = response.headers.get("content-type") || "";
|
|
224
|
+
if (!contentType.startsWith("image/"))
|
|
225
|
+
return void res.status(415).json({ ok: false, error: "目标不是图片" });
|
|
226
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
227
|
+
if (buffer.byteLength > REMOTE_IMAGE_MAX_BYTES)
|
|
228
|
+
return void res.status(413).json({ ok: false, error: "图片超过大小上限" });
|
|
229
|
+
res.setHeader("Cache-Control", "no-store");
|
|
230
|
+
res.type(contentType).send(buffer);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
234
|
+
res.status(502).json({ ok: false, error: `图片下载失败:${message}` });
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
}
|
|
239
|
+
}));
|
|
186
240
|
app.post("/api/tools", route(async (req, res) => res.json({ ok: true, result: await session.callTool(req.body?.name, req.body?.input || {}) })));
|
|
187
241
|
app.get("/agent/codex/workspace", (_req, res) => {
|
|
188
242
|
const workspace = ensureSiteWorkspace(config);
|