Files
wp-agentic-writer/wp-agentic-writer.php
2026-06-06 00:29:10 +07:00

360 lines
11 KiB
PHP

<?php
/**
* Plugin Name: WP Agentic Writer
* Plugin URI: https://github.com/wp-agentic-writer
* Description: Plan-first AI writing workflow for WordPress. Scribble → Research → Plan → Execute → Revise
* Version: 0.1.3
* Author: WP Agentic Writer
* Author URI: https://github.com/wp-agentic-writer
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: wp-agentic-writer
* Domain Path: /languages
* Requires at least: 6.6
* Requires PHP: 7.4
*
* @package WP_Agentic_Writer
*/
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Plugin version.
define( 'WP_AGENTIC_WRITER_VERSION', '0.1.3' );
// Plugin file path.
define( 'WP_AGENTIC_WRITER_FILE', __FILE__ );
// Plugin directory path.
define( 'WP_AGENTIC_WRITER_DIR', plugin_dir_path( __FILE__ ) );
// Plugin directory URL (plugin_dir_url already includes trailing slash).
define( 'WP_AGENTIC_WRITER_URL', untrailingslashit( plugin_dir_url( __FILE__ ) ) . '/' );
// Include autoloader.
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-autoloader.php';
// Include provider interface and manager.
require_once WP_AGENTIC_WRITER_DIR . 'includes/interface-ai-provider.php';
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-provider-manager.php';
// Include WordPress 7.0 AI Client wrapper (backward compatible).
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-wp-ai-client-wrapper.php';
// Include conversation manager for session-based chat history.
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-conversation-migration.php';
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-conversation-manager.php';
// Include model registry for centralized model defaults.
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-model-registry.php';
// Initialize the plugin.
function wp_agentic_writer_init() {
// Load plugin text domain.
load_plugin_textdomain(
'wp-agentic-writer',
false,
dirname( plugin_basename( __FILE__ ) ) . '/languages/'
);
// Always initialize Gutenberg sidebar for REST API routes.
WP_Agentic_Writer_Gutenberg_Sidebar::get_instance();
// Always initialize cost tracker hooks (REST API calls need this).
WP_Agentic_Writer_Cost_Tracker::get_instance();
// Schedule image cleanup cron job if not already scheduled.
if ( ! wp_next_scheduled( 'wpaw_cleanup_temp_images' ) ) {
wp_schedule_event( time(), 'daily', 'wpaw_cleanup_temp_images' );
}
// Initialize SEO Schema Agent
WP_Agentic_Writer_SEO_Schema::get_instance();
// Check if we're on the admin side.
if ( is_admin() ) {
// Initialize settings - V2 is now the default.
$use_settings_v2 = true;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['wpaw_settings_v2'] ) ) {
$use_settings_v2 = ( $_GET['wpaw_settings_v2'] === '1' );
}
// Also check if this is an AJAX request for V2 endpoints.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_POST['action'] ) ) {
$v2_ajax_actions = array(
'wpaw_get_cost_log_data',
'wpaw_get_header_stats',
'wpaw_test_api_connection',
'wpaw_refresh_models',
);
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( in_array( $_POST['action'], $v2_ajax_actions, true ) ) {
$use_settings_v2 = true;
}
}
if ( $use_settings_v2 ) {
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-settings-v2.php';
WP_Agentic_Writer_Settings_V2::get_instance();
} else {
WP_Agentic_Writer_Settings::get_instance();
}
// Initialize admin columns.
WP_Agentic_Writer_Admin_Columns::get_instance();
}
}
add_action( 'plugins_loaded', 'wp_agentic_writer_init' );
// Hook for image cleanup cron job.
add_action( 'wpaw_cleanup_temp_images', 'wp_agentic_writer_cleanup_temp_images' );
/**
* Cleanup old temp images (7+ days).
*
* @since 0.1.0
*/
function wp_agentic_writer_cleanup_temp_images() {
WP_Agentic_Writer_Image_Manager::get_instance()->cleanup_old_temp_images();
}
// Activation hook.
register_activation_hook( __FILE__, 'wp_agentic_writer_activate' );
/**
* Plugin activation.
*
* @since 0.1.0
*/
function wp_agentic_writer_activate() {
// Set default options using model registry for consistency.
$registry_defaults = WPAW_Model_Registry::get_activation_defaults();
$default_options = array(
'openrouter_api_key' => '',
'planning_model' => $registry_defaults['planning_model'],
'execution_model' => $registry_defaults['execution_model'],
'image_model' => $registry_defaults['image_model'],
'web_search_enabled' => false,
'search_engine' => 'auto',
'search_depth' => 'medium',
'cost_tracking_enabled' => true,
'enable_clarification_quiz' => true,
'clarity_confidence_threshold' => '0.6',
'chat_history_limit' => 20,
'preferred_languages' => array( 'auto', 'English', 'Indonesian' ),
'custom_languages' => array(),
);
add_option( 'wp_agentic_writer_settings', $default_options );
// Set default custom models (separate option for custom models)
$default_custom_models = array(
array(
'id' => 'black-forest-labs/flux-1.1-pro',
'name' => 'FLUX 1.1 Pro',
'type' => 'image',
),
array(
'id' => 'black-forest-labs/flux-pro',
'name' => 'FLUX Pro',
'type' => 'image',
),
array(
'id' => 'recraft-ai/recraft-v3',
'name' => 'Recraft V3',
'type' => 'image',
),
);
add_option( 'wp_agentic_writer_custom_models', $default_custom_models );
// Create cost tracking table.
wp_agentic_writer_create_cost_table();
// Create image management tables.
WP_Agentic_Writer_Image_Manager::get_instance()->create_tables();
// Create conversations table.
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-conversation-migration.php';
wpaw_create_conversations_table();
}
/**
* Create cost tracking table.
*
* @since 0.1.0
*/
function wp_agentic_writer_create_cost_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'wpaw_cost_tracking';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE $table_name (
id bigint(20) NOT NULL AUTO_INCREMENT,
post_id bigint(20) NOT NULL,
session_id varchar(32) DEFAULT '' NOT NULL,
model varchar(255) NOT NULL,
provider varchar(50) DEFAULT 'openrouter' NOT NULL,
action varchar(50) NOT NULL,
input_tokens int(11) NOT NULL,
output_tokens int(11) NOT NULL,
cost decimal(10,6) NOT NULL,
status varchar(20) DEFAULT 'success' NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (id),
KEY post_id (post_id),
KEY session_id (session_id),
KEY provider (provider),
KEY action (action),
KEY created_at (created_at)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
// Version-based table creation (runs on plugins_loaded to ensure tables exist).
add_action( 'plugins_loaded', 'wp_agentic_writer_maybe_create_tables' );
/**
* Create database tables if they don't exist or version is outdated.
*
* @since 0.1.0
*/
function wp_agentic_writer_maybe_create_tables() {
// Cost tracking table - controlled by main db version.
$current_version = get_option( 'wpaw_db_version', '0' );
$required_version = '1.1.0';
if ( version_compare( $current_version, $required_version, '<' ) ) {
// Create cost tracking table.
wp_agentic_writer_create_cost_table();
// Create image management tables.
WP_Agentic_Writer_Image_Manager::get_instance()->create_tables();
// Create conversations table (migrate if needed).
if ( ! function_exists( 'wpaw_create_conversations_table' ) ) {
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-conversation-migration.php';
}
wpaw_create_conversations_table();
// Update version.
update_option( 'wpaw_db_version', $required_version );
} else {
// Even if main version is current, still check conversation table independently.
if ( ! function_exists( 'wpaw_create_conversations_table' ) ) {
require_once WP_AGENTIC_WRITER_DIR . 'includes/class-conversation-migration.php';
}
$conv_version = get_option( 'wpaw_conversations_db_version', '0' );
if ( version_compare( $conv_version, '0.1.4', '<' ) ) {
wpaw_create_conversations_table();
}
}
}
// Deactivation hook.
register_deactivation_hook( __FILE__, 'wp_agentic_writer_deactivate' );
/**
* Plugin deactivation.
*
* @since 0.1.0
*/
function wp_agentic_writer_deactivate() {
// Clear scheduled cron jobs.
$timestamp = wp_next_scheduled( 'wpaw_cleanup_temp_images' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'wpaw_cleanup_temp_images' );
}
wp_clear_scheduled_hook( 'wpaw_cleanup_old_sessions' );
}
// Uninstall hook.
register_uninstall_hook( __FILE__, 'wp_agentic_writer_uninstall' );
/**
* Plugin uninstall.
*
* Removes all plugin data including settings, database tables,
* post meta, user meta, transients, scheduled events, and temp files.
*
* @since 0.1.0
*/
function wp_agentic_writer_uninstall() {
global $wpdb;
// Delete options.
delete_option( 'wp_agentic_writer_settings' );
delete_option( 'wpaw_db_version' );
delete_option( 'wpaw_conversations_db_version' );
delete_option( 'wp_agentic_writer_custom_models' );
// Delete post meta.
$post_meta_keys = array(
'_wpaw_plan',
'_wpaw_chat_history',
'_wpaw_memory',
'_wpaw_post_config',
'_wpaw_focus_keyword',
'_wpaw_writing_status',
'_wpaw_current_section',
'_wpaw_sections_written',
'_wpaw_writing_state_updated',
'_wpaw_plan_id',
'_wpaw_resume_token',
);
foreach ( $post_meta_keys as $key ) {
delete_post_meta_by_key( $key );
}
// Delete user meta.
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
'wpaw_%'
)
);
// Delete database tables.
$tables = array(
$wpdb->prefix . 'wpaw_cost_tracking',
$wpdb->prefix . 'wpaw_images',
$wpdb->prefix . 'wpaw_images_variants',
$wpdb->prefix . 'wpaw_conversations',
);
foreach ( $tables as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
}
// Clear transients.
delete_transient( 'wpaw_openrouter_models' );
delete_transient( 'wpaw_openrouter_model_objects' );
delete_transient( 'wpaw_openrouter_model_ids' );
// Unschedule cron events.
wp_clear_scheduled_hook( 'wpaw_cleanup_old_sessions' );
wp_clear_scheduled_hook( 'wpaw_daily_cleanup' );
// Delete temp image directory.
$upload_dir = wp_upload_dir();
$temp_dir = $upload_dir['basedir'] . '/wpaw';
if ( file_exists( $temp_dir ) ) {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $temp_dir, RecursiveDirectoryIterator::SKIP_DOTS ),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ( $files as $fileinfo ) {
$todo = ( $fileinfo->isDir() ? 'rmdir' : 'unlink' );
$todo( $fileinfo->getRealPath() );
}
rmdir( $temp_dir );
}
}