Files
video-create/.claude/skills/klingai-1.1.0/scripts/shared/args.mjs

206 lines
7.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Kling AI CLI helpers (zero external deps)
* Argument parsing, auth, media file reading
*/
import { readFile } from 'node:fs/promises';
import { resolve, relative, sep } from 'node:path';
import { platform } from 'node:process';
import {
getBearerToken,
CredentialsMissingError,
setSkillVersion,
} from './auth.mjs';
import { runDeviceBindFlow } from './client.mjs';
/** 是否允许读取/写入 cwd 与 KLING_MEDIA_ROOTS 以外的本地路径(默认关闭) */
function allowAbsolutePaths() {
const v = (process.env.KLING_ALLOW_ABSOLUTE_PATHS || '').trim().toLowerCase();
return v === '1' || v === 'true' || v === 'yes';
}
/** 额外允许的根目录逗号分隔用于下载目录、WSL 跨盘路径等 */
function extraMediaRoots() {
const raw = (process.env.KLING_MEDIA_ROOTS || '').trim();
if (!raw) return [];
return raw.split(',').map((s) => s.trim()).filter(Boolean).map((p) => resolve(p));
}
function allAllowedRoots() {
const roots = [resolve(process.cwd()), ...extraMediaRoots()];
return roots;
}
/** Windows仅在同盘内做 relative 校验 */
function sameDriveRoot(a, b) {
if (platform !== 'win32') return true;
const ra = resolve(a);
const rb = resolve(b);
const da = ra.match(/^([A-Za-z]:)/);
const db = rb.match(/^([A-Za-z]:)/);
if (!da || !db) return true;
return da[1].toLowerCase() === db[1].toLowerCase();
}
/**
* 判断绝对路径是否落在任一允许根下(用于本地文件读、输出目录写)
* @param {string} absPath 已 resolve 的绝对路径
*/
export function isAllowedLocalPath(absPath) {
if (allowAbsolutePaths()) return true;
const normalized = resolve(absPath);
for (const root of allAllowedRoots()) {
if (!sameDriveRoot(root, normalized)) continue;
const rel = relative(root, normalized);
if (rel === '') return true;
if (!rel.startsWith('..') && !rel.includes(`${sep}..`)) return true;
}
return false;
}
/**
* 校验并返回用于读文件的绝对路径URL 不适用)
* @param {string} userPath 用户传入的本地路径
* @returns {string}
*/
export function resolveAllowedReadPath(userPath) {
const normalized = resolve(userPath.trim());
if (!isAllowedLocalPath(normalized)) {
const roots = allAllowedRoots().join(', ');
throw new Error(
`Local path outside allowed roots / 本地路径不在允许范围内: ${normalized}\n`
+ `Allowed / 允许: cwd + KLING_MEDIA_ROOTS, or set KLING_ALLOW_ABSOLUTE_PATHS=1\n`
+ `Roots / 当前根: ${roots}\n`
+ `Example / 示例: export KLING_MEDIA_ROOTS="/mnt/c/Users/you/Downloads,/tmp/claw-downloads"`,
);
}
return normalized;
}
/**
* 校验输出目录(相对路径相对于 cwd 解析)
* @param {string} userPath 如 ./output 或绝对路径
* @returns {string} 绝对路径
*/
export function resolveAllowedOutputDir(userPath) {
const normalized = resolve(userPath.trim());
if (!isAllowedLocalPath(normalized)) {
const roots = allAllowedRoots().join(', ');
throw new Error(
`Output dir outside allowed roots / 输出目录不在允许范围内: ${normalized}\n`
+ `Allowed / 允许: under cwd, KLING_MEDIA_ROOTS, or KLING_ALLOW_ABSOLUTE_PATHS=1\n`
+ `Roots / 当前根: ${roots}`,
);
}
return normalized;
}
/** 消费 --skill-version */
function consumeSkillVersionArgv(argv) {
for (let i = 2; i < argv.length - 1; i++) {
if (argv[i] === '--skill-version') {
setSkillVersion(argv[i + 1]);
argv.splice(i, 2);
return;
}
}
}
/**
* 解析命令行参数
* @param {string[]} argv process.argv会原地消费 --skill-version
* @param {string[]} [booleanFlags] 额外的布尔标志名(不需要跟值的 --flag
* @returns {object} 参数键值对
*/
export function parseArgs(argv, booleanFlags = []) {
consumeSkillVersionArgv(argv);
const boolSet = new Set(['no-wait', 'download', 'wait', 'help', ...booleanFlags]);
const args = {};
for (let i = 2; i < argv.length; i++) {
const key = argv[i];
if (!key.startsWith('--')) continue;
const name = key.slice(2);
if (name === 'no-wait') { args.wait = false; continue; }
if (boolSet.has(name)) { args[name] = true; continue; }
const val = argv[i + 1];
if (val !== undefined && !val.startsWith('--')) {
args[name] = val; i++;
} else {
args[name] = true;
}
}
return args;
}
/**
* 获取 Bearer优先进程内 KLING_TOKEN否则 credentials 中 AK/SK → JWT。
* 若皆无首次或仅有空凭证自动执行设备绑定bind后再取 token。
*/
export async function getTokenOrExit() {
try {
return getBearerToken();
} catch (e) {
const missing = e instanceof CredentialsMissingError || e?.name === 'CredentialsMissingError';
if (!missing) {
throw new Error(`Auth error / 鉴权错误: ${e?.message || e}`);
}
try {
console.error('\n── No credentials / 无可用凭证,启动设备绑定 bind ────\n');
await runDeviceBindFlow({});
return getBearerToken();
} catch (err) {
const lines = [
`Bind failed / 绑定失败: ${err?.message || err}`,
];
if (err?.bindAuthorizeUrl) {
lines.push(`Bind URL / 手动绑定链接: ${err.bindAuthorizeUrl}`);
}
lines.push('Fallback / 备选:');
lines.push(' node skills/klingai/scripts/kling.mjs account --bind-url');
lines.push(' set KLING_ACCESS_KEY_ID + KLING_SECRET_ACCESS_KEY, then');
lines.push(' node skills/klingai/scripts/kling.mjs account --import-env');
lines.push(' or pass args: --import-credentials --access_key_id <AK> --secret_access_key <SK>');
lines.push(' or set KLING_TOKEN for this session / 或设置 KLING_TOKEN');
throw new Error(lines.join('\n'));
}
}
}
/**
* 读取媒体文件URL 直接返回,本地文件读为 base64路径受 KLING_MEDIA_ROOTS / KLING_ALLOW_ABSOLUTE_PATHS 约束)
* @param {string} pathOrUrl 文件路径或 URL
* @returns {Promise<string>} URL 或 base64 字符串
*/
export async function readMediaAsValue(pathOrUrl) {
if (!pathOrUrl) return undefined;
const s = pathOrUrl.trim();
try {
const u = new URL(s);
if (u.protocol === 'http:' || u.protocol === 'https:') return s;
} catch {
// Not a URL: treat as local path below.
}
const abs = resolveAllowedReadPath(s);
const buf = await readFile(abs);
return buf.toString('base64');
}
/**
* Omni-Video 参考视频字段 `video_list[].video_url`:仅接受公网 `http://` 或 `https://` 链接,不接受本地路径或 Base64。
* @param {string} pathOrUrl
* @returns {string|undefined}
*/
export function readOmniVideoRefUrl(pathOrUrl) {
if (!pathOrUrl) return undefined;
const s = pathOrUrl.trim();
try {
const u = new URL(s);
if (u.protocol === 'http:' || u.protocol === 'https:') return s;
} catch {
// Fallthrough to unified error below.
}
throw new Error(
'Omni --video must be a public http(s) URL / Omni --video 须为公网 http(s) 链接(不接受本地路径或 Base64。\n'
+ 'Upload the file and pass the URL / 请先上传视频再传入 URL。',
);
}