feat(ai): 写作模式拆成自由创作与模板两条路(issue 18)

自由创作不再解析自然语言:原话整段作为 userContent 下发,docType 留空,由后端转
「AI 创意助手」。选了文体才提交 docType 等结构化条件,走「内容创作与宣传助手」。

- writing-params.ts 删掉 parseWritingRequest、字段别名表与字数兜底表,只留选项常量。
- chatgpt/index.vue:字数选择器只在选中文体后出现;撤掉图片比例选择器(上游开始
  节点无比例变量,只能拼进 user_content,等于替上游写 prompt);结果不再用入参补标题。
- gxmu/manuscriptGen:新增「自由创作」入口与 freeContent 文本域,模板模式才展开活动
  信息等可选项。
- 顺带删除 chatgpt/index.bak.vue 旧备份。
This commit is contained in:
2026-10-21 12:39:54 +08:00
parent 4e725c7840
commit d8ef29afe7
5 changed files with 193 additions and 701 deletions
+115 -65
View File
@@ -22,18 +22,12 @@
<a-button block @click="fillDemo('news')">活动新闻稿</a-button>
<a-button block @click="fillDemo('wechat')">公众号推文</a-button>
<a-button block @click="fillDemo('speech')">发言稿提纲</a-button>
<a-button block @click="fillDemo('free')">自由创作示例</a-button>
</div>
</div>
<a-card :bordered="false" class="panel-card">
<template #title>创作配置</template>
<a-form layout="vertical">
<a-form-item label="稿件标题">
<a-input
v-model:value="form.theme"
allow-clear
placeholder="例如:广西医科大学开展2026年春季志愿服务活动"
/>
</a-form-item>
<a-form-item label="稿件类型">
<a-select
v-model:value="form.docType"
@@ -41,60 +35,78 @@
placeholder="请选择稿件类型"
/>
</a-form-item>
<a-form-item label="活动类型">
<a-select
v-model:value="form.eventType"
:options="eventTypeOptions"
placeholder="请选择活动类型"
/>
</a-form-item>
<a-form-item label="活动时间">
<a-input
v-model:value="form.time"
allow-clear
placeholder="例如:2026年4月8日下午"
/>
</a-form-item>
<a-form-item label="活动地点">
<a-input
v-model:value="form.location"
allow-clear
placeholder="例如:广西医科大学校史馆广场"
/>
</a-form-item>
<a-form-item label="参与人员">
<a-input
v-model:value="form.participants"
allow-clear
placeholder="例如:校团委老师、青年志愿者代表共120人"
/>
</a-form-item>
<a-form-item label="亮点内容">
<a-form-item v-if="!templateMode" label="想创作的内容">
<a-textarea
v-model:value="form.highlights"
:auto-size="{ minRows: 5, maxRows: 8 }"
placeholder="填写活动亮点、流程、成果、典型细节等关键信息"
v-model:value="form.freeContent"
:auto-size="{ minRows: 6, maxRows: 12 }"
placeholder="例如:帮我写一段迎新晚会的主持人开场词,口语化一点,时长两分钟左右"
/>
<div class="upload-tip">
不套模板时,这段内容会作为 `user_content` 交给「AI 创意助手」;选上面的文稿模板后可填写活动信息等可选参数。
</div>
</a-form-item>
<a-form-item label="建议篇幅">
<a-input-number
v-model:value="form.number"
class="full-width"
:min="100"
:max="5000"
:step="100"
placeholder="请输入建议字数"
/>
</a-form-item>
<a-form-item label="附件材料">
<a-upload
:multiple="true"
:show-upload-list="false"
:customRequest="handleUpload"
>
<a-button>
<template #icon>
<UploadOutlined />
<template v-if="templateMode">
<a-form-item label="稿件标题">
<a-input
v-model:value="form.theme"
allow-clear
placeholder="例如:广西医科大学开展2026年春季志愿服务活动"
/>
</a-form-item>
<a-form-item label="活动类型">
<a-select
v-model:value="form.eventType"
:options="eventTypeOptions"
placeholder="请选择活动类型"
/>
</a-form-item>
<a-form-item label="活动时间">
<a-input
v-model:value="form.time"
allow-clear
placeholder="例如:2026年4月8日下午"
/>
</a-form-item>
<a-form-item label="活动地点">
<a-input
v-model:value="form.location"
allow-clear
placeholder="例如:广西医科大学校史馆广场"
/>
</a-form-item>
<a-form-item label="参与人员">
<a-input
v-model:value="form.participants"
allow-clear
placeholder="例如:校团委老师、青年志愿者代表共120人"
/>
</a-form-item>
<a-form-item label="亮点内容">
<a-textarea
v-model:value="form.highlights"
:auto-size="{ minRows: 5, maxRows: 8 }"
placeholder="填写活动亮点、流程、成果、典型细节等关键信息"
/>
</a-form-item>
<a-form-item label="建议篇幅">
<a-input-number
v-model:value="form.number"
class="full-width"
:min="100"
:max="5000"
:step="100"
placeholder="请输入建议字数"
/>
</a-form-item>
<a-form-item label="附件材料">
<a-upload
:multiple="true"
:show-upload-list="false"
:customRequest="handleUpload"
>
<a-button>
<template #icon>
<UploadOutlined />
</template>
上传附件
</a-button>
@@ -119,6 +131,7 @@
</div>
</div>
</a-form-item>
</template>
</a-form>
<div class="submit-bar">
@@ -136,8 +149,9 @@
<div class="hint-block">
<div class="hint-title">提交说明</div>
<div class="hint-text">
页面会先提交 `POST /ai/manuscriptGen`,再按 `/ai` 页面的方式通过 WebSocket
流式接收生成结果;如果后端仍然直接返回正文,页面也会自动兼容解析。
未选模板(自由创作)时只提交一段内容,后端转「AI 创意助手」;选了模板才提交
`docType` 等结构化条件。两种都会先 `POST /ai/manuscriptGen`,再按 `/ai` 页面的方式
通过 WebSocket 流式接收生成结果;如果后端仍然直接返回正文,页面也会自动兼容解析。
</div>
</div>
</a-card>
@@ -227,7 +241,9 @@
import { resolveDisplayText, isFailedPayload, pickDisplayable, FAILED_TEXT } from '@/utils/ai-result';
interface FormState {
/** 空串 = 自由创作(不套模板);非空 = 文稿模板,走「内容创作与宣传助手」 */
docType: string;
freeContent: string;
eventType: string;
theme: string;
time: string;
@@ -243,7 +259,7 @@
summary: string;
}
type DemoType = 'news' | 'wechat' | 'speech';
type DemoType = 'news' | 'wechat' | 'speech' | 'free';
type StreamResolver = {
resolve: () => void;
reject: (error: Error) => void;
@@ -275,8 +291,13 @@
const THINK_OPEN_TAG = '<think>';
const THINK_CLOSE_TAG = '</think>';
/** 「自由创作」的取值:选中它时不下发 docType,后端转「AI 创意助手」 */
const DOC_TYPE_FREE = '自由创作';
const form = reactive<FormState>({
docType: '新闻稿',
// 默认自由创作:未选模板就说清想写什么,交给「AI 创意助手」
docType: DOC_TYPE_FREE,
freeContent: '',
eventType: '校园活动',
theme: '',
time: '',
@@ -287,6 +308,8 @@
});
const docTypeOptions = [
// 自由创作:表单只留一个内容输入框,提交时不带 docType
{ label: '自由创作(不套模板)', value: DOC_TYPE_FREE },
{ label: '新闻稿', value: '新闻稿' },
{ label: '公众号推文', value: '公众号推文' },
{ label: '宣传稿', value: '宣传稿' },
@@ -304,7 +327,13 @@
{ label: '品牌项目', value: '品牌项目' }
];
/** 选了文稿模板才展开其余可选参数 */
const templateMode = computed(() => !!form.docType && form.docType !== DOC_TYPE_FREE);
const canSubmit = computed(() => {
if (!templateMode.value) {
return !!form.freeContent.trim();
}
return !!(form.theme.trim() || form.highlights.trim() || uploadedFiles.value.length);
});
@@ -375,6 +404,13 @@
});
const buildPayload = (): ManuscriptGenParams => {
if (!templateMode.value) {
// 自由创作:只发一段内容,后端见 docType 为空就转「AI 创意助手」
return {
docType: '',
userContent: form.freeContent.trim()
};
}
return {
docType: form.docType,
eventType: form.eventType,
@@ -718,7 +754,9 @@
};
const resetForm = () => {
form.docType = '新闻稿';
// 重置回自由创作,而不是上一次的模板
form.docType = DOC_TYPE_FREE;
form.freeContent = '';
form.eventType = '校园活动';
form.theme = '';
form.time = '';
@@ -730,6 +768,12 @@
};
const fillDemo = (type: DemoType) => {
if (type === 'free') {
form.docType = DOC_TYPE_FREE;
form.freeContent =
'帮我写一段迎新晚会的主持人开场词,口语化一点,时长两分钟左右,面向大一新生。';
return;
}
if (type === 'news') {
form.theme = '广西医科大学青年志愿者开展“清洁校园”志愿服务活动';
form.docType = '新闻稿';
@@ -805,7 +849,11 @@
const handleGenerate = async () => {
if (!canSubmit.value) {
message.warning('请至少填写稿件标题、亮点内容或上传附件中的一项');
message.warning(
templateMode.value
? '请至少填写稿件标题、亮点内容或上传附件中的一项'
: '请先描述你想创作的内容'
);
return;
}
loading.value = true;
@@ -814,7 +862,9 @@
responseSummary.value = '正在建立 WebSocket 连接...';
try {
await connectWebSocket();
responseSummary.value = '连接成功,已提交生成任务,等待流式返回...';
responseSummary.value = templateMode.value
? '连接成功,已提交生成任务,等待流式返回...'
: '自由创作已提交给 AI 创意助手,生成完成后一次性返回...';
const waitStream = createStreamWaiter();
const result = await manuscriptGen(buildPayload());
if (!streamStarted.value && shouldTreatAsDirectResult(result)) {