agent-log.mjs 7.8 KB

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