@xiaohhhh1/canvas-agent 0.4.37 → 0.4.38

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.
@@ -1,5 +1,5 @@
1
1
  import type { AgentAttachment } from "../agent/types.js";
2
- import type { FastMossIntegration } from "../integrations/fastmoss.js";
2
+ import { type FastMossIntegration } from "../integrations/fastmoss.js";
3
3
  export type LocalVideoLearningSource = {
4
4
  id?: string;
5
5
  sourceUrl?: string;
@@ -4,6 +4,7 @@ import { mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promise
4
4
  import { createRequire } from "node:module";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
+ import { videoEvidenceTimestamps } from "../integrations/fastmoss.js";
7
8
  const ANALYSIS_TIMEOUT_MS = 10 * 60_000;
8
9
  const TRANSCRIPT_LANGUAGES = "en.*,es.*,zh.*,pt.*,fr.*,de.*,vi.*,th.*,id.*,ms.*,ja.*,ko.*";
9
10
  export class LocalVideoIntelligence {
@@ -52,7 +53,9 @@ export class LocalVideoIntelligence {
52
53
  const workDir = await mkdtemp(path.join(os.tmpdir(), "canvas-video-intelligence-"));
53
54
  try {
54
55
  const [visualEvidence, transcript] = await Promise.all([
55
- this.fastmoss.capturePublicVideoFrames(mediaUrl, platformVideoId),
56
+ hasArchivedMedia
57
+ ? captureArchivedVideoFrames(mediaUrl, platformVideoId, workDir)
58
+ : this.fastmoss.capturePublicVideoFrames(mediaUrl, platformVideoId),
56
59
  hasArchivedMedia ? Promise.resolve(archivedTranscript) : extractTimedTranscript(sourceUrl, workDir),
57
60
  ]);
58
61
  if (!visualEvidence.frames.length)
@@ -185,6 +188,48 @@ async function materializeFrames(frames, workDir) {
185
188
  }
186
189
  return attachments;
187
190
  }
191
+ async function captureArchivedVideoFrames(playerUrl, platformVideoId, workDir) {
192
+ const response = await fetch(playerUrl, { signal: AbortSignal.timeout(5 * 60_000) });
193
+ if (!response.ok)
194
+ throw new Error(`读取站内视频副本失败(HTTP ${response.status})`);
195
+ const declaredLength = Number(response.headers.get("content-length") || 0);
196
+ if (declaredLength > 500 * 1024 * 1024)
197
+ throw new Error("站内视频副本超过 500 MiB,已停止分析");
198
+ const bytes = Buffer.from(await response.arrayBuffer());
199
+ if (!bytes.length)
200
+ throw new Error("站内视频副本为空");
201
+ if (bytes.length > 500 * 1024 * 1024)
202
+ throw new Error("站内视频副本超过 500 MiB,已停止分析");
203
+ if (!bytes.subarray(0, 64).includes(Buffer.from("ftyp")))
204
+ throw new Error("站内视频副本不是可识别的 MP4 文件");
205
+ const safeId = /^\d+$/.test(platformVideoId) ? platformVideoId : "video";
206
+ const videoFile = path.join(workDir, `${safeId}.mp4`);
207
+ await writeFile(videoFile, bytes);
208
+ const probe = await runProcess(process.platform === "win32" ? "ffprobe.exe" : "ffprobe", [
209
+ "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", videoFile,
210
+ ], { cwd: workDir, timeoutMs: 60_000, allowMissing: true });
211
+ if (!probe.ok)
212
+ throw new Error(`无法读取站内 MP4 时长:${probe.error}`);
213
+ const durationMs = Math.round(Number.parseFloat(probe.stdout.trim()) * 1_000);
214
+ if (!Number.isFinite(durationMs) || durationMs < 1_000)
215
+ throw new Error("站内 MP4 时长无效");
216
+ const frames = [];
217
+ for (const [index, timestampMs] of videoEvidenceTimestamps(durationMs).entries()) {
218
+ const frameFile = path.join(workDir, `archive-frame-${String(index + 1).padStart(2, "0")}.jpg`);
219
+ const result = await runProcess(process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg", [
220
+ "-v", "error", "-ss", (timestampMs / 1_000).toFixed(3), "-i", videoFile,
221
+ "-frames:v", "1", "-q:v", "3", "-y", frameFile,
222
+ ], { cwd: workDir, timeoutMs: 60_000, allowMissing: true });
223
+ if (!result.ok)
224
+ throw new Error(`站内 MP4 抽帧失败(${timestampMs}ms):${result.error}`);
225
+ const image = await readFile(frameFile);
226
+ if (image.length)
227
+ frames.push({ timestampMs, dataUrl: `data:image/jpeg;base64,${image.toString("base64")}` });
228
+ }
229
+ if (!frames.length)
230
+ throw new Error("站内 MP4 没有生成可分析画面");
231
+ return { videoId: safeId, playerUrl, durationMs, frames };
232
+ }
188
233
  async function extractTimedTranscript(sourceUrl, workDir) {
189
234
  const output = path.join(workDir, "%(id)s.%(ext)s");
190
235
  const args = [
@@ -292,14 +337,15 @@ export function resolveLocalCodexEntrypoint() {
292
337
  }
293
338
  async function runProcess(command, args, options) {
294
339
  return await new Promise((resolve) => {
340
+ let stdout = "";
295
341
  let stderr = "";
296
342
  let settled = false;
297
343
  let child;
298
344
  try {
299
- child = spawn(command, args, { cwd: options.cwd, windowsHide: true, stdio: ["pipe", "ignore", "pipe"] });
345
+ child = spawn(command, args, { cwd: options.cwd, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
300
346
  }
301
347
  catch (error) {
302
- resolve({ ok: false, error: error instanceof Error ? error.message : String(error) });
348
+ resolve({ ok: false, error: error instanceof Error ? error.message : String(error), stdout: "" });
303
349
  return;
304
350
  }
305
351
  const finish = (ok, error = "") => {
@@ -307,12 +353,14 @@ async function runProcess(command, args, options) {
307
353
  return;
308
354
  settled = true;
309
355
  clearTimeout(timer);
310
- resolve({ ok, error: error.trim().slice(-2_000) });
356
+ resolve({ ok, error: error.trim().slice(-2_000), stdout: stdout.trim().slice(-8_000) });
311
357
  };
312
358
  const timer = setTimeout(() => {
313
359
  child.kill();
314
360
  finish(false, `处理超过 ${Math.round(options.timeoutMs / 1000)} 秒`);
315
361
  }, options.timeoutMs);
362
+ child.stdout.on("data", (chunk) => { stdout += String(chunk); if (stdout.length > 16_000)
363
+ stdout = stdout.slice(-16_000); });
316
364
  child.stderr.on("data", (chunk) => { stderr += String(chunk); if (stderr.length > 8_000)
317
365
  stderr = stderr.slice(-8_000); });
318
366
  child.on("error", (error) => finish(false, options.allowMissing && error.code === "ENOENT" ? "未安装命令" : error.message));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaohhhh1/canvas-agent",
3
- "version": "0.4.37",
3
+ "version": "0.4.38",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",