file-upload.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. /**
  2. * 附件上传到 DMS 的文件服务(`POST {DMS}/file/uploadFile`)。
  3. *
  4. * **实测契约(2026-09-23,打真实服务)**:
  5. * - `multipart/form-data`,**五个字段都必填**:
  6. * `file` / `type` / `columnId` / `contentId` / `paraName` ——
  7. * 少任何一个都返回 `{"code":-1,"message":"缺少必传字段"}`
  8. * - `type` 决定**存储目录**:`0` 图 → `/static/photo/…`;`1` 音 / `2` 视 / `3` 文件 → `/static/file/…`
  9. * - 成功返回 `{"code":200,"message":"上传成功!","content":"/static/photo/1889-c_question-<uuid>.png"}`
  10. * —— **`content` 就是那个相对路径**
  11. * - ⚠️ 返回的相对路径**当前直接取不到**(10081 / 10011 都 404)——
  12. * 所以聊天里的缩略图**不能拿它当 `<img>` 的 src**,预览要用本地 blob(见上传模块)
  13. * - ⚠️ 走 `/dms-api` 代理:页面是 HTTPS、接口是 HTTP,直连会被混合内容拦掉
  14. * (代理已注入 DMS token,实测这个端点不校验 token 也无妨)
  15. *
  16. * 这个相对路径要放进聊天请求体的 `pictures` / `files` 里(用户 2026-09-23 指定)。
  17. */
  18. import { getDmsApiBaseUrl } from "@/utils/runtime-config";
  19. /**
  20. * 上传时随文件一起提交的三个 DMS 定位字段(**会进文件名**)。
  21. *
  22. * 取值由用户 2026-09-23 指定:附件按「属于 1889 助手问答记录的问题内容」落库 ——
  23. * 会话/问答记录都在 1889,附件跟着问题走。
  24. */
  25. export const DMS_ATTACHMENT_COLUMN_ID = 1889;
  26. export const DMS_ATTACHMENT_CONTENT_ID = 0;
  27. export const DMS_ATTACHMENT_PARA_NAME = "c_question";
  28. /** `type`:0 图 · 1 音 · 2 视 · 3 文件(实测 0 与 3 的差别就是落在 photo/ 还是 file/ 目录) */
  29. export const DMS_ATTACHMENT_TYPE_IMAGE = "0";
  30. export const DMS_ATTACHMENT_TYPE_FILE = "3";
  31. /**
  32. * 把上传返回的**相对路径**变成一个**可显示**的地址(用于聊天里的缩略图预览)。
  33. *
  34. * 实测(2026-09-23):相对路径本身取不到(10081 / 10011 的根路径都 404),
  35. * 但**在 DMS 主机前加上 `/dms` 就能取到** ——
  36. * `http://121.43.55.7:10081/dms/static/photo/….png` → **200**。
  37. * 而 `/dms-api` 代理补的正好就是 `/dms`,所以这里直接拼代理前缀即可。
  38. *
  39. * ⚠️ 存进消息里的就是这个地址:它依赖代理前缀保持不变;
  40. * 发给**后端**的仍是相对路径(见 `toChatAttachmentUrls` 用的 `fileUrl` 字段),两者不要混。
  41. */
  42. export const buildDmsFileDisplayUrl = (relativePath: string): string => {
  43. const path = String(relativePath || "").trim();
  44. if (!path) return "";
  45. if (/^https?:\/\//i.test(path)) return path; // 已经是绝对地址就原样返回
  46. const base = getDmsApiBaseUrl();
  47. return base ? `${base}${path.startsWith("/") ? "" : "/"}${path}` : "";
  48. };
  49. /**
  50. * 上传一个附件,返回**相对路径**(如 `/static/photo/1889-c_question-<uuid>.png`)。
  51. * 任何失败都抛 `Error`(消息可直接给用户看),由调用方更新上传项状态。
  52. */
  53. export const uploadDmsAttachment = async (
  54. file: File,
  55. options: { isImage: boolean; signal?: AbortSignal }
  56. ): Promise<string> => {
  57. const base = getDmsApiBaseUrl();
  58. if (!base) {
  59. throw new Error("未配置 DMS 接口地址,无法上传附件");
  60. }
  61. const form = new FormData();
  62. form.append("file", file);
  63. form.append("type", options.isImage ? DMS_ATTACHMENT_TYPE_IMAGE : DMS_ATTACHMENT_TYPE_FILE);
  64. form.append("columnId", String(DMS_ATTACHMENT_COLUMN_ID));
  65. form.append("contentId", String(DMS_ATTACHMENT_CONTENT_ID));
  66. form.append("paraName", DMS_ATTACHMENT_PARA_NAME);
  67. const response = await fetch(`${base}/file/uploadFile`, {
  68. method: "POST",
  69. body: form,
  70. signal: options.signal,
  71. });
  72. if (!response.ok) {
  73. throw new Error(`上传失败(HTTP ${response.status})`);
  74. }
  75. const payload = await response.json().catch(() => null);
  76. const path = String(payload?.content ?? "").trim();
  77. if (payload?.code !== 200 || !path) {
  78. throw new Error(String(payload?.message || "上传失败"));
  79. }
  80. return path;
  81. };