2026-04-19 23:24:28 +00:00
|
|
|
|
#!/usr/bin/env bun
|
|
|
|
|
|
import { resolve } from 'path';
|
|
|
|
|
|
import type { Command } from '../src/types.ts';
|
|
|
|
|
|
import { run } from '../src/index.ts';
|
2026-04-19 23:40:15 +00:00
|
|
|
|
const SKILL_NAME = 'video-product-snapshot';
|
2026-04-19 23:24:28 +00:00
|
|
|
|
|
|
|
|
|
|
// Load .env from skill root (does not override existing env vars)
|
|
|
|
|
|
loadDotenv(resolve(import.meta.dir, '../.env'));
|
|
|
|
|
|
|
|
|
|
|
|
function loadDotenv(path: string): void {
|
|
|
|
|
|
let raw: string;
|
|
|
|
|
|
try { raw = require('fs').readFileSync(path, 'utf-8'); } catch { return; }
|
|
|
|
|
|
for (const line of raw.split('\n')) {
|
|
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
|
|
|
|
const eq = trimmed.indexOf('=');
|
|
|
|
|
|
if (eq < 0) continue;
|
|
|
|
|
|
const key = trimmed.slice(0, eq).trim();
|
|
|
|
|
|
const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
|
|
|
|
|
|
if (key && !(key in process.env)) process.env[key] = val;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function printUsage(): void {
|
2026-04-25 07:13:07 +00:00
|
|
|
|
console.error(`用法:
|
2026-04-19 23:24:28 +00:00
|
|
|
|
bun scripts/run.ts [--api-base=<url>] <command> [args...] [--dry-run]
|
|
|
|
|
|
|
2026-04-25 07:13:07 +00:00
|
|
|
|
命令:
|
2026-04-19 23:24:28 +00:00
|
|
|
|
session
|
2026-04-25 07:13:07 +00:00
|
|
|
|
获取认证 session token
|
2026-04-19 23:24:28 +00:00
|
|
|
|
|
|
|
|
|
|
detect <video-path> [options]
|
2026-04-25 07:13:07 +00:00
|
|
|
|
从视频抽帧并检测商品画面
|
|
|
|
|
|
选项:
|
|
|
|
|
|
--interval=<秒> 抽帧间隔(默认: 1)
|
|
|
|
|
|
--max-frames=<数量> 最多分析帧数(默认: 60)
|
|
|
|
|
|
--output-dir=<目录> 截图保存目录(默认: 视频所在目录)
|
|
|
|
|
|
--min-confidence=<0-1> 最低检测置信度(默认: 0.7)
|
2026-04-19 23:24:28 +00:00
|
|
|
|
|
|
|
|
|
|
search <image-path>
|
2026-04-25 07:13:07 +00:00
|
|
|
|
用图片搜索商品(调用 ecom image-search API)
|
2026-04-19 23:24:28 +00:00
|
|
|
|
|
|
|
|
|
|
detect-and-search <video-path> [options]
|
2026-04-25 07:13:07 +00:00
|
|
|
|
检测最佳商品画面 → 图片搜索 → 关键词重排序
|
2026-04-19 23:24:28 +00:00
|
|
|
|
|
2026-04-26 07:01:42 +00:00
|
|
|
|
detect-best <video-path> [options]
|
|
|
|
|
|
从视频抽帧并选择最佳商品画面(更快更稳定)
|
|
|
|
|
|
|
|
|
|
|
|
detect-best-and-search <video-path> [options]
|
|
|
|
|
|
最佳画面 → 图片搜索 → 关键词重排序
|
|
|
|
|
|
|
2026-04-25 08:30:01 +00:00
|
|
|
|
detect-video <video-path>
|
2026-04-26 07:01:42 +00:00
|
|
|
|
识别商品描述和搜索关键词(当前实现:从视频抽帧选最佳帧)
|
2026-04-25 08:30:01 +00:00
|
|
|
|
|
|
|
|
|
|
detect-video-and-search <video-path>
|
2026-04-26 07:01:42 +00:00
|
|
|
|
识别商品 → 图片搜索 → 1688 关键词重排序(当前实现:从视频抽帧选最佳帧)
|
2026-04-25 08:30:01 +00:00
|
|
|
|
|
2026-04-19 23:40:15 +00:00
|
|
|
|
rerank --image-results=<json> [--description=<text>] [--keyword=<text>] [--top=<n>]
|
2026-04-25 07:13:07 +00:00
|
|
|
|
通过关键词交并集过滤搜索结果
|
2026-04-19 23:24:28 +00:00
|
|
|
|
|
2026-04-25 07:13:07 +00:00
|
|
|
|
配置文件: ~/.openclaw/.env (CLIENT_KEY), skill 目录 .env (VISION_API_KEY)
|
2026-04-19 23:24:28 +00:00
|
|
|
|
`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 23:46:45 +00:00
|
|
|
|
function reportTelemetry(payload: object): void {
|
|
|
|
|
|
const endpoint = process.env.TELEMETRY_ENDPOINT;
|
|
|
|
|
|
if (!endpoint) return;
|
|
|
|
|
|
fetch(endpoint, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(payload),
|
|
|
|
|
|
}).catch(() => {});
|
2026-04-19 23:40:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 23:24:28 +00:00
|
|
|
|
async function main(): Promise<void> {
|
|
|
|
|
|
const positionals: string[] = [];
|
|
|
|
|
|
let dryRun = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (const arg of process.argv.slice(2)) {
|
|
|
|
|
|
if (arg === '--dry-run') {
|
|
|
|
|
|
dryRun = true;
|
|
|
|
|
|
} else if (arg.startsWith('--api-base=')) {
|
|
|
|
|
|
process.env.API_BASE = arg.slice('--api-base='.length).trim();
|
2026-04-26 10:35:55 +00:00
|
|
|
|
} else if (arg.startsWith('--session-id=')) {
|
|
|
|
|
|
process.env.SKILL_SESSION_ID = arg.slice('--session-id='.length).trim();
|
2026-04-19 23:24:28 +00:00
|
|
|
|
} else if (arg === '-h' || arg === '--help') {
|
|
|
|
|
|
printUsage(); process.exit(0);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
positionals.push(arg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (positionals.length < 1) { printUsage(); process.exit(1); }
|
|
|
|
|
|
|
2026-04-19 23:40:15 +00:00
|
|
|
|
const command = positionals[0] as Command;
|
2026-04-26 10:44:01 +00:00
|
|
|
|
|
|
|
|
|
|
// Resolve session ID: explicit CLI arg > env > auto-generate structured ID
|
|
|
|
|
|
const sessionId = process.env.SKILL_SESSION_ID || (() => {
|
|
|
|
|
|
const ts = new Date();
|
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
|
|
|
const tsPart = `${ts.getFullYear()}${pad(ts.getMonth()+1)}${pad(ts.getDate())}-${pad(ts.getHours())}${pad(ts.getMinutes())}${pad(ts.getSeconds())}`;
|
|
|
|
|
|
const rand = Math.random().toString(36).slice(2, 6);
|
|
|
|
|
|
return `vps-${tsPart}-${rand}`;
|
|
|
|
|
|
})();
|
|
|
|
|
|
process.env.SKILL_SESSION_ID = sessionId;
|
|
|
|
|
|
|
2026-04-19 23:40:15 +00:00
|
|
|
|
const startMs = Date.now();
|
|
|
|
|
|
let result: Awaited<ReturnType<typeof run>>;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
result = await run(command, positionals.slice(1), dryRun);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
const error = err instanceof Error ? err.message : String(err);
|
2026-04-26 10:44:01 +00:00
|
|
|
|
console.log(JSON.stringify({ status: 'failed', command, dryRun, sessionId, error }, null, 2));
|
|
|
|
|
|
if (!dryRun) reportTelemetry({ skill: SKILL_NAME, command, sessionId, status: 'failed', durationMs: Date.now() - startMs, error });
|
2026-04-19 23:40:15 +00:00
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-26 10:44:01 +00:00
|
|
|
|
const output = { ...result, sessionId } as Record<string, unknown>;
|
|
|
|
|
|
console.log(JSON.stringify(output, null, 2));
|
|
|
|
|
|
if (!dryRun) reportTelemetry({ skill: SKILL_NAME, command, sessionId, status: result.status, durationMs: Date.now() - startMs, error: (result as any).error });
|
2026-04-19 23:24:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main().catch((err) => {
|
|
|
|
|
|
console.error(JSON.stringify({
|
|
|
|
|
|
status: 'failed',
|
|
|
|
|
|
error: err instanceof Error ? err.message : String(err),
|
|
|
|
|
|
}, null, 2));
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|