mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-15 05:12:49 +00:00
perf: 支持AI在项目群里创建任务
This commit is contained in:
parent
61ebbac333
commit
08153cd99b
@ -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. 搜索在线表情
|
||||
*
|
||||
|
||||
@ -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)
|
||||
|
||||
177
resources/assets/js/store/markdown.js
vendored
177
resources/assets/js/store/markdown.js
vendored
@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
|
||||
// 验证输入
|
||||
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))
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user