feat(ai): 创意助手各页下线参考素材上传,图片模式改传 mode(issue 19)

- 聊天页「图片」模式与独立创意助手页都删掉上传入口、上传状态与相关样式;
  上游出图工具只做文生图,开始节点的 file-list 变量没有任何节点消费(issue 16)。
- 图片模式请求体改为 { user_content, mode: 'image' },由后端包装意图声明;
  独立页不传 mode(通用入口,视频/脚本/图片靠用户文字说明)。
- CreativeAssistantRequest 增加 mode 字段,user_files 保留待上游补齐后回退。
This commit is contained in:
2026-10-21 15:42:58 +08:00
parent 00e0e70e85
commit 038e789cb0
3 changed files with 41 additions and 411 deletions
+11 -212
View File
@@ -334,44 +334,6 @@
</div>
</div>
</div>
<div v-if="activeChatMode === 'image'" class="ratio-panel attach-panel">
<span class="ratio-label">参考素材</span>
<button
class="ratio-option"
type="button"
:disabled="loading || uploadingFiles || imageFiles.length >= IMAGE_FILE_LIMIT"
@click="pickImageFiles"
>
{{ uploadingFiles ? "上传中..." : "添加附件" }}
</button>
<span class="attach-hint">
最多 {{ IMAGE_FILE_LIMIT }} 个,支持图片 / 视频 / 音频 / 文档
</span>
<ul v-if="imageFiles.length" class="attach-list">
<li
v-for="(item, index) in imageFiles"
:key="item.id"
class="attach-item"
>
<span class="attach-name" :title="item.name">{{ item.name }}</span>
<button
class="attach-remove"
type="button"
:disabled="loading"
@click="removeImageFile(index)"
>
移除
</button>
</li>
</ul>
<input
ref="fileInputRef"
class="attach-input"
type="file"
multiple
@change="onImageFilesPicked"
/>
</div>
<textarea
v-model="content"
:placeholder="inputPlaceholder"
@@ -434,8 +396,7 @@ import {
WRITING_EVENT_TYPE_OPTIONS,
WRITING_WORD_COUNT_OPTIONS
} from "./writing-params";
import { creativeAssistant, uploadAiFile } from "@/api/ai/creativeAssistant";
import type { CreativeAssistantUploadFile } from "@/api/ai/creativeAssistant/model";
import { creativeAssistant } from "@/api/ai/creativeAssistant";
import { API_BASE_URL } from "@/config/setting";
import type { Menu } from "@/api/system/menu/model";
import { useUserStore } from "@/store/modules/user";
@@ -460,13 +421,6 @@ interface HistorySection {
items: AiChatList[];
}
/** 图片模式的参考素材(上传成功后保留 Dify 返回的 file id 与应用内类型) */
interface CreativeAttachment {
id: string;
name: string;
type: CreativeAssistantUploadFile["type"];
}
interface AuthorityTreeItem extends Menu {
children?: AuthorityTreeItem[];
}
@@ -485,17 +439,6 @@ const loading = ref(false);
const stopping = ref(false);
const content = ref("");
const activeChatMode = ref<ChatMode>("chat");
/**
* 图片模式的参考素材,对应 Dify「AI 创意助手」开始节点的 file-list 变量 `user_files`。
*
* 两个硬约束(都来自上游 `/parameters` 实测):
* 1. `max_length = 5`,最多 5 个;
* 2. **必须用创意助手自己的应用凭证上传** —— Dify 的 /files/upload 按应用鉴权,
* 用别的应用(如 chat)的 Key 上传,工作流引用该 id 会直接报 `Invalid upload file`。
*/
const imageFiles = ref<CreativeAttachment[]>([]);
const uploadingFiles = ref(false);
const fileInputRef = ref<HTMLInputElement>();
// 写作模式的显式选择:默认 auto = 不下发这两个字段的值,交给上游 Dify 识别
const writingDocType = ref(WRITING_DOC_TYPE_FREE);
const writingWordCount = ref("auto");
@@ -539,7 +482,7 @@ const userName = computed(
const showAdminEntry = computed(() => !isOnlyStudent());
const canSend = computed(() => {
if (loading.value || uploadingFiles.value) {
if (loading.value) {
return false;
}
if (content.value.trim().length > 0) {
@@ -567,12 +510,7 @@ const chatModeOptions: { label: string; value: ChatMode }[] = [
// 图片比例选择器已撤掉:上游创意助手开始节点没有比例变量(实测只有 user_content /
// user_files),前端只能把它拼进 user_content —— 那是替上游写 prompt。
// 要恢复这个功能,先在上游开始节点加变量,再按变量契约接上来。
/** 参考素材上限,与 Dify 开始节点 user_files 的 max_length 一致 */
const IMAGE_FILE_LIMIT = 5;
const IMAGE_FILE_EXTS = ["PNG", "JPG", "JPEG", "GIF", "WEBP", "BMP", "SVG"];
const VIDEO_FILE_EXTS = ["MP4", "MOV", "MPEG", "MPGA", "AVI", "M4V"];
const AUDIO_FILE_EXTS = ["MP3", "M4A", "WAV", "AMR", "AAC", "OGG", "WEBM"];
// user_files 本身也已不再上传:开始节点挂着这个变量,但没有任何下游节点消费它(issue 16)。
/** 选中文体 = 套模板走「内容创作与宣传助手」;否则是自由创作走「AI 创意助手」 */
const writingTemplateMode = computed(
@@ -1367,102 +1305,20 @@ const formatCreativeResult = (payload: unknown) => {
});
};
/** 按 MIME 或扩展名判定 Dify file-list 要求的 type(image/video/audio/document) */
const resolveUploadFileType = (
contentType: string,
fileName: string
): CreativeAssistantUploadFile["type"] => {
const mime = String(contentType || "").toLowerCase();
if (mime.startsWith("image/")) {
return "image";
}
if (mime.startsWith("video/")) {
return "video";
}
if (mime.startsWith("audio/")) {
return "audio";
}
const ext = String(fileName || "").split(".").pop()?.toUpperCase() || "";
if (IMAGE_FILE_EXTS.includes(ext)) {
return "image";
}
if (VIDEO_FILE_EXTS.includes(ext)) {
return "video";
}
if (AUDIO_FILE_EXTS.includes(ext)) {
return "audio";
}
return "document";
};
const pickImageFiles = () => {
if (loading.value || uploadingFiles.value) {
return;
}
fileInputRef.value?.click();
};
const removeImageFile = (index: number) => {
imageFiles.value.splice(index, 1);
};
const onImageFilesPicked = async (event: Event) => {
const input = event.target as HTMLInputElement;
const picked = Array.from(input.files || []);
// 清空 value,否则连续选同一个文件不会再触发 change
input.value = "";
if (!picked.length) {
return;
}
const remain = IMAGE_FILE_LIMIT - imageFiles.value.length;
if (remain <= 0) {
message.warning(`参考素材最多 ${IMAGE_FILE_LIMIT} 个`);
return;
}
const selected = picked.slice(0, remain);
if (picked.length > remain) {
message.warning(`参考素材最多 ${IMAGE_FILE_LIMIT} 个,本次只上传前 ${remain} 个`);
}
uploadingFiles.value = true;
try {
for (const file of selected) {
// app 必须传 creativeAssistant:Dify 按应用鉴权上传文件,
// 用别的应用凭证上传的 id,工作流会报 Invalid upload file。
const record = await uploadAiFile(file, "creativeAssistant");
imageFiles.value.push({
id: String(record.id),
name: record.name || file.name,
type: resolveUploadFileType(
record.contentType || file.type,
record.name || file.name
)
});
}
message.success("参考素材已上传");
} catch (e: any) {
message.error(e?.message || "参考素材上传失败");
} finally {
uploadingFiles.value = false;
}
};
/**
* 图片模式入参:`user_content` 就是**用户原话** + `user_files`(Dify file-list 变量)。
* 图片模式入参。
*
* 不往 `user_content` 里追加任何我们自己写的话(如「图片比例:1:1」
* 「请直接生成图片…」):上游创意助手只有 `user_content` / `user_files` 两个变量,
* 没有比例变量,拼进去等于替上游写 prompt。比例选择器已撤掉。
* <p>只下发两样东西:用户原话 {@code user_content},以及 {@code mode=image}。
* 参考素材({@code user_files})已下线:上游出图工具只做文生图,开始节点虽然挂着
* 这个 file-list 变量,但没有任何节点消费它(见 issue 16),传上去只是空等上传。
*
* <p>{@code mode} 不写进 user_content,由后端在 image 模式下统一包装成
* 「生成图片」的意图声明 —— 上游「问题分类器」因此不会把画面描述判成文案分支。
*/
const buildImagePayload = (query: string) => {
const userFiles: CreativeAssistantUploadFile[] = imageFiles.value.map((item) => ({
transfer_method: "local_file",
type: item.type,
upload_file_id: item.id,
url: ""
}));
return {
user_content: query,
user_files: userFiles
mode: "image"
};
};
@@ -1579,10 +1435,6 @@ const onStopGenerate = async () => {
};
const onSend = async () => {
if (uploadingFiles.value) {
message.warning("参考素材正在上传,请稍候");
return;
}
if (!canSend.value) {
return;
}
@@ -2433,59 +2285,6 @@ export default {
}
}
.attach-hint {
color: #8aa4bd;
font-size: 12px;
}
.attach-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
margin: 0;
padding: 0;
list-style: none;
}
.attach-item {
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 100%;
padding: 4px 8px;
border: 1px solid rgba(21, 144, 255, 0.28);
border-radius: 8px;
background: #f5fbff;
color: #1590ff;
font-size: 12px;
}
.attach-name {
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attach-remove {
padding: 0;
border: none;
background: transparent;
color: #8aa4bd;
font-size: 12px;
cursor: pointer;
}
.attach-remove:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.attach-input {
display: none;
}
.input-shell textarea {
width: 100%;
height: 120px;