agent-log.mjs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/env node
  2. // Copyright (c) 未来飞马
  3. //
  4. // MIT License — Copyright (c) 2026 Fmode (未来飞马)
  5. //
  6. // Trademark Notice:
  7. // The MIT license grants copyright permissions for source code only.
  8. // It does NOT grant any rights to use trademarks including "未来飞马",
  9. // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
  10. // Any use of these trademarks requires separate written permission.
  11. import { spawnSync } from 'node:child_process';
  12. import { fileURLToPath } from 'node:url';
  13. import path from 'node:path';
  14. import { resolveIdentity, identitySummary } from '../lib/identity.mjs';
  15. import { uploadJson, ENDPOINTS } from '../lib/upload.mjs';
  16. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  17. const COLLECT_PY = path.join(__dirname, '..', 'lib', 'collect.py');
  18. const VERSION = '2.0.0';
  19. // ──────────────────────────────────────────────────────────────────────────────
  20. // 工具函数
  21. // ──────────────────────────────────────────────────────────────────────────────
  22. function makeLogKey(now = new Date()) {
  23. const pad = n => String(n).padStart(2, '0');
  24. const d = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
  25. const t = `${pad(now.getHours())}${pad(now.getMinutes())}`;
  26. return `log/${d}/${t}.json`;
  27. }
  28. function collectMetrics(identity) {
  29. const args = [COLLECT_PY];
  30. if (identity.agentId) args.push('--agent-id', identity.agentId);
  31. if (identity.userid) args.push('--userid', identity.userid);
  32. const result = spawnSync('python3', args, {
  33. timeout: 30_000,
  34. encoding: 'utf-8',
  35. });
  36. if (result.error) {
  37. throw new Error(`python3 启动失败: ${result.error.message}`);
  38. }
  39. if (result.status !== 0) {
  40. throw new Error(`采集脚本非零退出 (${result.status}): ${(result.stderr || '').slice(0, 300)}`);
  41. }
  42. try {
  43. return JSON.parse(result.stdout);
  44. } catch {
  45. throw new Error(`指标 JSON 解析失败,输出: ${(result.stdout || '').slice(0, 200)}`);
  46. }
  47. }
  48. function assertToken(identity) {
  49. if (!identity.sessionToken) {
  50. console.error('[错误] 缺少 sessionToken,无法调用云函数上传通道');
  51. console.error('\n修复方式(任选其一):');
  52. console.error(' 1. export FMODE_SESSION_TOKEN=r:xxxxxxxxxxxxxxxxxxxxxxxx');
  53. console.error(' 2. 在 ~/.fmode/config.json 中添加 "sessionToken": "r:xxx"');
  54. console.error(' 3. 在 ~/.fmode/config/user.json 中添加 "sessionToken": "r:xxx"');
  55. console.error('\ntoken 可在 Fmode 控制台 → 账号设置 → Session Token 处获取。');
  56. process.exit(2);
  57. }
  58. }
  59. // ──────────────────────────────────────────────────────────────────────────────
  60. // 命令实现
  61. // ──────────────────────────────────────────────────────────────────────────────
  62. async function cmdReport() {
  63. const identity = resolveIdentity();
  64. assertToken(identity);
  65. if (!identity.userid) {
  66. console.log('[report] 未找到本地 userid,将由云函数从 token 自动推断');
  67. }
  68. console.log('[report] 采集本机指标...');
  69. const metrics = collectMetrics(identity);
  70. const key = makeLogKey();
  71. console.log(`[report] 上传到 ${key} ...`);
  72. const { publicUrl } = await uploadJson({
  73. sessionToken: identity.sessionToken,
  74. key,
  75. content: metrics,
  76. });
  77. console.log('[report] 上报成功');
  78. console.log(`publicUrl: ${publicUrl}`);
  79. return publicUrl;
  80. }
  81. async function cmdCollect() {
  82. const identity = resolveIdentity();
  83. const metrics = collectMetrics(identity);
  84. process.stdout.write(JSON.stringify(metrics, null, 2) + '\n');
  85. }
  86. async function cmdCheck() {
  87. const identity = resolveIdentity();
  88. const summary = identitySummary(identity);
  89. console.log('=== 身份解析 ===');
  90. console.log(`userid: ${summary.userid}`);
  91. console.log(`sessionToken: ${summary.sessionToken}`);
  92. console.log(`agentId: ${summary.agentId}`);
  93. console.log('\n=== 云函数连通性 ===');
  94. console.log(`端点: ${ENDPOINTS.functions}`);
  95. console.log(`函数 ID: AlP56LCKFm (fmodeagent-upload-url)`);
  96. if (!identity.sessionToken) {
  97. console.error('\n[错误] 无 sessionToken,无法测试连通性');
  98. console.error('修复: export FMODE_SESSION_TOKEN=r:xxx 或写入 ~/.fmode/config.json');
  99. process.exit(2);
  100. }
  101. // 用一次真实的轻量上报来验证连通性(check 专用路径)
  102. const testContent = {
  103. schema_version: '2.0',
  104. type: 'connectivity-check',
  105. timestamp: new Date().toISOString(),
  106. agent_id: identity.agentId,
  107. };
  108. const now = new Date();
  109. const pad = n => String(n).padStart(2, '0');
  110. const testKey = `log/check/${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}/${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.json`;
  111. try {
  112. const { publicUrl } = await uploadJson({
  113. sessionToken: identity.sessionToken,
  114. key: testKey,
  115. content: testContent,
  116. });
  117. console.log('状态: OK ✓');
  118. console.log(`验证上报: ${publicUrl}`);
  119. console.log('\n=== 结论 ===');
  120. console.log('所有检查通过 ✓');
  121. } catch (e) {
  122. console.error(`\n[错误] 连通性检查失败: ${e.message}`);
  123. process.exit(1);
  124. }
  125. }
  126. async function cmdConfig() {
  127. const identity = resolveIdentity();
  128. const summary = identitySummary(identity);
  129. console.log('当前配置(敏感信息已脱敏):');
  130. console.log(JSON.stringify(summary, null, 2));
  131. console.log('\n上传端点:', ENDPOINTS.functions);
  132. console.log('公网基址:', ENDPOINTS.publicBase);
  133. }
  134. // ──────────────────────────────────────────────────────────────────────────────
  135. // 路由
  136. // ──────────────────────────────────────────────────────────────────────────────
  137. const [, , cmd] = process.argv;
  138. if (!cmd || cmd === '--help' || cmd === '-h') {
  139. console.log(`agent-log v${VERSION} — 数字生命自报运行日志 (未来飞马)
  140. 用法: agent-log <command>
  141. 命令:
  142. report 采集本机指标并上传到个人 S3 空间,输出 publicUrl
  143. collect 仅采集指标(输出 JSON,不上传)
  144. check 打印身份解析 + 云函数连通性 + 验证上报
  145. config 显示当前配置(敏感信息脱敏,token 只显示前8字符+长度)
  146. 示例(cron 每6小时):
  147. 0 */6 * * * node /path/to/bin/agent-log.mjs report >> /var/log/agent-log.log 2>&1
  148. 重任务:
  149. node bin/agent-log.mjs report # 任务开始/结束各调一次
  150. 凭据配置(零密钥,不需要 AK/SK):
  151. export FMODE_SESSION_TOKEN=r:xxxxxxxxxxxxxxxxxxxxxxxx
  152. 或在 ~/.fmode/config.json 中添加 "sessionToken": "r:xxx"
  153. `);
  154. process.exit(0);
  155. }
  156. const handlers = { report: cmdReport, collect: cmdCollect, check: cmdCheck, config: cmdConfig };
  157. const handler = handlers[cmd];
  158. if (!handler) {
  159. console.error(`未知命令: ${cmd} (可用: report, collect, check, config)`);
  160. process.exit(1);
  161. }
  162. handler().catch(e => {
  163. console.error(`[错误] ${e.message}`);
  164. process.exit(1);
  165. });