fix(ai): 写作入口采集上游主控字段,用户诉求不再被默认值覆盖(issue 13)

聊天页「写作」模式没有表单,只按「键:值」解析输入,普通自然语言解析为空后一律回落
doc_type=新闻稿 / Number=800。上游「AI内容创作与宣传助手」的 prompt 把这两项当主控指令,
实测模型会主动忽略用户原话里的冲突要求,思维链原文:

  I will ignore the conflicting "300字推文" instruction inside the bullet points
  because the main task says "文稿类型为:新闻稿" and "篇幅要求(字数限制):800"

这就是用户报的「写作模块听不懂要求」——不是模型听不懂,是把「新闻稿/800字」当硬指令发给了它。

- 新增 src/views/oa/chatgpt/writing-params.ts:从自然语言抽取文体/字数/主题/时间/地点/
  人员/活动类型,保留既有「键:值」入参,界面显式选择优先于自动识别;纯函数零依赖,
  可直接用 node 运行测试
- 文体识别对齐上游真实存在的 4 个模板(新闻稿/活动总结/微信推文/通知公告);认不出的文体
  (发言稿、短视频脚本等)原样下发交给模型发挥,强行映射成新闻稿会更糟
- 用户没提字数时按文体给常规篇幅,而不是不下发——上游对空字数的处理会在正文里留一行
  「(未设定有效字数限制)」
- 写作模式输入框上方新增「文体 / 字数」两组按钮,复用既有 ratio-panel 样式

验证:解析用例 8/8(.scratch/ai-assistant/probe/test-writing-params.ts);同一句输入做端到端
对照,修复前得 800 字新闻稿,修复后得 281 字微信推文(引用块 + ## 模块小标题 + 【互动时间】)。
This commit is contained in:
2026-10-13 18:53:29 +08:00
parent 623404ce9a
commit bc398bb652
2 changed files with 412 additions and 25 deletions
+47 -25
View File
@@ -213,6 +213,32 @@
<span>{{ assistantTitle }}</span>
</div>
<div class="input-shell">
<div v-if="activeChatMode === 'writing'" class="ratio-panel">
<span class="ratio-label">文体</span>
<button
v-for="item in WRITING_DOC_TYPE_OPTIONS"
:key="item.value"
class="ratio-option"
type="button"
:class="{ 'is-active': writingDocType === item.value }"
:disabled="loading"
@click="writingDocType = item.value"
>
{{ item.label }}
</button>
<span class="ratio-label">字数</span>
<button
v-for="item in WRITING_WORD_COUNT_OPTIONS"
:key="item.value"
class="ratio-option"
type="button"
:class="{ 'is-active': writingWordCount === item.value }"
:disabled="loading"
@click="writingWordCount = item.value"
>
{{ item.label }}
</button>
</div>
<div v-if="activeChatMode === 'image'" class="ratio-panel">
<span class="ratio-label">图片比例</span>
<button
@@ -316,6 +342,12 @@ import { listAiChatList, removeAiChatList } from "@/api/ai/aiChatList";
import type { AiChatList } from "@/api/ai/aiChatList/model";
import { sendMessage, stopMessage } from "@/api/oa/chatgpt";
import { manuscriptGen } from "@/api/ai/manuscriptGen";
import {
parseWritingRequest,
WRITING_DOC_TYPE_OPTIONS,
WRITING_WORD_COUNT_OPTIONS
} from "./writing-params";
import type { WritingOverrides } from "./writing-params";
import { creativeAssistant } from "@/api/ai/creativeAssistant";
import { API_BASE_URL } from "@/config/setting";
import type { Menu } from "@/api/system/menu/model";
@@ -360,6 +392,9 @@ const stopping = ref(false);
const content = ref("");
const activeChatMode = ref<ChatMode>("chat");
const imageRatio = ref("1:1");
// 写作模式的显式选择:默认 auto,即由 writing-params 从用户输入里自动识别
const writingDocType = ref("auto");
const writingWordCount = ref("auto");
const wsConnected = ref(false);
const deletingChatId = ref<number>();
const messages = ref<ChatMessageItem[]>([]);
@@ -413,7 +448,7 @@ const imageRatioOptions = [
const inputPlaceholder = computed(() => {
if (activeChatMode.value === "writing") {
return "请输入写作需求,例如新闻稿、推文文案、小论文、发言稿,也可以补充标题、受众、字数和风格";
return "直接描述需求即可,例如:帮我写一篇300字关于迎新晚会的推文,风格活泼,面向大一新生(也可在下方直接指定文体和字数)";
}
if (activeChatMode.value === "image") {
return "请输入图片生成需求,例如校园活动海报、公众号头图、志愿服务插画,并选择图片比例";
@@ -960,32 +995,19 @@ const pickString = (...values: unknown[]) => {
return "";
};
const parseKeyValueInput = (value: string) => {
const result: Record<string, string> = {};
value
.split(/\n+/)
.map((item) => item.trim())
.filter(Boolean)
.forEach((line) => {
const match = line.match(/^([^::]+)[::]\s*(.*)$/);
if (match) {
result[match[1].trim()] = match[2].trim();
}
});
return result;
};
const buildWritingPayload = (query: string) => {
const fields = parseKeyValueInput(query);
// 界面上显式选了文体/字数就用显式值,否则由 writing-params 从自然语言里识别。
// 过去这里没有表单,一律回落 doc_type=新闻稿 / Number=800,而上游提示词把这两项
// 当作主控指令,会直接压掉用户原话里的诉求(实测见 .scratch/ai-assistant/probe/)。
const overrides: WritingOverrides = {};
if (writingDocType.value !== "auto") {
overrides.docType = writingDocType.value;
}
if (writingWordCount.value !== "auto") {
overrides.number = Number(writingWordCount.value);
}
return {
docType: fields["稿件类型"] || fields["文体"] || fields["类型"] || "新闻稿",
eventType: fields["活动类型"] || fields["场景"] || "校园内容",
theme: fields["稿件标题"] || fields["标题"] || fields["主题"] || query,
time: fields["活动时间"] || fields["时间"] || "",
location: fields["活动地点"] || fields["地点"] || "",
participants: fields["参与人员"] || fields["受众"] || fields["人员"] || "",
highlights: fields["亮点内容"] || fields["要点"] || fields["亮点"] || fields["内容"] || query,
number: Number(fields["建议篇幅"] || fields["字数"] || 800),
...parseWritingRequest(query, overrides),
files: []
};
};