Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/andy17269_lyrargon.json
T

1 line
15 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "andy17269/lyrargon", "files": {"inc/agent.php": {"728592386a6da1ecc4cd1fd39a95110ab42be839": {"t": 1785746144.719484, "n": 12781, "h": "b4bd76bc996d3e9f", "c": "<?php\n/**\n * AI Agent Module\n *\n * 提供 REST API 端点用于 AI 助手功能:\n * - GET /lyrargon/v1/agent/posts 获取最近文章\n * - GET /lyrargon/v1/agent/shuoshuo 获取最近说说\n * - GET /lyrargon/v1/agent/announcement 获取侧边栏公告\n * - POST /lyrargon/v1/agent/chat 代理调用 OpenAI 兼容 API\n *\n * 内置 IP 时间 QoS 和日总消耗 QoS 限制。\n */\n\ndefined( 'ABSPATH' ) || exit;\n\n// ============== QoS 辅助函数 ==============\n\n/**\n * 获取客户端真实 IP(考虑代理)\n */\nfunction lyrargon_agent_get_client_ip() {\n\tif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {\n\t\t$ips = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );\n\t\treturn trim( $ips[0] );\n\t}\n\tif ( ! empty( $_SERVER['HTTP_X_REAL_IP'] ) ) {\n\t\treturn $_SERVER['HTTP_X_REAL_IP'];\n\t}\n\tif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {\n\t\treturn $_SERVER['REMOTE_ADDR'];\n\t}\n\treturn '127.0.0.1';\n}\n\n/**\n * 检查 IP 时间 QoS(每秒请求数限制)\n *\n * @param int $max_requests 允许的最大请求数\n * @param int $window_seconds 时间窗口(秒)\n * @return array [allowed: bool, remaining: int]\n */\nfunction lyrargon_agent_check_ip_qos( $max_requests = 6, $window_seconds = 60 ) {\n\tif ( $max_requests <= 0 ) {\n\t\treturn array( 'allowed' => true, 'remaining' => 999 );\n\t}\n\t$ip = lyrargon_agent_get_client_ip();\n\t$key = 'lyr_agent_ip_qos_' . md5( $ip );\n\t$data = get_transient( $key );\n\tif ( false === $data || ! is_array( $data ) ) {\n\t\t$data = array();\n\t}\n\t$now = time();\n\t// 清理过期记录\n\t$data = array_values( array_filter( $data, function ( $t ) use ( $now, $window_seconds ) {\n\t\treturn $t > $now - $window_seconds;\n\t} ) );\n\t$remaining = max( 0, $max_requests - count( $data ) );\n\tif ( $remaining <= 0 ) {\n\t\treturn array( 'allowed' => false, 'remaining' => 0 );\n\t}\n\t$data[] = $now;\n\tset_transient( $key, $data, $window_seconds );\n\treturn array( 'allowed' => true, 'remaining' => $remaining );\n}\n\n/**\n * 检查日总消耗 QoS。\n * 每日消费计数存储为 WordPress 选项,每天零点自动重置。\n *\n * @param float $estimated_cost 预估消耗(以 token 数为单位,简化为每次请求 = 1 单位)\n * @param int $daily_limit 每日上限(单位数)\n * @return array [allowed: bool, current: int, limit: int]\n */\nfunction lyrargon_agent_check_daily_qos( $daily_limit = 100 ) {\n\tif ( $daily_limit <= 0 ) {\n\t\treturn array( 'allowed' => true, 'current' => 0, 'limit' => 0 );\n\t}\n\t$today = gmdate( 'Y-m-d' );\n\t$key = 'lyr_agent_daily_count_' . $today;\n\t$current = (int) get_option( $key, 0 );\n\tif ( $current >= $daily_limit ) {\n\t\treturn array( 'allowed' => false, 'current' => $current, 'limit' => $daily_limit );\n\t}\n\tupdate_option( $key, $current + 1 );\n\t// 确保每天过期后会重新计数(用 cron 或下次写入时重置)\n\tif ( $current === 0 ) {\n\t\twp_schedule_single_event( strtotime( 'tomorrow' ) + 60, 'lyrargon_agent_reset_daily_qos' );\n\t}\n\treturn array( 'allowed' => true, 'current' => $current + 1, 'limit' => $daily_limit );\n}\n\n/**\n * 重置日消耗计数器(由 cron 触发)\n */\nfunction lyrargon_agent_reset_daily_qos_action() {\n\t$today = gmdate( 'Y-m-d' );\n\tdelete_option( 'lyr_agent_daily_count_' . $today );\n}\nadd_action( 'lyrargon_agent_reset_daily_qos', 'lyrargon_agent_reset_daily_qos_action' );\n\n// ============== REST API 回调 ==============\n\n/**\n * 获取最近文章\n */\nfunction lyrargon_agent_rest_posts( $request ) {\n\t$per_page = min( (int) $request->get_param( 'per_page' ), 50 );\n\tif ( $per_page <= 0 ) {\n\t\t$per_page = 20;\n\t}\n\t$query = new WP_Query( array(\n\t\t'post_type' => array( 'post' ),\n\t\t'posts_per_page' => $per_page,\n\t\t'post_status' => 'publish',\n\t\t'no_found_rows' => true,\n\t\t'ignore_sticky_posts' => true,\n\t) );\n\t$results = array();\n\tforeach ( $query->posts as $post ) {\n\t\t$results[] = array(\n\t\t\t'id' => $post->ID,\n\t\t\t'title' => get_the_title( $post ),\n\t\t\t'url' => get_permalink( $post ),\n\t\t\t'excerpt' => wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post->post_content ) ), 60, '...' ),\n\t\t\t'date' => $post->post_date,\n\t\t);\n\t}\n\treturn new WP_REST_Response( $results, 200 );\n}\n\n/**\n * 获取最近说说\n */\nfunction lyrargon_agent_rest_shuoshuo( $request ) {\n\t$per_page = min( (int) $request->get_param( 'per_page' ), 50 );\n\tif ( $per_page <= 0 ) {\n\t\t$per_page = 20;\n\t}\n\t$query = new WP_Query( array(\n\t\t'post_type' => array( 'shuoshuo' ),\n\t\t'posts_per_page' => $per_page,\n\t\t'post_status' => 'publish',\n\t\t'no_found_rows' => true,\n\t\t'ignore_sticky_posts' => true,\n\t) );\n\t$results = array();\n\tforeach ( $query->posts as $post ) {\n\t\t$results[] = array(\n\t\t\t'id' => $post->ID,\n\t\t\t'content' => wp_strip_all_tags( $post->post_content ),\n\t\t\t'date' => $post->post_date,\n\t\t);\n\t}\n\treturn new WP_REST_Response( $results, 200 );\n}\n\n/**\n * 获取侧边栏公告\n */\nfunction lyrargon_agent_rest_announcement() {\n\t$content = get_option( 'lyrargon_sidebar_announcement', '' );\n\treturn new WP_REST_Response( array(\n\t\t'content' => $content,\n\t), 200 );\n}\n\n/**\n * 构建知识库上下文文本\n */\nfunction lyrargon_agent_build_context() {\n\t$parts = array();\n\n\t// 公告\n\t$announcement = get_option( 'lyrargon_sidebar_announcement', '' );\n\tif ( ! empty( $announcement ) ) {\n\t\t$parts[] = \"=== 博客公告 ===\\n\" . wp_strip_all_tags( $announcement ) . \"\\n\";\n\t}\n\n\t// 最近文章\n\t$post_count = (int) get_option( 'lyrargon_agent_context_post_count', 10 );\n\tif ( $post_count < 1 ) {\n\t\t$post_count = 10;\n\t}\n\t$posts = get_posts( array(\n\t\t'post_type' => 'post',\n\t\t'posts_per_page' => $post_count,\n\t\t'post_status' => 'publish',\n\t) );\n\tif ( ! empty( $posts ) ) {\n\t\t$lines = array();\n\t\tforeach ( $posts as $p ) {\n\t\t\t$lines[] = '- ' . get_the_title( $p ) . ' | ' . get_permalink( $p );\n\t\t}\n\t\t$parts[] = \"=== 最近文章 ===\\n\" . implode( \"\\n\", $lines ) . \"\\n\";\n\t}\n\n\t// 最近说说\n\t$shuoshuo_count = (int) get_option( 'lyrargon_agent_context_shuoshuo_count', 10 );\n\tif ( $shuoshuo_count < 1 ) {\n\t\t$shuoshuo_count = 10;\n\t}\n\t$shuoshuo = get_posts( array(\n\t\t'post_type' => 'shuoshuo',\n\t\t'posts_per_page' => $shuoshuo_count,\n\t\t'post_status' => 'publish',\n\t) );\n\tif ( ! empty( $shuoshuo ) ) {\n\t\t$lines = array();\n\t\tforeach ( $shuoshuo as $s ) {\n\t\t\t$lines[] = '- ' . wp_strip_all_tags( $s->post_content );\n\t\t}\n\t\t$parts[] = \"=== 最近说说 ===\\n\" . implode( \"\\n\", $lines ) . \"\\n\";\n\t}\n\n\treturn implode( \"\\n\", $parts );\n}\n\n/**\n * 搜索博客内容(用于 Agent 搜索功能)\n */\nfunction lyrargon_agent_search_content( $term ) {\n\tif ( empty( trim( $term ) ) ) {\n\t\treturn array();\n\t}\n\t$types = array( 'post', 'shuoshuo' );\n\t$query = new WP_Query( array(\n\t\t'post_type' => $types,\n\t\t's' => $term,\n\t\t'posts_per_page' => 8,\n\t\t'post_status' => 'publish',\n\t\t'no_found_rows' => true,\n\t\t'ignore_sticky_posts' => true,\n\t) );\n\t$results = array();\n\tforeach ( $query->posts as $post ) {\n\t\t$results[] = array(\n\t\t\t'id' => $post->ID,\n\t\t\t'title' => get_the_title( $post ),\n\t\t\t'url' => get_permalink( $post ),\n\t\t\t'type' => $post->post_type,\n\t\t\t'excerpt' => wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post->post_content ) ), 40, '...' ),\n\t\t);\n\t}\n\treturn $results;\n}\n\n/**\n * 代理调用 OpenAI 兼容 API(chat/completions)\n */\nfunction lyrargon_agent_rest_chat( $request ) {\n\t// --- QoS 检查 ---\n\t$ip_limit = (int) get_option( 'lyrargon_agent_ip_qos_limit', 30 );\n\t$ip_window = (int) get_option( 'lyrargon_agent_ip_qos_window', 60 );\n\t$daily_limit = (int) get_option( 'lyrargon_agent_daily_qos_limit', 200 );\n\n\t$ip_check = lyrargon_agent_check_ip_qos( $ip_limit, $ip_window );\n\tif ( ! $ip_check['allowed'] ) {\n\t\treturn new WP_REST_Response( array(\n\t\t\t'error' => 'rate_limited',\n\t\t\t'message' => __( '请求过于频繁,请稍后再试', 'lyrargon' ),\n\t\t\t'retry_after_seconds' => 60,\n\t\t), 429 );\n\t}\n\n\t$daily_check = lyrargon_agent_check_daily_qos( $daily_limit );\n\tif ( ! $daily_check['allowed'] ) {\n\t\treturn new WP_REST_Response( array(\n\t\t\t'error' => 'daily_limit_reached',\n\t\t\t'message' => __( '今日 API 调用次数已达上限', 'lyrargon' ),\n\t\t), 429 );\n\t}\n\n\t// --- 读取配置 ---\n\t$api_endpoint = get_option( 'lyrargon_agent_api_endpoint', 'https://api.deepseek.com/v1/chat/completions' );\n\t$api_key = get_option( 'lyrargon_agent_api_key', '' );\n\t$model = get_option( 'lyrargon_agent_model', 'deepseek-chat' );\n\t$temperature = (float) get_option( 'lyrargon_agent_temperature', '0.7' );\n\t$max_tokens = (int) get_option( 'lyrargon_agent_max_tokens', '2048' );\n\t$system_prompt_template = get_option( 'lyrargon_agent_system_prompt',\n\t\t'你是一个博客助手,基于以下博客内容回答用户问题。请从博客内容中查找相关信息并给出准确回答。如果找不到相关信息,请如实告知。回答使用 Markdown 格式,文章链接请用 [标题](URL) 形式展示。'\n\t);\n\n\tif ( empty( $api_key ) ) {\n\t\treturn new WP_REST_Response( array(\n\t\t\t'error' => 'not_configured',\n\t\t\t'message' => __( 'AI Agent 尚未配置 API Key', 'lyrargon' ),\n\t\t), 503 );\n\t}\n\n\t// --- 解析请求 ---\n\t$body_json = $request->get_body();\n\t$body = json_decode( $body_json, true );\n\tif ( empty( $body ) || ! isset( $body['message'] ) ) {\n\t\treturn new WP_REST_Response( array(\n\t\t\t'error' => 'invalid_request',\n\t\t\t'message' => __( '请求参数无效', 'lyrargon' ),\n\t\t), 400 );\n\t}\n\n\t$user_message = trim( $body['message'] );\n\t$search_enabled = get_option( 'lyrargon_agent_search_enabled', 'true' ) === 'true';\n\t$context = '';\n\n\t// 获取知识库上下文\n\tif ( $search_enabled ) {\n\t\t$context = lyrargon_agent_build_context();\n\t}\n\n\t// 如有搜索词,额外搜索\n\t$search_results = array();\n\tif ( $search_enabled && strlen( $user_message ) > 1 ) {\n\t\t$search_results = lyrargon_agent_search_content( $user_message );\n\t}\n\tif ( ! empty( $search_results ) ) {\n\t\t$search_text = \"=== 搜索结果 ===\\n\";\n\t\tforeach ( $search_results as $r ) {\n\t\t\t$search_text .= \"- [{$r['title']}]({$r['url']}) ({$r['type']}): {$r['excerpt']}\\n\";\n\t\t}\n\t\t$context .= \"\\n\" . $search_text;\n\t}\n\n\t// 构建 System Prompt\n\t$system_content = $system_prompt_template;\n\tif ( ! empty( $context ) ) {\n\t\t$system_content .= \"\\n\\n以下是博客当前的内容,请基于此回答:\\n\" . $context;\n\t}\n\n\t// --- 构建请求消息 ---\n\t$messages = array(\n\t\tarray( 'role' => 'system', 'content' => $system_content ),\n\t);\n\tif ( ! empty( $body['history'] ) && is_array( $body['history'] ) ) {\n\t\tforeach ( $body['history'] as $msg ) {\n\t\t\tif ( isset( $msg['role'], $msg['content'] ) ) {\n\t\t\t\t$messages[] = array(\n\t\t\t\t\t'role' => $msg['role'],\n\t\t\t\t\t'content' => $msg['content'],\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\t$messages[] = array( 'role' => 'user', 'content' => $user_message );\n\n\t// --- 调用 AI API ---\n\t$request_args = array(\n\t\t'headers' => array(\n\t\t\t'Content-Type' => 'application/json',\n\t\t\t'Authorization' => 'Bearer ' . $api_key,\n\t\t),\n\t\t'body' => wp_json_encode( array(\n\t\t\t'model' => $model,\n\t\t\t'messages' => $messages,\n\t\t\t'temperature' => $temperature,\n\t\t\t'max_tokens' => $max_tokens,\n\t\t\t'stream' => false,\n\t\t) ),\n\t\t'timeout' => 60,\n\t);\n\n\t$response = wp_remote_post( $api_endpoint, $request_args );\n\n\tif ( is_wp_error( $response ) ) {\n\t\treturn new WP_REST_Response( array(\n\t\t\t'error' => 'api_error',\n\t\t\t'message' => $response->get_error_message(),\n\t\t), 502 );\n\t}\n\n\t$response_code = wp_remote_retrieve_response_code( $response );\n\t$response_body = wp_remote_retrieve_body( $response );\n\t$response_data = json_decode( $response_body, true );\n\n\tif ( $response_code !== 200 || empty( $response_data ) ) {\n\t\treturn new WP_REST_Response( array(\n\t\t\t'error' => 'api_error',\n\t\t\t'message' => isset( $response_data['error']['message'] ) ? $response_data['error']['message'] : __( 'AI API 返回错误', 'lyrargon' ),\n\t\t\t'response_code' => $response_code,\n\t\t), 502 );\n\t}\n\n\t$reply = '';\n\tif ( isset( $response_data['choices'][0]['message']['content'] ) ) {\n\t\t$reply = $response_data['choices'][0]['message']['content'];\n\t}\n\n\treturn new WP_REST_Response( array(\n\t\t'reply' => $reply,\n\t\t'qos' => array(\n\t\t\t'ip_remaining' => $ip_check['remaining'],\n\t\t\t'daily_current' => $daily_check['current'],\n\t\t\t'daily_limit' => $daily_check['limit'],\n\t\t),\n\t), 200 );\n}\n\n// ============== 注册 REST API 路由 ==============\n\nadd_action( 'rest_api_init', function () {\n\t// 获取文章\n\tregister_rest_route( 'lyrargon/v1', '/agent/posts', array(\n\t\t'methods' => WP_REST_Server::READABLE,\n\t\t'callback' => 'lyrargon_agent_rest_posts',\n\t\t'permission_callback' => '__return_true',\n\t\t'args' => array(\n\t\t\t'per_page' => array(\n\t\t\t\t'required' => false,\n\t\t\t\t'sanitize_callback' => 'absint',\n\t\t\t\t'default' => 20,\n\t\t\t),\n\t\t),\n\t) );\n\n\t// 获取说说\n\tregister_rest_route( 'lyrargon/v1', '/agent/shuoshuo', array(\n\t\t'methods' => WP_REST_Server::READABLE,\n\t\t'callback' => 'lyrargon_agent_rest_shuoshuo',\n\t\t'permission_callback' => '__return_true',\n\t\t'args' => array(\n\t\t\t'per_page' => array(\n\t\t\t\t'required' => false,\n\t\t\t\t'sanitize_callback' => 'absint',\n\t\t\t\t'default' => 20,\n\t\t\t),\n\t\t),\n\t) );\n\n\t// 获取公告\n\tregister_rest_route( 'lyrargon/v1', '/agent/announcement', array(\n\t\t'methods' => WP_REST_Server::READABLE,\n\t\t'callback' => 'lyrargon_agent_rest_announcement',\n\t\t'permission_callback' => '__return_true',\n\t) );\n\n\t// 聊天代理(POST)\n\tregister_rest_route( 'lyrargon/v1', '/agent/chat', array(\n\t\t'methods' => WP_REST_Server::CREATABLE,\n\t\t'callback' => 'lyrargon_agent_rest_chat',\n\t\t'permission_callback' => '__return_true',\n\t) );\n} );\n"}}}}