|
@@ -0,0 +1,201 @@
|
|
|
|
|
+#!/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 <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);
|
|
|
|
|
+});
|