dootask/app/Module/Manticore/ManticoreProject.php
kuaifan c08323e1ea feat: 迁移至 MVA 权限方案
- 表结构:为 file/project/task_vectors 添加 allowed_users MULTI 字段
- 删除关系表:file_users, project_users, task_users
- 搜索:使用 allowed_users = userid 进行权限过滤
- 同步:sync 时自动计算并写入 allowed_users
- 级联:项目成员变更异步级联 v=1 任务,任务成员变更递归更新子任务
- 覆盖场景:visibility/parent_id/project_id 变更、子任务升级主任务等
2026-01-02 02:03:21 +00:00

311 lines
8.7 KiB
PHP
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.

<?php
namespace App\Module\Manticore;
use App\Models\Project;
use App\Models\ProjectUser;
use App\Module\Apps;
use App\Module\Base;
use App\Module\AI;
use Illuminate\Support\Facades\Log;
/**
* Manticore Search 项目搜索类MVA 权限方案)
*
* 使用方法:
*
* 1. 搜索方法
* - 搜索项目: search($userid, $keyword, $searchType, $limit);
*
* 2. 同步方法
* - 单个同步: sync(Project $project);
* - 批量同步: batchSync($projects);
* - 删除索引: delete($projectId);
*
* 3. 权限更新方法
* - 更新权限: updateAllowedUsers($projectId);
*
* 4. 工具方法
* - 清空索引: clear();
*/
class ManticoreProject
{
/**
* 搜索项目(支持全文、向量、混合搜索)
*
* @param int $userid 用户ID权限过滤
* @param string $keyword 搜索关键词
* @param string $searchType 搜索类型: text/vector/hybrid
* @param int $limit 返回数量
* @return array 搜索结果
*/
public static function search(int $userid, string $keyword, string $searchType = 'hybrid', int $limit = 20): array
{
if (empty($keyword)) {
return [];
}
if (!Apps::isInstalled("manticore")) {
return [];
}
try {
switch ($searchType) {
case 'text':
return self::formatSearchResults(
ManticoreBase::projectFullTextSearch($keyword, $userid, $limit, 0)
);
case 'vector':
$embedding = self::getEmbedding($keyword);
if (empty($embedding)) {
return self::formatSearchResults(
ManticoreBase::projectFullTextSearch($keyword, $userid, $limit, 0)
);
}
return self::formatSearchResults(
ManticoreBase::projectVectorSearch($embedding, $userid, $limit)
);
case 'hybrid':
default:
$embedding = self::getEmbedding($keyword);
return self::formatSearchResults(
ManticoreBase::projectHybridSearch($keyword, $embedding, $userid, $limit)
);
}
} catch (\Exception $e) {
Log::error('Manticore project search error: ' . $e->getMessage());
return [];
}
}
/**
* 获取文本的 Embedding 向量
*
* @param string $text 文本
* @return array 向量数组(空数组表示失败)
*/
private static function getEmbedding(string $text): array
{
if (empty($text)) {
return [];
}
try {
$result = AI::getEmbedding($text);
if (Base::isSuccess($result)) {
return $result['data'] ?? [];
}
} catch (\Exception $e) {
Log::warning('Get embedding error: ' . $e->getMessage());
}
return [];
}
/**
* 格式化搜索结果
*
* @param array $results Manticore 返回的结果
* @return array 格式化后的结果
*/
private static function formatSearchResults(array $results): array
{
$formatted = [];
foreach ($results as $item) {
$formatted[] = [
'project_id' => $item['project_id'],
'id' => $item['project_id'],
'userid' => $item['userid'],
'personal' => $item['personal'],
'name' => $item['project_name'],
'desc_preview' => isset($item['project_desc']) ? mb_substr($item['project_desc'], 0, 300) : null,
'relevance' => $item['relevance'] ?? $item['similarity'] ?? $item['rrf_score'] ?? 0,
];
}
return $formatted;
}
// ==============================
// 同步方法
// ==============================
/**
* 获取项目的 allowed_users 列表
*
* @param int $projectId 项目ID
* @return array 有权限的用户ID数组
*/
public static function getAllowedUsers(int $projectId): array
{
return ProjectUser::where('project_id', $projectId)
->pluck('userid')
->toArray();
}
/**
* 同步单个项目到 Manticore含 allowed_users
*
* @param Project $project 项目模型
* @return bool 是否成功
*/
public static function sync(Project $project): bool
{
if (!Apps::isInstalled("manticore")) {
return false;
}
// 已归档的项目不索引
if ($project->archived_at) {
return self::delete($project->id);
}
try {
// 构建用于搜索的文本内容
$searchableContent = self::buildSearchableContent($project);
// 获取 embedding如果 AI 可用)
$embedding = null;
if (!empty($searchableContent) && Apps::isInstalled('ai')) {
$embeddingResult = self::getEmbedding($searchableContent);
if (!empty($embeddingResult)) {
$embedding = '[' . implode(',', $embeddingResult) . ']';
}
}
// 获取项目成员列表(作为 allowed_users
$allowedUsers = self::getAllowedUsers($project->id);
// 写入 Manticore含 allowed_users
$result = ManticoreBase::upsertProjectVector([
'project_id' => $project->id,
'userid' => $project->userid ?? 0,
'personal' => $project->personal ?? 0,
'project_name' => $project->name ?? '',
'project_desc' => $project->desc ?? '',
'content_vector' => $embedding,
'allowed_users' => $allowedUsers,
]);
return $result;
} catch (\Exception $e) {
Log::error('Manticore project sync error: ' . $e->getMessage(), [
'project_id' => $project->id,
'project_name' => $project->name,
]);
return false;
}
}
/**
* 构建可搜索的文本内容
*
* @param Project $project 项目模型
* @return string 可搜索的文本
*/
private static function buildSearchableContent(Project $project): string
{
$parts = [];
if (!empty($project->name)) {
$parts[] = $project->name;
}
if (!empty($project->desc)) {
$parts[] = $project->desc;
}
return implode(' ', $parts);
}
/**
* 批量同步项目
*
* @param iterable $projects 项目列表
* @return int 成功同步的数量
*/
public static function batchSync(iterable $projects): int
{
if (!Apps::isInstalled("manticore")) {
return 0;
}
$count = 0;
foreach ($projects as $project) {
if (self::sync($project)) {
$count++;
}
}
return $count;
}
/**
* 删除项目索引
*
* @param int $projectId 项目ID
* @return bool 是否成功
*/
public static function delete(int $projectId): bool
{
if (!Apps::isInstalled("manticore")) {
return false;
}
return ManticoreBase::deleteProjectVector($projectId);
}
/**
* 清空所有索引
*
* @return bool 是否成功
*/
public static function clear(): bool
{
if (!Apps::isInstalled("manticore")) {
return false;
}
return ManticoreBase::clearAllProjectVectors();
}
/**
* 获取已索引项目数量
*
* @return int 数量
*/
public static function getIndexedCount(): int
{
if (!Apps::isInstalled("manticore")) {
return 0;
}
return ManticoreBase::getIndexedProjectCount();
}
// ==============================
// 权限更新方法MVA 方案)
// ==============================
/**
* 更新项目的 allowed_users 权限列表
* 从 MySQL 获取最新的项目成员并更新到 Manticore
*
* @param int $projectId 项目ID
* @return bool 是否成功
*/
public static function updateAllowedUsers(int $projectId): bool
{
if (!Apps::isInstalled("manticore") || $projectId <= 0) {
return false;
}
try {
$userids = self::getAllowedUsers($projectId);
return ManticoreBase::updateProjectAllowedUsers($projectId, $userids);
} catch (\Exception $e) {
Log::error('Manticore updateAllowedUsers error: ' . $e->getMessage(), ['project_id' => $projectId]);
return false;
}
}
}