文件操作 - ai.php
返回文件管理
返回主菜单
删除本文件
文件: /usr/local/softaculous/lib/ai/ai.php
编辑文件内容
<?php if(!defined('SOFTACULOUS')){ die('Hacking Attempt'); } function ai(){ global $user, $globals, $l, $theme, $softpanel, $error, $insid, $software, $soft; global $edited, $settings, $iscripts, $catwise, $scripts, $noheader; global $softpath, $custom_path; if(version_compare(PHP_VERSION, '7.1', '<')){ echo 'PHP 7.1 required to use this feature'; die(); } if(empty($globals['lictype']) || !empty($globals['licexpired'])){ echo 'An active '.APP.' license is required to use this feature!'; die(); } $insid = GET('insid', ''); $custom_path = GET('path', ''); $project_id = GET('project_id', ''); $username = $softpanel->user['name']; include_once(__DIR__.'/ai_launcher.php'); require_once(__DIR__ . '/core/class_ai_file_handler.php'); $api_sp = ''; if(!empty($project_id)){ ai_php_init_classes(); require_once(__DIR__ . '/core/class_project.php'); $_proj = AIProject::load($username, $project_id); if($_proj && !empty($_proj['path'])) $api_sp = $_proj['path']; }elseif(!empty($insid) && !empty($user['ins'][$insid])){ $api_sp = $user['ins'][$insid]['softpath']; }elseif(!empty($custom_path)){ $_hd = !empty($softpanel->user['homedir']) ? $softpanel->user['homedir'] : ai_get_homedir($username); $_cp = trim($custom_path); if(strpos($_cp, $_hd) === 0) $_cp = substr($_cp, strlen($_hd) + 1); $api_sp = cleanpath($_hd . '/' . $_cp); } if(optGET('ai_php_api')){ ai_handle_php_api($username, $api_sp); die(); } if(optGET('ai_chat_stream')){ ai_php_init_classes(); $content = !empty($_POST['content']) ? $_POST['content'] : ''; $options = array(); if(!empty($_POST['conversation_id'])) $options['conversation_id'] = $_POST['conversation_id']; if(!empty($_POST['attachments'])) $options['attachments'] = json_decode($_POST['attachments'], true) ? json_decode($_POST['attachments'], true) : array(); ai_php_send_prompt_stream($username, $api_sp, $content, $options); die(); } if(empty($insid) && empty($custom_path) && empty($project_id)){ $theme['init_theme'] = 'ai'; $theme['init_theme_name'] = 'AI Assistant'; $theme['init_theme_func'] = array('ai_theme'); $theme['call_theme_func'] = 'ai_theme'; return true; } if(!empty($project_id)){ include_once(__DIR__.'/ai_launcher.php'); ai_php_init_classes(); require_once(__DIR__ . '/core/class_project.php'); $project = AIProject::load($softpanel->user['name'], $project_id); if($project && !empty($project['path'])){ $custom_path = $project['path']; $insid = !empty($project['insid']) ? $project['insid'] : ''; } } if(!empty($insid)){ if(empty($user['ins'][$insid])){ reporterror(__('Invalid Installation'), __('The installation ID is invalid or does not exist')); return false; } $data = $user['ins'][$insid]; $soft = get_sid_by_version($data['ver'], $data['sid']); $software = !empty($iscripts[$soft]) ? $iscripts[$soft] : array('name' => 'Software'); $softpath = $data['softpath']; }else{ $home_dir = $softpanel->user['homedir']; $custom_path = trim($custom_path); if(empty($custom_path)){ $custom_path = $home_dir; } if(strpos($custom_path, './') !== false || strpos($custom_path, '../') !== false || strpos($custom_path, '/..') !== false || strpos($custom_path, '..') === 0){ reporterror(__('Invalid Path'), __('Path traversal is not allowed')); return false; } if(strpos($custom_path, $home_dir) === 0){ $custom_path = substr($custom_path, strlen($home_dir) + 1); } $softpath = cleanpath($home_dir . '/' . $custom_path); if($softpath !== $home_dir && strpos($softpath, $home_dir . '/') !== 0){ reporterror(__('Invalid Path'), __('The path must be within your home directory')); return false; } if(empty($softpath) || !@is_dir($softpath)){ reporterror(__('Invalid Path'), __('The directory path is invalid or does not exist.')); return false; } $software = array('name' => basename($softpath)); $insid = ''; } $username = $softpanel->user['name']; include_once(__DIR__.'/ai_launcher.php'); if(optGET('ai_chat_stream')){ ai_php_init_classes(); $content = !empty($_POST['content']) ? $_POST['content'] : ''; $options = array(); if(!empty($_POST['conversation_id'])) $options['conversation_id'] = $_POST['conversation_id']; if(!empty($_POST['attachments'])) $options['attachments'] = json_decode($_POST['attachments'], true) ? json_decode($_POST['attachments'], true) : array(); ai_php_send_prompt_stream($username, $softpath, $content, $options); die(); } $theme['init_theme'] = 'ai'; $theme['init_theme_name'] = 'AI Assistant'; $theme['init_theme_func'] = array('ai_theme'); $theme['call_theme_func'] = 'ai_theme'; } function ai_handle_php_api($username, $softpath){ global $globals, $user, $softpanel, $iscripts; ai_php_init_classes(); require_once(__DIR__ . '/core/class_project.php'); $action = optGET('ai_php_api'); header('Content-Type: application/json; charset='.$globals['charset']); $user_home_dir = !empty($softpanel->user['homedir']) ? $softpanel->user['homedir'] : ai_get_homedir($username); $needs_path = array('status', 'start', 'stop', 'file_tree', 'read_file', 'write_file', 'search', 'snapshot', 'snapshots', 'restore', 'diff', 'project_info', 'conversations', 'conversation', 'new_session', 'switch_conversation', 'delete_conversation', 'clear', 'set_mode', 'regenerate', 'edit_message', 'auto_title', 'abort', 'check_lock', 'force_unlock', 'undo', 'redo', 'fork', 'compact', 'ai_compact', 'rename_conversation', 'toggle_permission', 'shell'); if(empty($softpath) && in_array($action, $needs_path)){ echo json_encode(array('error' => 'No project path set')); die(); } switch($action){ case 'projects_list': $projects = AIProject::list_all($username); echo json_encode($projects); break; case 'projects_get': $project_id = isset($_GET['project_id']) ? $_GET['project_id'] : ''; if(empty($project_id)){ echo json_encode(array('error' => 'Project ID is required')); break; } $project = AIProject::load($username, $project_id); if(!$project){ echo json_encode(array('error' => 'Project not found')); break; } echo json_encode($project); break; case 'projects_create': $path = isset($_POST['path']) ? $_POST['path'] : ''; $name = isset($_POST['name']) ? $_POST['name'] : ''; $type = isset($_POST['type']) ? $_POST['type'] : 'custom'; $insid = isset($_POST['insid']) ? $_POST['insid'] : ''; $home_dir = $softpanel->user['homedir']; if(empty($path)){ $path = $home_dir; } $path = trim($path); if(strpos($path, './') !== false || strpos($path, '../') !== false || strpos($path, '/..') !== false || strpos($path, '..') === 0){ echo json_encode(array('error' => 'Path traversal is not allowed')); break; } if(strpos($path, $home_dir) === 0){ $path = substr($path, strlen($home_dir) + 1); } $full_path = cleanpath($home_dir . '/' . $path); if($full_path !== $home_dir && strpos($full_path, $home_dir . '/') !== 0){ echo json_encode(array('error' => 'The path must be within your home directory')); break; } if(empty($full_path) || !@is_dir($full_path)){ echo json_encode(array('error' => 'The directory path is invalid or does not exist')); break; } if(!empty($insid)){ $project_id = AIProject::create_from_installation($username, $insid, $full_path, $name, $type); } else { $project_id = AIProject::create_from_path($username, $full_path, $name); } $project = AIProject::load($username, $project_id); echo json_encode($project); break; case 'projects_update': $project_id = isset($_POST['project_id']) ? $_POST['project_id'] : ''; $name = isset($_POST['name']) ? $_POST['name'] : ''; if(empty($project_id)){ echo json_encode(array('error' => 'Project ID is required')); break; } $data = array(); if(!empty($name)) $data['name'] = $name; AIProject::update($username, $project_id, $data); echo json_encode(AIProject::load($username, $project_id)); break; case 'projects_delete': $project_id = isset($_POST['project_id']) ? $_POST['project_id'] : ''; if(empty($project_id)){ echo json_encode(array('error' => 'Project ID is required')); break; } AIProject::delete($username, $project_id); echo json_encode(array('success' => true)); break; case 'projects_wordpress': $installations = array(); if(!empty($user['ins'])){ foreach($user['ins'] as $insid => $idata){ $soft = get_sid_by_version($idata['ver'], $idata['sid']); $software = !empty($iscripts[$soft]) ? $iscripts[$soft] : array('name' => 'Software'); $installations[] = array( 'insid' => $insid, 'name' => $software['name'], 'path' => $idata['softpath'], 'url' => !empty($idata['softurl']) ? $idata['softurl'] : '' ); } } echo json_encode($installations); break; case 'status': $session = AISession::load($username, $softpath); if(!empty($session['active_conversation'])){ $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv_file = $conv_dir . '/' . $session['active_conversation'] . '.json.php'; if(!file_exists($conv_file)){ $found = false; require_once(__DIR__ . '/core/class_ai_file_handler.php'); foreach(AIFileHandler::list_files($conv_dir, 'conv_*.json.php') as $f){ $d = AIFileHandler::read($f); if(!empty($d['id']) && $d['id'] === $session['active_conversation']){ $found = true; break; } } if(!$found){ unset($session['active_conversation']); AISession::save($username, $softpath, $session); } } } $settings = new AISettings($username); $lock_status = AISession::check_lock($username, $softpath); $fav_models = $settings->get_favorite_models(); $permissions = $settings->get_permissions(); // Auto-detect model metadata from models.dev $models_dev = ai_get_models_dev_cache(); $model_info = array(); $providers_out = $settings->get_all_providers(); foreach($providers_out as &$p){ if(empty($p['models'])) continue; foreach($p['models'] as $mid => $mname){ $key = ($p['id'] ?? '') . '/' . $mid; if(isset($models_dev[$key])){ $model_info[$mid] = array( 'context' => $models_dev[$key]['context'], 'output' => $models_dev[$key]['output'], 'reasoning' => $models_dev[$key]['reasoning'], 'tool_call' => $models_dev[$key]['tool_call'], 'caching' => ai_model_supports_caching($p['id'] ?? '', $mid) ); }elseif(isset($models_dev[$mid])){ $model_info[$mid] = array( 'context' => $models_dev[$mid]['context'], 'output' => $models_dev[$mid]['output'], 'reasoning' => $models_dev[$mid]['reasoning'], 'tool_call' => $models_dev[$mid]['tool_call'], 'caching' => ai_model_supports_caching($p['id'] ?? '', $mid) ); } } } unset($p); echo json_encode(array( 'session' => $session, 'providers' => $providers_out, 'lock' => $lock_status, 'favorite_models' => $fav_models, 'permissions' => $permissions, 'model_info' => $model_info )); break; case 'start': $provider = isset($_POST['provider']) ? $_POST['provider'] : 'anthropic'; $model = isset($_POST['model']) ? $_POST['model'] : ''; $mode = isset($_POST['mode']) ? $_POST['mode'] : 'build'; $variant = isset($_POST['variant']) ? $_POST['variant'] : ''; $session = AISession::load($username, $softpath); if(empty($session)) $session = array(); $session['provider'] = $provider; $session['model'] = $model; $session['mode'] = $mode; if(!empty($variant) && $variant !== 'default'){ $session['variant'] = $variant; }else{ unset($session['variant']); } AISession::save($username, $softpath, $session); echo json_encode(array('success' => true)); break; case 'stop': AISession::delete($username, $softpath); echo json_encode(array('success' => true)); break; case 'providers': $settings = new AISettings($username); echo json_encode($settings->get_all_providers()); break; case 'models': $settings = new AISettings($username); echo json_encode($settings->get_all_models()); break; case 'conversation': $conv_id = isset($_GET['conversation_id']) ? $_GET['conversation_id'] : (isset($_POST['conversation_id']) ? $_POST['conversation_id'] : AISession::get_active_conversation_id($username, $softpath)); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv_file = $conv_dir . '/' . $conv_id . '.json.php'; $conv = null; if(file_exists($conv_file)){ $conv = AIConversation::load($conv_file); } if(!$conv){ require_once(__DIR__ . '/core/class_ai_file_handler.php'); foreach(AIFileHandler::list_files($conv_dir, 'conv_*.json.php') as $f){ $d = AIFileHandler::read($f); if(!empty($d['id']) && $d['id'] === $conv_id){ $conv = AIConversation::load($f); break; } } } echo json_encode($conv ? $conv->get_all() : array('messages' => array())); break; case 'conversations': $conv_dir = AISession::get_conversations_dir($username, $softpath); echo json_encode(AIConversation::list_for_project($conv_dir)); break; case 'new_session': $conv_id = 'conv_' . substr(md5(uniqid(mt_rand(), true)), 0, 12); AISession::set_active_conversation($username, $softpath, $conv_id); $conv_dir = AISession::get_conversations_dir($username, $softpath); AIConversation::create($conv_dir . '/' . $conv_id . '.json.php', $softpath, $conv_id); echo json_encode(array('success' => true, 'id' => $conv_id)); break; case 'switch_conversation': $conv_id = isset($_POST['conversation_id']) ? $_POST['conversation_id'] : ''; if(!empty($conv_id)){ AISession::set_active_conversation($username, $softpath, $conv_id); } echo json_encode(array('success' => true)); break; case 'delete_conversation': $conv_id = isset($_POST['conversation_id']) ? $_POST['conversation_id'] : ''; if(!empty($conv_id)){ $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv_file = $conv_dir . '/' . $conv_id . '.json.php'; if(file_exists($conv_file)){ AIConversation::delete($conv_file); }else{ foreach(AIFileHandler::list_files($conv_dir, 'conv_*.json.php') as $f){ $d = AIFileHandler::read($f); if(!empty($d['id']) && $d['id'] === $conv_id){ AIConversation::delete($f); break; } } } $session = AISession::load($username, $softpath); $session = $session ? $session : array(); if(!empty($session['active_conversation']) && $session['active_conversation'] === $conv_id){ unset($session['active_conversation']); AISession::save($username, $softpath, $session); } } echo json_encode(array('success' => true)); break; case 'clear': $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ $conv->clear(); $conv->save(); } echo json_encode(array('success' => true)); break; case 'set_mode': $mode = isset($_POST['mode']) ? $_POST['mode'] : 'build'; $session = AISession::load($username, $softpath); $session = $session ? $session : array(); $session['mode'] = $mode; AISession::save($username, $softpath, $session); $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ $conv->set_mode($mode); $conv->save(); } echo json_encode(array('success' => true)); break; case 'file_tree': $path = isset($_GET['path']) ? $_GET['path'] : '/'; $depth = intval(isset($_GET['depth']) ? $_GET['depth'] : 3); $fm = new AIFileManager($softpath, $user_home_dir); $result = $fm->list_directory($path, $depth); echo json_encode($result); break; case 'read_file': $path = isset($_GET['path']) ? $_GET['path'] : ''; $fm = new AIFileManager($softpath, $user_home_dir); echo json_encode($fm->read_file($path)); break; case 'write_file': $path = isset($_POST['path']) ? $_POST['path'] : ''; $content = isset($_POST['content']) ? $_POST['content'] : ''; $fm = new AIFileManager($softpath, $user_home_dir); echo json_encode($fm->write_file($path, $content, true)); break; case 'search': $pattern = isset($_GET['pattern']) ? $_GET['pattern'] : ''; $path = isset($_GET['path']) ? $_GET['path'] : '/'; $ext = isset($_GET['include']) ? $_GET['include'] : ''; $fm = new AIFileManager($softpath, $user_home_dir); $exts = !empty($ext) ? array($ext) : array(); echo json_encode($fm->search_in_files($pattern, $path, $exts)); break; case 'snapshot': $message = isset($_POST['message']) ? $_POST['message'] : 'Snapshot at '.date('Y-m-d H:i:s'); $sm = new AISnapshotManager($softpath, true, $user_home_dir); echo json_encode($sm->create_snapshot($message)); break; case 'snapshots': $limit = intval(isset($_GET['limit']) ? $_GET['limit'] : 50); $sm = new AISnapshotManager($softpath, true, $user_home_dir); echo json_encode($sm->list_snapshots($limit)); break; case 'restore': $id = isset($_POST['id']) ? $_POST['id'] : ''; $sm = new AISnapshotManager($softpath, true, $user_home_dir); echo json_encode($sm->restore_snapshot($id)); break; case 'diff': $sm = new AISnapshotManager($softpath, true, $user_home_dir); echo json_encode($sm->get_working_diff()); break; case 'project_info': $ctx = new ProjectContext($softpath); echo json_encode(array( 'type' => $ctx->detect_type(), 'overview' => $ctx->get_overview(), 'path' => $softpath )); break; case 'settings_load': $settings = new AISettings($username); $providers = $settings->get_connected_providers(); foreach($providers as &$p){ if(!empty($p['api_key'])){ $p['api_key_masked'] = substr($p['api_key'], 0, 8) . '...' . substr($p['api_key'], -4); unset($p['api_key']); } } $fav_models = $settings->get_favorite_models(); $permissions = $settings->get_permissions(); echo json_encode(array('providers' => $providers, 'favorite_models' => $fav_models, 'permissions' => $permissions)); break; case 'settings_save': $provider_id = isset($_POST['provider_id']) ? $_POST['provider_id'] : ''; $api_key = isset($_POST['api_key']) ? $_POST['api_key'] : ''; $name = isset($_POST['name']) ? $_POST['name'] : ''; $base_url = isset($_POST['base_url']) ? $_POST['base_url'] : ''; $models_raw = isset($_POST['models']) ? $_POST['models'] : 'array()'; $models = @json_decode($models_raw, true) ? @json_decode($models_raw, true) : array(); $no_key_providers = array('ollama', 'opencode_zen'); if(strpos($provider_id, 'custom:') === 0){ $no_key_providers[] = $provider_id; } if(empty($provider_id) || (empty($api_key) && !in_array($provider_id, $no_key_providers))){ echo json_encode(array('error' => 'Provider ID and API key are required')); break; } $config = array('api_key' => $api_key, 'connected_at' => time()); if(!empty($name)) $config['name'] = $name; if(!empty($base_url)) $config['base_url'] = $base_url; if(!empty($models)) $config['models'] = $models; if(strpos($provider_id, 'custom:') === 0){ if(empty($base_url)){ echo json_encode(array('error' => 'Base URL is required for custom providers')); break; } if(empty($name)) $config['name'] = str_replace('custom:', '', $provider_id); if(empty($models)) $config['models'] = array('default' => 'Default Model'); } $settings = new AISettings($username); $settings->save_provider($provider_id, $config); echo json_encode(array('success' => true)); break; case 'settings_delete': $provider_id = isset($_POST['provider_id']) ? $_POST['provider_id'] : ''; $settings = new AISettings($username); $settings->delete_provider($provider_id); echo json_encode(array('success' => true)); break; case 'test_connection': $provider_id = isset($_POST['provider_id']) ? $_POST['provider_id'] : ''; $api_key = isset($_POST['api_key']) ? $_POST['api_key'] : ''; $model = isset($_POST['model']) ? $_POST['model'] : ''; $base_url = isset($_POST['base_url']) ? $_POST['base_url'] : ''; $models_raw = isset($_POST['models']) ? $_POST['models'] : ''; $models = $models_raw ? (@json_decode($models_raw, true) ?: array()) : array(); $settings = new AISettings($username); $provider_config = $settings->get_provider_config($provider_id); $provider_config = $provider_config ? $provider_config : array(); // Pull filter-managed api_key/base_url from the filter at runtime. // These are NEVER stored in the user's settings file. $filter_provider = function_exists('ai_php_get_filter_provider_config') ? ai_php_get_filter_provider_config($provider_id) : null; if(is_array($filter_provider)){ $filter_auth_type = !empty($filter_provider['auth_type']) ? $filter_provider['auth_type'] : 'api_key'; $filter_managed = ($filter_auth_type === 'none') || !empty($filter_provider['api_key']); if($filter_managed){ if(!empty($filter_provider['api_key'])) $provider_config['api_key'] = (string)$filter_provider['api_key']; if(!empty($filter_provider['base_url'])) $provider_config['base_url'] = (string)$filter_provider['base_url']; if(!empty($filter_provider['models'])) $provider_config['models'] = $filter_provider['models']; } } // User-supplied values from the popup still win over filter values, // unless the provider is filter-managed (in which case the filter is authoritative). if(empty($filter_provider) || empty($filter_managed)){ if(!empty($api_key)) $provider_config['api_key'] = $api_key; if(!empty($base_url)) $provider_config['base_url'] = $base_url; } if(!empty($models)) $provider_config['models'] = $models; $provider_instance = ai_php_get_provider_instance($provider_id, $provider_config); $test_model = !empty($model) ? $model : $provider_instance->get_default_model(); if(empty($test_model)){ if(!empty($provider_config['models'])){ $test_model = array_key_first($provider_config['models']); } if(empty($test_model)){ echo json_encode(array('error' => 'No model specified. Please add at least one model.')); break; } } $client = new AIClient($provider_instance, isset($provider_config['api_key']) ? $provider_config['api_key'] : '', $test_model, $provider_config); echo json_encode($client->test_connection()); break; case 'edit_message': $msg_id = isset($_POST['message_id']) ? $_POST['message_id'] : ''; $new_content = isset($_POST['content']) ? $_POST['content'] : ''; if(empty($msg_id) || empty($new_content)){ echo json_encode(array('error' => 'message_id and content are required')); break; } $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ echo json_encode(array('success' => true)); }else{ echo json_encode(array('error' => 'Conversation not found')); } break; case 'regenerate': $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ $last_asst = $conv->get_last_assistant_message_id(); if($last_asst){ $conv->truncate_after_message($last_asst); $conv->save(); } echo json_encode(array('success' => true)); }else{ echo json_encode(array('error' => 'Conversation not found')); } break; case 'changes': $sm = new AISnapshotManager($softpath, true, $user_home_dir); $diff = $sm->get_working_diff(); $snapshots = $sm->list_snapshots(20); echo json_encode(array('diff' => $diff, 'snapshots' => $snapshots)); break; case 'resolve_file': $path = isset($_GET['path']) ? $_GET['path'] : ''; $fm = new AIFileManager($softpath, $user_home_dir); $result = $fm->read_file($path); if(!empty($result['error'])){ echo json_encode(array('error' => $result['error'])); }else{ echo json_encode(array('content' => $result['content'], 'path' => $path)); } break; case 'auto_title': $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ $msgs = $conv->get_messages(); $title = ''; $user_msg = ''; $asst_msg = ''; foreach($msgs as $m){ if(($m['role'] ?? '') === 'user' && !empty($m['content']) && empty($user_msg)){ $user_msg = $m['content']; } if(($m['role'] ?? '') === 'assistant' && !empty($m['content'])){ foreach($m['parts'] ?? array() as $p){ if(($p['type'] ?? '') === 'text' && !empty($p['text'])){ $asst_msg .= $p['text']; } } if(empty($asst_msg)){ $asst_msg = $m['content'] ?? ''; } if(!empty($asst_msg)) break; } } // Try AI-generated title first if(!empty($user_msg)){ $session = AISession::load($username, $softpath); if(!empty($session['provider'])){ require_once(__DIR__ . '/ai_launcher.php'); ai_php_init_classes(); $settings = new AISettings($username); $provider_config = $settings->get_provider_config($session['provider']) ?: array(); $provider_instance = ai_php_get_provider_instance($session['provider'], $provider_config); $api_key = $settings->get_api_key($session['provider']); if($provider_instance && $api_key){ $model = !empty($session['model']) ? $session['model'] : $provider_instance->get_default_model(); $client = new AIClient($provider_instance, $api_key, $model, $provider_config); $title_prompt = array( array('role' => 'system', 'content' => 'Generate a very short title (max 6 words) for a coding conversation. Respond with ONLY the title, nothing else. No quotes.'), array('role' => 'user', 'content' => 'User asked: ' . mb_substr($user_msg, 0, 300)) ); if(!empty($asst_msg)){ $title_prompt[1]['content'] .= "\nAssistant responded about: " . mb_substr($asst_msg, 0, 200); } try{ $result = $client->chat($title_prompt, array(), array('max_tokens' => 30, 'timeout' => 15)); if(!empty($result['content'])){ $title = trim($result['content']); $title = preg_replace('/^["\']|["\']$/', '', $title); $title = mb_substr($title, 0, 80); } }catch(\Exception $e){} } } } // Fallback to first message snippet if(empty($title)){ foreach($msgs as $m){ if(($m['role'] ?? '') === 'user' && !empty($m['content'])){ $title = mb_substr($m['content'], 0, 60); break; } } } if($title && $conv->count_user_messages() <= 1){ $conv->set_title($title); $conv->save(); } echo json_encode(array('success' => true, 'title' => $title)); }else{ echo json_encode(array('error' => 'Conversation not found')); } break; case 'abort': $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $abort_file = $conv_dir . '/' . $conv_id . '.abort'; @file_put_contents($abort_file, time()); echo json_encode(array('success' => true)); break; case 'check_lock': $lock_status = AISession::check_lock($username, $softpath); echo json_encode($lock_status); break; case 'force_unlock': $unlock_result = AISession::force_unlock($username, $softpath); echo json_encode($unlock_result); break; case 'undo': $msg_id = isset($_POST['message_id']) ? $_POST['message_id'] : ''; if(empty($msg_id)){ echo json_encode(array('error' => 'message_id is required')); break; } $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ // Remove the target message and everything after it; save removed for redo $messages = $conv->get_messages(); $removed = array(); $new_messages = array(); $found = false; foreach($messages as $m){ if(!$found){ if(($m['id'] ?? '') === $msg_id){ $found = true; $removed[] = $m; continue; } $new_messages[] = $m; }else{ $removed[] = $m; } } if(!$found){ echo json_encode(array('error' => 'Message not found')); break; } $conv->set_messages($new_messages); $conv->save(); // Store redo data using AIFileHandler $redo_file = $conv_dir . '/' . $conv_id . '.redo.json.php'; AIFileHandler::write($redo_file, array('message_id' => $msg_id, 'removed' => $removed)); echo json_encode(array('success' => true, 'messages' => $conv->get_messages(), 'can_redo' => !empty($removed))); }else{ echo json_encode(array('error' => 'Conversation not found')); } break; case 'redo': $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $redo_file = $conv_dir . '/' . $conv_id . '.redo.json.php'; $redo = AIFileHandler::read($redo_file); if(!$redo || empty($redo['removed'])){ echo json_encode(array('error' => 'Nothing to redo')); break; } $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ // Restore the removed messages using the appropriate method foreach($redo['removed'] as $msg){ $role = $msg['role'] ?? ''; if($role === 'user'){ $conv->add_user_message($msg['content'] ?? '', $msg['attachments'] ?? array()); }elseif($role === 'assistant'){ $conv->add_assistant_content($msg['parts'] ?? array(), $msg['model'] ?? '', $msg['usage'] ?? array(), $msg['provider'] ?? ''); }elseif($role === 'tool_result'){ $conv->add_tool_result($msg['tool_call_id'] ?? '', $msg['content'] ?? '', !empty($msg['is_error'])); } } $conv->save(); // Delete redo file AIFileHandler::delete($redo_file); echo json_encode(array('success' => true, 'messages' => $conv->get_messages())); }else{ echo json_encode(array('error' => 'Conversation not found')); } break; case 'fork': $msg_id = isset($_POST['message_id']) ? $_POST['message_id'] : ''; if(empty($msg_id)){ echo json_encode(array('error' => 'message_id is required')); break; } $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ $messages = $conv->get_messages(); $fork_messages = array(); $forked_content = ''; $found = false; foreach($messages as $m){ if(($m['id'] ?? '') === $msg_id){ $found = true; $forked_content = $m['content'] ?? ''; break; // Stop BEFORE this message - it goes to input field } $fork_messages[] = $m; } if(!$found){ echo json_encode(array('error' => 'Message not found')); break; } $new_conv_id = 'conv_' . substr(md5(uniqid(mt_rand(), true)), 0, 12); $new_conv = AIConversation::create($conv_dir . '/' . $new_conv_id . '.json.php', $softpath, $new_conv_id); $new_conv->set_title(($conv->get_title() ?: 'Untitled') . ' (fork)'); $new_conv->set_mode($conv->get_mode()); foreach($fork_messages as $m){ if(($m['role'] ?? '') === 'user'){ $new_conv->add_user_message($m['content'] ?? '', $m['attachments'] ?? array()); }elseif(($m['role'] ?? '') === 'assistant'){ $new_conv->add_assistant_content($m['parts'] ?? array(), $m['model'] ?? '', $m['usage'] ?? array(), $m['provider'] ?? ''); }elseif(($m['role'] ?? '') === 'tool_result'){ $new_conv->add_tool_result($m['tool_call_id'] ?? '', $m['content'] ?? '', !empty($m['is_error'])); } } $new_conv->save(); AISession::set_active_conversation($username, $softpath, $new_conv->get_id()); echo json_encode(array('success' => true, 'conversation_id' => $new_conv->get_id(), 'title' => $new_conv->get_title(), 'forked_content' => $forked_content)); }else{ echo json_encode(array('error' => 'Conversation not found')); } break; case 'compact': $keep_last_n = isset($_POST['keep_last_n']) ? intval($_POST['keep_last_n']) : 3; if($keep_last_n < 1) $keep_last_n = 3; $conv_id = AISession::get_active_conversation_id($username, $softpath); $conv_dir = AISession::get_conversations_dir($username, $softpath); $conv = AIConversation::load($conv_dir . '/' . $conv_id . '.json.php'); if($conv){ $conv->compact($keep_last_n); $conv->save(); echo json_encode(array('success' => true, 'messages' => $conv->get_messages())); }else{ echo json_encode(array('error' => 'Conversation not found')); } break; case 'ai_compact': $keep_last_n_ai = isset($_POST['keep_last_n']) ? intval($_POST['keep_last_n']) : 4; if($keep_last_n_ai < 1) $keep_last_n_ai = 4; $conv_id_ai = AISession::get_active_conversation_id($username, $softpath); $conv_dir_ai = AISession::get_conversations_dir($username, $softpath); $conv_ai = AIConversation::load($conv_dir_ai . '/' . $conv_id_ai . '.json.php'); if(!$conv_ai){ echo json_encode(array('error' => 'Conversation not found')); break; } $conv_text = $conv_ai->get_messages_for_compaction($keep_last_n_ai); if(empty($conv_text)){ echo json_encode(array('success' => true, 'messages' => $conv_ai->get_messages(), 'summary' => 'Nothing to compact')); break; } $session_ai = AISession::load($username, $softpath); $settings_ai = new AISettings($username); $provider_id_ai = $session_ai['provider'] ?? ''; $provider_config_ai = $settings_ai->get_provider_config($provider_id_ai) ?: array(); $filter_provider_ai = ai_php_get_filter_provider_config($provider_id_ai); if(is_array($filter_provider_ai)){ $filter_managed_ai = !empty($filter_provider_ai['api_key']) || (($filter_provider_ai['auth_type'] ?? '') === 'none'); if($filter_managed_ai){ $provider_config_ai = array( 'api_key' => (string)($filter_provider_ai['api_key'] ?? ''), 'base_url' => (string)($filter_provider_ai['base_url'] ?? ''), 'auth_type' => (string)($filter_provider_ai['auth_type'] ?? 'api_key'), 'models' => !empty($filter_provider_ai['models']) ? $filter_provider_ai['models'] : (!empty($provider_config_ai['models']) ? $provider_config_ai['models'] : array()) ); } } $api_key_ai = !empty($provider_config_ai['api_key']) ? $provider_config_ai['api_key'] : ''; $model_ai = $session_ai['model'] ?? ''; if(empty($api_key_ai) && !in_array($provider_id_ai, array('opencode_zen')) && strpos($provider_id_ai, 'custom:') !== 0){ echo json_encode(array('error' => 'No API key configured for compaction')); break; } $provider_inst_ai = ai_php_get_provider_instance($provider_id_ai, $provider_config_ai ?: array()); $client_ai = new AIClient($provider_inst_ai, $api_key_ai, $model_ai, $provider_config_ai ?: array()); $compact_prompt = "Summarize the following conversation concisely, preserving all key decisions, code changes, file paths, and technical details. Focus on what was accomplished and any important context:\n\n" . mb_substr($conv_text, 0, 12000); $compact_msgs = array( array('role' => 'system', 'content' => 'You are a helpful assistant that creates concise summaries of coding conversations. Preserve all file paths, code snippets, and technical decisions.'), array('role' => 'user', 'content' => $compact_prompt) ); $compact_result = $client_ai->chat($compact_msgs, array(), array('max_tokens' => 2048, 'timeout' => 60)); $summary_text = ''; if(!empty($compact_result['parts'])){ foreach($compact_result['parts'] as $part){ if(($part['type'] ?? '') === 'text' && !empty($part['text'])){ $summary_text .= $part['text']; } } } if(empty($summary_text)){ $summary_text = $compact_result['content'] ?? ''; } if(empty($summary_text)){ $conv_ai->compact($keep_last_n_ai); $conv_ai->save(); echo json_encode(array('success' => true, 'messages' => $conv_ai->get_messages(), 'summary' => 'Compacted with basic method (AI summary failed)')); break; } $conv_ai->compact_with_summary($summary_text, $keep_last_n_ai); $conv_ai->save(); echo json_encode(array('success' => true, 'messages' => $conv_ai->get_messages(), 'summary' => mb_substr($summary_text, 0, 500))); break; case 'rename_conversation': $conv_id_rename = isset($_POST['conversation_id']) ? $_POST['conversation_id'] : ''; $new_title = isset($_POST['title']) ? $_POST['title'] : ''; if(empty($conv_id_rename) || empty($new_title)){ echo json_encode(array('error' => 'conversation_id and title are required')); break; } $conv_dir_rename = AISession::get_conversations_dir($username, $softpath); $conv_rename = AIConversation::load($conv_dir_rename . '/' . $conv_id_rename . '.json.php'); if($conv_rename){ $conv_rename->set_title($new_title); $conv_rename->save(); echo json_encode(array('success' => true, 'title' => $new_title)); }else{ echo json_encode(array('error' => 'Conversation not found')); } break; case 'favorite_model': $model_id = isset($_POST['model_id']) ? $_POST['model_id'] : ''; if(empty($model_id)){ echo json_encode(array('error' => 'model_id is required')); break; } $settings = new AISettings($username); $settings->add_favorite_model($model_id); echo json_encode(array('success' => true, 'favorite_models' => $settings->get_favorite_models())); break; case 'unfavorite_model': $model_id_rm = isset($_POST['model_id']) ? $_POST['model_id'] : ''; if(empty($model_id_rm)){ echo json_encode(array('error' => 'model_id is required')); break; } $settings = new AISettings($username); $settings->remove_favorite_model($model_id_rm); echo json_encode(array('success' => true, 'favorite_models' => $settings->get_favorite_models())); break; case 'toggle_permission': $perm_key = isset($_POST['permission']) ? $_POST['permission'] : ''; $perm_val = isset($_POST['value']) ? $_POST['value'] : ''; if(empty($perm_key)){ echo json_encode(array('error' => 'permission is required')); break; } $settings = new AISettings($username); $perm_val_norm = ($perm_val === 'allow_always') ? 'allow_always' : (($perm_val === 'deny') ? 'deny' : 'ask'); $settings->set_permission($perm_key, $perm_val_norm); echo json_encode(array('success' => true, 'permissions' => $settings->get_permissions())); break; case 'shell': $command = isset($_POST['command']) ? $_POST['command'] : ''; if(empty($command)){ echo json_encode(array('error' => 'Command is required')); break; } $safe_path = escapeshellarg($softpath); $output = ''; $ret = 0; $descriptors = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')); // Limit to the project directory for safety $cmd = 'cd '.$safe_path.' && '.escapeshellcmd($command).' 2>&1'; // Whitelist safe commands $allowed = array('ls', 'cat', 'grep', 'find', 'wc', 'head', 'tail', 'pwd', 'git', 'php', 'node', 'npm', 'composer', 'wp', 'curl', 'du', 'df', 'echo', 'which', 'whoami', 'date', 'uname'); $first_word = preg_match('/^(\S+)/', $command, $m) ? $m[1] : ''; $is_allowed = false; foreach($allowed as $a){ if(strpos($first_word, $a) === 0){ $is_allowed = true; break; } } if(!$is_allowed){ echo json_encode(array('error' => 'Command not allowed: '.$first_word.'. Only read-only and standard dev commands are permitted.', 'output' => '')); break; } $timeout = 30; $process = proc_open($cmd, $descriptors, $pipes, $softpath); if(is_resource($process)){ fclose($pipes[0]); $output = stream_get_contents($pipes[1]); $err = stream_get_contents($pipes[2]); fclose($pipes[1]); fclose($pipes[2]); $ret = proc_close($process); if($err) $output .= "\n" . $err; } echo json_encode(array('output' => $output, 'returncode' => $ret)); break; default: echo json_encode(array('error' => 'Unknown action: '.$action)); } die(); }
修改文件时间
将文件时间修改为当前时间的前一年
删除文件