Initial commit: Hermes AI Bridge WordPress plugin for cPanel agent runtime
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
/* Hermes AI Bridge Admin Styles */
|
||||
.toplevel_page_hermes-ai-bridge .wrap input[readonly] {
|
||||
background: #f0f0f1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toplevel_page_hermes-ai-bridge .wrap h2 {
|
||||
margin-top: 2em;
|
||||
}
|
||||
.toplevel_page_hermes-ai-bridge #hermes-regenerate-key {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Hermes AI Bridge
|
||||
* Plugin URI: https://falahos.my
|
||||
* Description: AI agent endpoint for Hermes agents on Contabo. REST API + WP-Cron + Nextcloud bridge.
|
||||
* Version: 1.0.0
|
||||
* Author: FalahOS / Hermes Agent
|
||||
* Author URI: https://falahos.my
|
||||
* License: GPL v2 or later
|
||||
* Text Domain: hermes-ai-bridge
|
||||
*
|
||||
* No exec(), shell_exec(), or system() calls anywhere.
|
||||
* Fully compatible with RoketServer shared hosting (no SSH, Cloudflare WAF).
|
||||
*/
|
||||
|
||||
defined('ABSPATH') or die('Direct access denied.');
|
||||
|
||||
define('HERMES_AI_BRIDGE_VERSION', '1.0.0');
|
||||
define('HERMES_AI_BRIDGE_FILE', __FILE__);
|
||||
define('HERMES_AI_BRIDGE_DIR', plugin_dir_path(__FILE__));
|
||||
define('HERMES_AI_BRIDGE_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
// Autoload
|
||||
require_once HERMES_AI_BRIDGE_DIR . 'includes/class-autoloader.php';
|
||||
HermesAiBridge\Autoloader::register();
|
||||
|
||||
// Activation / Deactivation
|
||||
register_activation_hook(__FILE__, ['HermesAiBridge\Installer', 'activate']);
|
||||
register_deactivation_hook(__FILE__, ['HermesAiBridge\Installer', 'deactivate']);
|
||||
|
||||
// Bootstrap
|
||||
add_action('plugins_loaded', ['HermesAiBridge\Core', 'init']);
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user