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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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. 工作流列表
|
||||
*
|
||||
|
||||
@ -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 上下文信息
|
||||
@ -494,6 +605,66 @@ class AI
|
||||
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 生成职场笑话、心灵鸡汤
|
||||
* @param bool $noCache 是否禁用缓存
|
||||
|
||||
@ -245,7 +245,16 @@
|
||||
v-bind="formOptions"
|
||||
@submit.native.prevent>
|
||||
<FormItem prop="name" :label="$L('项目名称')">
|
||||
<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 v-if="addData.columns" :label="$L('任务列表')">
|
||||
<TagInput v-model="addData.columns"/>
|
||||
@ -455,6 +464,7 @@ export default {
|
||||
columns: '',
|
||||
flow: 'open',
|
||||
},
|
||||
projectAiLoading: false,
|
||||
addRule: {
|
||||
name: [
|
||||
{ required: true, message: this.$L('请填写项目名称!'), trigger: 'change' },
|
||||
@ -989,12 +999,82 @@ export default {
|
||||
|
||||
onAddShow() {
|
||||
this.$store.dispatch("getColumnTemplate").catch(() => {})
|
||||
this.projectAiLoading = false;
|
||||
this.addShow = true;
|
||||
this.$nextTick(() => {
|
||||
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() {
|
||||
this.$refs.addProject.validate((valid) => {
|
||||
if (valid) {
|
||||
|
||||
@ -627,7 +627,7 @@ export default {
|
||||
onAI() {
|
||||
$A.modalInput({
|
||||
title: 'AI 生成',
|
||||
placeholder: `请输入任务需求,AI 将自动生成标题和详细描述`,
|
||||
placeholder: '请简要描述任务目标、背景或预期交付,AI 将生成标题、详细说明和子任务',
|
||||
inputProps: {
|
||||
type: 'textarea',
|
||||
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) {
|
||||
.page-manage {
|
||||
.manage-box-menu {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user