@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.
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dream-craft/canvas-agent",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",