aissia/src/modules/MonitoringModule.cpp
StillHammer bc3b6cbaba feat: Implement Phase 1 complete - All 6 core modules
## New Modules
- StorageModule: SQLite persistence for sessions, app usage, conversations
- MonitoringModule: Cross-platform window tracking (Win32/X11)
- AIModule: Multi-provider LLM integration with agentic tool loop
- VoiceModule: TTS/STT coordination with speak queue

## Shared Libraries
- AissiaLLM: ILLMProvider abstraction (Claude + OpenAI providers)
- AissiaPlatform: IWindowTracker abstraction (Win32 + X11)
- AissiaAudio: ITTSEngine (SAPI/espeak) + ISTTEngine (Whisper API)
- HttpClient: Header-only HTTP client with OpenSSL

## Configuration
- Added JSON configs for all modules (storage, monitoring, ai, voice)
- Multi-provider LLM config with Claude and OpenAI support

## Dependencies
- SQLite3, OpenSSL, cpp-httplib (FetchContent)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 00:42:18 +08:00

223 lines
8.1 KiB
C++

#include "MonitoringModule.h"
#include <grove/JsonDataNode.h>
#include <algorithm>
namespace aissia {
MonitoringModule::MonitoringModule() {
m_logger = spdlog::get("MonitoringModule");
if (!m_logger) {
m_logger = spdlog::stdout_color_mt("MonitoringModule");
}
m_config = std::make_unique<grove::JsonDataNode>("config");
}
void MonitoringModule::setConfiguration(const grove::IDataNode& configNode,
grove::IIO* io,
grove::ITaskScheduler* scheduler) {
m_io = io;
m_config = std::make_unique<grove::JsonDataNode>("config");
m_pollIntervalMs = configNode.getInt("poll_interval_ms", 1000);
m_idleThresholdSeconds = configNode.getInt("idle_threshold_seconds", 300);
m_enabled = configNode.getBool("enabled", true);
// Load productive apps list
m_productiveApps.clear();
auto* prodNode = configNode.getChildReadOnly("productive_apps");
if (prodNode) {
for (const auto& name : prodNode->getChildNames()) {
m_productiveApps.insert(prodNode->getString(name, ""));
}
}
// Default productive apps
if (m_productiveApps.empty()) {
m_productiveApps = {"Code", "code", "CLion", "clion", "Visual Studio",
"devenv", "rider", "idea", "pycharm"};
}
// Load distracting apps list
m_distractingApps.clear();
auto* distNode = configNode.getChildReadOnly("distracting_apps");
if (distNode) {
for (const auto& name : distNode->getChildNames()) {
m_distractingApps.insert(distNode->getString(name, ""));
}
}
if (m_distractingApps.empty()) {
m_distractingApps = {"Discord", "discord", "Steam", "steam",
"firefox", "chrome", "YouTube"};
}
// Create window tracker
m_tracker = WindowTrackerFactory::create();
m_logger->info("MonitoringModule configure: poll={}ms, idle={}s, platform={}",
m_pollIntervalMs, m_idleThresholdSeconds,
m_tracker ? m_tracker->getPlatformName() : "none");
}
const grove::IDataNode& MonitoringModule::getConfiguration() {
return *m_config;
}
void MonitoringModule::process(const grove::IDataNode& input) {
if (!m_enabled || !m_tracker || !m_tracker->isAvailable()) return;
float currentTime = input.getDouble("gameTime", 0.0);
// Poll based on interval
float pollIntervalSec = m_pollIntervalMs / 1000.0f;
if (currentTime - m_lastPollTime < pollIntervalSec) return;
m_lastPollTime = currentTime;
checkCurrentApp(currentTime);
checkIdleState(currentTime);
}
void MonitoringModule::checkCurrentApp(float currentTime) {
std::string newApp = m_tracker->getCurrentAppName();
std::string newTitle = m_tracker->getCurrentWindowTitle();
if (newApp != m_currentApp) {
// App changed
int duration = static_cast<int>(currentTime - m_appStartTime);
if (!m_currentApp.empty() && duration > 0) {
m_appDurations[m_currentApp] += duration;
// Update productivity counters
if (isProductiveApp(m_currentApp)) {
m_totalProductiveSeconds += duration;
} else if (isDistractingApp(m_currentApp)) {
m_totalDistractingSeconds += duration;
}
publishAppChanged(m_currentApp, newApp, duration);
}
m_currentApp = newApp;
m_currentWindowTitle = newTitle;
m_appStartTime = currentTime;
m_logger->debug("App: {} - {}", m_currentApp, m_currentWindowTitle.substr(0, 50));
}
}
void MonitoringModule::checkIdleState(float currentTime) {
bool wasIdle = m_isIdle;
m_isIdle = m_tracker->isUserIdle(m_idleThresholdSeconds);
if (m_isIdle && !wasIdle) {
m_logger->info("Utilisateur inactif ({}s)", m_idleThresholdSeconds);
if (m_io) {
auto event = std::make_unique<grove::JsonDataNode>("idle");
event->setString("type", "idle_detected");
event->setInt("idleSeconds", m_tracker->getIdleTimeSeconds());
m_io->publish("monitoring:idle_detected", std::move(event));
}
}
else if (!m_isIdle && wasIdle) {
m_logger->info("Activite reprise");
if (m_io) {
auto event = std::make_unique<grove::JsonDataNode>("active");
event->setString("type", "activity_resumed");
m_io->publish("monitoring:activity_resumed", std::move(event));
}
}
}
bool MonitoringModule::isProductiveApp(const std::string& appName) const {
// Check exact match
if (m_productiveApps.count(appName)) return true;
// Check if app name contains productive keyword
std::string lowerApp = appName;
std::transform(lowerApp.begin(), lowerApp.end(), lowerApp.begin(), ::tolower);
for (const auto& prod : m_productiveApps) {
std::string lowerProd = prod;
std::transform(lowerProd.begin(), lowerProd.end(), lowerProd.begin(), ::tolower);
if (lowerApp.find(lowerProd) != std::string::npos) return true;
}
return false;
}
bool MonitoringModule::isDistractingApp(const std::string& appName) const {
if (m_distractingApps.count(appName)) return true;
std::string lowerApp = appName;
std::transform(lowerApp.begin(), lowerApp.end(), lowerApp.begin(), ::tolower);
for (const auto& dist : m_distractingApps) {
std::string lowerDist = dist;
std::transform(lowerDist.begin(), lowerDist.end(), lowerDist.begin(), ::tolower);
if (lowerApp.find(lowerDist) != std::string::npos) return true;
}
return false;
}
void MonitoringModule::publishAppChanged(const std::string& oldApp, const std::string& newApp, int duration) {
if (!m_io) return;
auto event = std::make_unique<grove::JsonDataNode>("app_changed");
event->setString("oldApp", oldApp);
event->setString("newApp", newApp);
event->setInt("duration", duration);
event->setBool("wasProductive", isProductiveApp(oldApp));
event->setBool("wasDistracting", isDistractingApp(oldApp));
m_io->publish("monitoring:app_changed", std::move(event));
}
std::unique_ptr<grove::IDataNode> MonitoringModule::getHealthStatus() {
auto status = std::make_unique<grove::JsonDataNode>("status");
status->setString("status", m_enabled ? "running" : "disabled");
status->setString("currentApp", m_currentApp);
status->setBool("isIdle", m_isIdle);
status->setInt("totalProductiveSeconds", m_totalProductiveSeconds);
status->setInt("totalDistractingSeconds", m_totalDistractingSeconds);
status->setString("platform", m_tracker ? m_tracker->getPlatformName() : "none");
return status;
}
void MonitoringModule::shutdown() {
m_logger->info("MonitoringModule arrete. Productif: {}s, Distrait: {}s",
m_totalProductiveSeconds, m_totalDistractingSeconds);
}
std::unique_ptr<grove::IDataNode> MonitoringModule::getState() {
auto state = std::make_unique<grove::JsonDataNode>("state");
state->setString("currentApp", m_currentApp);
state->setDouble("appStartTime", m_appStartTime);
state->setBool("isIdle", m_isIdle);
state->setInt("totalProductiveSeconds", m_totalProductiveSeconds);
state->setInt("totalDistractingSeconds", m_totalDistractingSeconds);
return state;
}
void MonitoringModule::setState(const grove::IDataNode& state) {
m_currentApp = state.getString("currentApp", "");
m_appStartTime = state.getDouble("appStartTime", 0.0);
m_isIdle = state.getBool("isIdle", false);
m_totalProductiveSeconds = state.getInt("totalProductiveSeconds", 0);
m_totalDistractingSeconds = state.getInt("totalDistractingSeconds", 0);
m_logger->info("Etat restore: app={}, productif={}s", m_currentApp, m_totalProductiveSeconds);
}
} // namespace aissia
extern "C" {
grove::IModule* createModule() {
return new aissia::MonitoringModule();
}
void destroyModule(grove::IModule* module) {
delete module;
}
}