mirror of
https://github.com/kuaifan/dootask.git
synced 2026-03-17 03:03:41 +00:00
feat: 新增消息搜索功能
- 新增 msg_vectors 表,支持消息全文/向量/混合搜索 - 采用 MVA 权限方案,allowed_users 内联存储 - 新增 /api/search/message API - 新增 manticore:sync-msgs 同步命令 - Observer 触发消息创建/更新/删除同步 - Observer 触发对话成员变更时更新 allowed_users
This commit is contained in:
parent
c08323e1ea
commit
7a5ef3a491
227
app/Console/Commands/SyncMsgToManticore.php
Normal file
227
app/Console/Commands/SyncMsgToManticore.php
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreMsg;
|
||||||
|
use App\Module\Manticore\ManticoreKeyValue;
|
||||||
|
use Cache;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SyncMsgToManticore extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 更新数据(MVA 方案:allowed_users 在同步时自动写入)
|
||||||
|
* --f: 全量更新 (默认)
|
||||||
|
* --i: 增量更新(从上次更新的最后一个ID接上)
|
||||||
|
*
|
||||||
|
* 清理数据
|
||||||
|
* --c: 清除索引
|
||||||
|
*
|
||||||
|
* 其他选项
|
||||||
|
* --dialog: 指定对话ID(仅同步该对话的消息)
|
||||||
|
*/
|
||||||
|
|
||||||
|
protected $signature = 'manticore:sync-msgs {--f} {--i} {--c} {--batch=100} {--dialog=}';
|
||||||
|
protected $description = '同步消息数据到 Manticore Search(MVA 权限方案)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("manticore")) {
|
||||||
|
$this->error("应用「Manticore Search」未安装");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册信号处理器
|
||||||
|
if (extension_loaded('pcntl')) {
|
||||||
|
pcntl_async_signals(true);
|
||||||
|
pcntl_signal(SIGINT, [$this, 'handleSignal']);
|
||||||
|
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查锁
|
||||||
|
$lockInfo = $this->getLock();
|
||||||
|
if ($lockInfo) {
|
||||||
|
$this->error("命令已在运行中,开始时间: {$lockInfo['started_at']}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
// 清除索引
|
||||||
|
if ($this->option('c')) {
|
||||||
|
$this->info('清除索引...');
|
||||||
|
ManticoreMsg::clear();
|
||||||
|
$this->info("索引删除成功");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dialogId = $this->option('dialog') ? intval($this->option('dialog')) : 0;
|
||||||
|
|
||||||
|
if ($dialogId > 0) {
|
||||||
|
$this->info("开始同步对话 {$dialogId} 的消息数据(MVA 方案:allowed_users 自动内联)...");
|
||||||
|
$this->syncDialogMsgs($dialogId);
|
||||||
|
} else {
|
||||||
|
$this->info('开始同步消息数据(MVA 方案:allowed_users 自动内联)...');
|
||||||
|
$this->syncMsgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("\n同步完成");
|
||||||
|
$this->releaseLock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLock(): ?array
|
||||||
|
{
|
||||||
|
$lockKey = md5($this->signature);
|
||||||
|
return Cache::has($lockKey) ? Cache::get($lockKey) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setLock(): void
|
||||||
|
{
|
||||||
|
$lockKey = md5($this->signature);
|
||||||
|
Cache::put($lockKey, ['started_at' => date('Y-m-d H:i:s')], 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function releaseLock(): void
|
||||||
|
{
|
||||||
|
$lockKey = md5($this->signature);
|
||||||
|
Cache::forget($lockKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleSignal(int $signal): void
|
||||||
|
{
|
||||||
|
$this->releaseLock();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步所有消息
|
||||||
|
*/
|
||||||
|
private function syncMsgs(): void
|
||||||
|
{
|
||||||
|
$lastKey = "sync:manticoreMsgLastId";
|
||||||
|
$lastId = $this->option('i') ? intval(ManticoreKeyValue::get($lastKey, 0)) : 0;
|
||||||
|
|
||||||
|
if ($lastId > 0) {
|
||||||
|
$this->info("\n增量同步消息数据(从ID {$lastId} 开始)...");
|
||||||
|
} else {
|
||||||
|
$this->info("\n全量同步消息数据...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建基础查询条件
|
||||||
|
// 排除:软删除、机器人消息、空 key 消息
|
||||||
|
// 只包含:可索引的消息类型
|
||||||
|
$baseQuery = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES);
|
||||||
|
|
||||||
|
$num = 0;
|
||||||
|
$count = $baseQuery->count();
|
||||||
|
$batchSize = $this->option('batch');
|
||||||
|
|
||||||
|
$total = 0;
|
||||||
|
$lastNum = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$msgs = WebSocketDialogMsg::where('id', '>', $lastId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($batchSize)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($msgs->isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num += count($msgs);
|
||||||
|
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||||
|
if ($progress < 100) {
|
||||||
|
$progress = number_format($progress, 2);
|
||||||
|
}
|
||||||
|
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$msgs->first()->id} ~ {$msgs->last()->id} ({$total}|{$lastNum})");
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$lastNum = ManticoreMsg::batchSync($msgs);
|
||||||
|
$total += $lastNum;
|
||||||
|
|
||||||
|
$lastId = $msgs->last()->id;
|
||||||
|
ManticoreKeyValue::set($lastKey, $lastId);
|
||||||
|
} while (count($msgs) == $batchSize);
|
||||||
|
|
||||||
|
$this->info("同步消息结束 - 最后ID {$lastId}");
|
||||||
|
$this->info("已索引消息数量: " . ManticoreMsg::getIndexedCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步指定对话的消息
|
||||||
|
*
|
||||||
|
* @param int $dialogId 对话ID
|
||||||
|
*/
|
||||||
|
private function syncDialogMsgs(int $dialogId): void
|
||||||
|
{
|
||||||
|
$this->info("\n同步对话 {$dialogId} 的消息数据...");
|
||||||
|
|
||||||
|
$baseQuery = WebSocketDialogMsg::where('dialog_id', $dialogId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES);
|
||||||
|
|
||||||
|
$num = 0;
|
||||||
|
$count = $baseQuery->count();
|
||||||
|
$batchSize = $this->option('batch');
|
||||||
|
$lastId = 0;
|
||||||
|
|
||||||
|
$total = 0;
|
||||||
|
$lastNum = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$msgs = WebSocketDialogMsg::where('dialog_id', $dialogId)
|
||||||
|
->where('id', '>', $lastId)
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->where('bot', '!=', 1)
|
||||||
|
->whereNotNull('key')
|
||||||
|
->where('key', '!=', '')
|
||||||
|
->whereIn('type', ManticoreMsg::INDEXABLE_TYPES)
|
||||||
|
->orderBy('id')
|
||||||
|
->limit($batchSize)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($msgs->isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$num += count($msgs);
|
||||||
|
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||||
|
if ($progress < 100) {
|
||||||
|
$progress = number_format($progress, 2);
|
||||||
|
}
|
||||||
|
$this->info("{$num}/{$count} ({$progress}%) 正在同步消息ID {$msgs->first()->id} ~ {$msgs->last()->id} ({$total}|{$lastNum})");
|
||||||
|
|
||||||
|
$this->setLock();
|
||||||
|
|
||||||
|
$lastNum = ManticoreMsg::batchSync($msgs);
|
||||||
|
$total += $lastNum;
|
||||||
|
|
||||||
|
$lastId = $msgs->last()->id;
|
||||||
|
} while (count($msgs) == $batchSize);
|
||||||
|
|
||||||
|
$this->info("同步对话 {$dialogId} 消息结束");
|
||||||
|
$this->info("该对话已索引消息数量: " . \App\Module\Manticore\ManticoreBase::getDialogIndexedMsgCount($dialogId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,12 +5,14 @@ namespace App\Http\Controllers\Api;
|
|||||||
use Request;
|
use Request;
|
||||||
use App\Models\File;
|
use App\Models\File;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
use App\Module\Apps;
|
use App\Module\Apps;
|
||||||
use App\Module\Manticore\ManticoreFile;
|
use App\Module\Manticore\ManticoreFile;
|
||||||
use App\Module\Manticore\ManticoreUser;
|
use App\Module\Manticore\ManticoreUser;
|
||||||
use App\Module\Manticore\ManticoreProject;
|
use App\Module\Manticore\ManticoreProject;
|
||||||
use App\Module\Manticore\ManticoreTask;
|
use App\Module\Manticore\ManticoreTask;
|
||||||
|
use App\Module\Manticore\ManticoreMsg;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine search
|
* @apiDefine search
|
||||||
@ -242,5 +244,73 @@ class SearchController extends AbstractController
|
|||||||
|
|
||||||
return Base::retSuccess('success', []);
|
return Base::retSuccess('success', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/search/message AI 搜索消息
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,需要安装 Manticore Search 应用
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup search
|
||||||
|
* @apiName message
|
||||||
|
*
|
||||||
|
* @apiParam {String} key 搜索关键词
|
||||||
|
* @apiParam {String} [search_type] 搜索类型(text/vector/hybrid,默认:hybrid)
|
||||||
|
* @apiParam {Number} [take] 获取数量(默认:20,最大:50)
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function message()
|
||||||
|
{
|
||||||
|
$user = User::auth();
|
||||||
|
|
||||||
|
if (!Apps::isInstalled('manticore')) {
|
||||||
|
return Base::retError('Manticore Search 应用未安装');
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = trim(Request::input('key'));
|
||||||
|
$searchType = Request::input('search_type', 'hybrid');
|
||||||
|
$take = min(50, max(1, intval(Request::input('take', 20))));
|
||||||
|
|
||||||
|
if (empty($key)) {
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = ManticoreMsg::search($user->userid, $key, $searchType, 0, $take);
|
||||||
|
|
||||||
|
// 补充消息完整信息
|
||||||
|
$msgIds = array_column($results, 'msg_id');
|
||||||
|
if (!empty($msgIds)) {
|
||||||
|
$msgs = WebSocketDialogMsg::whereIn('id', $msgIds)
|
||||||
|
->with(['user' => function ($query) {
|
||||||
|
$query->select(User::$basicField);
|
||||||
|
}])
|
||||||
|
->get()
|
||||||
|
->keyBy('id');
|
||||||
|
|
||||||
|
$formattedResults = [];
|
||||||
|
foreach ($results as $item) {
|
||||||
|
$msgData = $msgs->get($item['msg_id']);
|
||||||
|
if ($msgData) {
|
||||||
|
$formattedResults[] = [
|
||||||
|
'id' => $msgData->id,
|
||||||
|
'msg_id' => $msgData->id,
|
||||||
|
'dialog_id' => $msgData->dialog_id,
|
||||||
|
'userid' => $msgData->userid,
|
||||||
|
'type' => $msgData->type,
|
||||||
|
'msg' => $msgData->msg,
|
||||||
|
'created_at' => $msgData->created_at,
|
||||||
|
'user' => $msgData->user,
|
||||||
|
'relevance' => $item['relevance'] ?? 0,
|
||||||
|
'content_preview' => $item['content_preview'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Base::retSuccess('success', $formattedResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('success', []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -143,6 +143,21 @@ class ManticoreBase
|
|||||||
) charset_table='chinese' morphology='icu_chinese'
|
) charset_table='chinese' morphology='icu_chinese'
|
||||||
");
|
");
|
||||||
|
|
||||||
|
// 创建消息向量表(含 allowed_users MVA 权限字段)
|
||||||
|
$pdo->exec("
|
||||||
|
CREATE TABLE IF NOT EXISTS msg_vectors (
|
||||||
|
id BIGINT,
|
||||||
|
msg_id BIGINT,
|
||||||
|
dialog_id BIGINT,
|
||||||
|
userid BIGINT,
|
||||||
|
msg_type STRING,
|
||||||
|
content TEXT,
|
||||||
|
allowed_users MULTI,
|
||||||
|
created_at BIGINT,
|
||||||
|
content_vector float_vector knn_type='hnsw' knn_dims='1536' hnsw_similarity='cosine'
|
||||||
|
) charset_table='chinese' morphology='icu_chinese'
|
||||||
|
");
|
||||||
|
|
||||||
Log::info('Manticore tables initialized successfully');
|
Log::info('Manticore tables initialized successfully');
|
||||||
} catch (PDOException $e) {
|
} catch (PDOException $e) {
|
||||||
Log::warning('Manticore initialization warning: ' . $e->getMessage());
|
Log::warning('Manticore initialization warning: ' . $e->getMessage());
|
||||||
@ -1493,5 +1508,338 @@ class ManticoreBase
|
|||||||
return $result ? (int) $result['cnt'] : 0;
|
return $result ? (int) $result['cnt'] : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 消息向量方法
|
||||||
|
// ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息全文搜索(使用 MVA allowed_users 权限过滤)
|
||||||
|
*
|
||||||
|
* @param string $keyword 关键词
|
||||||
|
* @param int $userid 用户ID(权限过滤)
|
||||||
|
* @param int $limit 返回数量
|
||||||
|
* @param int $offset 偏移量
|
||||||
|
* @return array 搜索结果
|
||||||
|
*/
|
||||||
|
public static function msgFullTextSearch(string $keyword, int $userid = 0, int $limit = 20, int $offset = 0): array
|
||||||
|
{
|
||||||
|
if (empty($keyword)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = new self();
|
||||||
|
$escapedKeyword = self::escapeMatch($keyword);
|
||||||
|
|
||||||
|
if ($userid > 0) {
|
||||||
|
// 使用 MVA 权限过滤
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
msg_id,
|
||||||
|
dialog_id,
|
||||||
|
userid,
|
||||||
|
msg_type,
|
||||||
|
content,
|
||||||
|
created_at,
|
||||||
|
WEIGHT() as relevance
|
||||||
|
FROM msg_vectors
|
||||||
|
WHERE MATCH('@content {$escapedKeyword}')
|
||||||
|
AND allowed_users = " . (int)$userid . "
|
||||||
|
ORDER BY relevance DESC
|
||||||
|
LIMIT " . (int)$limit . " OFFSET " . (int)$offset;
|
||||||
|
} else {
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
msg_id,
|
||||||
|
dialog_id,
|
||||||
|
userid,
|
||||||
|
msg_type,
|
||||||
|
content,
|
||||||
|
created_at,
|
||||||
|
WEIGHT() as relevance
|
||||||
|
FROM msg_vectors
|
||||||
|
WHERE MATCH('@content {$escapedKeyword}')
|
||||||
|
ORDER BY relevance DESC
|
||||||
|
LIMIT " . (int)$limit . " OFFSET " . (int)$offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $instance->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息向量搜索(使用 MVA allowed_users 权限过滤)
|
||||||
|
*
|
||||||
|
* @param array $queryVector 查询向量
|
||||||
|
* @param int $userid 用户ID(权限过滤)
|
||||||
|
* @param int $limit 返回数量
|
||||||
|
* @return array 搜索结果
|
||||||
|
*/
|
||||||
|
public static function msgVectorSearch(array $queryVector, int $userid = 0, int $limit = 20): array
|
||||||
|
{
|
||||||
|
if (empty($queryVector)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = new self();
|
||||||
|
$vectorStr = '(' . implode(',', $queryVector) . ')';
|
||||||
|
|
||||||
|
// KNN 搜索需要先获取更多结果,再在应用层过滤权限
|
||||||
|
$fetchLimit = $userid > 0 ? $limit * 5 : $limit;
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
msg_id,
|
||||||
|
dialog_id,
|
||||||
|
userid,
|
||||||
|
msg_type,
|
||||||
|
content,
|
||||||
|
created_at,
|
||||||
|
KNN_DIST() as distance
|
||||||
|
FROM msg_vectors
|
||||||
|
WHERE KNN(content_vector, " . (int)$fetchLimit . ", {$vectorStr})
|
||||||
|
ORDER BY distance ASC
|
||||||
|
";
|
||||||
|
|
||||||
|
$results = $instance->query($sql);
|
||||||
|
|
||||||
|
foreach ($results as &$item) {
|
||||||
|
$item['similarity'] = 1 - ($item['distance'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MVA 权限过滤
|
||||||
|
if ($userid > 0 && !empty($results)) {
|
||||||
|
$allowedMsgIds = $instance->query(
|
||||||
|
"SELECT msg_id FROM msg_vectors WHERE allowed_users = ? LIMIT 100000",
|
||||||
|
[$userid]
|
||||||
|
);
|
||||||
|
$allowedIds = array_column($allowedMsgIds, 'msg_id');
|
||||||
|
|
||||||
|
$results = array_filter($results, function ($item) use ($allowedIds) {
|
||||||
|
return in_array($item['msg_id'], $allowedIds);
|
||||||
|
});
|
||||||
|
$results = array_values($results);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_slice($results, 0, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息混合搜索
|
||||||
|
*
|
||||||
|
* @param string $keyword 关键词
|
||||||
|
* @param array $queryVector 查询向量
|
||||||
|
* @param int $userid 用户ID(权限过滤)
|
||||||
|
* @param int $limit 返回数量
|
||||||
|
* @return array 搜索结果
|
||||||
|
*/
|
||||||
|
public static function msgHybridSearch(string $keyword, array $queryVector, int $userid = 0, int $limit = 20): array
|
||||||
|
{
|
||||||
|
$textResults = self::msgFullTextSearch($keyword, $userid, 50, 0);
|
||||||
|
$vectorResults = !empty($queryVector) ? self::msgVectorSearch($queryVector, $userid, 50) : [];
|
||||||
|
|
||||||
|
$scores = [];
|
||||||
|
$items = [];
|
||||||
|
$k = 60;
|
||||||
|
|
||||||
|
foreach ($textResults as $rank => $item) {
|
||||||
|
$id = $item['msg_id'];
|
||||||
|
$scores[$id] = ($scores[$id] ?? 0) + 0.5 / ($k + $rank + 1);
|
||||||
|
$items[$id] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($vectorResults as $rank => $item) {
|
||||||
|
$id = $item['msg_id'];
|
||||||
|
$scores[$id] = ($scores[$id] ?? 0) + 0.5 / ($k + $rank + 1);
|
||||||
|
if (!isset($items[$id])) {
|
||||||
|
$items[$id] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arsort($scores);
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
$count = 0;
|
||||||
|
foreach ($scores as $id => $score) {
|
||||||
|
if ($count >= $limit) break;
|
||||||
|
$item = $items[$id];
|
||||||
|
$item['rrf_score'] = $score;
|
||||||
|
$results[] = $item;
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入或更新消息向量(含 allowed_users MVA 权限字段)
|
||||||
|
*
|
||||||
|
* @param array $data 消息数据,包含:
|
||||||
|
* - msg_id: 消息ID
|
||||||
|
* - dialog_id: 对话ID
|
||||||
|
* - userid: 发送者ID
|
||||||
|
* - msg_type: 消息类型
|
||||||
|
* - content: 消息内容
|
||||||
|
* - content_vector: 向量值
|
||||||
|
* - allowed_users: 有权限的用户ID数组
|
||||||
|
* - created_at: 创建时间戳
|
||||||
|
* @return bool 是否成功
|
||||||
|
*/
|
||||||
|
public static function upsertMsgVector(array $data): bool
|
||||||
|
{
|
||||||
|
$instance = new self();
|
||||||
|
|
||||||
|
$msgId = $data['msg_id'] ?? 0;
|
||||||
|
if ($msgId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先删除已存在的记录
|
||||||
|
$instance->execute("DELETE FROM msg_vectors WHERE msg_id = ?", [$msgId]);
|
||||||
|
|
||||||
|
// 构建 allowed_users MVA 值
|
||||||
|
$allowedUsers = $data['allowed_users'] ?? [];
|
||||||
|
$allowedUsersStr = !empty($allowedUsers) ? '(' . implode(',', array_map('intval', $allowedUsers)) . ')' : '()';
|
||||||
|
|
||||||
|
// 插入新记录
|
||||||
|
$vectorValue = $data['content_vector'] ?? null;
|
||||||
|
if ($vectorValue) {
|
||||||
|
$vectorValue = str_replace(['[', ']'], ['(', ')'], $vectorValue);
|
||||||
|
$sql = "INSERT INTO msg_vectors
|
||||||
|
(id, msg_id, dialog_id, userid, msg_type, content, allowed_users, created_at, content_vector)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, {$allowedUsersStr}, ?, {$vectorValue})";
|
||||||
|
} else {
|
||||||
|
$sql = "INSERT INTO msg_vectors
|
||||||
|
(id, msg_id, dialog_id, userid, msg_type, content, allowed_users, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, {$allowedUsersStr}, ?)";
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
$msgId,
|
||||||
|
$msgId,
|
||||||
|
$data['dialog_id'] ?? 0,
|
||||||
|
$data['userid'] ?? 0,
|
||||||
|
$data['msg_type'] ?? 'text',
|
||||||
|
$data['content'] ?? '',
|
||||||
|
$data['created_at'] ?? time()
|
||||||
|
];
|
||||||
|
|
||||||
|
return $instance->execute($sql, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新对话的 allowed_users 权限列表(批量更新该对话下所有消息)
|
||||||
|
*
|
||||||
|
* @param int $dialogId 对话ID
|
||||||
|
* @param array $userids 有权限的用户ID数组
|
||||||
|
* @return int 更新的消息数量
|
||||||
|
*/
|
||||||
|
public static function updateDialogAllowedUsers(int $dialogId, array $userids): int
|
||||||
|
{
|
||||||
|
if ($dialogId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = new self();
|
||||||
|
$allowedUsersStr = !empty($userids) ? '(' . implode(',', array_map('intval', $userids)) . ')' : '()';
|
||||||
|
|
||||||
|
// Manticore 支持按条件批量更新
|
||||||
|
return $instance->executeWithRowCount(
|
||||||
|
"UPDATE msg_vectors SET allowed_users = {$allowedUsersStr} WHERE dialog_id = ?",
|
||||||
|
[$dialogId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息向量
|
||||||
|
*
|
||||||
|
* @param int $msgId 消息ID
|
||||||
|
* @return bool 是否成功
|
||||||
|
*/
|
||||||
|
public static function deleteMsgVector(int $msgId): bool
|
||||||
|
{
|
||||||
|
if ($msgId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = new self();
|
||||||
|
return $instance->execute("DELETE FROM msg_vectors WHERE msg_id = ?", [$msgId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除对话下的所有消息向量
|
||||||
|
*
|
||||||
|
* @param int $dialogId 对话ID
|
||||||
|
* @return int 删除数量
|
||||||
|
*/
|
||||||
|
public static function deleteDialogMsgVectors(int $dialogId): int
|
||||||
|
{
|
||||||
|
if ($dialogId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = new self();
|
||||||
|
return $instance->executeWithRowCount(
|
||||||
|
"DELETE FROM msg_vectors WHERE dialog_id = ?",
|
||||||
|
[$dialogId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有消息向量
|
||||||
|
*
|
||||||
|
* @return bool 是否成功
|
||||||
|
*/
|
||||||
|
public static function clearAllMsgVectors(): bool
|
||||||
|
{
|
||||||
|
$instance = new self();
|
||||||
|
return $instance->execute("TRUNCATE TABLE msg_vectors");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已索引的消息数量
|
||||||
|
*
|
||||||
|
* @return int 消息数量
|
||||||
|
*/
|
||||||
|
public static function getIndexedMsgCount(): int
|
||||||
|
{
|
||||||
|
$instance = new self();
|
||||||
|
$result = $instance->queryOne("SELECT COUNT(*) as cnt FROM msg_vectors");
|
||||||
|
return $result ? (int) $result['cnt'] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话的已索引消息数量
|
||||||
|
*
|
||||||
|
* @param int $dialogId 对话ID
|
||||||
|
* @return int 消息数量
|
||||||
|
*/
|
||||||
|
public static function getDialogIndexedMsgCount(int $dialogId): int
|
||||||
|
{
|
||||||
|
if ($dialogId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$instance = new self();
|
||||||
|
$result = $instance->queryOne(
|
||||||
|
"SELECT COUNT(*) as cnt FROM msg_vectors WHERE dialog_id = ?",
|
||||||
|
[$dialogId]
|
||||||
|
);
|
||||||
|
return $result ? (int) $result['cnt'] : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最后索引的消息ID
|
||||||
|
*
|
||||||
|
* @return int 消息ID
|
||||||
|
*/
|
||||||
|
public static function getLastIndexedMsgId(): int
|
||||||
|
{
|
||||||
|
$instance = new self();
|
||||||
|
$result = $instance->queryOne("SELECT MAX(msg_id) as max_id FROM msg_vectors");
|
||||||
|
return $result ? (int) ($result['max_id'] ?? 0) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
360
app/Module/Manticore/ManticoreMsg.php
Normal file
360
app/Module/Manticore/ManticoreMsg.php
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Module\Manticore;
|
||||||
|
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Models\WebSocketDialogUser;
|
||||||
|
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, $from, $size);
|
||||||
|
*
|
||||||
|
* 2. 同步方法
|
||||||
|
* - 单个同步: sync(WebSocketDialogMsg $msg);
|
||||||
|
* - 批量同步: batchSync($msgs);
|
||||||
|
* - 删除索引: delete($msgId);
|
||||||
|
*
|
||||||
|
* 3. 权限更新方法
|
||||||
|
* - 更新对话权限: updateDialogAllowedUsers($dialogId);
|
||||||
|
*
|
||||||
|
* 4. 工具方法
|
||||||
|
* - 清空索引: clear();
|
||||||
|
* - 判断是否索引: shouldIndex($msg);
|
||||||
|
*/
|
||||||
|
class ManticoreMsg
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 可索引的消息类型
|
||||||
|
*/
|
||||||
|
public const INDEXABLE_TYPES = ['text', 'file', 'record', 'meeting', 'vote'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大内容长度(字符)
|
||||||
|
*/
|
||||||
|
public const MAX_CONTENT_LENGTH = 50000; // 50K 字符
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断消息是否应该被索引
|
||||||
|
*
|
||||||
|
* @param WebSocketDialogMsg $msg 消息模型
|
||||||
|
* @return bool 是否应该索引
|
||||||
|
*/
|
||||||
|
public static function shouldIndex(WebSocketDialogMsg $msg): bool
|
||||||
|
{
|
||||||
|
// 1. 排除机器人消息
|
||||||
|
if ($msg->bot === 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查消息类型
|
||||||
|
if (!in_array($msg->type, self::INDEXABLE_TYPES)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 排除 key 为空的消息
|
||||||
|
if (empty($msg->key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索消息(支持全文、向量、混合搜索)
|
||||||
|
*
|
||||||
|
* @param int $userid 用户ID
|
||||||
|
* @param string $keyword 搜索关键词
|
||||||
|
* @param string $searchType 搜索类型: text/vector/hybrid
|
||||||
|
* @param int $from 起始位置
|
||||||
|
* @param int $size 返回数量
|
||||||
|
* @return array 搜索结果
|
||||||
|
*/
|
||||||
|
public static function search(int $userid, string $keyword, string $searchType = 'hybrid', int $from = 0, int $size = 20): array
|
||||||
|
{
|
||||||
|
if (empty($keyword)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Apps::isInstalled("manticore")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($searchType) {
|
||||||
|
case 'text':
|
||||||
|
// 纯全文搜索
|
||||||
|
return self::formatSearchResults(
|
||||||
|
ManticoreBase::msgFullTextSearch($keyword, $userid, $size, $from)
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'vector':
|
||||||
|
// 纯向量搜索(需要先获取 embedding)
|
||||||
|
$embedding = self::getEmbedding($keyword);
|
||||||
|
if (empty($embedding)) {
|
||||||
|
// embedding 获取失败,降级到全文搜索
|
||||||
|
return self::formatSearchResults(
|
||||||
|
ManticoreBase::msgFullTextSearch($keyword, $userid, $size, $from)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return self::formatSearchResults(
|
||||||
|
ManticoreBase::msgVectorSearch($embedding, $userid, $size)
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'hybrid':
|
||||||
|
default:
|
||||||
|
// 混合搜索
|
||||||
|
$embedding = self::getEmbedding($keyword);
|
||||||
|
return self::formatSearchResults(
|
||||||
|
ManticoreBase::msgHybridSearch($keyword, $embedding, $userid, $size)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Manticore msg search error: ' . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文本的 Embedding 向量
|
||||||
|
*
|
||||||
|
* @param string $text 文本
|
||||||
|
* @return array 向量数组(空数组表示失败)
|
||||||
|
*/
|
||||||
|
private static function getEmbedding(string $text): array
|
||||||
|
{
|
||||||
|
if (empty($text)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用 AI 模块获取 embedding
|
||||||
|
$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[] = [
|
||||||
|
'id' => $item['msg_id'],
|
||||||
|
'msg_id' => $item['msg_id'],
|
||||||
|
'dialog_id' => $item['dialog_id'],
|
||||||
|
'userid' => $item['userid'],
|
||||||
|
'msg_type' => $item['msg_type'],
|
||||||
|
'content_preview' => isset($item['content']) ? mb_substr($item['content'], 0, 200) : null,
|
||||||
|
'created_at' => $item['created_at'] ?? null,
|
||||||
|
'relevance' => $item['relevance'] ?? $item['similarity'] ?? $item['rrf_score'] ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 权限计算方法(MVA 方案核心)
|
||||||
|
// ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息的 allowed_users 列表
|
||||||
|
*
|
||||||
|
* 对话的所有成员都有权限查看该对话的消息
|
||||||
|
*
|
||||||
|
* @param WebSocketDialogMsg $msg 消息模型
|
||||||
|
* @return array 有权限的用户ID数组
|
||||||
|
*/
|
||||||
|
public static function getAllowedUsers(WebSocketDialogMsg $msg): array
|
||||||
|
{
|
||||||
|
return self::getDialogUserIds($msg->dialog_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话的所有成员ID
|
||||||
|
*
|
||||||
|
* @param int $dialogId 对话ID
|
||||||
|
* @return array 成员用户ID数组
|
||||||
|
*/
|
||||||
|
public static function getDialogUserIds(int $dialogId): array
|
||||||
|
{
|
||||||
|
if ($dialogId <= 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return WebSocketDialogUser::where('dialog_id', $dialogId)
|
||||||
|
->pluck('userid')
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 同步方法
|
||||||
|
// ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步单个消息到 Manticore(含 allowed_users)
|
||||||
|
*
|
||||||
|
* @param WebSocketDialogMsg $msg 消息模型
|
||||||
|
* @return bool 是否成功
|
||||||
|
*/
|
||||||
|
public static function sync(WebSocketDialogMsg $msg): bool
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("manticore")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否应该索引
|
||||||
|
if (!self::shouldIndex($msg)) {
|
||||||
|
// 不符合索引条件,尝试删除已存在的索引
|
||||||
|
return ManticoreBase::deleteMsgVector($msg->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 提取消息内容(使用 key 字段)
|
||||||
|
$content = $msg->key ?? '';
|
||||||
|
|
||||||
|
// 限制内容长度
|
||||||
|
$content = mb_substr($content, 0, self::MAX_CONTENT_LENGTH);
|
||||||
|
|
||||||
|
// 获取 embedding(如果有内容且 AI 可用)
|
||||||
|
$embedding = null;
|
||||||
|
if (!empty($content) && Apps::isInstalled('ai')) {
|
||||||
|
$embeddingResult = self::getEmbedding($content);
|
||||||
|
if (!empty($embeddingResult)) {
|
||||||
|
$embedding = '[' . implode(',', $embeddingResult) . ']';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取消息的 allowed_users
|
||||||
|
$allowedUsers = self::getAllowedUsers($msg);
|
||||||
|
|
||||||
|
// 写入 Manticore(含 allowed_users)
|
||||||
|
$result = ManticoreBase::upsertMsgVector([
|
||||||
|
'msg_id' => $msg->id,
|
||||||
|
'dialog_id' => $msg->dialog_id,
|
||||||
|
'userid' => $msg->userid,
|
||||||
|
'msg_type' => $msg->type,
|
||||||
|
'content' => $content,
|
||||||
|
'content_vector' => $embedding,
|
||||||
|
'allowed_users' => $allowedUsers,
|
||||||
|
'created_at' => $msg->created_at ? $msg->created_at->timestamp : time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Manticore msg sync error: ' . $e->getMessage(), [
|
||||||
|
'msg_id' => $msg->id,
|
||||||
|
'dialog_id' => $msg->dialog_id,
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量同步消息
|
||||||
|
*
|
||||||
|
* @param iterable $msgs 消息列表
|
||||||
|
* @return int 成功同步的数量
|
||||||
|
*/
|
||||||
|
public static function batchSync(iterable $msgs): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("manticore")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = 0;
|
||||||
|
foreach ($msgs as $msg) {
|
||||||
|
if (self::sync($msg)) {
|
||||||
|
$count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除消息索引
|
||||||
|
*
|
||||||
|
* @param int $msgId 消息ID
|
||||||
|
* @return bool 是否成功
|
||||||
|
*/
|
||||||
|
public static function delete(int $msgId): bool
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("manticore")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ManticoreBase::deleteMsgVector($msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有索引
|
||||||
|
*
|
||||||
|
* @return bool 是否成功
|
||||||
|
*/
|
||||||
|
public static function clear(): bool
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("manticore")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ManticoreBase::clearAllMsgVectors();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已索引消息数量
|
||||||
|
*
|
||||||
|
* @return int 数量
|
||||||
|
*/
|
||||||
|
public static function getIndexedCount(): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("manticore")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ManticoreBase::getIndexedMsgCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 权限更新方法(MVA 方案)
|
||||||
|
// ==============================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新对话下所有消息的 allowed_users 权限列表
|
||||||
|
* 从 MySQL 获取最新的对话成员并更新到 Manticore
|
||||||
|
*
|
||||||
|
* @param int $dialogId 对话ID
|
||||||
|
* @return int 更新的消息数量
|
||||||
|
*/
|
||||||
|
public static function updateDialogAllowedUsers(int $dialogId): int
|
||||||
|
{
|
||||||
|
if (!Apps::isInstalled("manticore") || $dialogId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$userids = self::getDialogUserIds($dialogId);
|
||||||
|
return ManticoreBase::updateDialogAllowedUsers($dialogId, $userids);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Manticore updateDialogAllowedUsers error: ' . $e->getMessage(), ['dialog_id' => $dialogId]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,9 @@
|
|||||||
namespace App\Observers;
|
namespace App\Observers;
|
||||||
|
|
||||||
use App\Models\WebSocketDialogMsg;
|
use App\Models\WebSocketDialogMsg;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Module\Manticore\ManticoreMsg;
|
||||||
|
use App\Tasks\ManticoreSyncTask;
|
||||||
use App\Tasks\ZincSearchSyncTask;
|
use App\Tasks\ZincSearchSyncTask;
|
||||||
|
|
||||||
class WebSocketDialogMsgObserver extends AbstractObserver
|
class WebSocketDialogMsgObserver extends AbstractObserver
|
||||||
@ -15,7 +18,13 @@ class WebSocketDialogMsgObserver extends AbstractObserver
|
|||||||
*/
|
*/
|
||||||
public function created(WebSocketDialogMsg $webSocketDialogMsg)
|
public function created(WebSocketDialogMsg $webSocketDialogMsg)
|
||||||
{
|
{
|
||||||
|
// ZincSearch 同步
|
||||||
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
|
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
|
||||||
|
|
||||||
|
// Manticore 同步(仅在安装 Manticore 且符合索引条件时)
|
||||||
|
if (Apps::isInstalled('manticore') && ManticoreMsg::shouldIndex($webSocketDialogMsg)) {
|
||||||
|
self::taskDeliver(new ManticoreSyncTask('msg_sync', ['msg_id' => $webSocketDialogMsg->id]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,7 +35,13 @@ class WebSocketDialogMsgObserver extends AbstractObserver
|
|||||||
*/
|
*/
|
||||||
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
|
public function updated(WebSocketDialogMsg $webSocketDialogMsg)
|
||||||
{
|
{
|
||||||
|
// ZincSearch 同步
|
||||||
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
|
self::taskDeliver(new ZincSearchSyncTask('sync', $webSocketDialogMsg->toArray()));
|
||||||
|
|
||||||
|
// Manticore 同步(更新可能使消息符合或不再符合索引条件,由 sync 方法处理)
|
||||||
|
if (Apps::isInstalled('manticore')) {
|
||||||
|
self::taskDeliver(new ManticoreSyncTask('msg_sync', ['msg_id' => $webSocketDialogMsg->id]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,7 +52,13 @@ class WebSocketDialogMsgObserver extends AbstractObserver
|
|||||||
*/
|
*/
|
||||||
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
|
public function deleted(WebSocketDialogMsg $webSocketDialogMsg)
|
||||||
{
|
{
|
||||||
|
// ZincSearch 删除
|
||||||
self::taskDeliver(new ZincSearchSyncTask('delete', $webSocketDialogMsg->toArray()));
|
self::taskDeliver(new ZincSearchSyncTask('delete', $webSocketDialogMsg->toArray()));
|
||||||
|
|
||||||
|
// Manticore 删除
|
||||||
|
if (Apps::isInstalled('manticore')) {
|
||||||
|
self::taskDeliver(new ManticoreSyncTask('msg_delete', ['msg_id' => $webSocketDialogMsg->id]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -5,6 +5,8 @@ namespace App\Observers;
|
|||||||
use App\Models\Deleted;
|
use App\Models\Deleted;
|
||||||
use App\Models\UserBot;
|
use App\Models\UserBot;
|
||||||
use App\Models\WebSocketDialogUser;
|
use App\Models\WebSocketDialogUser;
|
||||||
|
use App\Module\Apps;
|
||||||
|
use App\Tasks\ManticoreSyncTask;
|
||||||
use App\Tasks\ZincSearchSyncTask;
|
use App\Tasks\ZincSearchSyncTask;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
@ -32,6 +34,14 @@ class WebSocketDialogUserObserver extends AbstractObserver
|
|||||||
}
|
}
|
||||||
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||||
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
|
self::taskDeliver(new ZincSearchSyncTask('userSync', $webSocketDialogUser->toArray()));
|
||||||
|
|
||||||
|
// Manticore: 更新对话下所有消息的 allowed_users
|
||||||
|
if (Apps::isInstalled('manticore')) {
|
||||||
|
self::taskDeliver(new ManticoreSyncTask('update_dialog_allowed_users', [
|
||||||
|
'dialog_id' => $webSocketDialogUser->dialog_id
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
$dialog = $webSocketDialogUser->webSocketDialog;
|
$dialog = $webSocketDialogUser->webSocketDialog;
|
||||||
if ($dialog) {
|
if ($dialog) {
|
||||||
@ -60,6 +70,14 @@ class WebSocketDialogUserObserver extends AbstractObserver
|
|||||||
{
|
{
|
||||||
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
Deleted::record('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||||
self::taskDeliver(new ZincSearchSyncTask('deleteUser', $webSocketDialogUser->toArray()));
|
self::taskDeliver(new ZincSearchSyncTask('deleteUser', $webSocketDialogUser->toArray()));
|
||||||
|
|
||||||
|
// Manticore: 更新对话下所有消息的 allowed_users
|
||||||
|
if (Apps::isInstalled('manticore')) {
|
||||||
|
self::taskDeliver(new ManticoreSyncTask('update_dialog_allowed_users', [
|
||||||
|
'dialog_id' => $webSocketDialogUser->dialog_id
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
$dialog = $webSocketDialogUser->webSocketDialog;
|
$dialog = $webSocketDialogUser->webSocketDialog;
|
||||||
if ($dialog) {
|
if ($dialog) {
|
||||||
|
|||||||
@ -6,12 +6,14 @@ use App\Models\File;
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Project;
|
use App\Models\Project;
|
||||||
use App\Models\ProjectTask;
|
use App\Models\ProjectTask;
|
||||||
|
use App\Models\WebSocketDialogMsg;
|
||||||
use App\Module\Apps;
|
use App\Module\Apps;
|
||||||
use App\Module\Manticore\ManticoreBase;
|
use App\Module\Manticore\ManticoreBase;
|
||||||
use App\Module\Manticore\ManticoreFile;
|
use App\Module\Manticore\ManticoreFile;
|
||||||
use App\Module\Manticore\ManticoreUser;
|
use App\Module\Manticore\ManticoreUser;
|
||||||
use App\Module\Manticore\ManticoreProject;
|
use App\Module\Manticore\ManticoreProject;
|
||||||
use App\Module\Manticore\ManticoreTask;
|
use App\Module\Manticore\ManticoreTask;
|
||||||
|
use App\Module\Manticore\ManticoreMsg;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
@ -152,6 +154,31 @@ class ManticoreSyncTask extends AbstractTask
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ==============================
|
||||||
|
// 消息同步动作
|
||||||
|
// ==============================
|
||||||
|
case 'msg_sync':
|
||||||
|
$msg = WebSocketDialogMsg::find($this->data['msg_id'] ?? 0);
|
||||||
|
if ($msg) {
|
||||||
|
ManticoreMsg::sync($msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'msg_delete':
|
||||||
|
$msgId = $this->data['msg_id'] ?? 0;
|
||||||
|
if ($msgId > 0) {
|
||||||
|
ManticoreMsg::delete($msgId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'update_dialog_allowed_users':
|
||||||
|
// 更新对话下所有消息的 allowed_users(成员变更时调用)
|
||||||
|
$dialogId = $this->data['dialog_id'] ?? 0;
|
||||||
|
if ($dialogId > 0) {
|
||||||
|
ManticoreMsg::updateDialogAllowedUsers($dialogId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 增量更新(定时任务调用)
|
// 增量更新(定时任务调用)
|
||||||
$this->incrementalUpdate();
|
$this->incrementalUpdate();
|
||||||
@ -181,6 +208,7 @@ class ManticoreSyncTask extends AbstractTask
|
|||||||
@shell_exec("php /var/www/artisan manticore:sync-users --i 2>&1 &");
|
@shell_exec("php /var/www/artisan manticore:sync-users --i 2>&1 &");
|
||||||
@shell_exec("php /var/www/artisan manticore:sync-projects --i 2>&1 &");
|
@shell_exec("php /var/www/artisan manticore:sync-projects --i 2>&1 &");
|
||||||
@shell_exec("php /var/www/artisan manticore:sync-tasks --i 2>&1 &");
|
@shell_exec("php /var/www/artisan manticore:sync-tasks --i 2>&1 &");
|
||||||
|
@shell_exec("php /var/www/artisan manticore:sync-msgs --i 2>&1 &");
|
||||||
|
|
||||||
// 执行完成
|
// 执行完成
|
||||||
Cache::put("ManticoreSyncTask:Time", time(), Carbon::now()->addMinutes(5));
|
Cache::put("ManticoreSyncTask:Time", time(), Carbon::now()->addMinutes(5));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user