feat: 添加 AI 助手生成项目功能

- 在 ProjectController 中新增 ai__generate 接口,支持根据用户需求自动生成项目名称及任务列表
- 在 AI 模块中实现 generateProject 方法,处理项目生成逻辑
- 更新前端管理页面,添加 AI 生成按钮,集成项目生成请求
- 增强样式以提升用户体验
This commit is contained in:
kuaifan 2025-09-23 13:43:46 +08:00
parent 0f71abdac3
commit c190aab8b9
5 changed files with 331 additions and 2 deletions

View File

@ -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. 工作流列表
* *

View File

@ -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 是否禁用缓存

View File

@ -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('项目名称')">
<div class="page-manage-project-ai-wrapper">
<Input ref="projectName" type="text" v-model="addData.name"></Input> <Input ref="projectName" type="text" v-model="addData.name"></Input>
<div
class="project-ai-button"
type="text"
:loading="projectAiLoading"
@click="onProjectAI">
<i class="taskfont">&#xe8a1;</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) {

View File

@ -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,

View File

@ -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 {