perf: 支持AI在项目群里创建任务

This commit is contained in:
kuaifan 2024-12-06 07:16:22 +08:00
parent 61ebbac333
commit 08153cd99b
4 changed files with 410 additions and 0 deletions

View File

@ -2885,6 +2885,58 @@ class DialogController extends AbstractController
return Base::retSuccess('success', $topMsg);
}
/**
* @api {get} api/dialog/msg/applied 55. 标记消息已应用
*
* @apiDescription 需要token身份
* @apiVersion 1.0.0
* @apiGroup dialog
* @apiName msg__applied
*
* @apiParam {String} type 类型
* - CreateTask: 创建任务
* @apiParam {Number} index 索引
* @apiParam {Number} msg_id 消息ID
*
* @apiSuccess {Number} ret 返回状态码1正确、0错误
* @apiSuccess {String} msg 返回信息(错误描述)
* @apiSuccess {Object} data 返回数据
*/
public function msg__applied()
{
User::auth();
//
$msg_id = intval(Request::input('msg_id'));
$type = trim(Request::input('type'));
$index = intval(Request::input('index'));
//
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
if (empty($msg)) {
return Base::retError("消息不存在或已被删除");
}
WebSocketDialog::checkDialog($msg->dialog_id);
//
$originalMsg = $msg->getRawOriginal('msg');
$pattern = '/```\s*' . preg_quote($type, '/') . '\s*(applying|applied)?\s*(\n|\\\\n)/';
$count = -1;
$updatedMsg = preg_replace_callback($pattern, function($matches) use (&$count, $index, $type) {
$count++;
if ($count === $index || ($index === 0 && $count === 1)) {
return "```{$type} applied{$matches[2]}";
}
return $matches[0];
}, $originalMsg);
if ($count === 0) {
return Base::retError("未找到可应用的规则");
}
$msg->msg = $updatedMsg;
$msg->save();
//
return Base::retSuccess("success");
}
/**
* @api {get} api/dialog/sticker/search 56. 搜索在线表情
*

View File

@ -3220,6 +3220,89 @@ export default {
});
},
async applyCreateTask(event, el) {
const currentTarget = event.target;
if (currentTarget.classList.contains('applying') || currentTarget.classList.contains('applied')) {
return;
}
currentTarget.classList.add('applying')
if (this.dialogData.group_type !== 'project') {
currentTarget.classList.remove('applying')
$A.modalError('只有在项目中才能创建任务')
return
}
let target = event.target;
while (target) {
if (target.classList.contains('apply-create-task')) {
break;
}
if (target.classList.contains('dialog-scroller')) {
target = null;
break;
}
target = target.parentElement;
}
if (!target) {
currentTarget.classList.remove('applying')
$A.modalError('未找到任务内容')
return
}
if (!this.dialogData.group_info) {
currentTarget.classList.remove('applying')
$A.modalError('项目不存在')
return;
}
const allTaskElements = el.querySelectorAll('.apply-create-task');
const taskIndex = Array.from(allTaskElements).indexOf(target);
const taskList = Array.from(target.querySelectorAll('li'))
.map(item => {
const title = item.querySelector('.title')?.innerText?.trim();
if (!title) return null;
const desc = item.querySelector('.desc')?.innerText?.trim() || '';
const content = desc ? desc.split('\n')
.filter(Boolean)
.map(line => `<p>${line.trim()}</p>`)
.join('') : '';
return {
project_id: this.dialogData.group_info.id,
name: title,
content
};
})
.filter(Boolean);
const results = await Promise.all(taskList.map(item =>
this.$store.dispatch("taskAdd", item).then(
success => ({ success: true, data: success }),
error => ({ success: false, error: error })
)
));
const successTasks = results.filter(r => r.success).map(r => r.data);
const failedTasks = results.filter(r => !r.success).map(r => r.error);
if (failedTasks.length > 0) {
$A.modalError(`成功创建 ${successTasks.length} 个任务,${failedTasks.length} 个任务创建失败`);
} else {
$A.messageSuccess(`成功创建 ${successTasks.length} 个任务`);
}
currentTarget.classList.remove('applying')
currentTarget.classList.add('applied')
await this.$store.dispatch("call", {
url: 'dialog/msg/applied',
data: {
type: 'CreateTask',
index: taskIndex,
msg_id: this.operateItem.id
},
});
},
openTranslationMenu(event) {
const list = Object.keys(languageList).map(item => ({
label: languageList[item],
@ -3325,6 +3408,13 @@ export default {
}
const {target, clientX} = event
//
if (target.classList.contains('apply-create-task-button')) {
this.operateItem = this.findMsgByElement(el)
this.applyCreateTask(event, el)
return;
}
//
if (target.classList.contains('translation-label')) {
this.operateItem = this.findMsgByElement(el)

View File

@ -9,6 +9,12 @@ import mdKatex from "@traptitech/markdown-it-katex";
const MarkdownUtils = {
mdi: null,
mds: null,
/**
* 解析Markdown
* @param {*} text
* @returns
*/
formatMsg: (text) => {
const array = text.match(/<img\s+[^>]*?>/g);
if (array) {
@ -18,11 +24,181 @@ const MarkdownUtils = {
}
return text
},
/**
* 高亮代码块
* @param {*} str
* @param {*} lang
* @returns
*/
highlightBlock: (str, lang = '') => {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${$A.L('复制')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
},
}
const MarkdownPluginUtils = {
// 配置选项
config: {
maxTitleLength: 200,
maxDescLength: 1000,
maxItems: 200,
defaultTitle: '创建任务'
},
// HTML转义函数
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
},
// 验证输入
validateInput(value, maxLength) {
if (!value) return '';
if (typeof value !== 'string') return '';
if (value.length > maxLength) {
return value.substring(0, maxLength) + '...';
}
return value;
},
// 解析任务项
parseTaskItems(content) {
const items = [];
let currentItem = {};
let itemCount = 0;
content.forEach(line => {
line = line.trim();
if (!line) {
if (Object.keys(currentItem).length > 0) {
items.push(currentItem);
currentItem = {};
itemCount++;
}
return;
}
if (itemCount >= this.config.maxItems) {
return;
}
const [key, ...valueParts] = line.split(':');
const value = valueParts.join(':').trim();
if (key === 'title' && value) {
if (Object.keys(currentItem).length > 0) {
items.push(currentItem);
currentItem = {};
itemCount++;
}
currentItem.title = this.validateInput(value, this.config.maxTitleLength);
} else if (key === 'desc' && value) {
currentItem.desc = this.validateInput(value, this.config.maxDescLength);
}
});
if (Object.keys(currentItem).length > 0 && itemCount < this.config.maxItems) {
items.push(currentItem);
}
return items;
},
// 生成HTML
generateTaskHtml(items, status = null) {
if (!Array.isArray(items) || items.length === 0) {
return '';
}
const html = [
'<div class="apply-create-task">',
'<ul>'
];
items.forEach((item, index) => {
if (item.title) {
html.push(`<li>`);
html.push(`<div class="task-index">${index + 1}.</div>`);
html.push(`<div class="task-item">`);
html.push(`<div class="title">${this.escapeHtml(item.title)}</div>`);
if (item.desc) {
html.push(`<div class="desc">${this.escapeHtml(item.desc)}</div>`);
}
html.push('</div>');
html.push('</li>');
}
});
html.push(
'</ul>',
`<div class="apply-button"><div class="apply-create-task-button ${status||''}">${this.escapeHtml($A.L(this.config.defaultTitle))}</div></div>`,
'</div>'
);
return html.join('\n');
},
// 修改初始化插件函数
initCreateTaskPlugin(md) {
md.block.ruler.before('fence', 'create_task', (state, startLine, endLine, silent) => {
try {
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
const match = state.src.slice(start, max).trim().match(/^```\s*CreateTask\s*(applying|applied)?$/);
if (!match) {
return false;
}
if (silent) {
return true;
}
let nextLine = startLine + 1;
let content = [];
let found = false;
while (nextLine < endLine) {
const line = state.src.slice(state.bMarks[nextLine], state.eMarks[nextLine]).trim();
if (line === '```') {
found = true;
break;
}
content.push(line);
nextLine++;
}
if (!found) {
return false;
}
// 创建 token 并设置为空字符串内容
const token = state.push('html_block', '', 0);
// 如果有内容则解析并生成HTML
if (content.length > 0) {
const items = this.parseTaskItems(content);
const html = this.generateTaskHtml(items, match[1]);
token.content = html || '';
} else {
token.content = ''; // 空内容直接返回空字符串
}
token.map = [startLine, nextLine + 1];
state.line = nextLine + 1;
return true;
} catch (error) {
console.error('Error in create_task parser:', error);
return false;
}
});
}
};
export function MarkdownConver(text) {
if (text === '...') {
return '<div class="input-blink"></div>'
@ -41,6 +217,7 @@ export function MarkdownConver(text) {
})
MarkdownUtils.mdi.use(mila, {attrs: {target: '_blank', rel: 'noopener'}})
MarkdownUtils.mdi.use(mdKatex, {blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000'})
MarkdownPluginUtils.initCreateTaskPlugin(MarkdownUtils.mdi);
}
return MarkdownUtils.formatMsg(MarkdownUtils.mdi.render(text))
}

View File

@ -2037,6 +2037,97 @@
visibility: hidden;
pointer-events: none;
}
.apply-create-task {
min-width: 160px;
margin-bottom: 16px;
ul {
max-height: 500px;
overflow: auto;
padding: 0;
margin: 0;
list-style-type: none;
li {
display: flex;
margin-bottom: 12px;
.task-index {
padding-right: 6px;
}
.task-item {
line-height: 18px;
.title,
.desc {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.title {
font-weight: bold;
}
.desc {
padding-top: 4px;
opacity: 0.6;
}
}
}
}
.apply-button {
display: inline-block;
user-select: none;
> div {
display: flex;
justify-content: center;
align-items: center;
height: 32px;
line-height: 32px;
padding: 0 12px;
font-size: 14px;
border-radius: 4px;
color: #515a6e;
background-color: #fff;
border-color: #dcdee2;
cursor: pointer;
&:before {
font-family: "taskfont", "serif" !important;
content: "\e6f2";
font-size: 14px;
width: 14px;
margin-right: 6px;
}
&.applying,
&.applied {
cursor: default;
}
&.applying {
&:before {
content: "";
width: 14px;
height: 14px;
border: 2px solid #dddd;
border-bottom-color: $primary-color;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: pureing-rotation 0.75s linear infinite;
}
}
&.applied {
color: #c5c8ce;
background-color: #f7f7f7;
border-color: #dcdee2;
&:before {
content: "\e684";
}
}
}
}
}
}
body:not(.window-touch) {