mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-26 20:48:12 +00:00
feat(ai): 优化 AI 提示词并完善建议交互功能
- 优化后端提示词:描述生成、子任务拆分、负责人推荐,新增栏目信息,去掉无效的 similar_count
- 优化前端提示词:去掉硬性字数限制,即时消息改为简短输出
- 新增 :::ai-action{...}::: 语法处理,支持单独采纳/忽略 assignee 和 similar
- 采纳/忽略后更新消息状态显示
- 负责人改为追加模式,保留现有负责人
- 新增任务关联功能,similar 采纳时自动创建双向关联
- 相似度阈值从 0.7 调整为 0.5,搜索结果增加到 200
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
parent
c253044f61
commit
29be29b9cf
@ -3838,6 +3838,8 @@ class ProjectController extends AbstractController
|
||||
* @apiParam {Number} task_id 任务ID
|
||||
* @apiParam {Number} msg_id 消息ID
|
||||
* @apiParam {String} type 建议类型:description/subtasks/assignee/similar
|
||||
* @apiParam {Number} [userid] 用户ID(assignee类型时用于指定采纳哪个推荐)
|
||||
* @apiParam {Number} [related] 关联任务ID(similar类型时用于指定采纳哪个相似任务)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@ -3853,6 +3855,8 @@ class ProjectController extends AbstractController
|
||||
$taskId = intval(Request::input('task_id'));
|
||||
$msgId = intval(Request::input('msg_id'));
|
||||
$type = trim(Request::input('type'));
|
||||
$userid = intval(Request::input('userid'));
|
||||
$related = intval(Request::input('related'));
|
||||
|
||||
// 验证建议类型
|
||||
if (!in_array($type, ProjectTaskAiEvent::getEventTypes())) {
|
||||
@ -3871,8 +3875,8 @@ class ProjectController extends AbstractController
|
||||
->where('msg_id', $msgId)
|
||||
->first();
|
||||
|
||||
if (!$event || $event->status !== ProjectTaskAiEvent::STATUS_COMPLETED) {
|
||||
return Base::retError('建议不存在或已处理');
|
||||
if (!$event) {
|
||||
return Base::retError('建议不存在');
|
||||
}
|
||||
|
||||
$result = $event->result;
|
||||
@ -3883,19 +3887,36 @@ class ProjectController extends AbstractController
|
||||
// 标记事件为已采纳
|
||||
$event->markApplied();
|
||||
|
||||
// 记录日志
|
||||
$task->addLog('AI建议:采纳' . $type . '建议');
|
||||
|
||||
// 更新消息状态
|
||||
if ($msgId > 0 && $task->dialog_id) {
|
||||
AiTaskSuggestion::updateMessageStatus($msgId, $task->dialog_id, $type, 'applied');
|
||||
// similar 类型:创建任务关联
|
||||
if ($type === 'similar' && $related > 0) {
|
||||
ProjectTaskRelation::createRelation(
|
||||
$taskId,
|
||||
$related,
|
||||
$task->dialog_id,
|
||||
$msgId,
|
||||
User::userid()
|
||||
);
|
||||
}
|
||||
|
||||
// 返回建议数据,由前端调用相应接口处理
|
||||
// 记录日志
|
||||
if ($type === 'assignee' && $userid > 0) {
|
||||
$user = User::find($userid);
|
||||
$task->addLog('AI建议:指派给 ' . ($user ? $user->nickname : $userid));
|
||||
} elseif ($type === 'similar' && $related > 0) {
|
||||
$task->addLog('AI建议:关联任务 #' . $related);
|
||||
} else {
|
||||
$task->addLog('AI建议:采纳' . $type . '建议');
|
||||
}
|
||||
|
||||
// 更新消息状态
|
||||
$msgResult = AiTaskSuggestion::updateMessageStatus($msgId, $task->dialog_id, $type, 'applied', $userid, $related);
|
||||
|
||||
// 返回建议数据和消息内容
|
||||
return Base::retSuccess('已采纳', [
|
||||
'type' => $type,
|
||||
'task_id' => $taskId,
|
||||
'result' => $result,
|
||||
'msg' => $msgResult['data'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -3909,6 +3930,8 @@ class ProjectController extends AbstractController
|
||||
* @apiParam {Number} task_id 任务ID
|
||||
* @apiParam {Number} msg_id 消息ID
|
||||
* @apiParam {String} type 建议类型
|
||||
* @apiParam {Number} [userid] 用户ID(assignee类型时用于忽略单个推荐)
|
||||
* @apiParam {Number} [related] 关联任务ID(similar类型时用于忽略单个推荐)
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@ -3921,6 +3944,8 @@ class ProjectController extends AbstractController
|
||||
$taskId = intval(Request::input('task_id'));
|
||||
$msgId = intval(Request::input('msg_id'));
|
||||
$type = trim(Request::input('type'));
|
||||
$userid = intval(Request::input('userid'));
|
||||
$related = intval(Request::input('related'));
|
||||
|
||||
// 验证建议类型
|
||||
if (!in_array($type, ProjectTaskAiEvent::getEventTypes())) {
|
||||
@ -3939,18 +3964,19 @@ class ProjectController extends AbstractController
|
||||
->where('msg_id', $msgId)
|
||||
->first();
|
||||
|
||||
if (!$event || $event->status !== ProjectTaskAiEvent::STATUS_COMPLETED) {
|
||||
return Base::retError('建议不存在或已处理');
|
||||
if (!$event) {
|
||||
return Base::retError('建议不存在');
|
||||
}
|
||||
|
||||
// 标记事件为已忽略
|
||||
$event->markDismissed();
|
||||
|
||||
// 更新消息状态
|
||||
if ($msgId > 0 && $task->dialog_id) {
|
||||
AiTaskSuggestion::updateMessageStatus($msgId, $task->dialog_id, $type, 'dismissed');
|
||||
}
|
||||
$msgResult = AiTaskSuggestion::updateMessageStatus($msgId, $task->dialog_id, $type, 'dismissed', $userid, $related);
|
||||
|
||||
return Base::retSuccess('已忽略');
|
||||
// 返回消息内容
|
||||
return Base::retSuccess('已忽略', [
|
||||
'msg' => $msgResult['data'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,86 @@ class ProjectTaskRelation extends AbstractModel
|
||||
return $this->belongsTo(ProjectTask::class, 'related_task_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建双向任务关联
|
||||
*
|
||||
* @param int $sourceTaskId 源任务ID
|
||||
* @param int $targetTaskId 目标任务ID
|
||||
* @param int|null $dialogId 来源对话ID
|
||||
* @param int|null $msgId 来源消息ID
|
||||
* @param int|null $userid 操作人
|
||||
* @param bool $push 是否推送更新
|
||||
* @return bool 是否创建成功
|
||||
*/
|
||||
public static function createRelation(
|
||||
int $sourceTaskId,
|
||||
int $targetTaskId,
|
||||
?int $dialogId = null,
|
||||
?int $msgId = null,
|
||||
?int $userid = null,
|
||||
bool $push = true
|
||||
): bool {
|
||||
if ($sourceTaskId === $targetTaskId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sourceTask = ProjectTask::with('project')->find($sourceTaskId);
|
||||
$targetTask = ProjectTask::with('project')->find($targetTaskId);
|
||||
|
||||
if (!$sourceTask || !$targetTask) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($sourceTask->deleted_at || $targetTask->deleted_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建正向关联:源任务提及目标任务
|
||||
$mentionRelation = static::updateOrCreate(
|
||||
[
|
||||
'task_id' => $sourceTaskId,
|
||||
'related_task_id' => $targetTaskId,
|
||||
'direction' => self::DIRECTION_MENTION,
|
||||
],
|
||||
[
|
||||
'dialog_id' => $dialogId,
|
||||
'msg_id' => $msgId,
|
||||
'userid' => $userid,
|
||||
]
|
||||
);
|
||||
|
||||
// 创建反向关联:目标任务被源任务提及
|
||||
$reverseRelation = static::updateOrCreate(
|
||||
[
|
||||
'task_id' => $targetTaskId,
|
||||
'related_task_id' => $sourceTaskId,
|
||||
'direction' => self::DIRECTION_MENTIONED_BY,
|
||||
],
|
||||
[
|
||||
'dialog_id' => $dialogId,
|
||||
'msg_id' => $msgId,
|
||||
'userid' => $userid,
|
||||
]
|
||||
);
|
||||
|
||||
// 推送关联更新
|
||||
if ($push) {
|
||||
$needPush = $mentionRelation->wasRecentlyCreated || $mentionRelation->wasChanged()
|
||||
|| $reverseRelation->wasRecentlyCreated || $reverseRelation->wasChanged();
|
||||
|
||||
if ($needPush) {
|
||||
if ($sourceTask->project) {
|
||||
$sourceTask->pushMsg('relation', null, null, false);
|
||||
}
|
||||
if ($targetTask->project) {
|
||||
$targetTask->pushMsg('relation', null, null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function recordMentionsFromMessage(WebSocketDialogMsg $msg): void
|
||||
{
|
||||
if ($msg->type !== 'text') {
|
||||
@ -84,71 +164,25 @@ class ProjectTaskRelation extends AbstractModel
|
||||
return;
|
||||
}
|
||||
|
||||
$sourceTasks = ProjectTask::with('project')->whereDialogId($msg->dialog_id)->get();
|
||||
if ($sourceTasks->isEmpty()) {
|
||||
$sourceTaskIds = ProjectTask::whereDialogId($msg->dialog_id)
|
||||
->whereNull('deleted_at')
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
if (empty($sourceTaskIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$targetTasks = ProjectTask::with('project')->whereIn('id', $targetIds)->get()->keyBy('id');
|
||||
if ($targetTasks->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pushTasks = [];
|
||||
foreach ($sourceTasks as $sourceTask) {
|
||||
foreach ($sourceTaskIds as $sourceTaskId) {
|
||||
foreach ($targetIds as $targetId) {
|
||||
if ($targetId === $sourceTask->id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetTask = $targetTasks->get($targetId);
|
||||
if (!$targetTask) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mentionRelation = static::updateOrCreate(
|
||||
[
|
||||
'task_id' => $sourceTask->id,
|
||||
'related_task_id' => $targetTask->id,
|
||||
'direction' => self::DIRECTION_MENTION,
|
||||
],
|
||||
[
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg->id,
|
||||
'userid' => $msg->userid,
|
||||
]
|
||||
self::createRelation(
|
||||
$sourceTaskId,
|
||||
$targetId,
|
||||
$msg->dialog_id,
|
||||
$msg->id,
|
||||
$msg->userid
|
||||
);
|
||||
|
||||
if ($mentionRelation->wasRecentlyCreated || $mentionRelation->wasChanged()) {
|
||||
$pushTasks[$sourceTask->id] = $sourceTask;
|
||||
}
|
||||
|
||||
$reverseRelation = static::updateOrCreate(
|
||||
[
|
||||
'task_id' => $targetTask->id,
|
||||
'related_task_id' => $sourceTask->id,
|
||||
'direction' => self::DIRECTION_MENTIONED_BY,
|
||||
],
|
||||
[
|
||||
'dialog_id' => $msg->dialog_id,
|
||||
'msg_id' => $msg->id,
|
||||
'userid' => $msg->userid,
|
||||
]
|
||||
);
|
||||
|
||||
if ($reverseRelation->wasRecentlyCreated || $reverseRelation->wasChanged()) {
|
||||
$pushTasks[$targetTask->id] = $targetTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pushTasks as $task) {
|
||||
$task->loadMissing('project');
|
||||
if (!$task->project) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$task->pushMsg('relation', null, null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ use App\Models\ProjectTaskAiEvent;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\ProjectUser;
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Manticore\ManticoreBase;
|
||||
@ -43,9 +44,7 @@ class AiTaskSuggestion
|
||||
|
||||
case ProjectTaskAiEvent::EVENT_ASSIGNEE:
|
||||
// 未指定负责人
|
||||
$hasOwner = ProjectTaskUser::where('task_id', $task->id)
|
||||
->where('owner', 1)
|
||||
->exists();
|
||||
$hasOwner = ProjectTaskUser::where('task_id', $task->id)->where('owner', 1)->exists();
|
||||
return !$hasOwner;
|
||||
|
||||
case ProjectTaskAiEvent::EVENT_SIMILAR:
|
||||
@ -145,7 +144,10 @@ class AiTaskSuggestion
|
||||
public static function findSimilarTasks(ProjectTask $task): ?array
|
||||
{
|
||||
// 使用 AI 模块的 Embedding 搜索
|
||||
$searchText = $task->name . ' ' . ($task->content ?? '');
|
||||
$searchText = $task->name;
|
||||
if (empty($searchText)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = AI::getEmbedding($searchText);
|
||||
@ -179,12 +181,12 @@ class AiTaskSuggestion
|
||||
/**
|
||||
* 转义用户输入以防止 Prompt 注入
|
||||
*/
|
||||
private static function escapeUserInput(string $input): string
|
||||
private static function escapeUserInput(string $input, int $length = 500): string
|
||||
{
|
||||
// 移除可能影响 AI Prompt 解析的特殊字符
|
||||
$input = str_replace(['```', '---', '==='], '', $input);
|
||||
// 截断过长的输入
|
||||
return mb_substr(trim($input), 0, 500);
|
||||
return mb_substr(trim($input), 0, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -192,32 +194,30 @@ class AiTaskSuggestion
|
||||
*/
|
||||
private static function buildDescriptionPrompt(ProjectTask $task): string
|
||||
{
|
||||
$taskName = self::escapeUserInput($task->name);
|
||||
$projectName = self::escapeUserInput($task->project->name ?? '未知项目');
|
||||
$taskName = self::escapeUserInput($task->name, 100);
|
||||
$projectName = self::escapeUserInput($task->project->name ?? '未知项目', 100);
|
||||
$columnName = self::escapeUserInput($task->projectColumn->name ?? '未知栏目', 50);
|
||||
|
||||
return <<<PROMPT
|
||||
你是一个专业的项目管理助手。请根据以下任务标题,生成结构化的任务描述。
|
||||
你是一名任务规划助手,擅长根据任务标题推断并补充任务描述。
|
||||
|
||||
任务标题:{$taskName}
|
||||
所属项目:{$projectName}
|
||||
所属项目:{$projectName}
|
||||
所属栏目:{$columnName}
|
||||
任务标题:{$taskName}
|
||||
|
||||
请按以下格式生成任务描述(使用 Markdown):
|
||||
你的任务:
|
||||
根据标题、项目和栏目信息,推断任务意图并生成实用的任务描述。
|
||||
|
||||
**背景**:[描述任务的背景和上下文]
|
||||
生成原则:
|
||||
1. 基于标题关键词和上下文进行合理推断,内容要具体、可执行
|
||||
2. 使用 Markdown 格式,根据任务性质灵活组织结构(可包含目标、要求、验收标准等)
|
||||
3. 简单任务保持简洁,复杂任务可适当展开,避免空泛的套话
|
||||
4. 与标题语言保持一致
|
||||
|
||||
**目标**:[明确任务要达成的目标]
|
||||
|
||||
**验收标准**:
|
||||
- [验收标准1]
|
||||
- [验收标准2]
|
||||
- [验收标准3]
|
||||
|
||||
要求:
|
||||
1. 内容要专业、简洁
|
||||
2. 验收标准要具体、可衡量
|
||||
3. 与用户输入语言保持一致
|
||||
4. 只返回 Markdown 内容,不要返回其他文字
|
||||
PROMPT;
|
||||
输出要求:
|
||||
- 仅返回 Markdown 格式的描述内容
|
||||
- 禁止输出额外说明、引导语或与任务无关的内容
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -225,26 +225,37 @@ PROMPT;
|
||||
*/
|
||||
private static function buildSubtasksPrompt(ProjectTask $task): string
|
||||
{
|
||||
$taskName = self::escapeUserInput($task->name);
|
||||
$taskName = self::escapeUserInput($task->name, 100);
|
||||
$projectName = self::escapeUserInput($task->project->name ?? '未知项目', 100);
|
||||
$columnName = self::escapeUserInput($task->projectColumn->name ?? '未知栏目', 50);
|
||||
$content = self::escapeUserInput($task->content ?? '');
|
||||
|
||||
return <<<PROMPT
|
||||
你是一个专业的项目管理助手。请将以下任务拆分为可执行的子任务。
|
||||
你是一名任务拆解助手,擅长将复杂任务分解为可执行的子任务。
|
||||
|
||||
任务标题:{$taskName}
|
||||
任务描述:{$content}
|
||||
所属项目:{$projectName}
|
||||
所属栏目:{$columnName}
|
||||
任务标题:{$taskName}
|
||||
任务描述:{$content}
|
||||
|
||||
请返回 3-5 个子任务,每行一个,格式如下:
|
||||
1. [子任务名称]
|
||||
2. [子任务名称]
|
||||
...
|
||||
你的任务:
|
||||
分析任务内容,拆解出关键的执行步骤作为子任务。
|
||||
|
||||
要求:
|
||||
1. 每个子任务要具体、可执行
|
||||
2. 子任务之间有合理的顺序
|
||||
3. 子任务名称简洁明了(不超过30字)
|
||||
4. 只返回子任务列表,不要其他内容
|
||||
PROMPT;
|
||||
拆解原则:
|
||||
1. 每个子任务聚焦单一可执行动作,避免含糊或重复
|
||||
2. 根据任务复杂度灵活决定数量(通常 2-5 个),简单任务少拆,复杂任务多拆
|
||||
3. 子任务之间保持合理的执行顺序或逻辑关系
|
||||
4. 子任务名称简洁明了,控制在 8-30 个字符内
|
||||
5. 与任务标题语言保持一致
|
||||
|
||||
输出格式:
|
||||
1. [子任务名称]
|
||||
2. [子任务名称]
|
||||
...
|
||||
|
||||
输出要求:
|
||||
- 仅返回子任务列表,禁止输出额外说明或引导语
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -252,45 +263,49 @@ PROMPT;
|
||||
*/
|
||||
private static function buildAssigneePrompt(ProjectTask $task, array $members): string
|
||||
{
|
||||
$taskName = self::escapeUserInput($task->name, 100);
|
||||
$projectName = self::escapeUserInput($task->project->name ?? '未知项目', 100);
|
||||
$columnName = self::escapeUserInput($task->projectColumn->name ?? '未知栏目', 50);
|
||||
$taskContent = self::escapeUserInput($task->content ?? '');
|
||||
|
||||
$membersText = '';
|
||||
foreach ($members as $member) {
|
||||
$nickname = self::escapeUserInput($member['nickname']);
|
||||
$nickname = self::escapeUserInput($member['nickname'], 20);
|
||||
$membersText .= "- {$nickname}(ID:{$member['userid']})";
|
||||
if (!empty($member['profession'])) {
|
||||
$profession = self::escapeUserInput($member['profession']);
|
||||
$profession = self::escapeUserInput($member['profession'], 50);
|
||||
$membersText .= ",职位:{$profession}";
|
||||
}
|
||||
$membersText .= ",进行中任务:{$member['in_progress_count']}个";
|
||||
$membersText .= ",进行中:{$member['in_progress_count']}个";
|
||||
$membersText .= ",近期完成:{$member['completed_count']}个";
|
||||
if ($member['similar_count'] > 0) {
|
||||
$membersText .= ",处理过类似任务:{$member['similar_count']}个";
|
||||
}
|
||||
$membersText .= "\n";
|
||||
}
|
||||
|
||||
$taskName = self::escapeUserInput($task->name);
|
||||
$taskContent = self::escapeUserInput($task->content ?? '');
|
||||
|
||||
return <<<PROMPT
|
||||
你是一个专业的项目管理助手。请根据任务内容和团队成员情况,推荐最合适的负责人。
|
||||
你是一名任务分配助手,根据任务内容和成员情况推荐合适的负责人。
|
||||
|
||||
任务标题:{$taskName}
|
||||
任务描述:{$taskContent}
|
||||
所属项目:{$projectName}
|
||||
所属栏目:{$columnName}
|
||||
任务标题:{$taskName}
|
||||
任务描述:{$taskContent}
|
||||
|
||||
团队成员:
|
||||
{$membersText}
|
||||
可选成员:
|
||||
{$membersText}
|
||||
|
||||
请推荐 2 名最合适的负责人,按优先级排序,格式如下:
|
||||
1. [userid]|[推荐理由,简短说明]
|
||||
2. [userid]|[推荐理由,简短说明]
|
||||
推荐原则:
|
||||
1. 分析任务内容,匹配成员职位或专业方向
|
||||
2. 优先推荐进行中任务较少的成员,平衡工作负载
|
||||
3. 近期完成任务多说明执行力强,可作为参考
|
||||
|
||||
推荐依据:
|
||||
1. 优先选择处理过类似任务的成员
|
||||
2. 考虑当前工作负载(进行中任务较少的优先)
|
||||
3. 考虑专业匹配度
|
||||
输出格式:
|
||||
1. [userid]|[推荐理由]
|
||||
2. [userid]|[推荐理由]
|
||||
|
||||
只返回推荐列表,不要其他内容。
|
||||
PROMPT;
|
||||
输出要求:
|
||||
- 推荐 1-2 名最合适的负责人,按优先级排序
|
||||
- 推荐理由需具体说明为何此人适合该任务,不超过 20 字
|
||||
- 仅返回推荐列表,禁止输出额外说明
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -439,7 +454,7 @@ PROMPT;
|
||||
try {
|
||||
// 使用 ManticoreBase 进行向量搜索
|
||||
// userid=0 跳过权限过滤,我们通过 project_id 过滤
|
||||
$results = ManticoreBase::taskVectorSearch($embedding, 0, 50);
|
||||
$results = ManticoreBase::taskVectorSearch($embedding, 0, 200);
|
||||
|
||||
if (empty($results)) {
|
||||
return [];
|
||||
@ -469,9 +484,9 @@ PROMPT;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 相似度阈值(0.7 以上才算相似)
|
||||
// 相似度阈值(0.5 以上才算相似)
|
||||
$similarity = $item['similarity'] ?? 0;
|
||||
if ($similarity < 0.7) {
|
||||
if ($similarity < 0.5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -499,7 +514,7 @@ PROMPT;
|
||||
*/
|
||||
public static function buildMarkdownMessage(int $taskId, array $suggestions, int $msgId = 0): string
|
||||
{
|
||||
$parts = ["## AI 任务建议\n"];
|
||||
$parts = [];
|
||||
|
||||
foreach ($suggestions as $suggestion) {
|
||||
switch ($suggestion['type']) {
|
||||
@ -526,15 +541,12 @@ PROMPT;
|
||||
*/
|
||||
private static function buildDescriptionMarkdown(int $taskId, int $msgId, string $content): string
|
||||
{
|
||||
$applyUrl = "dootask://ai-apply/description/{$taskId}/{$msgId}";
|
||||
$dismissUrl = "dootask://ai-dismiss/description/{$taskId}/{$msgId}";
|
||||
|
||||
return <<<MD
|
||||
### 建议补充任务描述
|
||||
|
||||
{$content}
|
||||
|
||||
[✅ 采纳描述]({$applyUrl}) [❌ 忽略]({$dismissUrl})
|
||||
:::ai-action{type="description" task="{$taskId}" msg="{$msgId}"}:::
|
||||
MD;
|
||||
}
|
||||
|
||||
@ -549,14 +561,11 @@ MD;
|
||||
$list .= "{$num}. {$name}\n";
|
||||
}
|
||||
|
||||
$applyUrl = "dootask://ai-apply/subtasks/{$taskId}/{$msgId}";
|
||||
$dismissUrl = "dootask://ai-dismiss/subtasks/{$taskId}/{$msgId}";
|
||||
|
||||
return <<<MD
|
||||
### 建议拆分子任务
|
||||
|
||||
{$list}
|
||||
[✅ 创建子任务]({$applyUrl}) [❌ 忽略]({$dismissUrl})
|
||||
:::ai-action{type="subtasks" task="{$taskId}" msg="{$msgId}"}:::
|
||||
MD;
|
||||
}
|
||||
|
||||
@ -566,18 +575,16 @@ MD;
|
||||
private static function buildAssigneeMarkdown(int $taskId, int $msgId, array $recommendations): string
|
||||
{
|
||||
$list = '';
|
||||
$buttons = '';
|
||||
foreach ($recommendations as $rec) {
|
||||
$list .= "- **{$rec['nickname']}** - {$rec['reason']}\n";
|
||||
$applyUrl = "dootask://ai-apply/assignee/{$taskId}/{$msgId}?userid={$rec['userid']}";
|
||||
$buttons .= "[指派给{$rec['nickname']}]({$applyUrl}) ";
|
||||
$stUserId = $rec['userid'];
|
||||
$viewUrl = "dootask://contact/{$stUserId}";
|
||||
$list .= "- **[{$rec['nickname']}]({$viewUrl})** - {$rec['reason']} :::ai-action{type=\"assignee\" task=\"{$taskId}\" msg=\"{$msgId}\" userid=\"{$stUserId}\"}:::\n";
|
||||
}
|
||||
|
||||
return <<<MD
|
||||
### 推荐负责人
|
||||
|
||||
{$list}
|
||||
{$buttons}
|
||||
MD;
|
||||
}
|
||||
|
||||
@ -589,22 +596,17 @@ MD;
|
||||
$list = '';
|
||||
foreach ($similarTasks as $i => $st) {
|
||||
$num = $i + 1;
|
||||
$viewUrl = "dootask://task/{$st['id']}";
|
||||
$addUrl = "dootask://ai-apply/similar/{$taskId}/{$msgId}?related={$st['id']}";
|
||||
$similarity = round($st['similarity'] * 100);
|
||||
$list .= "{$num}. **#{$st['id']} {$st['name']}** - 相似度 {$similarity}%\n";
|
||||
$list .= " [查看任务]({$viewUrl}) [添加关联]({$addUrl})\n\n";
|
||||
$stTaskId = $st['task_id'];
|
||||
$viewUrl = "dootask://task/{$stTaskId}";
|
||||
$list .= "{$num}. **[#{$stTaskId}]({$viewUrl})** {$st['name']} :::ai-action{type=\"similar\" task=\"{$taskId}\" msg=\"{$msgId}\" related=\"{$stTaskId}\"}:::\n";
|
||||
}
|
||||
|
||||
$dismissUrl = "dootask://ai-dismiss/similar/{$taskId}/{$msgId}";
|
||||
|
||||
return <<<MD
|
||||
### 发现相似任务
|
||||
|
||||
以下任务与当前任务内容相似,可能是重复任务或可作为参考:
|
||||
|
||||
{$list}
|
||||
[全部忽略]({$dismissUrl})
|
||||
MD;
|
||||
}
|
||||
|
||||
@ -613,13 +615,24 @@ MD;
|
||||
*/
|
||||
public static function sendSuggestionMessage(ProjectTask $task, array $suggestions): ?int
|
||||
{
|
||||
if (empty($suggestions) || empty($task->dialog_id)) {
|
||||
if (empty($suggestions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果任务没有对话,自动创建
|
||||
if (!$task->dialog_id) {
|
||||
$dialog = WebSocketDialog::createGroup($task->name, $task->relationUserids(), 'task');
|
||||
if ($dialog) {
|
||||
$task->dialog_id = $dialog->id;
|
||||
$task->save();
|
||||
$task->pushMsg('dialog');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 先发送消息获取 msg_id,然后更新消息内容带上 msg_id
|
||||
$tempMarkdown = self::buildMarkdownMessage($task->id, $suggestions, 0);
|
||||
|
||||
$result = WebSocketDialogMsg::sendMsg(
|
||||
null,
|
||||
$task->dialog_id,
|
||||
@ -630,72 +643,71 @@ MD;
|
||||
false, // push_retry
|
||||
true // push_silence
|
||||
);
|
||||
|
||||
if (Base::isSuccess($result)) {
|
||||
$msgId = $result['data']->id ?? 0;
|
||||
if ($msgId > 0) {
|
||||
// 更新消息,带上真实的 msg_id
|
||||
$finalMarkdown = self::buildMarkdownMessage($task->id, $suggestions, $msgId);
|
||||
WebSocketDialogMsg::sendMsg(
|
||||
'change-' . $msgId,
|
||||
$task->dialog_id,
|
||||
'text',
|
||||
['text' => $finalMarkdown, 'type' => 'md'],
|
||||
self::AI_ASSISTANT_USERID
|
||||
);
|
||||
return $msgId;
|
||||
}
|
||||
if (Base::isError($result)) {
|
||||
return null;
|
||||
}
|
||||
$msgId = $result['data']->id ?? 0;
|
||||
if (empty($msgId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
// 更新消息,带上真实的 msg_id
|
||||
$finalMarkdown = self::buildMarkdownMessage($task->id, $suggestions, $msgId);
|
||||
WebSocketDialogMsg::sendMsg(
|
||||
'change-' . $msgId,
|
||||
$task->dialog_id,
|
||||
'text',
|
||||
['text' => $finalMarkdown, 'type' => 'md'],
|
||||
self::AI_ASSISTANT_USERID,
|
||||
true, // push_self
|
||||
);
|
||||
return $msgId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息状态(采纳/忽略后)
|
||||
*
|
||||
* @param int $msgId 消息ID
|
||||
* @param int $dialogId 对话ID
|
||||
* @param string $type 建议类型
|
||||
* @param string $status 状态:applied/dismissed
|
||||
* @param int $userid 用户ID(assignee类型单独处理时使用)
|
||||
* @param int $related 关联任务ID(similar类型单独处理时使用)
|
||||
* @return array 更新后的消息数据
|
||||
*/
|
||||
public static function updateMessageStatus(int $msgId, int $dialogId, string $type, string $status): void
|
||||
public static function updateMessageStatus(int $msgId, int $dialogId, string $type, string $status, int $userid = 0, int $related = 0): array
|
||||
{
|
||||
// 验证消息存在且属于指定对话
|
||||
$msg = WebSocketDialogMsg::where('id', $msgId)
|
||||
->where('dialog_id', $dialogId)
|
||||
->first();
|
||||
if (!$msg) {
|
||||
return;
|
||||
return Base::retError('消息不存在');
|
||||
}
|
||||
|
||||
$content = $msg->msg['text'] ?? '';
|
||||
if (empty($content)) {
|
||||
return;
|
||||
return Base::retError('消息内容为空');
|
||||
}
|
||||
|
||||
// 根据状态替换对应的按钮
|
||||
$statusText = $status === 'applied' ? '✓ 已采纳' : '✗ 已忽略';
|
||||
|
||||
// 替换对应类型的按钮为状态文字
|
||||
$pattern = '/\[.*?\]\(dootask:\/\/ai-(apply|dismiss)\/' . preg_quote($type, '/') . '\/\d+\/\d+[^)]*\)\s*/';
|
||||
$newContent = preg_replace($pattern, '', $content);
|
||||
|
||||
// 在对应标题后添加状态
|
||||
$sectionTitles = [
|
||||
'description' => '### 建议补充任务描述',
|
||||
'subtasks' => '### 建议拆分子任务',
|
||||
'assignee' => '### 推荐负责人',
|
||||
'similar' => '### 发现相似任务',
|
||||
];
|
||||
|
||||
if (isset($sectionTitles[$type])) {
|
||||
$title = $sectionTitles[$type];
|
||||
$newContent = str_replace($title, $title . "\n\n**{$statusText}**", $newContent);
|
||||
// 根据类型和参数构建匹配模式,添加 status 属性
|
||||
if ($type === 'assignee' && $userid > 0) {
|
||||
$pattern = '/(:::ai-action\{type="assignee"[^}]*userid="' . $userid . '"[^}]*)\}:::/';
|
||||
} elseif ($type === 'similar' && $related > 0) {
|
||||
$pattern = '/(:::ai-action\{type="similar"[^}]*related="' . $related . '"[^}]*)\}:::/';
|
||||
} else {
|
||||
$pattern = '/(:::ai-action\{type="' . preg_quote($type, '/') . '"[^}]*)\}:::/';
|
||||
}
|
||||
|
||||
// 更新消息
|
||||
WebSocketDialogMsg::sendMsg(
|
||||
$newContent = preg_replace($pattern, '$1 status="' . $status . '"}:::', $content);
|
||||
|
||||
// 更新消息并返回结果
|
||||
return WebSocketDialogMsg::sendMsg(
|
||||
'change-' . $msgId,
|
||||
$dialogId,
|
||||
'text',
|
||||
['text' => $newContent, 'type' => 'md'],
|
||||
self::AI_ASSISTANT_USERID,
|
||||
true, // push_self
|
||||
self::AI_ASSISTANT_USERID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ namespace App\Tasks;
|
||||
use App\Models\ProjectTask;
|
||||
use App\Models\ProjectTaskAiEvent;
|
||||
use App\Module\AiTaskSuggestion;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* AI 任务分析异步任务
|
||||
@ -24,7 +23,7 @@ class AiTaskAnalyzeTask extends AbstractTask
|
||||
public function start()
|
||||
{
|
||||
$task = ProjectTask::with('project')->find($this->taskId);
|
||||
if (!$task || $task->deleted_at || !$task->dialog_id) {
|
||||
if (!$task || $task->deleted_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -66,7 +65,9 @@ class AiTaskAnalyzeTask extends AbstractTask
|
||||
|
||||
try {
|
||||
// 检查是否满足执行条件
|
||||
if (!AiTaskSuggestion::shouldExecute($task, $eventType)) {
|
||||
$shouldExecute = AiTaskSuggestion::shouldExecute($task, $eventType);
|
||||
|
||||
if (!$shouldExecute) {
|
||||
$event->markSkipped('不满足执行条件');
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -37,11 +37,6 @@ class AiTaskLoopTask extends AbstractTask
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查功能开关
|
||||
if (Base::settingFind('aiAssistant', 'taskSuggestion') !== 'open') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询待处理的任务
|
||||
$tasks = $this->findPendingTasks();
|
||||
|
||||
@ -72,7 +67,6 @@ class AiTaskLoopTask extends AbstractTask
|
||||
->whereNull('archived_at')
|
||||
->where('created_at', '<=', $delayTime) // 创建超过延迟时间
|
||||
->where('created_at', '>=', Carbon::now()->subDays(1)) // 只处理1天内的
|
||||
->whereNotNull('dialog_id') // 有对话ID
|
||||
->whereNotIn('id', $processedTaskIds)
|
||||
->orderBy('created_at', 'asc')
|
||||
->take(self::BATCH_SIZE)
|
||||
|
||||
@ -172,12 +172,29 @@ export default {
|
||||
const [, type, taskId, msgId, queryString] = match;
|
||||
const params = new URLSearchParams(queryString || '');
|
||||
|
||||
// 先调用接口标记为已采纳,获取建议数据
|
||||
this.$store.dispatch('applyAiSuggestion', {
|
||||
// 构建请求数据
|
||||
const requestData = {
|
||||
task_id: parseInt(taskId, 10),
|
||||
msg_id: parseInt(msgId, 10),
|
||||
type,
|
||||
}).then(({data}) => {
|
||||
};
|
||||
|
||||
// assignee 类型传递 userid
|
||||
if (type === 'assignee' && params.get('userid')) {
|
||||
requestData.userid = parseInt(params.get('userid'), 10);
|
||||
}
|
||||
|
||||
// similar 类型传递 related
|
||||
if (type === 'similar' && params.get('related')) {
|
||||
requestData.related = parseInt(params.get('related'), 10);
|
||||
}
|
||||
|
||||
// 调用接口标记为已采纳
|
||||
this.$store.dispatch('applyAiSuggestion', requestData).then(({data}) => {
|
||||
// 更新本地消息
|
||||
if (data.msg) {
|
||||
this.$store.dispatch('saveDialogMsg', data.msg);
|
||||
}
|
||||
// 根据类型调用对应的业务接口
|
||||
this.applyAiSuggestionByType(data.type, data.task_id, data.result, params);
|
||||
}).catch(({msg}) => {
|
||||
@ -208,15 +225,21 @@ export default {
|
||||
break;
|
||||
|
||||
case 'assignee':
|
||||
// 指派负责人
|
||||
// 增加负责人(保留现有负责人)
|
||||
const userid = params.get('userid');
|
||||
if (!userid || isNaN(parseInt(userid, 10))) {
|
||||
$A.modalError(this.$L('请选择负责人'));
|
||||
return;
|
||||
}
|
||||
const newUserId = parseInt(userid, 10);
|
||||
// 从缓存获取任务当前负责人
|
||||
const task = this.$store.state.cacheTasks.find(t => t.id === taskId);
|
||||
const currentOwners = task?.task_user?.filter(u => u.owner === 1).map(u => u.userid) || [];
|
||||
// 追加新负责人(避免重复)
|
||||
const owners = [...new Set([...currentOwners, newUserId])];
|
||||
this.$store.dispatch('taskUpdate', {
|
||||
task_id: taskId,
|
||||
owner: [parseInt(userid, 10)],
|
||||
owner: owners,
|
||||
}).then(() => {
|
||||
$A.messageSuccess(this.$L('应用成功'));
|
||||
}).catch(({msg}) => {
|
||||
@ -225,7 +248,7 @@ export default {
|
||||
break;
|
||||
|
||||
case 'similar':
|
||||
// 相似任务关联(当前功能未启用)
|
||||
// 相似任务关联(后端已处理)
|
||||
$A.messageSuccess(this.$L('应用成功'));
|
||||
break;
|
||||
|
||||
@ -274,20 +297,37 @@ export default {
|
||||
|
||||
/**
|
||||
* 处理 AI 建议忽略
|
||||
* 格式: dootask://ai-dismiss/{type}/{task_id}/{msg_id}
|
||||
* 格式: dootask://ai-dismiss/{type}/{task_id}/{msg_id}?userid=xxx&related=xxx
|
||||
*/
|
||||
handleAiDismiss(href) {
|
||||
const match = href.match(/^dootask:\/\/ai-dismiss\/(\w+)\/(\d+)\/(\d+)$/);
|
||||
const match = href.match(/^dootask:\/\/ai-dismiss\/(\w+)\/(\d+)\/(\d+)(\?.*)?$/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const [, type, taskId, msgId] = match;
|
||||
const [, type, taskId, msgId, queryString] = match;
|
||||
const params = new URLSearchParams(queryString || '');
|
||||
|
||||
this.$store.dispatch('dismissAiSuggestion', {
|
||||
const data = {
|
||||
task_id: parseInt(taskId, 10),
|
||||
msg_id: parseInt(msgId, 10),
|
||||
type,
|
||||
}).then(() => {
|
||||
};
|
||||
|
||||
// assignee 类型传递 userid 用于单独忽略
|
||||
if (type === 'assignee' && params.get('userid')) {
|
||||
data.userid = parseInt(params.get('userid'), 10);
|
||||
}
|
||||
|
||||
// similar 类型传递 related 用于单独忽略
|
||||
if (type === 'similar' && params.get('related')) {
|
||||
data.related = parseInt(params.get('related'), 10);
|
||||
}
|
||||
|
||||
this.$store.dispatch('dismissAiSuggestion', data).then(({data: respData}) => {
|
||||
// 更新本地消息
|
||||
if (respData.msg) {
|
||||
this.$store.dispatch('saveDialogMsg', respData.msg);
|
||||
}
|
||||
$A.messageSuccess(this.$L('已忽略'));
|
||||
}).catch(({msg}) => {
|
||||
$A.modalError(msg);
|
||||
|
||||
19
resources/assets/js/utils/ai.js
vendored
19
resources/assets/js/utils/ai.js
vendored
@ -213,18 +213,17 @@ const AISystemConfig = {
|
||||
/**
|
||||
* 即时消息生成系统提示词
|
||||
*/
|
||||
const MESSAGE_AI_SYSTEM_PROMPT = `你是一名专业的沟通助手,协助用户编写得体、清晰且具行动指向的即时消息。
|
||||
const MESSAGE_AI_SYSTEM_PROMPT = `你是一名沟通助手,帮助用户编写即时消息。
|
||||
|
||||
写作要求:
|
||||
1. 根据用户提供的需求与上下文生成完整消息,语气需符合业务沟通场景,保持真诚、礼貌且高效
|
||||
2. 默认使用简洁的短段落,可使用 Markdown 基础格式(加粗、列表、引用)增强结构,但不要输出代码块或 JSON
|
||||
3. 如果上下文包含引用信息或草稿,请在消息中自然呼应相关要点
|
||||
4. 如无特别说明,将消息长度控制在 60-180 字;若需更短或更长,遵循用户描述
|
||||
5. 如需提出行动或问题,请明确表达,避免含糊
|
||||
1. 生成简短、得体的消息,语气符合业务沟通场景
|
||||
2. 可使用 Markdown 基础格式(加粗、列表),但不要输出代码块或 JSON
|
||||
3. 如有引用内容或草稿,自然呼应相关要点
|
||||
4. 默认生成简短消息(一到几句话),仅在用户明确要求时才写长
|
||||
|
||||
输出规范:
|
||||
- 仅返回可直接发送的消息内容
|
||||
- 禁止在内容前后添加额外说明、标签或引导语`;
|
||||
- 禁止添加额外说明或引导语`;
|
||||
|
||||
/**
|
||||
* 任务生成系统提示词
|
||||
@ -237,7 +236,7 @@ const TASK_AI_SYSTEM_PROMPT = `你是一个专业的任务管理专家,擅长
|
||||
3. 描述需覆盖任务背景、具体要求、交付标准、风险提示等关键信息
|
||||
4. 描述内容使用Markdown格式,合理组织标题、列表、加粗等结构
|
||||
5. 内容需适配项目管理系统,表述专业、逻辑清晰,并与用户输入语言保持一致
|
||||
6. 优先遵循用户在输入中给出的风格、长度或复杂度要求;默认情况下将详细描述控制在120-200字内,如用户要求简单或简短,则控制在80-120字内
|
||||
6. 根据任务复杂度灵活调整描述长度,简单任务保持简洁,复杂任务可适当展开
|
||||
7. 当任务具有多个执行步骤、阶段或协作角色时,请拆解出 2-6 个关键子任务;如无必要,可返回空数组
|
||||
8. 子任务应聚焦单一可执行动作,名称控制在8-30个字符内,避免重复和含糊表述
|
||||
|
||||
@ -322,12 +321,12 @@ const REPORT_ANALYSIS_SYSTEM_PROMPT = `你是一名经验丰富的团队管理
|
||||
2. 先给出整体概览,再列出具体亮点、风险或问题,以及明确的改进建议
|
||||
3. 如有数据或目标,应评估其完成情况和后续跟进要点
|
||||
4. 语气保持专业、客观、中立,不过度夸赞或批评
|
||||
5. 控制在 200-400 字之间,可视内容复杂度略微增减,但保持紧凑`;
|
||||
5. 根据汇报内容的复杂度调整分析篇幅,保持紧凑、避免冗余`;
|
||||
|
||||
/**
|
||||
* 智能搜索系统提示词
|
||||
*/
|
||||
const SEARCH_AI_SYSTEM_PROMPT = `你是一个智能搜索助手,负责帮助用户在 DooTask 系统中搜索和整理信息。
|
||||
const SEARCH_AI_SYSTEM_PROMPT = `你是一个智能搜索助手,帮助用户搜索和整理信息。
|
||||
你可以使用 intelligent_search 工具来搜索任务、项目、文件和联系人。
|
||||
|
||||
请根据用户的搜索需求:
|
||||
|
||||
60
resources/assets/js/utils/markdown.js
vendored
60
resources/assets/js/utils/markdown.js
vendored
@ -10,6 +10,65 @@ const MarkdownUtils = {
|
||||
mdi: null,
|
||||
mds: null,
|
||||
|
||||
/**
|
||||
* 处理 AI 建议操作按钮语法
|
||||
* 格式: :::ai-action{type="xxx" task="123" msg="456" userid="789" related="123" status="applied"}:::
|
||||
* @param {string} text
|
||||
* @returns {string}
|
||||
*/
|
||||
processAiAction: (text) => {
|
||||
// 匹配 :::ai-action{...}::: 语法
|
||||
return text.replace(/:::ai-action\{([^}]+)\}:::/g, (match, attrs) => {
|
||||
// 解析属性
|
||||
const params = {};
|
||||
attrs.replace(/(\w+)="([^"]+)"/g, (m, key, value) => {
|
||||
params[key] = value;
|
||||
});
|
||||
|
||||
const type = params.type || '';
|
||||
const status = params.status || '';
|
||||
|
||||
// 如果有 status,显示状态文字
|
||||
if (status) {
|
||||
const statusLabels = {
|
||||
description: { applied: '✓ 已采纳', dismissed: '✗ 已忽略' },
|
||||
subtasks: { applied: '✓ 已创建', dismissed: '✗ 已忽略' },
|
||||
assignee: { applied: '✓ 已指派', dismissed: '✗ 已忽略' },
|
||||
similar: { applied: '✓ 已关联', dismissed: '✗ 已忽略' },
|
||||
};
|
||||
const label = statusLabels[type]?.[status] || (status === 'applied' ? '✓ 已采纳' : '✗ 已忽略');
|
||||
const statusClass = status === 'applied' ? 'ai-status-applied' : 'ai-status-dismissed';
|
||||
return `<span class="ai-status ${statusClass}">${label}</span>`;
|
||||
}
|
||||
|
||||
const taskId = params.task || '';
|
||||
const msgId = params.msg || '';
|
||||
const userid = params.userid || '';
|
||||
const related = params.related || '';
|
||||
|
||||
// 根据类型生成按钮文案
|
||||
const buttonLabels = {
|
||||
description: ['采纳描述', '忽略'],
|
||||
subtasks: ['创建子任务', '忽略'],
|
||||
assignee: ['指派', '忽略'],
|
||||
similar: ['关联', '忽略'],
|
||||
};
|
||||
const [applyLabel, dismissLabel] = buttonLabels[type] || ['采纳', '忽略'];
|
||||
|
||||
// 构建 URL 查询参数
|
||||
let queryParams = [];
|
||||
if (userid) queryParams.push(`userid=${userid}`);
|
||||
if (related) queryParams.push(`related=${related}`);
|
||||
const queryString = queryParams.length > 0 ? '?' + queryParams.join('&') : '';
|
||||
|
||||
const applyUrl = `dootask://ai-apply/${type}/${taskId}/${msgId}${queryString}`;
|
||||
const dismissUrl = `dootask://ai-dismiss/${type}/${taskId}/${msgId}${queryString}`;
|
||||
|
||||
// 返回按钮 HTML
|
||||
return `<span class="ai-action-buttons"><a href="${applyUrl}" class="ai-btn ai-btn-apply">✓ ${applyLabel}</a> <a href="${dismissUrl}" class="ai-btn ai-btn-dismiss">✗ ${dismissLabel}</a></span>`;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 解析Markdown
|
||||
* @param {*} text
|
||||
@ -369,6 +428,7 @@ export function MarkdownConver(text) {
|
||||
}
|
||||
text = MarkdownPluginUtils.clearEmptyReasoning(text);
|
||||
text = mergeConsecutiveToolUse(text);
|
||||
text = MarkdownUtils.processAiAction(text);
|
||||
text = MarkdownUtils.mdi.render(text);
|
||||
return MarkdownUtils.formatMsg(text)
|
||||
}
|
||||
|
||||
@ -95,6 +95,33 @@ body {
|
||||
animation: blink-animate 1.2s infinite steps(1, start);
|
||||
}
|
||||
}
|
||||
|
||||
.ai-action-buttons {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
|
||||
.ai-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.ai-btn-apply {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.ai-btn-dismiss {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ai-status {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.self {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user