' . esc_html__( 'The Smart Assistant is already provided by this hosting platform, so this plugin is inactive. You can safely delete it.', 'aiel-smart-assistant' ) . '
'; } ); return; } const AIEL_EC_OPTION = 'aiel_embed_chat_options'; // 外掛自帶資源(assets/*.js、assets/*.css)的版本號,隨外掛版本同步更新, // 用於 wp_enqueue_script()/wp_enqueue_style() 的 $ver 參數(利於瀏覽器快取更新)。 const AIEL_EC_VERSION = '2.4.5'; if ( ! function_exists( 'aiel_ec_platform_base' ) ) : /** 連線端點所在的平台網域,可用常數覆寫(define 在 wp-config.php)。 */ function aiel_ec_platform_base() { if ( defined( 'AIEL_EC_PLATFORM_BASE' ) ) { return rtrim( AIEL_EC_PLATFORM_BASE, '/' ); } return 'https://saas.aiel.app'; } endif; if ( ! function_exists( 'aiel_ec_defaults' ) ) : function aiel_ec_defaults() { return array( 'connected' => 0, 'enabled' => 0, 'key' => '', 'subdomain' => '', 'host' => '', 'display_name' => '', 'ready' => 0, // App 內建瀏覽器警示(LINE/FB/IG)開關;與智能秘書連線狀態無關。 'inapp_warning' => 1, ); } endif; if ( ! function_exists( 'aiel_ec_get_options' ) ) : function aiel_ec_get_options() { $opts = get_option( AIEL_EC_OPTION, array() ); if ( ! is_array( $opts ) ) { $opts = array(); } return wp_parse_args( $opts, aiel_ec_defaults() ); } endif; /* ------------------------------------------------------------------------- * 前台注入(已連線且啟用時) * ---------------------------------------------------------------------- */ if ( ! function_exists( 'aiel_ec_is_builder_context' ) ) : /** * Whether this request is a page builder's editing canvas or a preview frame. * * `is_admin()` does NOT cover these. Elementor's editing canvas is an iframe * that loads the FRONT END (`?elementor-preview=…`), so `is_admin()` is false, * the front-end hooks fire, and the chat bubble ends up floating over the * canvas covering the widgets being edited. Reported from a live site. * * Deliberately checks only request parameters instead of calling * `\Elementor\Plugin::$instance` — that would be a fatal error on any site * without Elementor installed, and this plugin must never break a site * whatever theme/plugin combination it lands in. * * @return bool */ function aiel_ec_is_builder_context() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only decision about whether to print a script; changes no state. if ( isset( $_GET['elementor-preview'] ) ) { return true; // Elementor preview iframe. } if ( isset( $_GET['action'] ) && 'elementor' === sanitize_key( wp_unslash( $_GET['action'] ) ) ) { return true; // Elementor editor. } if ( isset( $_GET['preview'] ) ) { return true; // Core post preview. } if ( isset( $_GET['fl_builder'] ) ) { return true; // Beaver Builder. } if ( isset( $_GET['et_fb'] ) ) { return true; // Divi. } if ( isset( $_GET['vc_editable'] ) ) { return true; // WPBakery. } if ( isset( $_GET['brizy-edit'] ) || isset( $_GET['brizy-edit-iframe'] ) ) { return true; // Brizy. } // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) { return true; // Customizer preview. } return false; } endif; add_action( 'wp_enqueue_scripts', 'aiel_ec_enqueue_script' ); function aiel_ec_enqueue_script() { if ( aiel_ec_is_builder_context() ) { return; } $o = aiel_ec_get_options(); if ( empty( $o['connected'] ) || empty( $o['enabled'] ) ) { return; } $host = preg_replace( '/[^a-z0-9.\-]/i', '', (string) $o['host'] ); $sub = preg_replace( '/[^a-z0-9\-]/i', '', (string) $o['subdomain'] ); if ( '' === $host || '' === $sub ) { return; } // ?via=wp-plugin&v=<版本>:讓平台分得出「這個泡泡是外掛裝的還是手貼 script // 的」,以及還有多少站停在舊版。平台端用白名單正規化後才落地(見 // lib/embed-chat/last-seen.ts 的 normalizeVia)。 // // 這不是額外的追蹤:這個請求本來就會發生(沒有它就沒有泡泡),而且本來就 // 帶著 Referer。新增的資訊只有「來源是外掛」與外掛版本,不含任何訪客資料。 $src = sprintf( 'https://%s/embed/%s.js?via=wp-plugin&v=%s', $host, $sub, rawurlencode( AIEL_EC_VERSION ) ); // $ver=null 不帶 ?ver(泡泡永遠載最新);WP 6.3+ 以 strategy 非同步載入, // 舊版 WP 把陣列當 truthy $in_footer 處理(相容降級為 footer 同步載入)。 // 注意 phpcs:ignore 必須貼在 null 那一行——它只作用於「下一行」, // 放在函式呼叫前面不會蓋到報在 $ver 引數上的那筆。 wp_enqueue_script( 'aiel-smart-assistant', $src, array(), null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- remote SaaS script, intentionally unversioned so sites always load the latest embed. array( 'in_footer' => true, 'strategy' => 'async', ) ); } /* ------------------------------------------------------------------------- * 前台注入:App 內建瀏覽器(LINE / Facebook / Instagram)警告。 * 這些 App 的內建瀏覽器會擋 Google/第三方登入,導致會員登入失敗。偵測到就 * 提示改用 Chrome/Safari 開啟。與智能秘書是否連線無關;可於設定頁關閉 * (inapp_warning 選項,預設開啟)。 * wp_enqueue_scripts 只在前台觸發,所以這裡不需要(也不應該)再判斷 is_admin()。 * 但頁面建構器的預覽 iframe **也是**前台,那個橫幅一樣會蓋住編輯畫布—— * 見 aiel_ec_is_builder_context() 的說明。 * ---------------------------------------------------------------------- */ add_action( 'wp_enqueue_scripts', 'aiel_ec_enqueue_inapp_warning' ); function aiel_ec_enqueue_inapp_warning() { if ( aiel_ec_is_builder_context() ) { return; } $o = aiel_ec_get_options(); if ( empty( $o['inapp_warning'] ) ) { return; } wp_enqueue_script( 'aiel-smart-assistant-inapp-warning', plugins_url( 'assets/inapp-warning.js', __FILE__ ), array(), AIEL_EC_VERSION, // 用布林 $in_footer,而非 6.3 才有的 array( 'in_footer' => true ):本外掛 // 宣告 Requires at least 5.0,陣列在 6.3 以下不是支援的簽名(只是剛好 // truthy 才沒出錯)。上面 embed 那支要 strategy=async 才非用陣列不可, // 這支不需要,所以用最直接、各版本都正確的布林。 true ); // 畫面文字透過 wp_localize_script 傳給 JS,維持可翻譯(__()),而非寫死在 .js 檔內。 wp_localize_script( 'aiel-smart-assistant-inapp-warning', 'aielInappWarningL10n', array( 'heading' => __( 'Open this site in your browser', 'aiel-smart-assistant' ), 'body' => __( 'You are viewing this site in an in-app browser (LINE, Facebook and similar). Features such as member sign-in may not work correctly here. Please open the site in Chrome or Safari instead.', 'aiel-smart-assistant' ), 'openChrome' => __( 'Open in Chrome', 'aiel-smart-assistant' ), 'copyUrl' => __( 'Copy the address (paste it into a browser)', 'aiel-smart-assistant' ), 'copied' => __( 'Copied ✓', 'aiel-smart-assistant' ), 'continue' => __( 'Continue here', 'aiel-smart-assistant' ), ) ); } /* ------------------------------------------------------------------------- * 後台設定頁 * ---------------------------------------------------------------------- */ // 設定頁的 hook suffix 由 add_options_page() 回傳值決定(勿用猜的), // admin_menu 早於 admin_enqueue_scripts 執行,先存起來供後面比對。 $GLOBALS['aiel_ec_settings_hook'] = null; add_action( 'admin_menu', 'aiel_ec_admin_menu' ); if ( ! function_exists( 'aiel_ec_admin_menu' ) ) : function aiel_ec_admin_menu() { $GLOBALS['aiel_ec_settings_hook'] = add_options_page( __( 'Smart Assistant', 'aiel-smart-assistant' ), __( 'Smart Assistant', 'aiel-smart-assistant' ), 'manage_options', 'aiel-smart-assistant', 'aiel_ec_settings_page' ); } endif; add_action( 'admin_enqueue_scripts', 'aiel_ec_admin_enqueue_styles' ); function aiel_ec_admin_enqueue_styles( $hook_suffix ) { // 只在本外掛自己的設定頁載入,不要對每個後台畫面都掛樣式表。 if ( $hook_suffix !== $GLOBALS['aiel_ec_settings_hook'] ) { return; } wp_enqueue_style( 'aiel-smart-assistant-admin', plugins_url( 'assets/admin.css', __FILE__ ), array(), AIEL_EC_VERSION ); } if ( ! function_exists( 'aiel_ec_settings_url' ) ) : function aiel_ec_settings_url( $args = array() ) { return add_query_arg( array_merge( array( 'page' => 'aiel-smart-assistant' ), $args ), admin_url( 'options-general.php' ) ); } endif; /* ---- 動作:連線 ---- */ add_action( 'admin_post_aiel_ec_connect', 'aiel_ec_handle_connect' ); if ( ! function_exists( 'aiel_ec_handle_connect' ) ) : function aiel_ec_handle_connect() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'forbidden' ); } check_admin_referer( 'aiel_ec_connect' ); $key = isset( $_POST['aiel_ec_key'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['aiel_ec_key'] ) ) ) : ''; if ( '' === $key ) { wp_safe_redirect( aiel_ec_settings_url( array( 'err' => 'empty' ) ) ); exit; } $res = wp_remote_post( aiel_ec_platform_base() . '/api/embed-chat/connect', array( 'timeout' => 15, 'headers' => array( 'X-AIEL-Key' => $key, 'Content-Type' => 'application/json', ), 'body' => '{}', ) ); if ( is_wp_error( $res ) ) { wp_safe_redirect( aiel_ec_settings_url( array( 'err' => 'network' ) ) ); exit; } $code = wp_remote_retrieve_response_code( $res ); $data = json_decode( wp_remote_retrieve_body( $res ), true ); if ( 200 !== (int) $code || empty( $data['ok'] ) ) { $err = is_array( $data ) && isset( $data['error'] ) ? $data['error'] : 'failed'; wp_safe_redirect( aiel_ec_settings_url( array( 'err' => $err ) ) ); exit; } $o = aiel_ec_get_options(); $o['connected'] = 1; $o['enabled'] = 1; // 連線後預設啟用 $o['key'] = $key; $o['subdomain'] = isset( $data['subdomain'] ) ? sanitize_text_field( $data['subdomain'] ) : ''; $o['host'] = isset( $data['host'] ) ? sanitize_text_field( $data['host'] ) : ''; $o['display_name'] = isset( $data['displayName'] ) ? sanitize_text_field( $data['displayName'] ) : ''; $o['ready'] = ! empty( $data['ready'] ) ? 1 : 0; update_option( AIEL_EC_OPTION, $o ); wp_safe_redirect( aiel_ec_settings_url( array( 'ok' => 'connected' ) ) ); exit; } endif; /* ---- 動作:中斷連線 ---- */ add_action( 'admin_post_aiel_ec_disconnect', 'aiel_ec_handle_disconnect' ); if ( ! function_exists( 'aiel_ec_handle_disconnect' ) ) : function aiel_ec_handle_disconnect() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'forbidden' ); } check_admin_referer( 'aiel_ec_disconnect' ); update_option( AIEL_EC_OPTION, aiel_ec_defaults() ); wp_safe_redirect( aiel_ec_settings_url( array( 'ok' => 'disconnected' ) ) ); exit; } endif; /* ---- 動作:切換啟用 ---- */ add_action( 'admin_post_aiel_ec_toggle', 'aiel_ec_handle_toggle' ); if ( ! function_exists( 'aiel_ec_handle_toggle' ) ) : function aiel_ec_handle_toggle() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'forbidden' ); } check_admin_referer( 'aiel_ec_toggle' ); $o = aiel_ec_get_options(); // 泡泡開關只在已連線時有意義(未連線的表單不含此欄位,避免誤清)。 if ( ! empty( $o['connected'] ) ) { $o['enabled'] = ! empty( $_POST['aiel_ec_enabled'] ) ? 1 : 0; } $o['inapp_warning'] = ! empty( $_POST['aiel_ec_inapp_warning'] ) ? 1 : 0; update_option( AIEL_EC_OPTION, $o ); wp_safe_redirect( aiel_ec_settings_url( array( 'ok' => 'saved' ) ) ); exit; } endif; if ( ! function_exists( 'aiel_ec_settings_page' ) ) : function aiel_ec_settings_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $o = aiel_ec_get_options(); // 這些是使用者唯一會看到的失敗說明,必須可翻譯(先前漏包,等於在任何語言下 // 都只會顯示原文)。鍵是平台回傳的錯誤碼,不翻譯。 $errs = array( 'empty' => __( 'Please paste a connection key first.', 'aiel-smart-assistant' ), 'network' => __( 'Connection failed. Please try again later.', 'aiel-smart-assistant' ), 'invalid_key' => __( 'That key is invalid or has been revoked.', 'aiel-smart-assistant' ), 'missing_key' => __( 'No key was received.', 'aiel-smart-assistant' ), 'not_embed_chat' => __( 'That workspace is not a Smart Assistant workspace.', 'aiel-smart-assistant' ), 'workspace_unavailable' => __( 'That workspace is currently unavailable.', 'aiel-smart-assistant' ), 'rate_limited' => __( 'Too many attempts. Please try again later.', 'aiel-smart-assistant' ), 'failed' => __( 'Connection failed.', 'aiel-smart-assistant' ), ); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- display-only // feedback codes set by our own wp_safe_redirect above; no state is changed here. $err = isset( $_GET['err'] ) ? sanitize_text_field( wp_unslash( $_GET['err'] ) ) : ''; $ok = isset( $_GET['ok'] ) ? sanitize_text_field( wp_unslash( $_GET['ok'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended ?> ' . esc_html__( 'Settings', 'aiel-smart-assistant' ) . ''; array_unshift( $links, $settings ); return $links; } endif;