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

320 lines
10 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.
#!/usr/bin/env node
/**
* Kling AI — 账号:资源包查询、设备绑定、交互式配置 credentials
*/
import {
klingGet,
runDeviceBindFlow,
KLING_CONSOLE_URLS,
} from './shared/client.mjs';
import {
getActiveProfile,
getCredentialsFilePath,
getIdentityFilePath,
hasStoredAccessKeys,
promptInteractiveCredentialsFile,
writeCredentialsProfile,
} from './shared/auth.mjs';
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import { parseArgs, getTokenOrExit } from './shared/args.mjs';
const API_COSTS = '/account/costs';
const MS_PER_DAY = 24 * 60 * 60 * 1000;
function maskSecret(secret) {
const s = String(secret || '');
if (!s) return '';
if (s.length <= 6) return '***';
return `${s.slice(0, 3)}***${s.slice(-2)}`;
}
function maskAccessKey(accessKey) {
const s = String(accessKey || '');
if (!s) return '';
if (s.length <= 8) return `${s.slice(0, 2)}***`;
return `${s.slice(0, 4)}***${s.slice(-3)}`;
}
function printConsoleUrls() {
for (const [region, url] of Object.entries(KLING_CONSOLE_URLS || {})) {
const label = region === 'cn' ? 'China / 国内' : (region === 'global' ? 'Global / 国际' : region);
console.error(`${label}: ${url}`);
}
}
function printHelp() {
console.log(`Kling AI account — quota, device bind, configure credentials
Usage:
node kling.mjs account [options]
node kling.mjs account --costs (default)
node kling.mjs account --bind-url
node kling.mjs account --bind (alias of --bind-url, kept for compatibility)
node kling.mjs account --configure
node kling.mjs account --import-env
node kling.mjs account --import-credentials --access_key_id <ak> --secret_access_key <sk>
--costs (default)
GET ${API_COSTS} (Bearer from credentials JWT or KLING_TOKEN)
--days, --start_time, --end_time, --resource_pack_name
--bind-url
init → verify → print URL (manual open) → poll
--bind is equivalent to --bind-url (compatibility alias)
--force Re-bind even if credentials already exist
writes ~/.config/kling/.credentials after exchange succeeds
--import-env
Read KLING_ACCESS_KEY_ID + KLING_SECRET_ACCESS_KEY from env and save (no prompt)
--import-credentials
Write AK/SK via args in one step, no prompts
--configure
Interactive prompts → credentials file (hidden SK on TTY, paste supported)
Env:
KLING_STORAGE_ROOT Optional storage root for credentials/identity/env files
KLING_TOKEN Session Bearer (not loaded from kling.env; export or agent env)
KLING_API_BASE Optional API origin
KLING_ACCESS_KEY_ID With KLING_SECRET_ACCESS_KEY: used by import-env (not echoed)
KLING_SECRET_ACCESS_KEY (same)`);
}
function saveCredentialsQuietly(ak, sk, source = 'input') {
const savePath = writeCredentialsProfile(getActiveProfile(), String(ak || '').trim(), String(sk || '').trim());
console.error(`✓ Credentials saved / 凭证已保存(来源: ${source};密钥未在日志中输出)`);
console.error(` Path / 路径: ${savePath}\n`);
return {
savePath,
accessKeyMasked: maskAccessKey(ak),
secretKeyMasked: maskSecret(sk),
};
}
function getEnvCredentials() {
const ak = (process.env.KLING_ACCESS_KEY_ID || '').trim();
const sk = (process.env.KLING_SECRET_ACCESS_KEY || '').trim();
return { ak, sk };
}
export function importCredentialsFromEnv() {
const { ak, sk } = getEnvCredentials();
if (!ak || !sk) {
throw new Error(
'Set both KLING_ACCESS_KEY_ID and KLING_SECRET_ACCESS_KEY / '
+ '请同时设置 KLING_ACCESS_KEY_ID 与 KLING_SECRET_ACCESS_KEY',
);
}
return saveCredentialsQuietly(ak, sk, 'env');
}
export function importCredentialsFromArgs(accessKey, secretKey) {
const ak = String(accessKey || '').trim();
const sk = String(secretKey || '').trim();
if (!ak || !sk) {
throw new Error(
'import-credentials requires --access_key_id and --secret_access_key / '
+ 'import-credentials 需要 --access_key_id 与 --secret_access_key',
);
}
return saveCredentialsQuietly(ak, sk, 'args');
}
function parseMs(name, raw) {
const n = parseInt(String(raw).trim(), 10);
if (!Number.isFinite(n)) {
console.error(`Error / 错误: ${name} must be a valid integer (ms) / 须为有效整数(毫秒)`);
process.exit(1);
}
return n;
}
function buildCostsQueryPath(args) {
let endMs;
let startMs;
if (args.end_time != null) {
endMs = parseMs('--end_time', args.end_time);
} else {
endMs = Date.now();
}
if (args.start_time != null) {
startMs = parseMs('--start_time', args.start_time);
} else {
const days = Math.max(1, parseInt(String(args.days ?? '30'), 10) || 30);
startMs = endMs - days * MS_PER_DAY;
}
if (startMs >= endMs) {
console.error('Error / 错误: start_time must be < end_time / start_time 须小于 end_time');
process.exit(1);
}
const params = new URLSearchParams();
params.set('start_time', String(startMs));
params.set('end_time', String(endMs));
if (args.resource_pack_name) {
params.set('resource_pack_name', String(args.resource_pack_name).trim());
}
return `${API_COSTS}?${params.toString()}`;
}
function printAccountStateNoAccount(detail = '') {
console.error('Account State / 账号状态: NO_ACCOUNT / 无可用账号凭证');
if (detail) {
console.error(` Detail / 详情: ${detail}`);
}
}
function isPermissionOrServerIssue(errorMessage = '') {
const msg = String(errorMessage || '').toLowerCase();
return (
msg.includes('http 401')
|| msg.includes('http 403')
|| msg.includes('code=1000')
|| msg.includes('code=1002')
|| msg.includes('permission')
|| msg.includes('forbidden')
|| msg.includes('unauthorized')
|| msg.includes('api service error')
|| msg.includes('http 500')
|| msg.includes('http 502')
|| msg.includes('http 503')
|| msg.includes('http 504')
|| msg.includes('server error')
);
}
async function runBindUrlAction(args, options = {}) {
const viaAliasBind = options.viaAliasBind === true;
if (!args.force && hasStoredAccessKeys()) {
console.error('Credentials already present / 已存在凭证(使用 --force 重新绑定)');
console.error(`Credentials file / 凭证文件: ${getCredentialsFilePath()}`);
process.exit(0);
}
if (viaAliasBind) {
console.error('Info / 提示: --bind is an alias of --bind-url / --bind 与 --bind-url 等价');
}
try {
const result = await runDeviceBindFlow();
console.error('\n✓ Bind succeeded / 绑定成功');
console.error(` Saved / 已写入: ${result.savePath || getCredentialsFilePath()}`);
} catch (e) {
console.error(`\nBind failed / 绑定失败: ${e?.message || e}\n`);
console.error('Hint / 提示:');
console.error(' 1) Check network/DNS/proxy / 检查网络、DNS、代理');
console.error(' 2) Check configured API base in ~/.config/kling/kling.env / 检查 ~/.config/kling/kling.env 中的 API 基址配置');
console.error(' 3) Re-probe business API base: remove KLING_API_BASE then run account --costs / 重新探测业务 API 基址:删除 KLING_API_BASE 后执行 account --costs');
console.error('Fallback / 备选:');
console.error(' 1) Create keys Manually / 手动创建密钥:');
printConsoleUrls();
console.error(' 2) Set env then: node skills/klingai/scripts/kling.mjs account --import-env');
console.error(' 3) or Pass args: node skills/klingai/scripts/kling.mjs account --import-credentials --access_key_id <AK> --secret_access_key <SK>\n');
process.exit(1);
}
}
export async function main() {
const args = parseArgs(process.argv);
if (args.help) {
printHelp();
return;
}
if (args.action != null) {
console.error('Error / 错误: --action has been removed. Use one flag: --costs | --bind-url (or alias --bind) | --import-env | --import-credentials | --configure');
process.exit(1);
}
const modes = ['costs', 'bind', 'bind-url', 'configure', 'import-env', 'import-credentials'];
const selected = modes.filter((m) => args[m]);
if (selected.length > 1) {
console.error(`Error / 错误: account mode flags are mutually exclusive / account 模式参数互斥: ${selected.map((s) => `--${s}`).join(', ')}`);
process.exit(1);
}
const action = selected[0] || 'costs';
if (action === 'bind') {
await runBindUrlAction(args, { viaAliasBind: true });
return;
}
if (action === 'bind-url') {
await runBindUrlAction(args);
return;
}
if (action === 'import-env') {
try {
importCredentialsFromEnv();
} catch (e) {
console.error(`Error / 错误: ${e?.message || e}`);
process.exit(1);
}
return;
}
if (action === 'import-credentials') {
try {
importCredentialsFromArgs(args.access_key_id, args.secret_access_key);
} catch (e) {
console.error(`Error / 错误: ${e?.message || e}`);
process.exit(1);
}
return;
}
if (action === 'configure') {
try {
console.error('Get keys / 获取密钥:');
printConsoleUrls();
await promptInteractiveCredentialsFile();
} catch (e) {
console.error(`Error / 错误: ${e?.message || e}`);
process.exit(1);
}
return;
}
let token;
try {
token = await getTokenOrExit();
} catch (e) {
const msg = e?.message || String(e);
printAccountStateNoAccount(msg);
console.error(`Error / 错误: ${msg}`);
console.error('Get keys / 获取密钥:');
printConsoleUrls();
process.exit(1);
}
const pathWithQuery = buildCostsQueryPath(args);
try {
const data = await klingGet(pathWithQuery, token, { contentType: 'application/json' });
const infos = Array.isArray(data?.resource_pack_subscribe_infos) ? data.resource_pack_subscribe_infos : [];
console.error(`Account State / 账号状态: ACCOUNT_OK / 账号正常(资源包 ${infos.length}`);
console.log('Account / 账户资源 (API data):');
console.log(JSON.stringify(data, null, 2));
return;
} catch (e) {
const msg = e?.message || String(e);
if (isPermissionOrServerIssue(msg)) {
console.error('Account State / 账号状态: BOUND_BUT_PERMISSION_OR_SERVER_ERROR / 已绑定但权限或服务异常');
}
console.error(`Error / 错误: ${msg}`);
process.exit(1);
}
}
const __filename = fileURLToPath(import.meta.url);
if (process.argv[1] && resolve(__filename) === resolve(process.argv[1])) {
main().catch((e) => {
console.error(`Error / 错误: ${e?.message || e}`);
process.exit(1);
});
}