mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-10 07:48:12 +00:00
feat: 扩展 SeekDB 支持联系人、项目、任务的 AI 搜索
- 合并 SeekDBFileSyncTask 到 SeekDBSyncTask - 统一 AI 搜索 API 入口
This commit is contained in:
parent
23faf28f7f
commit
fe7a2a0e73
166
app/Console/Commands/SyncProjectToSeekDB.php
Normal file
166
app/Console/Commands/SyncProjectToSeekDB.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Module\Apps;
|
||||
use App\Module\SeekDB\SeekDBProject;
|
||||
use App\Module\SeekDB\SeekDBKeyValue;
|
||||
use Cache;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncProjectToSeekDB extends Command
|
||||
{
|
||||
/**
|
||||
* 更新数据
|
||||
* --f: 全量更新 (默认)
|
||||
* --i: 增量更新(从上次更新的最后一个ID接上)
|
||||
* --u: 仅同步项目成员关系(不同步项目内容)
|
||||
*
|
||||
* 清理数据
|
||||
* --c: 清除索引
|
||||
*/
|
||||
|
||||
protected $signature = 'seekdb:sync-projects {--f} {--i} {--c} {--u} {--batch=100}';
|
||||
protected $description = '同步项目数据到 SeekDB';
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
$this->error("应用「SeekDB」未安装");
|
||||
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('清除索引...');
|
||||
SeekDBProject::clear();
|
||||
SeekDBKeyValue::set('sync:seekdbProjectLastId', 0);
|
||||
$this->info("索引删除成功");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 仅同步项目成员关系
|
||||
if ($this->option('u')) {
|
||||
$this->info('开始同步项目成员关系...');
|
||||
$count = SeekDBProject::syncAllProjectUsers(function ($count) {
|
||||
if ($count % 1000 === 0) {
|
||||
$this->info(" 已同步 {$count} 条关系...");
|
||||
}
|
||||
});
|
||||
$this->info("项目成员关系同步完成,共 {$count} 条");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('开始同步项目数据...');
|
||||
$this->syncProjects();
|
||||
|
||||
// 全量同步时,同步项目成员关系
|
||||
if ($this->option('f') || (!$this->option('i') && !$this->option('u'))) {
|
||||
$this->info("\n同步项目成员关系...");
|
||||
$count = SeekDBProject::syncAllProjectUsers(function ($count) {
|
||||
if ($count % 1000 === 0) {
|
||||
$this->info(" 已同步 {$count} 条关系...");
|
||||
}
|
||||
});
|
||||
$this->info("项目成员关系同步完成,共 {$count} 条");
|
||||
}
|
||||
|
||||
$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')], 300);
|
||||
}
|
||||
|
||||
private function releaseLock(): void
|
||||
{
|
||||
Cache::forget(md5($this->signature));
|
||||
}
|
||||
|
||||
public function handleSignal(int $signal): void
|
||||
{
|
||||
$this->releaseLock();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
private function syncProjects(): void
|
||||
{
|
||||
$lastKey = "sync:seekdbProjectLastId";
|
||||
$lastId = $this->option('i') ? intval(SeekDBKeyValue::get($lastKey, 0)) : 0;
|
||||
|
||||
if ($lastId > 0) {
|
||||
$this->info("\n增量同步项目数据(从ID: {$lastId})...");
|
||||
} else {
|
||||
$this->info("\n全量同步项目数据...");
|
||||
}
|
||||
|
||||
// 只同步未归档的项目
|
||||
$query = Project::where('id', '>', $lastId)
|
||||
->whereNull('archived_at');
|
||||
|
||||
$num = 0;
|
||||
$count = $query->count();
|
||||
$batchSize = $this->option('batch');
|
||||
$total = 0;
|
||||
|
||||
do {
|
||||
$projects = Project::where('id', '>', $lastId)
|
||||
->whereNull('archived_at')
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($projects->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($projects);
|
||||
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步项目ID {$projects->first()->id} ~ {$projects->last()->id}");
|
||||
|
||||
$this->setLock();
|
||||
|
||||
$synced = SeekDBProject::batchSync($projects);
|
||||
$total += $synced;
|
||||
|
||||
$lastId = $projects->last()->id;
|
||||
SeekDBKeyValue::set($lastKey, $lastId);
|
||||
} while (count($projects) == $batchSize);
|
||||
|
||||
$this->info("同步项目结束 - 最后ID {$lastId},共同步 {$total} 个项目");
|
||||
$this->info("已索引项目数量: " . SeekDBProject::getIndexedCount());
|
||||
}
|
||||
}
|
||||
|
||||
166
app/Console/Commands/SyncTaskToSeekDB.php
Normal file
166
app/Console/Commands/SyncTaskToSeekDB.php
Normal file
@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\ProjectTask;
|
||||
use App\Module\Apps;
|
||||
use App\Module\SeekDB\SeekDBTask;
|
||||
use App\Module\SeekDB\SeekDBKeyValue;
|
||||
use Cache;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncTaskToSeekDB extends Command
|
||||
{
|
||||
/**
|
||||
* 更新数据
|
||||
* --f: 全量更新 (默认)
|
||||
* --i: 增量更新(从上次更新的最后一个ID接上)
|
||||
* --u: 仅同步任务成员关系(不同步任务内容)
|
||||
*
|
||||
* 清理数据
|
||||
* --c: 清除索引
|
||||
*/
|
||||
|
||||
protected $signature = 'seekdb:sync-tasks {--f} {--i} {--c} {--u} {--batch=100}';
|
||||
protected $description = '同步任务数据到 SeekDB';
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
$this->error("应用「SeekDB」未安装");
|
||||
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('清除索引...');
|
||||
SeekDBTask::clear();
|
||||
SeekDBKeyValue::set('sync:seekdbTaskLastId', 0);
|
||||
$this->info("索引删除成功");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 仅同步任务成员关系
|
||||
if ($this->option('u')) {
|
||||
$this->info('开始同步任务成员关系...');
|
||||
$count = SeekDBTask::syncAllTaskUsers(function ($count) {
|
||||
if ($count % 1000 === 0) {
|
||||
$this->info(" 已同步 {$count} 条关系...");
|
||||
}
|
||||
});
|
||||
$this->info("任务成员关系同步完成,共 {$count} 条");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('开始同步任务数据...');
|
||||
$this->syncTasks();
|
||||
|
||||
// 全量同步时,同步任务成员关系
|
||||
if ($this->option('f') || (!$this->option('i') && !$this->option('u'))) {
|
||||
$this->info("\n同步任务成员关系...");
|
||||
$count = SeekDBTask::syncAllTaskUsers(function ($count) {
|
||||
if ($count % 1000 === 0) {
|
||||
$this->info(" 已同步 {$count} 条关系...");
|
||||
}
|
||||
});
|
||||
$this->info("任务成员关系同步完成,共 {$count} 条");
|
||||
}
|
||||
|
||||
$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); // 任务可能较多,10分钟
|
||||
}
|
||||
|
||||
private function releaseLock(): void
|
||||
{
|
||||
Cache::forget(md5($this->signature));
|
||||
}
|
||||
|
||||
public function handleSignal(int $signal): void
|
||||
{
|
||||
$this->releaseLock();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
private function syncTasks(): void
|
||||
{
|
||||
$lastKey = "sync:seekdbTaskLastId";
|
||||
$lastId = $this->option('i') ? intval(SeekDBKeyValue::get($lastKey, 0)) : 0;
|
||||
|
||||
if ($lastId > 0) {
|
||||
$this->info("\n增量同步任务数据(从ID: {$lastId})...");
|
||||
} else {
|
||||
$this->info("\n全量同步任务数据...");
|
||||
}
|
||||
|
||||
// 只同步未归档的任务(包括软删除恢复的情况)
|
||||
$query = ProjectTask::where('id', '>', $lastId)
|
||||
->whereNull('archived_at');
|
||||
|
||||
$num = 0;
|
||||
$count = $query->count();
|
||||
$batchSize = $this->option('batch');
|
||||
$total = 0;
|
||||
|
||||
do {
|
||||
$tasks = ProjectTask::where('id', '>', $lastId)
|
||||
->whereNull('archived_at')
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($tasks->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($tasks);
|
||||
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步任务ID {$tasks->first()->id} ~ {$tasks->last()->id}");
|
||||
|
||||
$this->setLock();
|
||||
|
||||
$synced = SeekDBTask::batchSync($tasks);
|
||||
$total += $synced;
|
||||
|
||||
$lastId = $tasks->last()->id;
|
||||
SeekDBKeyValue::set($lastKey, $lastId);
|
||||
} while (count($tasks) == $batchSize);
|
||||
|
||||
$this->info("同步任务结束 - 最后ID {$lastId},共同步 {$total} 个任务");
|
||||
$this->info("已索引任务数量: " . SeekDBTask::getIndexedCount());
|
||||
}
|
||||
}
|
||||
|
||||
143
app/Console/Commands/SyncUserToSeekDB.php
Normal file
143
app/Console/Commands/SyncUserToSeekDB.php
Normal file
@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Module\Apps;
|
||||
use App\Module\SeekDB\SeekDBUser;
|
||||
use App\Module\SeekDB\SeekDBKeyValue;
|
||||
use Cache;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncUserToSeekDB extends Command
|
||||
{
|
||||
/**
|
||||
* 更新数据
|
||||
* --f: 全量更新 (默认)
|
||||
* --i: 增量更新(从上次更新的最后一个ID接上)
|
||||
*
|
||||
* 清理数据
|
||||
* --c: 清除索引
|
||||
*/
|
||||
|
||||
protected $signature = 'seekdb:sync-users {--f} {--i} {--c} {--batch=100}';
|
||||
protected $description = '同步用户数据到 SeekDB(联系人搜索)';
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
$this->error("应用「SeekDB」未安装");
|
||||
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('清除索引...');
|
||||
SeekDBUser::clear();
|
||||
SeekDBKeyValue::set('sync:seekdbUserLastId', 0);
|
||||
$this->info("索引删除成功");
|
||||
$this->releaseLock();
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info('开始同步用户数据...');
|
||||
$this->syncUsers();
|
||||
|
||||
$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')], 300);
|
||||
}
|
||||
|
||||
private function releaseLock(): void
|
||||
{
|
||||
Cache::forget(md5($this->signature));
|
||||
}
|
||||
|
||||
public function handleSignal(int $signal): void
|
||||
{
|
||||
$this->releaseLock();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
private function syncUsers(): void
|
||||
{
|
||||
$lastKey = "sync:seekdbUserLastId";
|
||||
$lastId = $this->option('i') ? intval(SeekDBKeyValue::get($lastKey, 0)) : 0;
|
||||
|
||||
if ($lastId > 0) {
|
||||
$this->info("\n增量同步用户数据(从ID: {$lastId})...");
|
||||
} else {
|
||||
$this->info("\n全量同步用户数据...");
|
||||
}
|
||||
|
||||
// 只同步非机器人且未禁用的用户
|
||||
$query = User::where('userid', '>', $lastId)
|
||||
->where('bot', 0)
|
||||
->whereNull('disable_at');
|
||||
|
||||
$num = 0;
|
||||
$count = $query->count();
|
||||
$batchSize = $this->option('batch');
|
||||
$total = 0;
|
||||
|
||||
do {
|
||||
$users = User::where('userid', '>', $lastId)
|
||||
->where('bot', 0)
|
||||
->whereNull('disable_at')
|
||||
->orderBy('userid')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$num += count($users);
|
||||
$progress = $count > 0 ? round($num / $count * 100, 2) : 100;
|
||||
$this->info("{$num}/{$count} ({$progress}%) 正在同步用户ID {$users->first()->userid} ~ {$users->last()->userid}");
|
||||
|
||||
$this->setLock();
|
||||
|
||||
$synced = SeekDBUser::batchSync($users);
|
||||
$total += $synced;
|
||||
|
||||
$lastId = $users->last()->userid;
|
||||
SeekDBKeyValue::set($lastKey, $lastId);
|
||||
} while (count($users) == $batchSize);
|
||||
|
||||
$this->info("同步用户结束 - 最后ID {$lastId},共同步 {$total} 个用户");
|
||||
$this->info("已索引用户数量: " . SeekDBUser::getIndexedCount());
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ use App\Models\FileLink;
|
||||
use App\Models\FileUser;
|
||||
use App\Models\User;
|
||||
use App\Models\UserRecentItem;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Module\Down;
|
||||
use App\Module\Timer;
|
||||
@ -118,7 +117,7 @@ class FileController extends AbstractController
|
||||
/**
|
||||
* @api {get} api/file/search 搜索文件列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiDescription 需要token身份(仅搜索文件名,AI 内容搜索请使用 api/search/file)
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup file
|
||||
* @apiName search
|
||||
@ -126,8 +125,6 @@ class FileController extends AbstractController
|
||||
* @apiParam {String} [link] 通过分享地址搜索(如:https://t.hitosea.com/single/file/ODcwOCwzOSxpa0JBS2lmVQ==)
|
||||
* @apiParam {String} [key] 关键词
|
||||
* @apiParam {Number} [take] 获取数量(默认:50,最大:100)
|
||||
* @apiParam {String} [search_content] 是否搜索文件内容(yes/no,默认:no)
|
||||
* @apiParam {String} [search_type] 搜索类型(text/vector/hybrid,默认:hybrid,仅 search_content=yes 时有效)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@ -139,8 +136,6 @@ class FileController extends AbstractController
|
||||
//
|
||||
$link = trim(Request::input('link'));
|
||||
$key = trim(Request::input('key'));
|
||||
$searchContent = Request::input('search_content', 'no') === 'yes';
|
||||
$searchType = Request::input('search_type', 'hybrid');
|
||||
$id = 0;
|
||||
$take = Base::getPaginate(100, 50, 'take');
|
||||
if (preg_match("/\/single\/file\/(.*?)$/i", $link, $match)) {
|
||||
@ -151,34 +146,6 @@ class FileController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
// 如果需要搜索文件内容且有关键词
|
||||
if ($searchContent && $key && !$id && Apps::isInstalled('seekdb')) {
|
||||
$results = \App\Module\SeekDB\SeekDBFile::search(
|
||||
$user->userid,
|
||||
$key,
|
||||
$searchType,
|
||||
0,
|
||||
$take
|
||||
);
|
||||
// 获取完整的文件信息
|
||||
if (!empty($results)) {
|
||||
$fileIds = array_column($results, 'file_id');
|
||||
$files = File::whereIn('id', $fileIds)->get()->keyBy('id');
|
||||
$array = [];
|
||||
foreach ($results as $result) {
|
||||
$file = $files->get($result['file_id']);
|
||||
if ($file) {
|
||||
$temp = $file->toArray();
|
||||
$temp['content_preview'] = $result['content_preview'] ?? null;
|
||||
$temp['relevance'] = $result['relevance'] ?? 0;
|
||||
$array[] = $temp;
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', $array);
|
||||
}
|
||||
return Base::retSuccess('success', []);
|
||||
}
|
||||
|
||||
// 搜索自己的
|
||||
$builder = File::whereUserid($user->userid);
|
||||
if ($id) {
|
||||
|
||||
246
app/Http/Controllers/Api/SearchController.php
Normal file
246
app/Http/Controllers/Api/SearchController.php
Normal file
@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Request;
|
||||
use App\Models\File;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\Apps;
|
||||
use App\Module\SeekDB\SeekDBFile;
|
||||
use App\Module\SeekDB\SeekDBUser;
|
||||
use App\Module\SeekDB\SeekDBProject;
|
||||
use App\Module\SeekDB\SeekDBTask;
|
||||
|
||||
/**
|
||||
* @apiDefine search
|
||||
*
|
||||
* 智能搜索
|
||||
*/
|
||||
class SearchController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @api {get} api/search/contact AI 搜索联系人
|
||||
*
|
||||
* @apiDescription 需要token身份,需要安装 SeekDB 应用
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup search
|
||||
* @apiName contact
|
||||
*
|
||||
* @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 contact()
|
||||
{
|
||||
User::auth();
|
||||
|
||||
if (!Apps::isInstalled('seekdb')) {
|
||||
return Base::retError('SeekDB 应用未安装');
|
||||
}
|
||||
|
||||
$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 = SeekDBUser::search($key, $searchType, $take);
|
||||
|
||||
// 补充用户完整信息
|
||||
$userids = array_column($results, 'userid');
|
||||
if (!empty($userids)) {
|
||||
$users = User::whereIn('userid', $userids)
|
||||
->select(User::$basicField)
|
||||
->get()
|
||||
->keyBy('userid');
|
||||
|
||||
foreach ($results as &$item) {
|
||||
$userData = $users->get($item['userid']);
|
||||
if ($userData) {
|
||||
$item = array_merge($userData->toArray(), [
|
||||
'relevance' => $item['relevance'] ?? 0,
|
||||
'introduction_preview' => $item['introduction_preview'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/search/project AI 搜索项目
|
||||
*
|
||||
* @apiDescription 需要token身份,需要安装 SeekDB 应用
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup search
|
||||
* @apiName project
|
||||
*
|
||||
* @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 project()
|
||||
{
|
||||
$user = User::auth();
|
||||
|
||||
if (!Apps::isInstalled('seekdb')) {
|
||||
return Base::retError('SeekDB 应用未安装');
|
||||
}
|
||||
|
||||
$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 = SeekDBProject::search($user->userid, $key, $searchType, $take);
|
||||
|
||||
// 补充项目完整信息
|
||||
$projectIds = array_column($results, 'project_id');
|
||||
if (!empty($projectIds)) {
|
||||
$projects = \App\Models\Project::whereIn('id', $projectIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
foreach ($results as &$item) {
|
||||
$projectData = $projects->get($item['project_id']);
|
||||
if ($projectData) {
|
||||
$item = array_merge($projectData->toArray(), [
|
||||
'relevance' => $item['relevance'] ?? 0,
|
||||
'desc_preview' => $item['desc_preview'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/search/task AI 搜索任务
|
||||
*
|
||||
* @apiDescription 需要token身份,需要安装 SeekDB 应用
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup search
|
||||
* @apiName task
|
||||
*
|
||||
* @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 task()
|
||||
{
|
||||
$user = User::auth();
|
||||
|
||||
if (!Apps::isInstalled('seekdb')) {
|
||||
return Base::retError('SeekDB 应用未安装');
|
||||
}
|
||||
|
||||
$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 = SeekDBTask::search($user->userid, $key, $searchType, $take);
|
||||
|
||||
// 补充任务完整信息
|
||||
$taskIds = array_column($results, 'task_id');
|
||||
if (!empty($taskIds)) {
|
||||
$tasks = \App\Models\ProjectTask::whereIn('id', $taskIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
foreach ($results as &$item) {
|
||||
$taskData = $tasks->get($item['task_id']);
|
||||
if ($taskData) {
|
||||
$item = array_merge($taskData->toArray(), [
|
||||
'relevance' => $item['relevance'] ?? 0,
|
||||
'desc_preview' => $item['desc_preview'] ?? null,
|
||||
'content_preview' => $item['content_preview'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/search/file AI 搜索文件
|
||||
*
|
||||
* @apiDescription 需要token身份,需要安装 SeekDB 应用
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup search
|
||||
* @apiName file
|
||||
*
|
||||
* @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 file()
|
||||
{
|
||||
$user = User::auth();
|
||||
|
||||
if (!Apps::isInstalled('seekdb')) {
|
||||
return Base::retError('SeekDB 应用未安装');
|
||||
}
|
||||
|
||||
$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 = SeekDBFile::search($user->userid, $key, $searchType, 0, $take);
|
||||
|
||||
// 补充文件完整信息
|
||||
$fileIds = array_column($results, 'file_id');
|
||||
if (!empty($fileIds)) {
|
||||
$files = File::whereIn('id', $fileIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
$formattedResults = [];
|
||||
foreach ($results as $item) {
|
||||
$fileData = $files->get($item['file_id']);
|
||||
if ($fileData) {
|
||||
$formattedResults[] = array_merge($fileData->toArray(), [
|
||||
'relevance' => $item['relevance'] ?? 0,
|
||||
'content_preview' => $item['content_preview'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', $formattedResults);
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', []);
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ use App\Tasks\DeleteBotMsgTask;
|
||||
use App\Tasks\CheckinRemindTask;
|
||||
use App\Tasks\CloseMeetingRoomTask;
|
||||
use App\Tasks\ZincSearchSyncTask;
|
||||
use App\Tasks\SeekDBFileSyncTask;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
use App\Tasks\UnclaimedTaskRemindTask;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Laravolt\Avatar\Avatar;
|
||||
@ -274,8 +274,8 @@ class IndexController extends InvokeController
|
||||
Task::deliver(new CloseMeetingRoomTask());
|
||||
// ZincSearch 同步
|
||||
Task::deliver(new ZincSearchSyncTask());
|
||||
// SeekDB 文件同步
|
||||
Task::deliver(new SeekDBFileSyncTask());
|
||||
// SeekDB 同步(文件/用户/项目/任务)
|
||||
Task::deliver(new SeekDBSyncTask());
|
||||
|
||||
return "success";
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
384
app/Module/SeekDB/SeekDBProject.php
Normal file
384
app/Module/SeekDB/SeekDBProject.php
Normal file
@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\SeekDB;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* SeekDB 项目搜索类
|
||||
*
|
||||
* 使用方法:
|
||||
*
|
||||
* 1. 搜索方法
|
||||
* - 搜索项目: search($userid, $keyword, $searchType, $limit);
|
||||
*
|
||||
* 2. 同步方法
|
||||
* - 单个同步: sync(Project $project);
|
||||
* - 批量同步: batchSync($projects);
|
||||
* - 删除索引: delete($projectId);
|
||||
*
|
||||
* 3. 成员关系方法
|
||||
* - 添加成员: addProjectUser($projectId, $userid);
|
||||
* - 删除成员: removeProjectUser($projectId, $userid);
|
||||
* - 同步所有成员: syncProjectUsers($projectId);
|
||||
*
|
||||
* 4. 工具方法
|
||||
* - 清空索引: clear();
|
||||
*/
|
||||
class SeekDBProject
|
||||
{
|
||||
/**
|
||||
* 搜索项目(支持全文、向量、混合搜索)
|
||||
*
|
||||
* @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("seekdb")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($searchType) {
|
||||
case 'text':
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::projectFullTextSearch($keyword, $userid, $limit, 0)
|
||||
);
|
||||
|
||||
case 'vector':
|
||||
$embedding = self::getEmbedding($keyword);
|
||||
if (empty($embedding)) {
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::projectFullTextSearch($keyword, $userid, $limit, 0)
|
||||
);
|
||||
}
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::projectVectorSearch($embedding, $userid, $limit)
|
||||
);
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
$embedding = self::getEmbedding($keyword);
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::projectHybridSearch($keyword, $embedding, $userid, $limit)
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB 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 SeekDB 返回的结果
|
||||
* @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' => $item['project_desc_preview'] ?? null,
|
||||
'relevance' => $item['relevance'] ?? $item['similarity'] ?? $item['rrf_score'] ?? 0,
|
||||
];
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 同步方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 同步单个项目到 SeekDB
|
||||
*
|
||||
* @param Project $project 项目模型
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function sync(Project $project): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
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) . ']';
|
||||
}
|
||||
}
|
||||
|
||||
// 写入 SeekDB
|
||||
$result = SeekDBBase::upsertProjectVector([
|
||||
'project_id' => $project->id,
|
||||
'userid' => $project->userid ?? 0,
|
||||
'personal' => $project->personal ?? 0,
|
||||
'project_name' => $project->name ?? '',
|
||||
'project_desc' => $project->desc ?? '',
|
||||
'content_vector' => $embedding,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB 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("seekdb")) {
|
||||
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("seekdb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 删除项目索引
|
||||
SeekDBBase::deleteProjectVector($projectId);
|
||||
// 删除项目成员关系
|
||||
SeekDBBase::deleteAllProjectUsers($projectId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有索引
|
||||
*
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function clear(): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SeekDBBase::clearAllProjectVectors();
|
||||
SeekDBBase::clearAllProjectUsers();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已索引项目数量
|
||||
*
|
||||
* @return int 数量
|
||||
*/
|
||||
public static function getIndexedCount(): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return SeekDBBase::getIndexedProjectCount();
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 成员关系方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 添加项目成员到 SeekDB
|
||||
*
|
||||
* @param int $projectId 项目ID
|
||||
* @param int $userid 用户ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function addProjectUser(int $projectId, int $userid): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb") || $projectId <= 0 || $userid <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SeekDBBase::upsertProjectUser($projectId, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项目成员
|
||||
*
|
||||
* @param int $projectId 项目ID
|
||||
* @param int $userid 用户ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function removeProjectUser(int $projectId, int $userid): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb") || $projectId <= 0 || $userid <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SeekDBBase::deleteProjectUser($projectId, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步项目的所有成员到 SeekDB
|
||||
*
|
||||
* @param int $projectId 项目ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function syncProjectUsers(int $projectId): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb") || $projectId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从 MySQL 获取项目成员
|
||||
$userids = ProjectUser::where('project_id', $projectId)
|
||||
->pluck('userid')
|
||||
->toArray();
|
||||
|
||||
// 同步到 SeekDB
|
||||
return SeekDBBase::syncProjectUsers($projectId, $userids);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB syncProjectUsers error: ' . $e->getMessage(), ['project_id' => $projectId]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步所有项目成员关系(全量同步)
|
||||
*
|
||||
* @param callable|null $progressCallback 进度回调
|
||||
* @return int 同步数量
|
||||
*/
|
||||
public static function syncAllProjectUsers(?callable $progressCallback = null): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$lastId = 0;
|
||||
$batchSize = 1000;
|
||||
|
||||
// 先清空 SeekDB 中的 project_users 表
|
||||
SeekDBBase::clearAllProjectUsers();
|
||||
|
||||
// 分批同步
|
||||
while (true) {
|
||||
$records = ProjectUser::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($records->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
SeekDBBase::upsertProjectUser($record->project_id, $record->userid);
|
||||
$count++;
|
||||
$lastId = $record->id;
|
||||
}
|
||||
|
||||
if ($progressCallback) {
|
||||
$progressCallback($count);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
571
app/Module/SeekDB/SeekDBTask.php
Normal file
571
app/Module/SeekDB/SeekDBTask.php
Normal file
@ -0,0 +1,571 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\SeekDB;
|
||||
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\ProjectTaskContent;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\ProjectTaskVisibilityUser;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Module\AI;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* SeekDB 任务搜索类
|
||||
*
|
||||
* 权限逻辑说明:
|
||||
* - visibility = 1: 项目人员可见,通过 project_users 表过滤
|
||||
* - visibility = 2: 任务人员可见,通过 task_users 表过滤(ProjectTaskUser)
|
||||
* - visibility = 3: 指定成员可见,通过 task_users 表过滤(ProjectTaskUser + ProjectTaskVisibilityUser)
|
||||
*
|
||||
* 使用方法:
|
||||
*
|
||||
* 1. 搜索方法
|
||||
* - 搜索任务: search($userid, $keyword, $searchType, $limit);
|
||||
*
|
||||
* 2. 同步方法
|
||||
* - 单个同步: sync(ProjectTask $task);
|
||||
* - 批量同步: batchSync($tasks);
|
||||
* - 删除索引: delete($taskId);
|
||||
*
|
||||
* 3. 成员关系方法
|
||||
* - 添加成员: addTaskUser($taskId, $userid);
|
||||
* - 删除成员: removeTaskUser($taskId, $userid);
|
||||
* - 同步所有成员: syncTaskUsers($taskId);
|
||||
*
|
||||
* 4. 工具方法
|
||||
* - 清空索引: clear();
|
||||
*/
|
||||
class SeekDBTask
|
||||
{
|
||||
/**
|
||||
* 最大内容长度(字符)
|
||||
*/
|
||||
public const MAX_CONTENT_LENGTH = 50000; // 50K 字符
|
||||
|
||||
/**
|
||||
* 搜索任务(支持全文、向量、混合搜索)
|
||||
*
|
||||
* @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("seekdb")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($searchType) {
|
||||
case 'text':
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::taskFullTextSearch($keyword, $userid, $limit, 0)
|
||||
);
|
||||
|
||||
case 'vector':
|
||||
$embedding = self::getEmbedding($keyword);
|
||||
if (empty($embedding)) {
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::taskFullTextSearch($keyword, $userid, $limit, 0)
|
||||
);
|
||||
}
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::taskVectorSearch($embedding, $userid, $limit)
|
||||
);
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
$embedding = self::getEmbedding($keyword);
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::taskHybridSearch($keyword, $embedding, $userid, $limit)
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB task 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 SeekDB 返回的结果
|
||||
* @return array 格式化后的结果
|
||||
*/
|
||||
private static function formatSearchResults(array $results): array
|
||||
{
|
||||
$formatted = [];
|
||||
foreach ($results as $item) {
|
||||
$formatted[] = [
|
||||
'task_id' => $item['task_id'],
|
||||
'id' => $item['task_id'],
|
||||
'project_id' => $item['project_id'],
|
||||
'userid' => $item['userid'],
|
||||
'visibility' => $item['visibility'],
|
||||
'name' => $item['task_name'],
|
||||
'desc_preview' => $item['task_desc_preview'] ?? null,
|
||||
'content_preview' => $item['task_content_preview'] ?? null,
|
||||
'relevance' => $item['relevance'] ?? $item['similarity'] ?? $item['rrf_score'] ?? 0,
|
||||
];
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 同步方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 同步单个任务到 SeekDB
|
||||
*
|
||||
* @param ProjectTask $task 任务模型
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function sync(ProjectTask $task): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 已归档或已删除的任务不索引
|
||||
if ($task->archived_at || $task->deleted_at) {
|
||||
return self::delete($task->id);
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取任务详细内容
|
||||
$taskContent = self::getTaskContent($task);
|
||||
|
||||
// 构建用于搜索的文本内容
|
||||
$searchableContent = self::buildSearchableContent($task, $taskContent);
|
||||
|
||||
// 获取 embedding(如果 AI 可用)
|
||||
$embedding = null;
|
||||
if (!empty($searchableContent) && Apps::isInstalled('ai')) {
|
||||
$embeddingResult = self::getEmbedding($searchableContent);
|
||||
if (!empty($embeddingResult)) {
|
||||
$embedding = '[' . implode(',', $embeddingResult) . ']';
|
||||
}
|
||||
}
|
||||
|
||||
// 写入 SeekDB
|
||||
$result = SeekDBBase::upsertTaskVector([
|
||||
'task_id' => $task->id,
|
||||
'project_id' => $task->project_id ?? 0,
|
||||
'userid' => $task->userid ?? 0,
|
||||
'visibility' => $task->visibility ?? 1,
|
||||
'task_name' => $task->name ?? '',
|
||||
'task_desc' => $task->desc ?? '',
|
||||
'task_content' => $taskContent,
|
||||
'content_vector' => $embedding,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB task sync error: ' . $e->getMessage(), [
|
||||
'task_id' => $task->id,
|
||||
'task_name' => $task->name,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务详细内容
|
||||
*
|
||||
* @param ProjectTask $task 任务模型
|
||||
* @return string 任务内容
|
||||
*/
|
||||
private static function getTaskContent(ProjectTask $task): string
|
||||
{
|
||||
try {
|
||||
$content = ProjectTaskContent::where('task_id', $task->id)->first();
|
||||
if (!$content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 解析内容
|
||||
$contentData = Base::json2array($content->content);
|
||||
$text = '';
|
||||
|
||||
// 提取文本内容(内容可能是 blocks 格式)
|
||||
if (is_array($contentData)) {
|
||||
$text = self::extractTextFromContent($contentData);
|
||||
} elseif (is_string($contentData)) {
|
||||
$text = $contentData;
|
||||
}
|
||||
|
||||
// 限制内容长度
|
||||
return mb_substr($text, 0, self::MAX_CONTENT_LENGTH);
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Get task content error: ' . $e->getMessage(), ['task_id' => $task->id]);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从内容数组中提取文本
|
||||
*
|
||||
* @param array $contentData 内容数据
|
||||
* @return string 提取的文本
|
||||
*/
|
||||
private static function extractTextFromContent(array $contentData): string
|
||||
{
|
||||
$texts = [];
|
||||
|
||||
// 处理 blocks 格式
|
||||
if (isset($contentData['blocks']) && is_array($contentData['blocks'])) {
|
||||
foreach ($contentData['blocks'] as $block) {
|
||||
if (isset($block['text'])) {
|
||||
$texts[] = $block['text'];
|
||||
}
|
||||
if (isset($block['data']['text'])) {
|
||||
$texts[] = $block['data']['text'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他格式
|
||||
if (isset($contentData['text'])) {
|
||||
$texts[] = $contentData['text'];
|
||||
}
|
||||
|
||||
return implode(' ', $texts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建可搜索的文本内容
|
||||
*
|
||||
* @param ProjectTask $task 任务模型
|
||||
* @param string $taskContent 任务详细内容
|
||||
* @return string 可搜索的文本
|
||||
*/
|
||||
private static function buildSearchableContent(ProjectTask $task, string $taskContent): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (!empty($task->name)) {
|
||||
$parts[] = $task->name;
|
||||
}
|
||||
if (!empty($task->desc)) {
|
||||
$parts[] = $task->desc;
|
||||
}
|
||||
if (!empty($taskContent)) {
|
||||
$parts[] = $taskContent;
|
||||
}
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步任务
|
||||
*
|
||||
* @param iterable $tasks 任务列表
|
||||
* @return int 成功同步的数量
|
||||
*/
|
||||
public static function batchSync(iterable $tasks): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($tasks as $task) {
|
||||
if (self::sync($task)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务索引
|
||||
*
|
||||
* @param int $taskId 任务ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function delete(int $taskId): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 删除任务索引
|
||||
SeekDBBase::deleteTaskVector($taskId);
|
||||
// 删除任务成员关系
|
||||
SeekDBBase::deleteAllTaskUsers($taskId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务可见性
|
||||
*
|
||||
* @param int $taskId 任务ID
|
||||
* @param int $visibility 可见性
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function updateVisibility(int $taskId, int $visibility): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb") || $taskId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SeekDBBase::updateTaskVisibility($taskId, $visibility);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有索引
|
||||
*
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function clear(): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SeekDBBase::clearAllTaskVectors();
|
||||
SeekDBBase::clearAllTaskUsers();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已索引任务数量
|
||||
*
|
||||
* @return int 数量
|
||||
*/
|
||||
public static function getIndexedCount(): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return SeekDBBase::getIndexedTaskCount();
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 成员关系方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 添加任务成员到 SeekDB
|
||||
*
|
||||
* @param int $taskId 任务ID
|
||||
* @param int $userid 用户ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function addTaskUser(int $taskId, int $userid): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb") || $taskId <= 0 || $userid <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SeekDBBase::upsertTaskUser($taskId, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除任务成员
|
||||
*
|
||||
* @param int $taskId 任务ID
|
||||
* @param int $userid 用户ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function removeTaskUser(int $taskId, int $userid): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb") || $taskId <= 0 || $userid <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SeekDBBase::deleteTaskUser($taskId, $userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定可见成员(visibility=3 场景)
|
||||
*
|
||||
* 特殊处理:需要检查该用户是否仍是任务的负责人/协作人
|
||||
* 如果是,则不应该从 task_users 中删除
|
||||
*
|
||||
* @param int $taskId 任务ID
|
||||
* @param int $userid 用户ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function removeVisibilityUser(int $taskId, int $userid): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb") || $taskId <= 0 || $userid <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查用户是否仍是任务的负责人/协作人
|
||||
$isTaskMember = ProjectTaskUser::where('task_id', $taskId)
|
||||
->where('userid', $userid)
|
||||
->exists();
|
||||
|
||||
// 检查是否是父任务的成员(子任务场景)
|
||||
$task = \App\Models\ProjectTask::find($taskId);
|
||||
$isParentTaskMember = false;
|
||||
if ($task && $task->parent_id > 0) {
|
||||
$isParentTaskMember = ProjectTaskUser::where('task_id', $task->parent_id)
|
||||
->where('userid', $userid)
|
||||
->exists();
|
||||
}
|
||||
|
||||
// 如果仍是任务成员,不删除
|
||||
if ($isTaskMember || $isParentTaskMember) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 从 SeekDB 删除
|
||||
return SeekDBBase::deleteTaskUser($taskId, $userid);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB removeVisibilityUser error: ' . $e->getMessage(), [
|
||||
'task_id' => $taskId,
|
||||
'userid' => $userid,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步任务的所有成员到 SeekDB
|
||||
*
|
||||
* 包括:ProjectTaskUser 和 ProjectTaskVisibilityUser
|
||||
*
|
||||
* @param int $taskId 任务ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function syncTaskUsers(int $taskId): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb") || $taskId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取任务成员(负责人/协作人)
|
||||
$taskUserIds = ProjectTaskUser::where('task_id', $taskId)
|
||||
->orWhere('task_pid', $taskId)
|
||||
->pluck('userid')
|
||||
->toArray();
|
||||
|
||||
// 获取可见性指定成员
|
||||
$visibilityUserIds = ProjectTaskVisibilityUser::where('task_id', $taskId)
|
||||
->pluck('userid')
|
||||
->toArray();
|
||||
|
||||
// 合并去重
|
||||
$allUserIds = array_unique(array_merge($taskUserIds, $visibilityUserIds));
|
||||
|
||||
// 同步到 SeekDB
|
||||
return SeekDBBase::syncTaskUsers($taskId, $allUserIds);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB syncTaskUsers error: ' . $e->getMessage(), ['task_id' => $taskId]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步所有任务成员关系(全量同步)
|
||||
*
|
||||
* @param callable|null $progressCallback 进度回调
|
||||
* @return int 同步数量
|
||||
*/
|
||||
public static function syncAllTaskUsers(?callable $progressCallback = null): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$lastId = 0;
|
||||
$batchSize = 1000;
|
||||
|
||||
// 先清空 SeekDB 中的 task_users 表
|
||||
SeekDBBase::clearAllTaskUsers();
|
||||
|
||||
// 同步 ProjectTaskUser
|
||||
while (true) {
|
||||
$records = ProjectTaskUser::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($records->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
SeekDBBase::upsertTaskUser($record->task_id, $record->userid);
|
||||
// 如果有父任务,也添加到父任务
|
||||
if ($record->task_pid) {
|
||||
SeekDBBase::upsertTaskUser($record->task_pid, $record->userid);
|
||||
}
|
||||
$count++;
|
||||
$lastId = $record->id;
|
||||
}
|
||||
|
||||
if ($progressCallback) {
|
||||
$progressCallback($count);
|
||||
}
|
||||
}
|
||||
|
||||
// 同步 ProjectTaskVisibilityUser
|
||||
$lastId = 0;
|
||||
while (true) {
|
||||
$records = ProjectTaskVisibilityUser::where('id', '>', $lastId)
|
||||
->orderBy('id')
|
||||
->limit($batchSize)
|
||||
->get();
|
||||
|
||||
if ($records->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($records as $record) {
|
||||
SeekDBBase::upsertTaskUser($record->task_id, $record->userid);
|
||||
$count++;
|
||||
$lastId = $record->id;
|
||||
}
|
||||
|
||||
if ($progressCallback) {
|
||||
$progressCallback($count);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
275
app/Module/SeekDB/SeekDBUser.php
Normal file
275
app/Module/SeekDB/SeekDBUser.php
Normal file
@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace App\Module\SeekDB;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Module\AI;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* SeekDB 用户搜索类(联系人搜索)
|
||||
*
|
||||
* 使用方法:
|
||||
*
|
||||
* 1. 搜索方法
|
||||
* - 搜索用户: search($keyword, $searchType, $limit);
|
||||
*
|
||||
* 2. 同步方法
|
||||
* - 单个同步: sync(User $user);
|
||||
* - 批量同步: batchSync($users);
|
||||
* - 删除索引: delete($userid);
|
||||
*
|
||||
* 3. 工具方法
|
||||
* - 清空索引: clear();
|
||||
*/
|
||||
class SeekDBUser
|
||||
{
|
||||
/**
|
||||
* 搜索用户(支持全文、向量、混合搜索)
|
||||
*
|
||||
* @param string $keyword 搜索关键词
|
||||
* @param string $searchType 搜索类型: text/vector/hybrid
|
||||
* @param int $limit 返回数量
|
||||
* @return array 搜索结果
|
||||
*/
|
||||
public static function search(string $keyword, string $searchType = 'hybrid', int $limit = 20): array
|
||||
{
|
||||
if (empty($keyword)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($searchType) {
|
||||
case 'text':
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::userFullTextSearch($keyword, $limit, 0)
|
||||
);
|
||||
|
||||
case 'vector':
|
||||
$embedding = self::getEmbedding($keyword);
|
||||
if (empty($embedding)) {
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::userFullTextSearch($keyword, $limit, 0)
|
||||
);
|
||||
}
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::userVectorSearch($embedding, $limit)
|
||||
);
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
$embedding = self::getEmbedding($keyword);
|
||||
return self::formatSearchResults(
|
||||
SeekDBBase::userHybridSearch($keyword, $embedding, $limit)
|
||||
);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB user 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 SeekDB 返回的结果
|
||||
* @return array 格式化后的结果
|
||||
*/
|
||||
private static function formatSearchResults(array $results): array
|
||||
{
|
||||
$formatted = [];
|
||||
foreach ($results as $item) {
|
||||
$formatted[] = [
|
||||
'userid' => $item['userid'],
|
||||
'nickname' => $item['nickname'],
|
||||
'email' => $item['email'],
|
||||
'tel' => $item['tel'],
|
||||
'profession' => $item['profession'],
|
||||
'introduction_preview' => $item['introduction_preview'] ?? null,
|
||||
'relevance' => $item['relevance'] ?? $item['similarity'] ?? $item['rrf_score'] ?? 0,
|
||||
];
|
||||
}
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// 同步方法
|
||||
// ==============================
|
||||
|
||||
/**
|
||||
* 同步单个用户到 SeekDB
|
||||
*
|
||||
* @param User $user 用户模型
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function sync(User $user): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 不处理机器人账号
|
||||
if ($user->bot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 不处理已禁用的账号
|
||||
if ($user->disable_at) {
|
||||
return self::delete($user->userid);
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建用于搜索的文本内容
|
||||
$searchableContent = self::buildSearchableContent($user);
|
||||
|
||||
// 获取 embedding(如果 AI 可用)
|
||||
$embedding = null;
|
||||
if (!empty($searchableContent) && Apps::isInstalled('ai')) {
|
||||
$embeddingResult = self::getEmbedding($searchableContent);
|
||||
if (!empty($embeddingResult)) {
|
||||
$embedding = '[' . implode(',', $embeddingResult) . ']';
|
||||
}
|
||||
}
|
||||
|
||||
// 写入 SeekDB
|
||||
$result = SeekDBBase::upsertUserVector([
|
||||
'userid' => $user->userid,
|
||||
'nickname' => $user->nickname ?? '',
|
||||
'email' => $user->email ?? '',
|
||||
'tel' => $user->tel ?? '',
|
||||
'profession' => $user->profession ?? '',
|
||||
'introduction' => $user->introduction ?? '',
|
||||
'content_vector' => $embedding,
|
||||
]);
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('SeekDB user sync error: ' . $e->getMessage(), [
|
||||
'userid' => $user->userid,
|
||||
'nickname' => $user->nickname,
|
||||
]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建可搜索的文本内容
|
||||
*
|
||||
* @param User $user 用户模型
|
||||
* @return string 可搜索的文本
|
||||
*/
|
||||
private static function buildSearchableContent(User $user): string
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
if (!empty($user->nickname)) {
|
||||
$parts[] = $user->nickname;
|
||||
}
|
||||
if (!empty($user->email)) {
|
||||
$parts[] = $user->email;
|
||||
}
|
||||
if (!empty($user->profession)) {
|
||||
$parts[] = $user->profession;
|
||||
}
|
||||
if (!empty($user->introduction)) {
|
||||
$parts[] = $user->introduction;
|
||||
}
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步用户
|
||||
*
|
||||
* @param iterable $users 用户列表
|
||||
* @return int 成功同步的数量
|
||||
*/
|
||||
public static function batchSync(iterable $users): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($users as $user) {
|
||||
if (self::sync($user)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户索引
|
||||
*
|
||||
* @param int $userid 用户ID
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function delete(int $userid): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SeekDBBase::deleteUserVector($userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有索引
|
||||
*
|
||||
* @return bool 是否成功
|
||||
*/
|
||||
public static function clear(): bool
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SeekDBBase::clearAllUserVectors();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已索引用户数量
|
||||
*
|
||||
* @return int 数量
|
||||
*/
|
||||
public static function getIndexedCount(): int
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return SeekDBBase::getIndexedUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\File;
|
||||
use App\Tasks\SeekDBFileSyncTask;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
|
||||
class FileObserver extends AbstractObserver
|
||||
{
|
||||
@ -19,7 +19,7 @@ class FileObserver extends AbstractObserver
|
||||
if ($file->type === 'folder') {
|
||||
return;
|
||||
}
|
||||
self::taskDeliver(new SeekDBFileSyncTask('sync', $file->toArray()));
|
||||
self::taskDeliver(new SeekDBSyncTask('file_sync', $file->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,7 +41,7 @@ class FileObserver extends AbstractObserver
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
if (!empty($childFileIds)) {
|
||||
self::taskDeliver(new SeekDBFileSyncTask('update_pshare', [
|
||||
self::taskDeliver(new SeekDBSyncTask('file_pshare_update', [
|
||||
'file_ids' => $childFileIds,
|
||||
'pshare' => $newPshare,
|
||||
]));
|
||||
@ -53,7 +53,7 @@ class FileObserver extends AbstractObserver
|
||||
if ($file->type === 'folder') {
|
||||
return;
|
||||
}
|
||||
self::taskDeliver(new SeekDBFileSyncTask('sync', $file->toArray()));
|
||||
self::taskDeliver(new SeekDBSyncTask('file_sync', $file->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,7 +64,7 @@ class FileObserver extends AbstractObserver
|
||||
*/
|
||||
public function deleted(File $file)
|
||||
{
|
||||
self::taskDeliver(new SeekDBFileSyncTask('delete', $file->toArray()));
|
||||
self::taskDeliver(new SeekDBSyncTask('file_delete', $file->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -79,7 +79,7 @@ class FileObserver extends AbstractObserver
|
||||
if ($file->type === 'folder') {
|
||||
return;
|
||||
}
|
||||
self::taskDeliver(new SeekDBFileSyncTask('sync', $file->toArray()));
|
||||
self::taskDeliver(new SeekDBSyncTask('file_sync', $file->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,7 +90,7 @@ class FileObserver extends AbstractObserver
|
||||
*/
|
||||
public function forceDeleted(File $file)
|
||||
{
|
||||
self::taskDeliver(new SeekDBFileSyncTask('delete', $file->toArray()));
|
||||
self::taskDeliver(new SeekDBSyncTask('file_delete', $file->toArray()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\FileUser;
|
||||
use App\Tasks\SeekDBFileSyncTask;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
|
||||
class FileUserObserver extends AbstractObserver
|
||||
{
|
||||
@ -15,7 +15,7 @@ class FileUserObserver extends AbstractObserver
|
||||
*/
|
||||
public function created(FileUser $fileUser)
|
||||
{
|
||||
self::taskDeliver(new SeekDBFileSyncTask('add_file_user', [
|
||||
self::taskDeliver(new SeekDBSyncTask('file_user_add', [
|
||||
'file_id' => $fileUser->file_id,
|
||||
'userid' => $fileUser->userid,
|
||||
'permission' => $fileUser->permission,
|
||||
@ -30,7 +30,7 @@ class FileUserObserver extends AbstractObserver
|
||||
*/
|
||||
public function updated(FileUser $fileUser)
|
||||
{
|
||||
self::taskDeliver(new SeekDBFileSyncTask('add_file_user', [
|
||||
self::taskDeliver(new SeekDBSyncTask('file_user_add', [
|
||||
'file_id' => $fileUser->file_id,
|
||||
'userid' => $fileUser->userid,
|
||||
'permission' => $fileUser->permission,
|
||||
@ -45,7 +45,7 @@ class FileUserObserver extends AbstractObserver
|
||||
*/
|
||||
public function deleted(FileUser $fileUser)
|
||||
{
|
||||
self::taskDeliver(new SeekDBFileSyncTask('remove_file_user', [
|
||||
self::taskDeliver(new SeekDBSyncTask('file_user_remove', [
|
||||
'file_id' => $fileUser->file_id,
|
||||
'userid' => $fileUser->userid,
|
||||
]));
|
||||
|
||||
@ -5,8 +5,9 @@ namespace App\Observers;
|
||||
use App\Models\Deleted;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectUser;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
|
||||
class ProjectObserver
|
||||
class ProjectObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the Project "created" event.
|
||||
@ -16,7 +17,7 @@ class ProjectObserver
|
||||
*/
|
||||
public function created(Project $project)
|
||||
{
|
||||
//
|
||||
self::taskDeliver(new SeekDBSyncTask('project_sync', $project->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,6 +36,24 @@ class ProjectObserver
|
||||
Deleted::forget('project', $project->id, $userids);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有搜索相关字段变化
|
||||
$searchableFields = ['name', 'desc', 'archived_at'];
|
||||
$isDirty = false;
|
||||
foreach ($searchableFields as $field) {
|
||||
if ($project->isDirty($field)) {
|
||||
$isDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isDirty) {
|
||||
if ($project->archived_at) {
|
||||
self::taskDeliver(new SeekDBSyncTask('project_delete', ['project_id' => $project->id]));
|
||||
} else {
|
||||
self::taskDeliver(new SeekDBSyncTask('project_sync', $project->toArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,6 +65,7 @@ class ProjectObserver
|
||||
public function deleted(Project $project)
|
||||
{
|
||||
Deleted::record('project', $project->id, $this->userids($project));
|
||||
self::taskDeliver(new SeekDBSyncTask('project_delete', ['project_id' => $project->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -57,6 +77,7 @@ class ProjectObserver
|
||||
public function restored(Project $project)
|
||||
{
|
||||
Deleted::forget('project', $project->id, $this->userids($project));
|
||||
self::taskDeliver(new SeekDBSyncTask('project_sync', $project->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,7 +88,7 @@ class ProjectObserver
|
||||
*/
|
||||
public function forceDeleted(Project $project)
|
||||
{
|
||||
//
|
||||
self::taskDeliver(new SeekDBSyncTask('project_delete', ['project_id' => $project->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -7,8 +7,9 @@ use App\Models\ProjectTask;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\ProjectTaskVisibilityUser;
|
||||
use App\Models\ProjectUser;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
|
||||
class ProjectTaskObserver
|
||||
class ProjectTaskObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the ProjectTask "created" event.
|
||||
@ -18,7 +19,7 @@ class ProjectTaskObserver
|
||||
*/
|
||||
public function created(ProjectTask $projectTask)
|
||||
{
|
||||
//
|
||||
self::taskDeliver(new SeekDBSyncTask('task_sync', $projectTask->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,6 +32,11 @@ class ProjectTaskObserver
|
||||
{
|
||||
if ($projectTask->isDirty('visibility')) {
|
||||
self::visibilityUpdate($projectTask);
|
||||
// 同步 visibility 变化到 SeekDB
|
||||
self::taskDeliver(new SeekDBSyncTask('task_visibility_update', [
|
||||
'task_id' => $projectTask->id,
|
||||
'visibility' => $projectTask->visibility,
|
||||
]));
|
||||
}
|
||||
if ($projectTask->isDirty('archived_at')) {
|
||||
if ($projectTask->archived_at) {
|
||||
@ -39,6 +45,25 @@ class ProjectTaskObserver
|
||||
Deleted::forget('projectTask', $projectTask->id, self::userids($projectTask));
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有搜索相关字段变化
|
||||
// project_id 变化时也需要同步(任务移动到其他项目)
|
||||
$searchableFields = ['name', 'desc', 'archived_at', 'project_id'];
|
||||
$isDirty = false;
|
||||
foreach ($searchableFields as $field) {
|
||||
if ($projectTask->isDirty($field)) {
|
||||
$isDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isDirty) {
|
||||
if ($projectTask->archived_at) {
|
||||
self::taskDeliver(new SeekDBSyncTask('task_delete', ['task_id' => $projectTask->id]));
|
||||
} else {
|
||||
self::taskDeliver(new SeekDBSyncTask('task_sync', $projectTask->toArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,6 +75,7 @@ class ProjectTaskObserver
|
||||
public function deleted(ProjectTask $projectTask)
|
||||
{
|
||||
Deleted::record('projectTask', $projectTask->id, self::userids($projectTask));
|
||||
self::taskDeliver(new SeekDBSyncTask('task_delete', ['task_id' => $projectTask->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,6 +87,7 @@ class ProjectTaskObserver
|
||||
public function restored(ProjectTask $projectTask)
|
||||
{
|
||||
Deleted::forget('projectTask', $projectTask->id, self::userids($projectTask));
|
||||
self::taskDeliver(new SeekDBSyncTask('task_sync', $projectTask->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,7 +98,7 @@ class ProjectTaskObserver
|
||||
*/
|
||||
public function forceDeleted(ProjectTask $projectTask)
|
||||
{
|
||||
//
|
||||
self::taskDeliver(new SeekDBSyncTask('task_delete', ['task_id' => $projectTask->id]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -5,8 +5,9 @@ namespace App\Observers;
|
||||
use App\Models\Deleted;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\ProjectUser;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
|
||||
class ProjectTaskUserObserver
|
||||
class ProjectTaskUserObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the ProjectTaskUser "created" event.
|
||||
@ -20,6 +21,19 @@ class ProjectTaskUserObserver
|
||||
if ($projectTaskUser->task_pid) {
|
||||
Deleted::forget('projectTask', $projectTaskUser->task_pid, $projectTaskUser->userid);
|
||||
}
|
||||
|
||||
// 同步任务成员到 SeekDB
|
||||
self::taskDeliver(new SeekDBSyncTask('task_user_add', [
|
||||
'task_id' => $projectTaskUser->task_id,
|
||||
'userid' => $projectTaskUser->userid,
|
||||
]));
|
||||
// 如果是子任务,同时添加到父任务
|
||||
if ($projectTaskUser->task_pid) {
|
||||
self::taskDeliver(new SeekDBSyncTask('task_user_add', [
|
||||
'task_id' => $projectTaskUser->task_pid,
|
||||
'userid' => $projectTaskUser->userid,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,6 +58,12 @@ class ProjectTaskUserObserver
|
||||
if (!ProjectUser::whereProjectId($projectTaskUser->project_id)->whereUserid($projectTaskUser->userid)->exists()) {
|
||||
Deleted::record('projectTask', $projectTaskUser->task_id, $projectTaskUser->userid);
|
||||
}
|
||||
|
||||
// 从 SeekDB 删除任务成员关系
|
||||
self::taskDeliver(new SeekDBSyncTask('task_user_remove', [
|
||||
'task_id' => $projectTaskUser->task_id,
|
||||
'userid' => $projectTaskUser->userid,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
84
app/Observers/ProjectTaskVisibilityUserObserver.php
Normal file
84
app/Observers/ProjectTaskVisibilityUserObserver.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\ProjectTaskVisibilityUser;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
|
||||
/**
|
||||
* ProjectTaskVisibilityUser 观察者
|
||||
*
|
||||
* 用于处理任务 visibility=3(指定成员可见)时的成员变更同步
|
||||
*/
|
||||
class ProjectTaskVisibilityUserObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the ProjectTaskVisibilityUser "created" event.
|
||||
*
|
||||
* @param \App\Models\ProjectTaskVisibilityUser $visibilityUser
|
||||
* @return void
|
||||
*/
|
||||
public function created(ProjectTaskVisibilityUser $visibilityUser)
|
||||
{
|
||||
// 将指定成员添加到 SeekDB 的 task_users 表
|
||||
self::taskDeliver(new SeekDBSyncTask('task_user_add', [
|
||||
'task_id' => $visibilityUser->task_id,
|
||||
'userid' => $visibilityUser->userid,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ProjectTaskVisibilityUser "updated" event.
|
||||
*
|
||||
* @param \App\Models\ProjectTaskVisibilityUser $visibilityUser
|
||||
* @return void
|
||||
*/
|
||||
public function updated(ProjectTaskVisibilityUser $visibilityUser)
|
||||
{
|
||||
// 通常不会更新,但如果更新了也同步
|
||||
self::taskDeliver(new SeekDBSyncTask('task_user_add', [
|
||||
'task_id' => $visibilityUser->task_id,
|
||||
'userid' => $visibilityUser->userid,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ProjectTaskVisibilityUser "deleted" event.
|
||||
*
|
||||
* @param \App\Models\ProjectTaskVisibilityUser $visibilityUser
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(ProjectTaskVisibilityUser $visibilityUser)
|
||||
{
|
||||
// 从 SeekDB 的 task_users 表删除该成员
|
||||
// 注意:需要检查该用户是否仍是任务的负责人/协作人
|
||||
// 如果是,则不应该删除(因为 ProjectTaskUser 仍存在)
|
||||
self::taskDeliver(new SeekDBSyncTask('task_visibility_user_remove', [
|
||||
'task_id' => $visibilityUser->task_id,
|
||||
'userid' => $visibilityUser->userid,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ProjectTaskVisibilityUser "restored" event.
|
||||
*
|
||||
* @param \App\Models\ProjectTaskVisibilityUser $visibilityUser
|
||||
* @return void
|
||||
*/
|
||||
public function restored(ProjectTaskVisibilityUser $visibilityUser)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ProjectTaskVisibilityUser "force deleted" event.
|
||||
*
|
||||
* @param \App\Models\ProjectTaskVisibilityUser $visibilityUser
|
||||
* @return void
|
||||
*/
|
||||
public function forceDeleted(ProjectTaskVisibilityUser $visibilityUser)
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,9 @@ namespace App\Observers;
|
||||
|
||||
use App\Models\Deleted;
|
||||
use App\Models\ProjectUser;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
|
||||
class ProjectUserObserver
|
||||
class ProjectUserObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the ProjectUser "created" event.
|
||||
@ -16,6 +17,10 @@ class ProjectUserObserver
|
||||
public function created(ProjectUser $projectUser)
|
||||
{
|
||||
Deleted::forget('project', $projectUser->project_id, $projectUser->userid);
|
||||
self::taskDeliver(new SeekDBSyncTask('project_user_add', [
|
||||
'project_id' => $projectUser->project_id,
|
||||
'userid' => $projectUser->userid,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,6 +43,10 @@ class ProjectUserObserver
|
||||
public function deleted(ProjectUser $projectUser)
|
||||
{
|
||||
Deleted::record('project', $projectUser->project_id, $projectUser->userid);
|
||||
self::taskDeliver(new SeekDBSyncTask('project_user_remove', [
|
||||
'project_id' => $projectUser->project_id,
|
||||
'userid' => $projectUser->userid,
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
69
app/Observers/UserObserver.php
Normal file
69
app/Observers/UserObserver.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Tasks\SeekDBSyncTask;
|
||||
|
||||
class UserObserver extends AbstractObserver
|
||||
{
|
||||
/**
|
||||
* Handle the User "created" event.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return void
|
||||
*/
|
||||
public function created(User $user)
|
||||
{
|
||||
// 机器人账号不同步
|
||||
if ($user->bot) {
|
||||
return;
|
||||
}
|
||||
self::taskDeliver(new SeekDBSyncTask('user_sync', $user->toArray()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the User "updated" event.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return void
|
||||
*/
|
||||
public function updated(User $user)
|
||||
{
|
||||
// 机器人账号不同步
|
||||
if ($user->bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有搜索相关字段变化
|
||||
$searchableFields = ['nickname', 'email', 'tel', 'profession', 'introduction', 'disable_at'];
|
||||
$isDirty = false;
|
||||
foreach ($searchableFields as $field) {
|
||||
if ($user->isDirty($field)) {
|
||||
$isDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isDirty) {
|
||||
// 如果用户被禁用,删除索引;否则更新索引
|
||||
if ($user->disable_at) {
|
||||
self::taskDeliver(new SeekDBSyncTask('user_delete', ['userid' => $user->userid]));
|
||||
} else {
|
||||
self::taskDeliver(new SeekDBSyncTask('user_sync', $user->toArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the User "deleted" event.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(User $user)
|
||||
{
|
||||
self::taskDeliver(new SeekDBSyncTask('user_delete', ['userid' => $user->userid]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,9 @@ use App\Models\FileUser;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\ProjectTaskVisibilityUser;
|
||||
use App\Models\ProjectUser;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
@ -16,7 +18,9 @@ use App\Observers\FileUserObserver;
|
||||
use App\Observers\ProjectObserver;
|
||||
use App\Observers\ProjectTaskObserver;
|
||||
use App\Observers\ProjectTaskUserObserver;
|
||||
use App\Observers\ProjectTaskVisibilityUserObserver;
|
||||
use App\Observers\ProjectUserObserver;
|
||||
use App\Observers\UserObserver;
|
||||
use App\Observers\WebSocketDialogMsgObserver;
|
||||
use App\Observers\WebSocketDialogObserver;
|
||||
use App\Observers\WebSocketDialogUserObserver;
|
||||
@ -49,7 +53,9 @@ class EventServiceProvider extends ServiceProvider
|
||||
Project::observe(ProjectObserver::class);
|
||||
ProjectTask::observe(ProjectTaskObserver::class);
|
||||
ProjectTaskUser::observe(ProjectTaskUserObserver::class);
|
||||
ProjectTaskVisibilityUser::observe(ProjectTaskVisibilityUserObserver::class);
|
||||
ProjectUser::observe(ProjectUserObserver::class);
|
||||
User::observe(UserObserver::class);
|
||||
WebSocketDialog::observe(WebSocketDialogObserver::class);
|
||||
WebSocketDialogMsg::observe(WebSocketDialogMsgObserver::class);
|
||||
WebSocketDialogUser::observe(WebSocketDialogUserObserver::class);
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\File;
|
||||
use App\Module\Apps;
|
||||
use App\Module\SeekDB\SeekDBFile;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* 同步文件数据到 SeekDB
|
||||
*/
|
||||
class SeekDBFileSyncTask extends AbstractTask
|
||||
{
|
||||
private $action;
|
||||
|
||||
private $data;
|
||||
|
||||
public function __construct($action = null, $data = null)
|
||||
{
|
||||
parent::__construct(...func_get_args());
|
||||
$this->action = $action;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
// 如果没有安装 SeekDB 模块,则不执行
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->action) {
|
||||
case 'sync':
|
||||
// 同步文件数据
|
||||
$file = File::find($this->data['id'] ?? 0);
|
||||
if ($file) {
|
||||
SeekDBFile::sync($file);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
// 删除文件索引
|
||||
$fileId = $this->data['id'] ?? 0;
|
||||
if ($fileId > 0) {
|
||||
SeekDBFile::delete($fileId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'sync_file_user':
|
||||
// 同步文件用户关系
|
||||
$fileId = $this->data['file_id'] ?? 0;
|
||||
if ($fileId > 0) {
|
||||
SeekDBFile::syncFileUsers($fileId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'add_file_user':
|
||||
// 添加文件用户关系
|
||||
$fileId = $this->data['file_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? 0;
|
||||
$permission = $this->data['permission'] ?? 0;
|
||||
if ($fileId > 0) {
|
||||
SeekDBFile::addFileUser($fileId, $userid, $permission);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'remove_file_user':
|
||||
// 删除文件用户关系
|
||||
$fileId = $this->data['file_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? null;
|
||||
if ($fileId > 0) {
|
||||
SeekDBFile::removeFileUser($fileId, $userid);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'update_pshare':
|
||||
// 批量更新文件的 pshare(共享设置变化时调用)
|
||||
$fileIds = $this->data['file_ids'] ?? [];
|
||||
$pshare = $this->data['pshare'] ?? 0;
|
||||
if (!empty($fileIds)) {
|
||||
\App\Module\SeekDB\SeekDBBase::batchUpdatePshare($fileIds, $pshare);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// 增量更新
|
||||
$this->incrementalUpdate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量更新
|
||||
* @return void
|
||||
*/
|
||||
private function incrementalUpdate()
|
||||
{
|
||||
// 120分钟执行一次
|
||||
$time = intval(Cache::get("SeekDBFileSyncTask:Time"));
|
||||
if (time() - $time < 120 * 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行开始,120分钟后缓存标记失效
|
||||
Cache::put("SeekDBFileSyncTask:Time", time(), Carbon::now()->addMinutes(120));
|
||||
|
||||
// 开始执行同步
|
||||
@shell_exec("php /var/www/artisan seekdb:sync-files --i");
|
||||
|
||||
// 执行完成,5分钟后缓存标记失效(5分钟任务可重复执行)
|
||||
Cache::put("SeekDBFileSyncTask:Time", time(), Carbon::now()->addMinutes(5));
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
242
app/Tasks/SeekDBSyncTask.php
Normal file
242
app/Tasks/SeekDBSyncTask.php
Normal file
@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tasks;
|
||||
|
||||
use App\Models\File;
|
||||
use App\Models\User;
|
||||
use App\Models\Project;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Module\Apps;
|
||||
use App\Module\SeekDB\SeekDBBase;
|
||||
use App\Module\SeekDB\SeekDBFile;
|
||||
use App\Module\SeekDB\SeekDBUser;
|
||||
use App\Module\SeekDB\SeekDBProject;
|
||||
use App\Module\SeekDB\SeekDBTask;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/**
|
||||
* 通用 SeekDB 同步任务
|
||||
*
|
||||
* 支持文件、用户、项目、任务的同步操作
|
||||
*/
|
||||
class SeekDBSyncTask extends AbstractTask
|
||||
{
|
||||
private $action;
|
||||
|
||||
private $data;
|
||||
|
||||
public function __construct($action = null, $data = null)
|
||||
{
|
||||
parent::__construct(...func_get_args());
|
||||
$this->action = $action;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function start()
|
||||
{
|
||||
if (!Apps::isInstalled("seekdb")) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($this->action) {
|
||||
// ==============================
|
||||
// 文件同步动作
|
||||
// ==============================
|
||||
case 'file_sync':
|
||||
$file = File::find($this->data['id'] ?? 0);
|
||||
if ($file) {
|
||||
SeekDBFile::sync($file);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file_delete':
|
||||
$fileId = $this->data['id'] ?? 0;
|
||||
if ($fileId > 0) {
|
||||
SeekDBFile::delete($fileId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file_user_sync':
|
||||
$fileId = $this->data['file_id'] ?? 0;
|
||||
if ($fileId > 0) {
|
||||
SeekDBFile::syncFileUsers($fileId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file_user_add':
|
||||
$fileId = $this->data['file_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? 0;
|
||||
$permission = $this->data['permission'] ?? 0;
|
||||
if ($fileId > 0) {
|
||||
SeekDBFile::addFileUser($fileId, $userid, $permission);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file_user_remove':
|
||||
$fileId = $this->data['file_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? null;
|
||||
if ($fileId > 0) {
|
||||
SeekDBFile::removeFileUser($fileId, $userid);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file_pshare_update':
|
||||
$fileIds = $this->data['file_ids'] ?? [];
|
||||
$pshare = $this->data['pshare'] ?? 0;
|
||||
if (!empty($fileIds)) {
|
||||
SeekDBBase::batchUpdatePshare($fileIds, $pshare);
|
||||
}
|
||||
break;
|
||||
|
||||
// ==============================
|
||||
// 用户同步动作
|
||||
// ==============================
|
||||
case 'user_sync':
|
||||
$user = User::find($this->data['userid'] ?? 0);
|
||||
if ($user) {
|
||||
SeekDBUser::sync($user);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user_delete':
|
||||
$userid = $this->data['userid'] ?? 0;
|
||||
if ($userid > 0) {
|
||||
SeekDBUser::delete($userid);
|
||||
}
|
||||
break;
|
||||
|
||||
// ==============================
|
||||
// 项目同步动作
|
||||
// ==============================
|
||||
case 'project_sync':
|
||||
$project = Project::find($this->data['id'] ?? 0);
|
||||
if ($project) {
|
||||
SeekDBProject::sync($project);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'project_delete':
|
||||
$projectId = $this->data['project_id'] ?? 0;
|
||||
if ($projectId > 0) {
|
||||
SeekDBProject::delete($projectId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'project_user_add':
|
||||
$projectId = $this->data['project_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? 0;
|
||||
if ($projectId > 0 && $userid > 0) {
|
||||
SeekDBProject::addProjectUser($projectId, $userid);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'project_user_remove':
|
||||
$projectId = $this->data['project_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? 0;
|
||||
if ($projectId > 0 && $userid > 0) {
|
||||
SeekDBProject::removeProjectUser($projectId, $userid);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'project_users_sync':
|
||||
$projectId = $this->data['project_id'] ?? 0;
|
||||
if ($projectId > 0) {
|
||||
SeekDBProject::syncProjectUsers($projectId);
|
||||
}
|
||||
break;
|
||||
|
||||
// ==============================
|
||||
// 任务同步动作
|
||||
// ==============================
|
||||
case 'task_sync':
|
||||
$task = ProjectTask::find($this->data['id'] ?? 0);
|
||||
if ($task) {
|
||||
SeekDBTask::sync($task);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_delete':
|
||||
$taskId = $this->data['task_id'] ?? 0;
|
||||
if ($taskId > 0) {
|
||||
SeekDBTask::delete($taskId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_visibility_update':
|
||||
$taskId = $this->data['task_id'] ?? 0;
|
||||
$visibility = $this->data['visibility'] ?? 1;
|
||||
if ($taskId > 0) {
|
||||
SeekDBTask::updateVisibility($taskId, $visibility);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_user_add':
|
||||
$taskId = $this->data['task_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? 0;
|
||||
if ($taskId > 0 && $userid > 0) {
|
||||
SeekDBTask::addTaskUser($taskId, $userid);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_user_remove':
|
||||
$taskId = $this->data['task_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? 0;
|
||||
if ($taskId > 0 && $userid > 0) {
|
||||
SeekDBTask::removeTaskUser($taskId, $userid);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_visibility_user_remove':
|
||||
// 特殊处理:删除 visibility user 时需要检查是否仍是任务成员
|
||||
$taskId = $this->data['task_id'] ?? 0;
|
||||
$userid = $this->data['userid'] ?? 0;
|
||||
if ($taskId > 0 && $userid > 0) {
|
||||
SeekDBTask::removeVisibilityUser($taskId, $userid);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'task_users_sync':
|
||||
$taskId = $this->data['task_id'] ?? 0;
|
||||
if ($taskId > 0) {
|
||||
SeekDBTask::syncTaskUsers($taskId);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// 增量更新(定时任务调用)
|
||||
$this->incrementalUpdate();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增量更新(定时执行)
|
||||
* @return void
|
||||
*/
|
||||
private function incrementalUpdate()
|
||||
{
|
||||
// 60分钟执行一次
|
||||
$time = intval(Cache::get("SeekDBSyncTask:Time"));
|
||||
if (time() - $time < 60 * 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 执行开始
|
||||
Cache::put("SeekDBSyncTask:Time", time(), Carbon::now()->addMinutes(60));
|
||||
|
||||
// 执行同步命令(后台运行)
|
||||
@shell_exec("php /var/www/artisan seekdb:sync-files --i 2>&1 &");
|
||||
@shell_exec("php /var/www/artisan seekdb:sync-users --i 2>&1 &");
|
||||
@shell_exec("php /var/www/artisan seekdb:sync-projects --i 2>&1 &");
|
||||
@shell_exec("php /var/www/artisan seekdb:sync-tasks --i 2>&1 &");
|
||||
|
||||
// 执行完成
|
||||
Cache::put("SeekDBSyncTask:Time", time(), Carbon::now()->addMinutes(5));
|
||||
}
|
||||
|
||||
public function end()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,7 +347,16 @@ export default {
|
||||
|
||||
searchTask(key) {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
// 如果开启了 AI 搜索,使用新的 AI 搜索接口
|
||||
const useAiSearch = this.aiSearchAvailable && this.aiSearch;
|
||||
const requestConfig = useAiSearch ? {
|
||||
url: 'search/task',
|
||||
data: {
|
||||
key,
|
||||
search_type: 'hybrid',
|
||||
take: this.action ? 50 : 10,
|
||||
},
|
||||
} : {
|
||||
url: 'project/task/lists',
|
||||
data: {
|
||||
keys: {name: key},
|
||||
@ -355,10 +364,19 @@ export default {
|
||||
scope: 'all_project',
|
||||
pagesize: this.action ? 50 : 10,
|
||||
},
|
||||
}).then(({data}) => {
|
||||
};
|
||||
this.$store.dispatch("call", requestConfig).then(({data}) => {
|
||||
const nowTime = $A.dayjs().unix()
|
||||
const items = data.data.map(item => {
|
||||
const rawData = useAiSearch ? data : data.data;
|
||||
const items = rawData.map(item => {
|
||||
const tags = [];
|
||||
// AI 搜索标记
|
||||
if (useAiSearch && item.content_preview) {
|
||||
tags.push({
|
||||
name: 'AI',
|
||||
style: 'background-color:#4F46E5',
|
||||
})
|
||||
}
|
||||
if (item.complete_at) {
|
||||
tags.push({
|
||||
name: this.$L('已完成'),
|
||||
@ -389,7 +407,7 @@ export default {
|
||||
|
||||
id: item.id,
|
||||
title: item.name,
|
||||
desc: item.desc,
|
||||
desc: item.content_preview ? this.truncateContent(item.content_preview) : item.desc,
|
||||
activity: item.end_at,
|
||||
|
||||
rawData: item,
|
||||
@ -403,7 +421,16 @@ export default {
|
||||
|
||||
searchProject(key) {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
// 如果开启了 AI 搜索,使用新的 AI 搜索接口
|
||||
const useAiSearch = this.aiSearchAvailable && this.aiSearch;
|
||||
const requestConfig = useAiSearch ? {
|
||||
url: 'search/project',
|
||||
data: {
|
||||
key,
|
||||
search_type: 'hybrid',
|
||||
take: this.action ? 50 : 10,
|
||||
},
|
||||
} : {
|
||||
url: 'project/lists',
|
||||
data: {
|
||||
keys: {
|
||||
@ -412,9 +439,18 @@ export default {
|
||||
archived: 'all',
|
||||
pagesize: this.action ? 50 : 10,
|
||||
},
|
||||
}).then(({data}) => {
|
||||
const items = data.data.map(item => {
|
||||
};
|
||||
this.$store.dispatch("call", requestConfig).then(({data}) => {
|
||||
const rawData = useAiSearch ? data : data.data;
|
||||
const items = rawData.map(item => {
|
||||
const tags = [];
|
||||
// AI 搜索标记
|
||||
if (useAiSearch && item.desc_preview) {
|
||||
tags.push({
|
||||
name: 'AI',
|
||||
style: 'background-color:#4F46E5',
|
||||
})
|
||||
}
|
||||
if (item.owner) {
|
||||
tags.push({
|
||||
name: this.$L('负责人'),
|
||||
@ -435,7 +471,7 @@ export default {
|
||||
|
||||
id: item.id,
|
||||
title: item.name,
|
||||
desc: item.desc || '',
|
||||
desc: item.desc_preview ? this.truncateContent(item.desc_preview) : (item.desc || ''),
|
||||
activity: item.updated_at,
|
||||
|
||||
rawData: item,
|
||||
@ -499,23 +535,43 @@ export default {
|
||||
|
||||
searchContact(key) {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
// 如果开启了 AI 搜索,使用新的 AI 搜索接口
|
||||
const useAiSearch = this.aiSearchAvailable && this.aiSearch;
|
||||
const requestConfig = useAiSearch ? {
|
||||
url: 'search/contact',
|
||||
data: {
|
||||
key,
|
||||
search_type: 'hybrid',
|
||||
take: this.action ? 50 : 10,
|
||||
},
|
||||
} : {
|
||||
url: 'users/search',
|
||||
data: {
|
||||
keys: {key},
|
||||
pagesize: this.action ? 50 : 10,
|
||||
},
|
||||
}).then(({data}) => {
|
||||
};
|
||||
this.$store.dispatch("call", requestConfig).then(({data}) => {
|
||||
const items = data.map(item => {
|
||||
const tags = [];
|
||||
// AI 搜索标记
|
||||
if (useAiSearch && item.introduction_preview) {
|
||||
tags.push({
|
||||
name: 'AI',
|
||||
style: 'background-color:#4F46E5',
|
||||
})
|
||||
}
|
||||
return {
|
||||
key,
|
||||
type: 'contact',
|
||||
icons: ['user', item.userid],
|
||||
tags: [],
|
||||
tags,
|
||||
|
||||
id: item.userid,
|
||||
title: item.nickname,
|
||||
desc: item.profession || '',
|
||||
desc: item.introduction_preview
|
||||
? this.truncateContent(item.introduction_preview)
|
||||
: (item.profession || ''),
|
||||
activity: item.line_at,
|
||||
|
||||
rawData: item,
|
||||
@ -529,21 +585,23 @@ export default {
|
||||
|
||||
searchFile(key) {
|
||||
this.loadIng++;
|
||||
const requestData = {
|
||||
key,
|
||||
take: this.action ? 50 : 10,
|
||||
};
|
||||
|
||||
// 如果开启了 AI 搜索(需要同时安装 seekdb 和 ai)
|
||||
if (this.aiSearchAvailable && this.aiSearch) {
|
||||
requestData.search_content = 'yes';
|
||||
requestData.search_type = 'hybrid';
|
||||
}
|
||||
|
||||
this.$store.dispatch("call", {
|
||||
// 如果开启了 AI 搜索,使用统一的 AI 搜索接口
|
||||
const useAiSearch = this.aiSearchAvailable && this.aiSearch;
|
||||
const requestConfig = useAiSearch ? {
|
||||
url: 'search/file',
|
||||
data: {
|
||||
key,
|
||||
search_type: 'hybrid',
|
||||
take: this.action ? 50 : 10,
|
||||
},
|
||||
} : {
|
||||
url: 'file/search',
|
||||
data: requestData,
|
||||
}).then(({data}) => {
|
||||
data: {
|
||||
key,
|
||||
take: this.action ? 50 : 10,
|
||||
},
|
||||
};
|
||||
this.$store.dispatch("call", requestConfig).then(({data}) => {
|
||||
const items = data.map(item => {
|
||||
const tags = [];
|
||||
if (item.share) {
|
||||
@ -552,8 +610,8 @@ export default {
|
||||
style: 'background-color:#0bc037',
|
||||
})
|
||||
}
|
||||
// 如果有内容预览,显示 AI 搜索标记
|
||||
if (item.content_preview) {
|
||||
// AI 搜索标记
|
||||
if (useAiSearch && item.content_preview) {
|
||||
tags.push({
|
||||
name: 'AI',
|
||||
style: 'background-color:#4F46E5',
|
||||
@ -592,9 +650,16 @@ export default {
|
||||
this.aiSearch = !this.aiSearch;
|
||||
// 如果有搜索内容,重新搜索
|
||||
if (this.searchKey.trim()) {
|
||||
this.searchResults = this.searchResults.filter(item => item.type !== 'file');
|
||||
if (this.action === 'file' || !this.action) {
|
||||
this.searchFile(this.searchKey);
|
||||
// 清除所有支持 AI 搜索的类型的结果
|
||||
const aiSearchTypes = ['file', 'task', 'project', 'contact'];
|
||||
this.searchResults = this.searchResults.filter(item => !aiSearchTypes.includes(item.type));
|
||||
// 重新搜索
|
||||
if (this.action) {
|
||||
if (aiSearchTypes.includes(this.action)) {
|
||||
this.distSearch(this.action);
|
||||
}
|
||||
} else {
|
||||
aiSearchTypes.forEach(type => this.distSearch(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ use App\Http\Controllers\Api\ApproveController;
|
||||
use App\Http\Controllers\Api\AssistantController;
|
||||
use App\Http\Controllers\Api\ProjectController;
|
||||
use App\Http\Controllers\Api\ComplaintController;
|
||||
use App\Http\Controllers\Api\SearchController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@ -60,6 +61,9 @@ Route::prefix('api')->middleware(['webapi'])->group(function () {
|
||||
// 投诉
|
||||
Route::any('complaint/{method}', ComplaintController::class);
|
||||
Route::any('complaint/{method}/{action}', ComplaintController::class);
|
||||
// 智能搜索
|
||||
Route::any('search/{method}', SearchController::class);
|
||||
Route::any('search/{method}/{action}', SearchController::class);
|
||||
// 测试
|
||||
Route::any('test/{method}', TestController::class);
|
||||
Route::any('test/{method}/{action}', TestController::class);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user