#!/usr/bin/env node // Copyright (c) 未来飞马 // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // Trademark Notice: // The MPL-2.0 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 命令: 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); });