Initial commit: Hermes AI Bridge WordPress plugin for cPanel agent runtime

This commit is contained in:
2026-07-03 08:49:52 +02:00
commit 17d218c3b6
9 changed files with 981 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
<?php
namespace HermesAiBridge;
defined('ABSPATH') or die();
class AiProvider {
protected $api_key;
protected $base_url;
protected $model;
protected $max_tokens;
protected $provider;
public function __construct() {
$this->provider = get_option('hermes_ai_provider', 'openai');
$this->model = get_option('hermes_ai_model', 'gpt-4');
$this->max_tokens = intval(get_option('hermes_ai_max_tokens', 2048));
$this->api_key = get_option('hermes_ai_api_key', '');
$this->base_url = get_option('hermes_ai_base_url', 'https://api.openai.com/v1');
}
/**
* Generate content from a prompt.
*/
public function generate($prompt, $context = 'general') {
$system_prompt = $this->get_system_prompt($context);
return $this->call_ai_api($system_prompt, $prompt);
}
/**
* Call the configured AI provider API.
*/
protected function call_ai_api($system, $user_prompt) {
if (empty($this->api_key)) {
return new \WP_Error('no_api_key', 'AI provider API key not configured. Set it in WordPress Admin → Hermes AI.');
}
$body = [
'model' => $this->model,
'max_tokens' => $this->max_tokens,
'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user_prompt],
],
];
$response = wp_remote_post($this->base_url . '/chat/completions', [
'timeout' => 120,
'headers' => [
'Authorization' => 'Bearer ' . $this->api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode($body),
]);
if (is_wp_error($response)) {
RestController::log('error', 'AI API call failed', ['error' => $response->get_error_message()]);
return $response;
}
$status = wp_remote_retrieve_response_code($response);
$body = json_decode(wp_remote_retrieve_body($response), true);
if ($status !== 200) {
$error_msg = $body['error']['message'] ?? 'Unknown API error';
RestController::log('error', 'AI API error', ['status' => $status, 'error' => $error_msg]);
return new \WP_Error('ai_api_error', "AI API returned $status: $error_msg");
}
$content = $body['choices'][0]['message']['content'] ?? '';
RestController::log('info', 'AI content generated', ['model' => $this->model, 'tokens' => $body['usage']['total_tokens'] ?? 0]);
return $content;
}
/**
* Newsletter-specific batch generation.
*/
public function generate_newsletter_content() {
$prompt = "Write a newsletter for Falah Letter — Islamic fintech and community updates. "
. "Include sections: featured article, community news, upcoming events, featured project. "
. "Write in a professional but warm tone. Target length: 1500-2000 words.";
$content = $this->generate($prompt, 'newsletter');
if (is_wp_error($content)) return;
$post_id = wp_insert_post([
'post_title' => 'Falah Letter — ' . wp_date('F j, Y'),
'post_content' => $content,
'post_type' => 'post',
'post_status' => 'draft',
'tags_input' => ['newsletter', 'falah-letter', 'automated'],
]);
if ($post_id) {
RestController::log('info', 'Newsletter draft generated', ['post_id' => $post_id]);
}
}
protected function get_system_prompt($context) {
$prompts = [
'post' => 'You are a content writer for FalahOS, an Islamic fintech platform. '
. 'Write engaging, well-structured content. Use markdown for formatting. '
. 'Keep paragraphs short and scannable.',
'newsletter' => 'You are the editor of Falah Letter, a weekly newsletter about '
. 'Islamic fintech, community updates, and FalahOS platform news. '
. 'Write in a professional warm tone.',
'social' => 'You are a social media manager for FalahOS. Create engaging posts '
. 'optimized for LinkedIn and Twitter. Use emojis sparingly.',
];
return $prompts[$context] ?? $prompts['post'];
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace HermesAiBridge;
defined('ABSPATH') or die();
class Autoloader {
public static function register() {
spl_autoload_register([__CLASS__, 'autoload']);
}
private static function autoload($class) {
$prefix = 'HermesAiBridge\\';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) return;
$relative_class = substr($class, $len);
$file = HERMES_AI_BRIDGE_DIR . 'includes/' . strtolower(str_replace('_', '-', $relative_class)) . '.php';
if (file_exists($file)) require_once $file;
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace HermesAiBridge;
defined('ABSPATH') or die();
class Core {
public static function init() {
// Load text domain
load_plugin_textdomain('hermes-ai-bridge', false, dirname(plugin_basename(HERMES_AI_BRIDGE_FILE)) . '/languages');
// Register custom post type
add_action('init', ['HermesAiBridge\Scheduler', 'register_post_type']);
// REST API
add_action('rest_api_init', [__CLASS__, 'register_routes']);
// WP-Cron hooks
add_action('hermes_hourly_tick', [__CLASS__, 'hourly_tick']);
add_action('hermes_daily_tick', [__CLASS__, 'daily_tick']);
// AJAX handlers
add_action('wp_ajax_hermes_regenerate_key', [__CLASS__, 'ajax_regenerate_key']);
// Admin
if (is_admin()) {
add_action('admin_menu', [__CLASS__, 'admin_menu']);
add_action('admin_enqueue_scripts', [__CLASS__, 'admin_assets']);
}
}
public static function register_routes() {
$controller = new RestController();
$controller->register_routes();
}
public static function hourly_tick() {
$scheduler = new Scheduler();
$scheduler->process_due_tasks();
}
public static function daily_tick() {
$ai = new AiProvider();
$ai->generate_newsletter_content();
}
public static function admin_menu() {
add_menu_page(
'Hermes AI Bridge',
'Hermes AI',
'manage_options',
'hermes-ai-bridge',
[__CLASS__, 'admin_page'],
'dashicons-rest-api',
80
);
}
public static function admin_page() {
$api_key = get_option('hermes_api_key');
?>
<div class="wrap">
<h1>Hermes AI Bridge</h1>
<hr>
<h2>API Key</h2>
<p>Use this key in the <code>X-API-Key</code> header for REST API calls.</p>
<input type="text" readonly="readonly" value="<?php echo esc_attr($api_key); ?>" style="width:600px;font-family:monospace;" onclick="this.select()" />
<p><button id="hermes-regenerate-key" class="button">Regenerate Key</button></p>
<hr>
<h2>AI Provider Settings</h2>
<form method="post" action="options.php">
<?php settings_fields('hermes_ai_settings'); ?>
<table class="form-table">
<tr>
<th>Provider</th>
<td>
<select name="hermes_ai_provider">
<option value="openai" <?php selected(get_option('hermes_ai_provider'), 'openai'); ?>>OpenAI</option>
<option value="anthropic" <?php selected(get_option('hermes_ai_provider'), 'anthropic'); ?>>Anthropic</option>
<option value="opencode" <?php selected(get_option('hermes_ai_provider'), 'opencode'); ?>>OpenClaw (OpenCode)</option>
</select>
</td>
</tr>
<tr>
<th>Model</th>
<td><input type="text" name="hermes_ai_model" value="<?php echo esc_attr(get_option('hermes_ai_model', 'gpt-4')); ?>" class="regular-text" /></td>
</tr>
<tr>
<th>API Key (for AI provider)</th>
<td><input type="password" name="hermes_ai_api_key" value="<?php echo esc_attr(get_option('hermes_ai_api_key', '')); ?>" class="regular-text" /></td>
</tr>
<tr>
<th>API Base URL</th>
<td><input type="url" name="hermes_ai_base_url" value="<?php echo esc_attr(get_option('hermes_ai_base_url', 'https://api.openai.com/v1')); ?>" class="regular-text" /></td>
</tr>
<tr>
<th>Max Tokens</th>
<td><input type="number" name="hermes_ai_max_tokens" value="<?php echo esc_attr(get_option('hermes_ai_max_tokens', 2048)); ?>" min="1" max="128000" /></td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<script>
document.getElementById('hermes-regenerate-key')?.addEventListener('click', function() {
if (!confirm('Regenerate API key? Existing integrations will stop working.')) return;
fetch(ajaxurl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=hermes_regenerate_key&_wpnonce=<?php echo wp_create_nonce('hermes_regenerate_key'); ?>'
}).then(r => r.json()).then(d => { if(d.success) location.reload(); });
});
</script>
<?php
}
public static function admin_assets($hook) {
if ($hook !== 'toplevel_page_hermes-ai-bridge') return;
wp_enqueue_style('hermes-admin', HERMES_AI_BRIDGE_URL . 'assets/admin.css', [], HERMES_AI_BRIDGE_VERSION);
}
public static function ajax_regenerate_key() {
check_ajax_referer('hermes_regenerate_key');
if (!current_user_can('manage_options')) return;
$new_key = wp_hash(wp_generate_password(64, true, true));
update_option('hermes_api_key', $new_key);
wp_send_json_success(['key' => $new_key]);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace HermesAiBridge;
defined('ABSPATH') or die();
class Installer {
public static function activate() {
self::create_tables();
self::set_default_options();
self::schedule_cron_events();
flush_rewrite_rules();
}
public static function deactivate() {
self::clear_cron_events();
flush_rewrite_rules();
}
private static function create_tables() {
global $wpdb;
$charset = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}hermes_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
level VARCHAR(20) NOT NULL DEFAULT 'info',
message TEXT NOT NULL,
context LONGTEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_level (level),
INDEX idx_created (created_at)
) $charset;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
private static function set_default_options() {
if (!get_option('hermes_api_key')) {
update_option('hermes_api_key', wp_hash(wp_generate_password(64, true, true)));
}
add_option('hermes_ai_provider', 'openai');
add_option('hermes_ai_model', 'gpt-4');
add_option('hermes_ai_max_tokens', 2048);
add_option('hermes_cron_interval', 'hourly');
}
private static function schedule_cron_events() {
if (!wp_next_scheduled('hermes_hourly_tick')) {
wp_schedule_event(time(), 'hourly', 'hermes_hourly_tick');
}
if (!wp_next_scheduled('hermes_daily_tick')) {
wp_schedule_event(time(), 'daily', 'hermes_daily_tick');
}
}
private static function clear_cron_events() {
wp_clear_scheduled_hook('hermes_hourly_tick');
wp_clear_scheduled_hook('hermes_daily_tick');
}
}
+145
View File
@@ -0,0 +1,145 @@
<?php
/**
* Nextcloud Bridge — File operations on same-server Nextcloud instance.
*/
namespace HermesAiBridge;
defined('ABSPATH') or die();
class NextcloudBridge {
protected $base_url;
protected $username;
protected $password;
protected $webdav_url;
public function __construct() {
$this->base_url = get_option('hermes_nextcloud_url', '');
$this->username = get_option('hermes_nextcloud_user', '');
$this->password = get_option('hermes_nextcloud_pass', '');
$this->webdav_url = rtrim($this->base_url, '/') . '/remote.php/dav/files/' . $this->username . '/';
}
/**
* List files in Nextcloud directory.
*/
public function list_files($path = '/') {
if (!$this->is_configured()) return new \WP_Error('not_configured', 'Nextcloud not configured');
$url = $this->webdav_url . ltrim($path, '/');
$response = $this->propfind($url);
if (is_wp_error($response)) return $response;
$files = [];
$dom = new \DOMDocument();
$dom->loadXML($response);
foreach ($dom->getElementsByTagNameNS('DAV:', 'response') as $node) {
$href = $node->getElementsByTagNameNS('DAV:', 'href')->item(0)->nodeValue ?? '';
if ($href === $this->webdav_url) continue; // skip root
$files[] = [
'path' => $href,
'name' => basename($href),
'size' => $node->getElementsByTagNameNS('DAV:', 'getcontentlength')->item(0)->nodeValue ?? 0,
'modified' => $node->getElementsByTagNameNS('DAV:', 'getlastmodified')->item(0)->nodeValue ?? '',
'type' => $node->getElementsByTagNameNS('DAV:', 'getcontenttype')->item(0)->nodeValue ?? 'directory',
];
}
return $files;
}
/**
* Upload a file to Nextcloud from WordPress Media Library.
*/
public function upload_from_media($attachment_id, $destination_path = '/') {
if (!$this->is_configured()) return new \WP_Error('not_configured', 'Nextcloud not configured');
$file_path = get_attached_file($attachment_id);
if (!$file_path || !file_exists($file_path)) {
return new \WP_Error('file_not_found', 'Attachment file not found on disk');
}
$filename = basename($file_path);
$dest_url = $this->webdav_url . ltrim($destination_path, '/') . '/' . $filename;
$response = wp_remote_request($dest_url, [
'method' => 'PUT',
'headers' => [
'Authorization' => 'Basic ' . base64_encode($this->username . ':' . $this->password),
'Content-Type' => get_post_mime_type($attachment_id) ?: 'application/octet-stream',
],
'body' => file_get_contents($file_path),
'timeout' => 60,
]);
if (is_wp_error($response)) return $response;
$status = wp_remote_retrieve_response_code($response);
if ($status < 200 || $status >= 300) {
return new \WP_Error('upload_failed', "WebDAV upload returned $status");
}
RestController::log('info', 'File uploaded to Nextcloud', ['file' => $filename, 'dest' => $dest_url]);
return ['success' => true, 'path' => $dest_url];
}
/**
* Download a file from Nextcloud into WordPress Media Library.
*/
public function import_to_media($nextcloud_path) {
if (!$this->is_configured()) return new \WP_Error('not_configured', 'Nextcloud not configured');
$url = $this->webdav_url . ltrim($nextcloud_path, '/');
$response = wp_remote_get($url, [
'headers' => [
'Authorization' => 'Basic ' . base64_encode($this->username . ':' . $this->password),
],
'timeout' => 60,
]);
if (is_wp_error($response)) return $response;
$body = wp_remote_retrieve_body($response);
$filename = basename($nextcloud_path);
$upload = wp_upload_bits($filename, null, $body);
if ($upload['error']) {
return new \WP_Error('upload_failed', $upload['error']);
}
$attachment = [
'post_mime_type' => wp_check_filetype($filename)['type'] ?: 'application/octet-stream',
'post_title' => sanitize_file_name($filename),
'post_content' => '',
'post_status' => 'inherit',
'guid' => $upload['url'],
];
$attach_id = wp_insert_attachment($attachment, $upload['file']);
require_once ABSPATH . 'wp-admin/includes/image.php';
wp_generate_attachment_metadata($attach_id, $upload['file']);
RestController::log('info', 'File imported from Nextcloud', ['file' => $filename, 'attachment_id' => $attach_id]);
return ['success' => true, 'attachment_id' => $attach_id, 'url' => $upload['url']];
}
protected function propfind($url) {
$response = wp_remote_request($url, [
'method' => 'PROPFIND',
'headers' => [
'Authorization' => 'Basic ' . base64_encode($this->username . ':' . $this->password),
'Depth' => '1',
],
'timeout' => 30,
]);
if (is_wp_error($response)) return $response;
return wp_remote_retrieve_body($response);
}
protected function is_configured() {
return !empty($this->base_url) && !empty($this->username) && !empty($this->password);
}
}
+344
View File
@@ -0,0 +1,344 @@
<?php
namespace HermesAiBridge;
defined('ABSPATH') or die();
class RestController {
protected $namespace = 'hermes/v1';
public function register_routes() {
// Health
register_rest_route($this->namespace, '/health', [
'methods' => 'GET',
'callback' => [$this, 'health'],
'permission_callback' => '__return_true',
]);
// Content generation
register_rest_route($this->namespace, '/content/generate', [
'methods' => 'POST',
'callback' => [$this, 'generate_content'],
'permission_callback' => [$this, 'check_auth'],
'args' => [
'prompt' => ['required' => true, 'type' => 'string'],
'post_type' => ['default' => 'post', 'type' => 'string'],
'status' => ['default' => 'draft', 'type' => 'string'],
'title' => ['type' => 'string'],
],
]);
// Content schedule
register_rest_route($this->namespace, '/content/schedule', [
'methods' => 'POST',
'callback' => [$this, 'schedule_content'],
'permission_callback' => [$this, 'check_auth'],
'args' => [
'prompt' => ['required' => true, 'type' => 'string'],
'scheduled_date' => ['required' => true, 'type' => 'string'],
'post_type' => ['default' => 'post', 'type' => 'string'],
'title' => ['type' => 'string'],
],
]);
// Posts CRUD
register_rest_route($this->namespace, '/posts', [
'methods' => 'GET',
'callback' => [$this, 'list_posts'],
'permission_callback' => [$this, 'check_auth'],
]);
register_rest_route($this->namespace, '/posts/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_post'],
'permission_callback' => [$this, 'check_auth'],
]);
register_rest_route($this->namespace, '/posts/(?P<id>\d+)', [
'methods' => ['PUT', 'PATCH'],
'callback' => [$this, 'update_post'],
'permission_callback' => [$this, 'check_auth'],
]);
register_rest_route($this->namespace, '/posts/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'delete_post'],
'permission_callback' => [$this, 'check_auth'],
]);
// Users
register_rest_route($this->namespace, '/users', [
'methods' => 'GET',
'callback' => [$this, 'list_users'],
'permission_callback' => [$this, 'check_auth'],
]);
register_rest_route($this->namespace, '/users/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_user'],
'permission_callback' => [$this, 'check_auth'],
]);
// Settings
register_rest_route($this->namespace, '/settings', [
'methods' => 'GET',
'callback' => [$this, 'get_settings'],
'permission_callback' => [$this, 'check_auth'],
]);
register_rest_route($this->namespace, '/settings', [
'methods' => 'PUT',
'callback' => [$this, 'update_settings'],
'permission_callback' => [$this, 'check_auth'],
]);
// Schedules
register_rest_route($this->namespace, '/schedules', [
'methods' => 'GET',
'callback' => [$this, 'list_schedules'],
'permission_callback' => [$this, 'check_auth'],
]);
// Logs
register_rest_route($this->namespace, '/logs', [
'methods' => 'GET',
'callback' => [$this, 'get_logs'],
'permission_callback' => [$this, 'check_auth'],
'args' => [
'level' => ['type' => 'string'],
'limit' => ['default' => 50, 'type' => 'integer'],
'offset' => ['default' => 0, 'type' => 'integer'],
],
]);
}
public function check_auth($request) {
$key = $request->get_header('X-API-Key');
if (!$key) {
// Also check query param for webhook compatibility
$key = $request->get_param('api_key');
}
$stored = get_option('hermes_api_key');
return $stored && hash_equals($stored, $key);
}
public function health() {
return [
'status' => 'ok',
'version' => HERMES_AI_BRIDGE_VERSION,
'wordpress' => get_bloginfo('version'),
'php' => PHP_VERSION,
'site' => get_bloginfo('url'),
];
}
public function generate_content($request) {
$prompt = $request->get_param('prompt');
$post_type = $request->get_param('post_type');
$status = $request->get_param('status');
$title = $request->get_param('title');
$ai = new AiProvider();
$content = $ai->generate($prompt, 'post');
if (is_wp_error($content)) {
return new \WP_REST_Response(['error' => $content->get_error_message()], 500);
}
$post_id = wp_insert_post([
'post_title' => $title ?: wp_trim_words($content, 10, ''),
'post_content' => $content,
'post_type' => $post_type,
'post_status' => $status,
]);
if (is_wp_error($post_id)) {
return new \WP_REST_Response(['error' => $post_id->get_error_message()], 500);
}
self::log('info', 'Content generated', ['post_id' => $post_id, 'prompt' => substr($prompt, 0, 100)]);
return [
'success' => true,
'post_id' => $post_id,
'edit_url' => get_edit_post_link($post_id, 'raw'),
];
}
public function schedule_content($request) {
$prompt = $request->get_param('prompt');
$scheduled_date = $request->get_param('scheduled_date');
$post_type = $request->get_param('post_type');
$title = $request->get_param('title');
$task_id = wp_insert_post([
'post_title' => '[AI Task] ' . ($title ?: 'Scheduled: ' . date('Y-m-d H:i')),
'post_content' => wp_json_encode([
'prompt' => $prompt,
'post_type' => $post_type,
'scheduled_date' => $scheduled_date,
]),
'post_type' => 'hermes_schedule',
'post_status' => 'publish',
'meta_input' => [
'_hermes_scheduled_date' => $scheduled_date,
'_hermes_task_type' => 'content_generation',
'_hermes_prompt' => $prompt,
],
]);
if (is_wp_error($task_id)) {
return new \WP_REST_Response(['error' => $task_id->get_error_message()], 500);
}
self::log('info', 'Content scheduled', ['task_id' => $task_id, 'date' => $scheduled_date]);
return [
'success' => true,
'task_id' => $task_id,
'scheduled_date' => $scheduled_date,
];
}
public function list_posts($request) {
$posts = get_posts([
'post_type' => 'post',
'posts_per_page' => $request->get_param('per_page') ?: 20,
'post_status' => 'any',
]);
return array_map(function($p) {
return [
'id' => $p->ID,
'title' => $p->post_title,
'status' => $p->post_status,
'date' => $p->post_date,
'modified' => $p->post_modified,
'permalink' => get_permalink($p->ID),
];
}, $posts);
}
public function get_post($request) {
$post = get_post($request->get_param('id'));
if (!$post) return new \WP_REST_Response(['error' => 'Post not found'], 404);
return [
'id' => $post->ID,
'title' => $post->post_title,
'content' => $post->post_content,
'excerpt' => $post->post_excerpt,
'status' => $post->post_status,
'date' => $post->post_date,
'modified' => $post->post_modified,
'permalink' => get_permalink($post->ID),
];
}
public function update_post($request) {
$post_id = $request->get_param('id');
$data = [];
if ($request->has_param('title')) $data['post_title'] = $request->get_param('title');
if ($request->has_param('content')) $data['post_content'] = $request->get_param('content');
if ($request->has_param('status')) $data['post_status'] = $request->get_param('status');
if ($request->has_param('excerpt')) $data['post_excerpt'] = $request->get_param('excerpt');
$data['ID'] = $post_id;
$result = wp_update_post($data, true);
if (is_wp_error($result)) {
return new \WP_REST_Response(['error' => $result->get_error_message()], 500);
}
return ['success' => true, 'post_id' => $result];
}
public function delete_post($request) {
$result = wp_delete_post($request->get_param('id'), true);
if (!$result) return new \WP_REST_Response(['error' => 'Delete failed'], 500);
return ['success' => true, 'deleted' => $result->ID];
}
public function list_users($request) {
$users = get_users(['fields' => ['ID', 'user_login', 'user_email', 'display_name', 'roles']]);
return $users;
}
public function get_user($request) {
$user = get_userdata($request->get_param('id'));
if (!$user) return new \WP_REST_Response(['error' => 'User not found'], 404);
return [
'id' => $user->ID,
'login' => $user->user_login,
'email' => $user->user_email,
'display_name' => $user->display_name,
'roles' => array_values($user->roles),
];
}
public function get_settings() {
return [
'site_name' => get_bloginfo('name'),
'site_url' => get_bloginfo('url'),
'admin_email' => get_bloginfo('admin_email'),
'language' => get_bloginfo('language'),
'timezone' => get_option('timezone_string'),
'date_format' => get_option('date_format'),
'time_format' => get_option('time_format'),
'posts_per_page' => get_option('posts_per_page'),
'ai_provider' => get_option('hermes_ai_provider'),
'ai_model' => get_option('hermes_ai_model'),
];
}
public function update_settings($request) {
$allowed = ['hermes_ai_provider', 'hermes_ai_model', 'hermes_ai_api_key', 'hermes_ai_base_url', 'hermes_ai_max_tokens', 'hermes_cron_interval'];
foreach ($request->get_params() as $key => $value) {
if (in_array($key, $allowed)) {
update_option($key, sanitize_text_field($value));
}
}
return ['success' => true];
}
public function list_schedules() {
$tasks = get_posts([
'post_type' => 'hermes_schedule',
'posts_per_page' => 50,
'post_status' => 'any',
]);
return array_map(function($t) {
return [
'id' => $t->ID,
'type' => get_post_meta($t->ID, '_hermes_task_type', true),
'scheduled_date' => get_post_meta($t->ID, '_hermes_scheduled_date', true),
'status' => $t->post_status,
'created' => $t->post_date,
];
}, $tasks);
}
public function get_logs($request) {
global $wpdb;
$where = '1=1';
if ($request->get_param('level')) {
$where .= $wpdb->prepare(' AND level = %s', $request->get_param('level'));
}
$limit = intval($request->get_param('limit'));
$offset = intval($request->get_param('offset'));
$results = $wpdb->get_results(
"SELECT id, level, message, context, created_at FROM {$wpdb->prefix}hermes_log
WHERE $where ORDER BY id DESC LIMIT $offset, $limit"
);
return array_map(function($r) {
$r->context = $r->context ? json_decode($r->context) : null;
return $r;
}, $results);
}
public static function log($level, $message, $context = []) {
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'hermes_log',
[
'level' => $level,
'message' => $message,
'context' => wp_json_encode($context),
'created_at' => current_time('mysql'),
]
);
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
namespace HermesAiBridge;
defined('ABSPATH') or die();
class Scheduler {
/**
* Process all due scheduled AI tasks.
* Called by WP-Cron every hour.
*/
public function process_due_tasks() {
$tasks = get_posts([
'post_type' => 'hermes_schedule',
'posts_per_page' => 10,
'post_status' => 'publish',
'meta_query' => [
[
'key' => '_hermes_scheduled_date',
'value' => current_time('mysql'),
'compare' => '<=',
'type' => 'DATETIME',
],
[
'key' => '_hermes_processed',
'compare' => 'NOT EXISTS',
],
],
]);
foreach ($tasks as $task) {
$this->execute_task($task);
}
}
/**
* Execute a single scheduled task.
*/
protected function execute_task($task) {
$task_type = get_post_meta($task->ID, '_hermes_task_type', true);
switch ($task_type) {
case 'content_generation':
$this->execute_content_task($task);
break;
case 'publish_scheduled':
$this->publish_scheduled_posts();
break;
default:
RestController::log('warning', 'Unknown task type', ['task_id' => $task->ID, 'type' => $task_type]);
}
update_post_meta($task->ID, '_hermes_processed', time());
update_post_meta($task->ID, '_hermes_processed_date', current_time('mysql'));
}
/**
* Execute a content generation task.
*/
protected function execute_content_task($task) {
$prompt = get_post_meta($task->ID, '_hermes_prompt', true);
$scheduled_date = get_post_meta($task->ID, '_hermes_scheduled_date', true);
if (!$prompt) {
RestController::log('error', 'Task has no prompt', ['task_id' => $task->ID]);
return;
}
$ai = new AiProvider();
$content = $ai->generate($prompt, 'post');
if (is_wp_error($content)) {
RestController::log('error', 'Task generation failed', [
'task_id' => $task->ID,
'error' => $content->get_error_message(),
]);
return;
}
$post_id = wp_insert_post([
'post_title' => $task->post_title,
'post_content' => $content,
'post_status' => 'future',
'post_date' => $scheduled_date,
'post_type' => 'post',
]);
if ($post_id) {
RestController::log('info', 'Task executed: post scheduled', [
'task_id' => $task->ID,
'post_id' => $post_id,
'scheduled' => $scheduled_date,
]);
}
}
/**
* Publish posts that are scheduled but stuck in 'future' status.
*/
protected function publish_scheduled_posts() {
$scheduled = get_posts([
'post_status' => 'future',
'posts_per_page' => 10,
'post_type' => 'any',
]);
foreach ($scheduled as $post) {
if (strtotime($post->post_date) <= time()) {
wp_publish_post($post->ID);
RestController::log('info', 'Post published', ['post_id' => $post->ID, 'title' => $post->post_title]);
}
}
}
/**
* Register the hermes_schedule custom post type.
*/
public static function register_post_type() {
register_post_type('hermes_schedule', [
'labels' => ['name' => 'AI Tasks', 'singular_name' => 'AI Task'],
'public' => false,
'show_ui' => true,
'show_in_menu' => false,
'supports' => ['title', 'editor', 'custom-fields'],
]);
}
}