oss.mjs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. #!/usr/bin/env node
  2. // 技能直连 OSS 工具:列举桶内对象与目录结构(ADR-004 凭据边界,零硬编码)
  3. // 用法:
  4. // node oss.mjs # 桶顶层:按一级目录分组统计
  5. // node oss.mjs recordings/ # 列出该前缀全部对象(按目录分组)
  6. // node oss.mjs recordings/ --json # JSON 输出 {domain,prefix,objects}
  7. // node oss.mjs --find-env # 打印实际采用的 .env 路径
  8. // node oss.mjs records/ --env <server/.env 路径> # 显式指定 .env
  9. // 凭据回退链: --env > SMARTBADGE_ENV_FILE > 自动发现(见 lib.mjs) > 进程环境已有 OSS_* > 技能仓库根 .env
  10. // OSS 客户端两态: 有服务端项目(server/services/oss-client.js)时复用其模块;否则回退内置零依赖实现(oss-client-mini.mjs)
  11. import fs from 'fs';
  12. import path from 'path';
  13. import { pathToFileURL } from 'url';
  14. import { findEnvFile, loadEnv, fail, SCRIPTS_DIR } from './lib.mjs';
  15. const args = process.argv.slice(2);
  16. const isFlag = (a) => a.startsWith('--');
  17. const envIdx = args.indexOf('--env');
  18. const envPathFlag = envIdx >= 0 ? args[envIdx + 1] : undefined;
  19. // 非 flag 且非 flag 值 的参数才视为 prefix
  20. const prefix = args.filter((a, i) => !isFlag(a) && !(envIdx >= 0 && (i === envIdx + 1)))[0] ?? '';
  21. const json = args.includes('--json');
  22. const findEnv = args.includes('--find-env');
  23. function groupKey(name, pfx) {
  24. const rest = name.slice(pfx.length);
  25. const seg = rest.split('/')[0];
  26. return rest.includes('/') ? `${seg}/` : seg;
  27. }
  28. async function main() {
  29. const env = findEnvFile({ envPath: envPathFlag });
  30. if (findEnv) {
  31. if (env.hit) {
  32. console.log(`生效 .env: ${env.hit}`);
  33. return;
  34. }
  35. console.error('未找到 .env,候选路径:');
  36. for (const c of env.candidates) console.error(` ${c}`);
  37. process.exit(2);
  38. }
  39. // 凭据来源: .env 文件 > 进程环境已有 OSS_*(Docker -e 注入场景),文件缺失但环境变量齐备时允许继续
  40. const hasOssEnv = ['OSS_ACCESS_KEY_ID', 'OSS_ACCESS_KEY_SECRET', 'OSS_BUCKET'].every((k) => process.env[k]);
  41. if (!env.hit && !hasOssEnv) {
  42. console.error('未找到 OSS 凭据文件(探测序列如下),三选一:');
  43. for (const c of env.candidates) console.error(` ${c}`);
  44. console.error(' → 1. 复制 .env.example 为 <技能仓库>/.env 并填入 OSS_* 四项');
  45. console.error(' → 2. node oss.mjs --env <server/.env 路径>(服务端项目在别处时)');
  46. console.error(' → 3. 直接导出 OSS_* 环境变量(与 server/.env 同键名)');
  47. process.exit(2);
  48. }
  49. if (env.hit) loadEnv(env.hit);
  50. const serverDir = env.hit ? path.dirname(env.hit) : null;
  51. const missing = ['OSS_ACCESS_KEY_ID', 'OSS_ACCESS_KEY_SECRET', 'OSS_BUCKET'].filter((k) => !process.env[k]);
  52. if (missing.length) fail(`OSS 凭据不全,缺: ${missing.join(', ')}。请检查 --env 指向的 .env 或环境变量`, 4);
  53. // 客户端两态: 服务端项目模块优先(行为一致),否则内置零依赖实现
  54. const ossClientPath = serverDir ? path.join(serverDir, 'services', 'oss-client.js') : null;
  55. const { listObjects, publicDomain } = ossClientPath && fs.existsSync(ossClientPath)
  56. ? await import(pathToFileURL(ossClientPath))
  57. : await import(pathToFileURL(path.join(SCRIPTS_DIR, 'oss-client-mini.mjs')));
  58. const objects = await listObjects(prefix);
  59. const domain = publicDomain();
  60. if (json) {
  61. console.log(JSON.stringify({
  62. domain, prefix,
  63. objects: objects.map((o) => ({ name: o.name, size: o.size, lastModified: o.lastModified })),
  64. }));
  65. return;
  66. }
  67. if (!prefix) {
  68. const groups = new Map();
  69. for (const o of objects) {
  70. const seg = groupKey(o.name, '');
  71. if (!seg) continue;
  72. if (!groups.has(seg)) groups.set(seg, { count: 0, bytes: 0 });
  73. groups.get(seg).count++;
  74. groups.get(seg).bytes += o.size;
  75. }
  76. console.log(`OSS 桶顶层目录(共 ${objects.length} 个对象): ${domain}`);
  77. for (const [seg, g] of [...groups.entries()].sort()) {
  78. console.log(` ${seg.padEnd(16)} ${g.count} 个文件, ${(g.bytes / 1024 / 1024).toFixed(2)} MB`);
  79. }
  80. console.log('使用: node oss.mjs <prefix> 查看子目录(如 recordings/ cleansed/ transcripts/ reports/)');
  81. return;
  82. }
  83. if (objects.length === 0) {
  84. console.log(`[${prefix}] 无对象`);
  85. return;
  86. }
  87. console.log(`[${prefix}] 共 ${objects.length} 个对象:`);
  88. for (const o of objects) {
  89. console.log(` ${o.name} (${(o.size / 1024 / 1024).toFixed(2)} MB, ${(o.lastModified || '').slice(0, 10)})`);
  90. }
  91. const bytes = objects.reduce((s, o) => s + o.size, 0);
  92. console.log(`合计 ${(bytes / 1024 / 1024).toFixed(2)} MB`);
  93. }
  94. main().catch((err) => {
  95. console.error(err.message);
  96. process.exit(1);
  97. });