← 返回聊天
新建
删除
Models
gpt5.php
gpt5_file.php
gpt5_mini_file.php
openai_chat.php
Tools
get_time.php
get_weather.php
global_search_messages.php
math.php
memo.php
news.php
search_arxiv.php
search_crossref.php
search_github_code.php
search_pubmed.php
search_semantic_scholar.php
stock_market.php
url.php
<?php declare(strict_types=1); /** * 全局搜索聊天消息(优先 FULLTEXT,失败则降级 LIKE) * 可选:同时加载/搜索会话级记忆(conversation_summaries, summary_type=memory) * * 依赖: * - $context['pdo'] 或 $context['db'] 为 PDO 实例 * - 表: * - messages(id, conversation_id, role, name, content, meta_json, created_at) * - conversation_summaries(conversation_id, summary_type, summary, model, created_at, updated_at) */ return [ 'name' => 'global_search_messages', 'description' => '全局搜索 messages 表中的聊天内容(关键词)。支持 role / 会话 / 时间范围过滤与分页;优先使用 FULLTEXT,失败自动降级为 LIKE。可选 include_memory/include_summary 加载命中会话的记忆/总结;也可 search_in=memory/both 在会话记忆/总结中检索。', 'parameters' => [ 'type' => 'object', 'properties' => [ 'q' => [ 'type' => 'string', 'description' => '搜索关键词(例如:天气)。', ], 'conversation_id' => [ 'type' => ['integer', 'string', 'null'], 'description' => '可选:限制在某个会话内搜索(传会话ID)。不传则全局搜索。', ], 'role' => [ 'type' => ['string', 'null'], 'description' => "可选:限定角色(例如 user / assistant)。", ], 'from' => [ 'type' => ['string', 'null'], 'description' => "可选:起始时间('YYYY-MM-DD' 或 'YYYY-MM-DD HH:MM:SS')。", ], 'to' => [ 'type' => ['string', 'null'], 'description' => "可选:结束时间('YYYY-MM-DD' 或 'YYYY-MM-DD HH:MM:SS')。", ], 'limit' => [ 'type' => 'integer', 'description' => '返回条数(建议 10~50)。最大会被限制。', 'default' => 20, ], 'offset' => [ 'type' => 'integer', 'description' => '分页偏移量。', 'default' => 0, ], 'mode' => [ 'type' => 'string', 'description' => "搜索模式:'auto'(默认,优先 FULLTEXT) / 'fulltext'(强制) / 'like'(强制)。", 'default' => 'auto', ], 'order' => [ 'type' => 'string', 'description' => "排序:'relevance'(相关度优先,仅 FULLTEXT 生效) / 'time_desc'(最新优先) / 'time_asc'。", 'default' => 'relevance', ], 'max_content_chars' => [ 'type' => 'integer', 'description' => '返回内容截断长度(避免过大)。', 'default' => 280, ], // ✅ 新增:记忆/总结相关 'include_memory' => [ 'type' => 'boolean', 'description' => '是否在返回中附带命中会话的“会话级记忆”(conversation_summaries.summary_type=memory)。', 'default' => false, ], 'include_summary' => [ 'type' => 'boolean', 'description' => '是否在返回中附带命中会话的“会话总结”(conversation_summaries.summary_type=full)。', 'default' => false, ], 'attach_memory_to_each_result' => [ 'type' => 'boolean', 'description' => '是否把 memory/summary 直接附到每条 result 上(否则只在 memories_by_conversation 中返回)。', 'default' => false, ], 'max_memory_chars' => [ 'type' => 'integer', 'description' => '返回的 memory/summary 最大字符数(截断)。', 'default' => 2000, ], 'search_in' => [ 'type' => 'string', 'description' => "搜索范围:messages(默认)/ memory(仅会话记忆与总结)/ both(两者都搜并合并)。", 'default' => 'messages', ], ], 'required' => ['q'], 'additionalProperties' => false, ], 'run' => function(array $args, array $context) { $t0 = microtime(true); // 1) 拿 PDO $pdo = null; if (isset($context['pdo']) && $context['pdo'] instanceof PDO) $pdo = $context['pdo']; if (!$pdo && isset($context['db']) && $context['db'] instanceof PDO) $pdo = $context['db']; if (!$pdo) { return [ 'ok' => false, 'error' => 'PDO not found in context. Please provide $context["pdo"] or $context["db"].', ]; } // 2) 参数整理 $q = trim((string)($args['q'] ?? '')); if ($q === '') return ['ok' => false, 'error' => 'q is empty.']; $conversationId = $args['conversation_id'] ?? null; $role = $args['role'] ?? null; $from = $args['from'] ?? null; $to = $args['to'] ?? null; $limit = (int)($args['limit'] ?? 20); $offset = (int)($args['offset'] ?? 0); $limit = max(1, min($limit, 100)); $offset = max(0, $offset); $mode = (string)($args['mode'] ?? 'auto'); $order = (string)($args['order'] ?? 'relevance'); $maxChars = (int)($args['max_content_chars'] ?? 280); $maxChars = max(50, min($maxChars, 2000)); $includeMemory = (bool)($args['include_memory'] ?? false); $includeSummary = (bool)($args['include_summary'] ?? false); $attachPerResult = (bool)($args['attach_memory_to_each_result'] ?? false); $maxMemoryChars = (int)($args['max_memory_chars'] ?? 2000); $maxMemoryChars = max(200, min($maxMemoryChars, 20000)); $searchIn = (string)($args['search_in'] ?? 'messages'); if (!in_array($searchIn, ['messages','memory','both'], true)) $searchIn = 'messages'; // 工具:截断(UTF-8 安全) $truncate = function(string $s, int $max) { if (mb_strlen($s, 'UTF-8') <= $max) return $s; return mb_substr($s, 0, $max, 'UTF-8') . '…'; }; // 3) 构建 messages 公共 WHERE $where = []; $bind = []; if ($conversationId !== null && $conversationId !== '') { $where[] = 'conversation_id = :conversation_id'; $bind[':conversation_id'] = $conversationId; } if ($role !== null && $role !== '') { $where[] = 'role = :role'; $bind[':role'] = $role; } if ($from !== null && $from !== '') { if (strlen($from) === 10) $from .= ' 00:00:00'; $where[] = 'created_at >= :from'; $bind[':from'] = $from; } if ($to !== null && $to !== '') { if (strlen($to) === 10) $to .= ' 23:59:59'; $where[] = 'created_at <= :to'; $bind[':to'] = $to; } $whereSql = $where ? (' AND ' . implode(' AND ', $where)) : ''; // 4) 搜索 messages(FULLTEXT 优先) $usedMode = 'like'; $rowsMsg = []; $against = $q; $selectBase = "SELECT id, conversation_id, role, name, created_at, content, meta_json"; $orderSqlLike = "ORDER BY created_at DESC"; if ($order === 'time_asc') $orderSqlLike = "ORDER BY created_at ASC"; if ($order === 'time_desc') $orderSqlLike = "ORDER BY created_at DESC"; if ($searchIn === 'messages' || $searchIn === 'both') { $tryFulltext = ($mode === 'auto' || $mode === 'fulltext'); if ($tryFulltext) { $withScore = ($order === 'relevance'); $select = $withScore ? "SELECT id, conversation_id, role, name, created_at, content, meta_json, MATCH(content) AGAINST(:against IN BOOLEAN MODE) AS score" : $selectBase; $sql = $select . " FROM messages WHERE MATCH(content) AGAINST(:against IN BOOLEAN MODE) $whereSql " . ($withScore ? "ORDER BY score DESC, created_at DESC" : $orderSqlLike) . " LIMIT :limit OFFSET :offset"; try { $stmt = $pdo->prepare($sql); $stmt->bindValue(':against', $against, PDO::PARAM_STR); foreach ($bind as $k => $v) $stmt->bindValue($k, $v, PDO::PARAM_STR); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); $rowsMsg = $stmt->fetchAll(PDO::FETCH_ASSOC); $usedMode = 'fulltext'; } catch (Throwable $e) { if ($mode === 'fulltext') { return ['ok' => false, 'error' => 'FULLTEXT forced but failed: ' . $e->getMessage()]; } $rowsMsg = []; } } // LIKE 降级(或强制 like) if (!$rowsMsg) { $like = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $q); $like = '%' . $like . '%'; $sql = $selectBase . " FROM messages WHERE content LIKE :like ESCAPE '\\\\' $whereSql $orderSqlLike LIMIT :limit OFFSET :offset"; $stmt = $pdo->prepare($sql); $stmt->bindValue(':like', $like, PDO::PARAM_STR); foreach ($bind as $k => $v) $stmt->bindValue($k, $v, PDO::PARAM_STR); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); $rowsMsg = $stmt->fetchAll(PDO::FETCH_ASSOC); $usedMode = 'like'; } } // 5) 搜索 memory/summary(可选) // 注意:这里不依赖 FULLTEXT(你可以以后给 summary 加 FULLTEXT) $rowsMem = []; $memUsed = false; if ($searchIn === 'memory' || $searchIn === 'both') { $types = []; if ($includeMemory || $searchIn !== 'messages') $types[] = 'memory'; if ($includeSummary || $searchIn !== 'messages') $types[] = 'full'; if (!$types) $types = ['memory']; // 仅搜索 memory 时默认 memory // LIKE 搜 summary(简单可靠) $like = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $q); $like = '%' . $like . '%'; $typePlaceholders = implode(',', array_fill(0, count($types), '?')); $sql = "SELECT conversation_id, summary_type, summary, model, updated_at FROM conversation_summaries WHERE summary_type IN ($typePlaceholders) AND summary LIKE ? ESCAPE '\\\\'"; $params = $types; $params[] = $like; if ($conversationId !== null && $conversationId !== '') { $sql .= " AND conversation_id = ?"; $params[] = $conversationId; } // 最新更新时间优先 $sql .= " ORDER BY updated_at DESC LIMIT ? OFFSET ?"; $params[] = $limit; $params[] = $offset; $stmt = $pdo->prepare($sql); $stmt->execute($params); $rowsMem = $stmt->fetchAll(PDO::FETCH_ASSOC); $memUsed = true; } // 6) 整理 messages 输出(截断) $results = []; $convIds = []; foreach ($rowsMsg as $r) { $content = (string)($r['content'] ?? ''); if (mb_strlen($content, 'UTF-8') > $maxChars) { $content = mb_substr($content, 0, $maxChars, 'UTF-8') . '…'; } $cid = $r['conversation_id']; if ($cid !== null && $cid !== '') $convIds[(string)$cid] = true; $results[] = [ 'type' => 'message', 'id' => (int)$r['id'], 'conversation_id' => $cid, 'role' => $r['role'], 'name' => $r['name'], 'created_at' => $r['created_at'], 'content' => $content, 'score' => $r['score'] ?? null, 'meta_json' => $r['meta_json'] ?? null, ]; } // 7) 整理 memory 搜索结果(如果 search_in=memory/both) $memoryResults = []; foreach ($rowsMem as $r) { $cid = (string)($r['conversation_id'] ?? ''); if ($cid !== '') $convIds[$cid] = true; $summary = (string)($r['summary'] ?? ''); $memoryResults[] = [ 'type' => 'summary', 'conversation_id' => $cid, 'summary_type' => $r['summary_type'] ?? null, // memory/full 'updated_at' => $r['updated_at'] ?? null, 'model' => $r['model'] ?? null, 'summary' => $truncate($summary, $maxMemoryChars), ]; } // 8) include_memory/include_summary:加载命中会话的 memory/summary(一次性批量查) $memoriesByConversation = null; if (($includeMemory || $includeSummary) && $convIds) { $cids = array_keys($convIds); // summary_type 过滤 $types = []; if ($includeMemory) $types[] = 'memory'; if ($includeSummary) $types[] = 'full'; $inCids = implode(',', array_fill(0, count($cids), '?')); $inTypes = implode(',', array_fill(0, count($types), '?')); $sql = "SELECT conversation_id, summary_type, summary, model, created_at, updated_at FROM conversation_summaries WHERE conversation_id IN ($inCids) AND summary_type IN ($inTypes)"; $stmt = $pdo->prepare($sql); $stmt->execute(array_merge($cids, $types)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $memoriesByConversation = []; foreach ($rows as $r) { $cid = (string)$r['conversation_id']; $stype = (string)$r['summary_type']; if (!isset($memoriesByConversation[$cid])) $memoriesByConversation[$cid] = []; $memoriesByConversation[$cid][$stype] = [ 'summary' => $truncate((string)$r['summary'], $maxMemoryChars), 'model' => $r['model'] ?? null, 'created_at' => $r['created_at'] ?? null, 'updated_at' => $r['updated_at'] ?? null, ]; } // 可选:把 memory/summary 附到每条 message result 上 if ($attachPerResult) { foreach ($results as &$res) { $cid = (string)($res['conversation_id'] ?? ''); if ($cid !== '' && isset($memoriesByConversation[$cid])) { $res['conversation_info'] = $memoriesByConversation[$cid]; } else { $res['conversation_info'] = null; } } unset($res); } } $t1 = microtime(true); // 9) 返回 return [ 'ok' => true, 'mode_used' => $usedMode, // fulltext / like (messages) 'memory_search_used' => $memUsed, // true/false 'search_in' => $searchIn, 'query' => $q, 'filters' => [ 'conversation_id' => $conversationId, 'role' => $role, 'from' => $from, 'to' => $to, ], 'pagination' => [ 'limit' => $limit, 'offset' => $offset, 'returned_messages' => count($results), 'returned_summaries' => count($memoryResults), ], 'took_ms' => (int)round(($t1 - $t0) * 1000), // messages 命中 'results' => $results, // 仅当 search_in=memory/both 时有 'summary_results' => $memoryResults, // 仅当 include_memory/include_summary 且有命中会话时有 'memories_by_conversation' => $memoriesByConversation, ]; }, ];
保存