#!/usr/bin/env node // 技能直连 OSS 工具:列举桶内对象与目录结构(ADR-004 凭据边界,零硬编码) // 用法: // node oss.mjs # 桶顶层:按一级目录分组统计 // node oss.mjs recordings/ # 列出该前缀全部对象(按目录分组) // node oss.mjs recordings/ --json # JSON 输出 {domain,prefix,objects} // node oss.mjs --find-env # 打印实际采用的 .env 路径 // node oss.mjs reports/ --env D:/1/device-management-backend/server/.env # 显式指定 .env // 凭据回退链: --env > SMARTBADGE_ENV_FILE > 自动发现(见 lib.mjs) > 进程环境已有 OSS_* // 注意:需要项目 server/services/oss-client.js 所在仓库(自动发现通常命中 D:\1\device-management-backend) import fs from 'fs'; import path from 'path'; import { pathToFileURL } from 'url'; import { findEnvFile, loadEnv, fail } from './lib.mjs'; const args = process.argv.slice(2); const isFlag = (a) => a.startsWith('--'); const envIdx = args.indexOf('--env'); const envPathFlag = envIdx >= 0 ? args[envIdx + 1] : undefined; // 非 flag 且非 flag 值 的参数才视为 prefix const prefix = args.filter((a, i) => !isFlag(a) && !(envIdx >= 0 && (i === envIdx + 1)))[0] ?? ''; const json = args.includes('--json'); const findEnv = args.includes('--find-env'); function groupKey(name, pfx) { const rest = name.slice(pfx.length); const seg = rest.split('/')[0]; return rest.includes('/') ? `${seg}/` : seg; } async function main() { const env = findEnvFile({ envPath: envPathFlag }); if (findEnv) { if (env.hit) { console.log(`生效 .env: ${env.hit}`); return; } console.error('未找到 .env,候选路径:'); for (const c of env.candidates) console.error(` ${c}`); process.exit(2); } if (!env.hit) { console.error('未找到 OSS 凭据文件(探测序列如下),三选一:'); for (const c of env.candidates) console.error(` ${c}`); console.error(' → 1. node oss.mjs --env '); console.error(' → 2. 设置 SMARTBADGE_ENV_FILE 指向 server/.env'); console.error(' → 3. 直接导出 OSS_* 环境变量(与 server/.env 同键名)'); process.exit(2); } loadEnv(env.hit); const serverDir = path.dirname(env.hit); const missing = ['OSS_ACCESS_KEY_ID', 'OSS_ACCESS_KEY_SECRET', 'OSS_BUCKET'].filter((k) => !process.env[k]); if (missing.length) fail(`OSS 凭据不全,缺: ${missing.join(', ')}。请检查 --env 指向的 .env 或环境变量`, 4); const ossClientPath = path.join(serverDir, 'services', 'oss-client.js'); if (!fs.existsSync(ossClientPath)) { fail(`未找到 oss-client.js: ${ossClientPath}。自动发现命中路径可能不含项目 server/;请用 --env 显式指向 <项目>/server/.env`, 3); } const { listObjects, publicDomain } = await import(pathToFileURL(ossClientPath)); const objects = await listObjects(prefix); const domain = publicDomain(); if (json) { console.log(JSON.stringify({ domain, prefix, objects: objects.map((o) => ({ name: o.name, size: o.size, lastModified: o.lastModified })), })); return; } if (!prefix) { const groups = new Map(); for (const o of objects) { const seg = groupKey(o.name, ''); if (!seg) continue; if (!groups.has(seg)) groups.set(seg, { count: 0, bytes: 0 }); groups.get(seg).count++; groups.get(seg).bytes += o.size; } console.log(`OSS 桶顶层目录(共 ${objects.length} 个对象): ${domain}`); for (const [seg, g] of [...groups.entries()].sort()) { console.log(` ${seg.padEnd(16)} ${g.count} 个文件, ${(g.bytes / 1024 / 1024).toFixed(2)} MB`); } console.log('使用: node oss.mjs 查看子目录(如 recordings/ cleansed/ transcripts/ reports/)'); return; } if (objects.length === 0) { console.log(`[${prefix}] 无对象`); return; } console.log(`[${prefix}] 共 ${objects.length} 个对象:`); for (const o of objects) { console.log(` ${o.name} (${(o.size / 1024 / 1024).toFixed(2)} MB, ${(o.lastModified || '').slice(0, 10)})`); } const bytes = objects.reduce((s, o) => s + o.size, 0); console.log(`合计 ${(bytes / 1024 / 1024).toFixed(2)} MB`); } main().catch((err) => { console.error(err.message); process.exit(1); });