| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- #!/usr/bin/env node
- // Copyright (c) 未来飞马
- //
- // MIT License — Copyright (c) 2026 Fmode (未来飞马)
- //
- // Trademark Notice:
- // The MIT license grants copyright permissions for source code only.
- // It does NOT grant any rights to use trademarks including "未来飞马",
- // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
- // Any use of these trademarks requires separate written permission.
- import { spawnSync } from 'node:child_process';
- import { fileURLToPath } from 'node:url';
- import path from 'node:path';
- import { resolveIdentity, identitySummary } from '../lib/identity.mjs';
- import { uploadJson, ENDPOINTS } from '../lib/upload.mjs';
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
- const COLLECT_PY = path.join(__dirname, '..', 'lib', 'collect.py');
- const VERSION = '2.0.0';
- // ──────────────────────────────────────────────────────────────────────────────
- // 工具函数
- // ──────────────────────────────────────────────────────────────────────────────
- function makeLogKey(now = new Date()) {
- const pad = n => String(n).padStart(2, '0');
- const d = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
- const t = `${pad(now.getHours())}${pad(now.getMinutes())}`;
- return `log/${d}/${t}.json`;
- }
- function collectMetrics(identity) {
- const args = [COLLECT_PY];
- if (identity.agentId) args.push('--agent-id', identity.agentId);
- if (identity.userid) args.push('--userid', identity.userid);
- const result = spawnSync('python3', args, {
- timeout: 30_000,
- encoding: 'utf-8',
- });
- if (result.error) {
- throw new Error(`python3 启动失败: ${result.error.message}`);
- }
- if (result.status !== 0) {
- throw new Error(`采集脚本非零退出 (${result.status}): ${(result.stderr || '').slice(0, 300)}`);
- }
- try {
- return JSON.parse(result.stdout);
- } catch {
- throw new Error(`指标 JSON 解析失败,输出: ${(result.stdout || '').slice(0, 200)}`);
- }
- }
- function assertToken(identity) {
- if (!identity.sessionToken) {
- console.error('[错误] 缺少 sessionToken,无法调用云函数上传通道');
- console.error('\n修复方式(任选其一):');
- console.error(' 1. export FMODE_SESSION_TOKEN=r:xxxxxxxxxxxxxxxxxxxxxxxx');
- console.error(' 2. 在 ~/.fmode/config.json 中添加 "sessionToken": "r:xxx"');
- console.error(' 3. 在 ~/.fmode/config/user.json 中添加 "sessionToken": "r:xxx"');
- console.error('\ntoken 可在 Fmode 控制台 → 账号设置 → Session Token 处获取。');
- process.exit(2);
- }
- }
- // ──────────────────────────────────────────────────────────────────────────────
- // 命令实现
- // ──────────────────────────────────────────────────────────────────────────────
- async function cmdReport() {
- const identity = resolveIdentity();
- assertToken(identity);
- if (!identity.userid) {
- console.log('[report] 未找到本地 userid,将由云函数从 token 自动推断');
- }
- console.log('[report] 采集本机指标...');
- const metrics = collectMetrics(identity);
- const key = makeLogKey();
- console.log(`[report] 上传到 ${key} ...`);
- const { publicUrl } = await uploadJson({
- sessionToken: identity.sessionToken,
- key,
- content: metrics,
- });
- console.log('[report] 上报成功');
- console.log(`publicUrl: ${publicUrl}`);
- return publicUrl;
- }
- async function cmdCollect() {
- const identity = resolveIdentity();
- const metrics = collectMetrics(identity);
- process.stdout.write(JSON.stringify(metrics, null, 2) + '\n');
- }
- async function cmdCheck() {
- const identity = resolveIdentity();
- const summary = identitySummary(identity);
- console.log('=== 身份解析 ===');
- console.log(`userid: ${summary.userid}`);
- console.log(`sessionToken: ${summary.sessionToken}`);
- console.log(`agentId: ${summary.agentId}`);
- console.log('\n=== 云函数连通性 ===');
- console.log(`端点: ${ENDPOINTS.functions}`);
- console.log(`函数 ID: AlP56LCKFm (fmodeagent-upload-url)`);
- if (!identity.sessionToken) {
- console.error('\n[错误] 无 sessionToken,无法测试连通性');
- console.error('修复: export FMODE_SESSION_TOKEN=r:xxx 或写入 ~/.fmode/config.json');
- process.exit(2);
- }
- // 用一次真实的轻量上报来验证连通性(check 专用路径)
- const testContent = {
- schema_version: '2.0',
- type: 'connectivity-check',
- timestamp: new Date().toISOString(),
- agent_id: identity.agentId,
- };
- const now = new Date();
- const pad = n => String(n).padStart(2, '0');
- const testKey = `log/check/${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}/${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.json`;
- try {
- const { publicUrl } = await uploadJson({
- sessionToken: identity.sessionToken,
- key: testKey,
- content: testContent,
- });
- console.log('状态: OK ✓');
- console.log(`验证上报: ${publicUrl}`);
- console.log('\n=== 结论 ===');
- console.log('所有检查通过 ✓');
- } catch (e) {
- console.error(`\n[错误] 连通性检查失败: ${e.message}`);
- process.exit(1);
- }
- }
- async function cmdConfig() {
- const identity = resolveIdentity();
- const summary = identitySummary(identity);
- console.log('当前配置(敏感信息已脱敏):');
- console.log(JSON.stringify(summary, null, 2));
- console.log('\n上传端点:', ENDPOINTS.functions);
- console.log('公网基址:', ENDPOINTS.publicBase);
- }
- // ──────────────────────────────────────────────────────────────────────────────
- // 路由
- // ──────────────────────────────────────────────────────────────────────────────
- const [, , cmd] = process.argv;
- if (!cmd || cmd === '--help' || cmd === '-h') {
- console.log(`agent-log v${VERSION} — 数字生命自报运行日志 (未来飞马)
- 用法: agent-log <command>
- 命令:
- report 采集本机指标并上传到个人 S3 空间,输出 publicUrl
- collect 仅采集指标(输出 JSON,不上传)
- check 打印身份解析 + 云函数连通性 + 验证上报
- config 显示当前配置(敏感信息脱敏,token 只显示前8字符+长度)
- 示例(cron 每6小时):
- 0 */6 * * * node /path/to/bin/agent-log.mjs report >> /var/log/agent-log.log 2>&1
- 重任务:
- node bin/agent-log.mjs report # 任务开始/结束各调一次
- 凭据配置(零密钥,不需要 AK/SK):
- export FMODE_SESSION_TOKEN=r:xxxxxxxxxxxxxxxxxxxxxxxx
- 或在 ~/.fmode/config.json 中添加 "sessionToken": "r:xxx"
- `);
- process.exit(0);
- }
- const handlers = { report: cmdReport, collect: cmdCollect, check: cmdCheck, config: cmdConfig };
- const handler = handlers[cmd];
- if (!handler) {
- console.error(`未知命令: ${cmd} (可用: report, collect, check, config)`);
- process.exit(1);
- }
- handler().catch(e => {
- console.error(`[错误] ${e.message}`);
- process.exit(1);
- });
|