mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 18:42:54 +00:00
feat: 添加 AI 助手生成项目功能
- 在 ProjectController 中新增 ai__generate 接口,支持根据用户需求自动生成项目名称及任务列表 - 在 AI 模块中实现 generateProject 方法,处理项目生成逻辑 - 更新前端管理页面,添加 AI 生成按钮,集成项目生成请求 - 增强样式以提升用户体验
This commit is contained in:
parent
0f71abdac3
commit
c190aab8b9
@ -2651,6 +2651,55 @@ class ProjectController extends AbstractController
|
|||||||
return Base::retSuccess('生成任务成功', $result['data']);
|
return Base::retSuccess('生成任务成功', $result['data']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} api/project/ai/generate 41. 使用 AI 助手生成项目
|
||||||
|
*
|
||||||
|
* @apiDescription 需要token身份,根据需求说明自动生成项目名称及任务列表
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup project
|
||||||
|
* @apiName ai__generate
|
||||||
|
*
|
||||||
|
* @apiParam {String} content 项目需求或背景描述(必填)
|
||||||
|
* @apiParam {String} [current_name] 当前草拟的项目名称
|
||||||
|
* @apiParam {Array|String} [current_columns] 已有任务列表(数组或以逗号/换行分隔的字符串)
|
||||||
|
* @apiParam {Array} [template_examples] 可参考的模板示例,格式:[ {name: 模板名, columns: [列表...] }, ... ]
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
* @apiSuccess {String} data.name AI 生成的项目名称
|
||||||
|
* @apiSuccess {Array} data.columns AI 生成的任务列表名称数组
|
||||||
|
*/
|
||||||
|
public function ai__generate()
|
||||||
|
{
|
||||||
|
User::auth();
|
||||||
|
|
||||||
|
$content = trim((string)Request::input('content', ''));
|
||||||
|
if ($content === '') {
|
||||||
|
return Base::retError('项目需求描述不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateExamples = Request::input('template_examples', []);
|
||||||
|
if (!is_array($templateExamples)) {
|
||||||
|
$templateExamples = [];
|
||||||
|
} else {
|
||||||
|
$templateExamples = array_slice($templateExamples, 0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = [
|
||||||
|
'current_name' => Request::input('current_name', ''),
|
||||||
|
'current_columns' => Request::input('current_columns', []),
|
||||||
|
'template_examples' => $templateExamples,
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = AI::generateProject($content, $context);
|
||||||
|
if (Base::isError($result)) {
|
||||||
|
return Base::retError('生成项目失败', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess('生成项目成功', $result['data']);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/project/flow/list 40. 工作流列表
|
* @api {get} api/project/flow/list 40. 工作流列表
|
||||||
*
|
*
|
||||||
|
|||||||
@ -440,6 +440,117 @@ class AI
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 openAI 生成项目名称与任务列表
|
||||||
|
* @param string $text 项目需求或描述
|
||||||
|
* @param array $context 上下文信息
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function generateProject($text, $context = [])
|
||||||
|
{
|
||||||
|
$text = trim((string)$text);
|
||||||
|
if ($text === '') {
|
||||||
|
return Base::retError("项目描述不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
$context['current_name'] = trim($context['current_name'] ?? '');
|
||||||
|
$context['current_columns'] = self::normalizeProjectColumns($context['current_columns'] ?? []);
|
||||||
|
|
||||||
|
if (!empty($context['template_examples']) && is_array($context['template_examples'])) {
|
||||||
|
$examples = [];
|
||||||
|
foreach ($context['template_examples'] as $item) {
|
||||||
|
$name = trim($item['name'] ?? '');
|
||||||
|
$columns = self::normalizeProjectColumns($item['columns'] ?? []);
|
||||||
|
if (empty($columns)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$examples[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'columns' => $columns,
|
||||||
|
];
|
||||||
|
if (count($examples) >= 6) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$context['template_examples'] = $examples;
|
||||||
|
} else {
|
||||||
|
$context['template_examples'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$contextPrompt = self::buildProjectContextPrompt($context);
|
||||||
|
|
||||||
|
$post = json_encode([
|
||||||
|
"model" => "gpt-5-nano",
|
||||||
|
"messages" => [
|
||||||
|
[
|
||||||
|
"role" => "system",
|
||||||
|
"content" => <<<EOF
|
||||||
|
你是一名资深的项目规划顾问,帮助团队快速搭建符合需求的项目。
|
||||||
|
|
||||||
|
生成要求:
|
||||||
|
1. 产出一个简洁、有辨识度的项目名称(不超过18个汉字或36个字符)
|
||||||
|
2. 给出 3 - 8 个项目任务列表,用于看板列或阶段分组
|
||||||
|
3. 任务列表名称保持 4 - 12 个字符,聚焦阶段或责任划分,避免冗长描述
|
||||||
|
4. 结合用户描述的业务特征,必要时可包含里程碑或交付节点
|
||||||
|
5. 尽量参考上下文提供的现有内容或模板,不要与之完全重复
|
||||||
|
|
||||||
|
输出格式:
|
||||||
|
必须严格返回 JSON,禁止携带额外说明或 Markdown 代码块,结构如下:
|
||||||
|
{
|
||||||
|
"name": "项目名称",
|
||||||
|
"columns": ["列表1", "列表2", "列表3"]
|
||||||
|
}
|
||||||
|
|
||||||
|
校验标准:
|
||||||
|
- 列表名称应当互不重复且语义明确
|
||||||
|
- 若上下文包含已有名称或列表,请在此基础上迭代优化
|
||||||
|
EOF
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"role" => "user",
|
||||||
|
"content" => ($contextPrompt ? $contextPrompt . "\n\n" : "") . "请根据以上信息,为以下需求生成适合的项目名称和任务列表:\n\n" . $text
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ai = new self($post);
|
||||||
|
$ai->setTimeout(45);
|
||||||
|
|
||||||
|
$res = $ai->request();
|
||||||
|
if (Base::isError($res)) {
|
||||||
|
return Base::retError("项目生成失败", $res);
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = $res['data'];
|
||||||
|
$content = preg_replace('/^\s*```json\s*/', '', $content);
|
||||||
|
$content = preg_replace('/\s*```\s*$/', '', $content);
|
||||||
|
|
||||||
|
if (empty($content)) {
|
||||||
|
return Base::retError("项目生成结果为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsedData = Base::json2array($content);
|
||||||
|
if (!$parsedData || !isset($parsedData['name'])) {
|
||||||
|
return Base::retError("项目生成格式错误", $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = trim($parsedData['name']);
|
||||||
|
$columns = self::normalizeProjectColumns($parsedData['columns'] ?? []);
|
||||||
|
|
||||||
|
if ($name === '') {
|
||||||
|
return Base::retError("生成的项目名称为空", $parsedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($columns)) {
|
||||||
|
$columns = $context['current_columns'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Base::retSuccess("success", [
|
||||||
|
'name' => $name,
|
||||||
|
'columns' => $columns,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建任务生成的上下文提示信息
|
* 构建任务生成的上下文提示信息
|
||||||
* @param array $context 上下文信息
|
* @param array $context 上下文信息
|
||||||
@ -494,6 +605,66 @@ class AI
|
|||||||
return empty($prompts) ? "" : implode("\n", $prompts);
|
return empty($prompts) ? "" : implode("\n", $prompts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function buildProjectContextPrompt($context)
|
||||||
|
{
|
||||||
|
$prompts = [];
|
||||||
|
|
||||||
|
if (!empty($context['current_name']) || !empty($context['current_columns'])) {
|
||||||
|
$prompts[] = "## 当前项目草稿";
|
||||||
|
if (!empty($context['current_name'])) {
|
||||||
|
$prompts[] = "已有名称:" . $context['current_name'];
|
||||||
|
}
|
||||||
|
if (!empty($context['current_columns'])) {
|
||||||
|
$prompts[] = "现有任务列表:" . implode("、", $context['current_columns']);
|
||||||
|
}
|
||||||
|
$prompts[] = "请在此基础上进行优化和补充。";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($context['template_examples'])) {
|
||||||
|
$prompts[] = "## 常用模板示例";
|
||||||
|
foreach ($context['template_examples'] as $example) {
|
||||||
|
$line = '';
|
||||||
|
if (!empty($example['name'])) {
|
||||||
|
$line .= $example['name'] . ":";
|
||||||
|
}
|
||||||
|
$line .= implode("、", $example['columns']);
|
||||||
|
$prompts[] = "- " . $line;
|
||||||
|
}
|
||||||
|
$prompts[] = "可以借鉴以上结构,但要结合用户需求生成更贴合的方案。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return empty($prompts) ? "" : implode("\n", $prompts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function normalizeProjectColumns($columns)
|
||||||
|
{
|
||||||
|
if (is_string($columns)) {
|
||||||
|
$columns = preg_split('/[\n\r,,;;|]/u', $columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = [];
|
||||||
|
if (is_array($columns)) {
|
||||||
|
foreach ($columns as $item) {
|
||||||
|
if (is_array($item)) {
|
||||||
|
$item = $item['name'] ?? $item['title'] ?? reset($item);
|
||||||
|
}
|
||||||
|
$item = trim((string)$item);
|
||||||
|
if ($item === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$item = mb_substr($item, 0, 30);
|
||||||
|
if (!in_array($item, $normalized)) {
|
||||||
|
$normalized[] = $item;
|
||||||
|
}
|
||||||
|
if (count($normalized) >= 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 openAI 生成职场笑话、心灵鸡汤
|
* 通过 openAI 生成职场笑话、心灵鸡汤
|
||||||
* @param bool $noCache 是否禁用缓存
|
* @param bool $noCache 是否禁用缓存
|
||||||
|
|||||||
@ -245,7 +245,16 @@
|
|||||||
v-bind="formOptions"
|
v-bind="formOptions"
|
||||||
@submit.native.prevent>
|
@submit.native.prevent>
|
||||||
<FormItem prop="name" :label="$L('项目名称')">
|
<FormItem prop="name" :label="$L('项目名称')">
|
||||||
<Input ref="projectName" type="text" v-model="addData.name"></Input>
|
<div class="page-manage-project-ai-wrapper">
|
||||||
|
<Input ref="projectName" type="text" v-model="addData.name"></Input>
|
||||||
|
<div
|
||||||
|
class="project-ai-button"
|
||||||
|
type="text"
|
||||||
|
:loading="projectAiLoading"
|
||||||
|
@click="onProjectAI">
|
||||||
|
<i class="taskfont"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem v-if="addData.columns" :label="$L('任务列表')">
|
<FormItem v-if="addData.columns" :label="$L('任务列表')">
|
||||||
<TagInput v-model="addData.columns"/>
|
<TagInput v-model="addData.columns"/>
|
||||||
@ -455,6 +464,7 @@ export default {
|
|||||||
columns: '',
|
columns: '',
|
||||||
flow: 'open',
|
flow: 'open',
|
||||||
},
|
},
|
||||||
|
projectAiLoading: false,
|
||||||
addRule: {
|
addRule: {
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: this.$L('请填写项目名称!'), trigger: 'change' },
|
{ required: true, message: this.$L('请填写项目名称!'), trigger: 'change' },
|
||||||
@ -989,12 +999,82 @@ export default {
|
|||||||
|
|
||||||
onAddShow() {
|
onAddShow() {
|
||||||
this.$store.dispatch("getColumnTemplate").catch(() => {})
|
this.$store.dispatch("getColumnTemplate").catch(() => {})
|
||||||
|
this.projectAiLoading = false;
|
||||||
this.addShow = true;
|
this.addShow = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.projectName.focus();
|
this.$refs.projectName.focus();
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onProjectAI() {
|
||||||
|
if (this.projectAiLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$A.modalInput({
|
||||||
|
title: 'AI 生成',
|
||||||
|
placeholder: '请简要描述项目目标、范围或关键里程碑,AI 将生成名称和任务列表',
|
||||||
|
inputProps: {
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 2,
|
||||||
|
autosize: {minRows: 2, maxRows: 6},
|
||||||
|
maxlength: 500,
|
||||||
|
},
|
||||||
|
onOk: (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return '请输入项目需求';
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.projectAiLoading = true;
|
||||||
|
const parseColumns = (cols) => {
|
||||||
|
if (Array.isArray(cols)) {
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
if (typeof cols === 'string') {
|
||||||
|
return cols.split(/[\n\r,,;;|]/).map(item => item.trim()).filter(item => item);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
const templateExamples = this.columns
|
||||||
|
.filter((item, index) => index > 0 && item && item.columns && String(item.columns).trim() !== '')
|
||||||
|
.slice(0, 6)
|
||||||
|
.map(item => ({
|
||||||
|
name: item.name,
|
||||||
|
columns: parseColumns(item.columns)
|
||||||
|
}));
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
this.projectAiLoading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.dispatch("call", {
|
||||||
|
url: 'project/ai/generate',
|
||||||
|
data: {
|
||||||
|
content: value,
|
||||||
|
current_name: this.addData.name || '',
|
||||||
|
current_columns: this.addData.columns || '',
|
||||||
|
template_examples: templateExamples,
|
||||||
|
},
|
||||||
|
timeout: 45 * 1000,
|
||||||
|
}).then(({data}) => {
|
||||||
|
const columns = Array.isArray(data.columns) ? data.columns : parseColumns(data.columns);
|
||||||
|
this.$set(this.addData, 'name', data.name || '');
|
||||||
|
this.$set(this.addData, 'columns', columns.length > 0 ? columns.join(',') : '');
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.projectName) {
|
||||||
|
this.$refs.projectName.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
finish();
|
||||||
|
resolve();
|
||||||
|
}).catch(({msg}) => {
|
||||||
|
finish();
|
||||||
|
reject(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
onAddProject() {
|
onAddProject() {
|
||||||
this.$refs.addProject.validate((valid) => {
|
this.$refs.addProject.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
|||||||
@ -627,7 +627,7 @@ export default {
|
|||||||
onAI() {
|
onAI() {
|
||||||
$A.modalInput({
|
$A.modalInput({
|
||||||
title: 'AI 生成',
|
title: 'AI 生成',
|
||||||
placeholder: `请输入任务需求,AI 将自动生成标题和详细描述`,
|
placeholder: '请简要描述任务目标、背景或预期交付,AI 将生成标题、详细说明和子任务',
|
||||||
inputProps: {
|
inputProps: {
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
rows: 2,
|
rows: 2,
|
||||||
|
|||||||
29
resources/assets/sass/pages/page-manage.scss
vendored
29
resources/assets/sass/pages/page-manage.scss
vendored
@ -473,6 +473,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-manage-project-ai-wrapper {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.ivu-input-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-ai-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
height: 32px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
padding: 0 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
.taskfont {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (height <= 640px) {
|
@media (height <= 640px) {
|
||||||
.page-manage {
|
.page-manage {
|
||||||
.manage-box-menu {
|
.manage-box-menu {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user