ai-query对接新版freesun-agent接口的分支
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1622 lines
46 KiB

"use strict";
const superagent = require("superagent");
const qiniu = require("qiniu");
const { Readable } = require("stream");
const fsPromises = require("fs").promises;
const fs = require("fs");
const path = require("path");
const os = require("os");
const { randomUUID } = require("crypto");
const { convertPdfToImages } = require("../utils/pdfToImages");
const { getFastGptToken } = require("../utils/fastgptToken");
const pepTokenCache = {
token: "",
expiresAt: 0,
};
const knowledgeTokenCache = {
token: "",
expiresAt: 0,
};
const PEP_TOKEN_TTL_MS = 1000 * 60 * 60; // 50分钟
const KNOWLEDGE_TOKEN_TTL_MS = 1000 * 60 * 60;
const getPepToken = async (ctx) => {
const now = Date.now();
if (pepTokenCache.token && pepTokenCache.expiresAt > now) {
return pepTokenCache.token;
}
const { emisApi, username, password } = ctx.config.pep;
const loginRes = await superagent
.post(`${emisApi}/login`)
.send({ username, password });
if (loginRes.status !== 200 || !loginRes.body?.token) {
throw new Error("登录失败");
}
pepTokenCache.token = loginRes.body.token;
pepTokenCache.expiresAt = now + PEP_TOKEN_TTL_MS;
return pepTokenCache.token;
};
const getKnowledgeBaseUrl = (ctx) => {
const baseUrl =
ctx.config?.pep?.knowledgeUrl || process.env.FS_KNOWLEDGE_URL || "";
return String(baseUrl || "").replace(/\/+$/, "");
};
const resetKnowledgeTokenCache = () => {
knowledgeTokenCache.token = "";
knowledgeTokenCache.expiresAt = 0;
};
const getKnowledgeToken = async (ctx, { forceRefresh = false } = {}) => {
const now = Date.now();
if (
!forceRefresh &&
knowledgeTokenCache.token &&
knowledgeTokenCache.expiresAt > now
) {
return knowledgeTokenCache.token;
}
const knowledgeBaseUrl = getKnowledgeBaseUrl(ctx);
if (!knowledgeBaseUrl) {
throw new Error("未配置知识产权系统地址");
}
const { username, password } = ctx.config.pep || {};
const loginRes = await superagent
.post(`${knowledgeBaseUrl}/_api/login`)
.send({ phone: username, password });
if (loginRes.status !== 200 || !loginRes.body?.token) {
throw new Error("知识产权系统登录失败");
}
knowledgeTokenCache.token = loginRes.body.token;
knowledgeTokenCache.expiresAt = now + KNOWLEDGE_TOKEN_TTL_MS;
return knowledgeTokenCache.token;
};
const isKnowledgeTokenExpired = (error) => {
const status = error?.status || error?.response?.status;
if (status === 401) return true;
const message = String(
error?.response?.body?.message || error?.message || "",
).toLowerCase();
return (
status === 403 &&
(message.includes("token") ||
message.includes("登录") ||
message.includes("认证") ||
message.includes("unauthorized"))
);
};
const requestKnowledgeApi = async (
ctx,
{
method = "get",
path: requestPath,
query,
body,
buffer = false,
responseType,
} = {},
) => {
const knowledgeBaseUrl = getKnowledgeBaseUrl(ctx);
if (!knowledgeBaseUrl) {
throw new Error("未配置知识产权系统地址");
}
const execute = async (forceRefresh = false) => {
const token = await getKnowledgeToken(ctx, { forceRefresh });
let req = superagent[method](`${knowledgeBaseUrl}${requestPath}`).set(
"Token",
token,
);
if (query) req = req.query(query);
if (body) req = req.send(body);
if (buffer) req = req.buffer(true);
if (responseType) req = req.responseType(responseType);
return req;
};
try {
return await execute(false);
} catch (error) {
if (!isKnowledgeTokenExpired(error)) {
throw error;
}
resetKnowledgeTokenCache();
return execute(true);
}
};
const parseUserId = (value) => {
if (value == null) return null;
const str = String(value);
const match = str.match(/pep_(\d+)_/i);
const id = match ? match[1] : str;
const num = Number(id);
return Number.isNaN(num) ? null : num;
};
const normalizeVisibleUsers = (list) => {
if (!Array.isArray(list)) return [];
const ids = list
.map((item) => parseUserId(item))
.filter((val) => val != null);
return Array.from(new Set(ids));
};
const ensureCreatorVisible = (visibleUsers, creator) => {
const creatorId = parseUserId(creator);
if (!creatorId) return visibleUsers;
if (visibleUsers.includes(creatorId)) return visibleUsers;
return [...visibleUsers, creatorId];
};
const uploadFileToQiniu = async (ctx, filePath, key) => {
const {
qiniu: { dmn: domain, bkt: bucket, ak: accessKey, sk: secretKey },
} = ctx.config;
const resolvedDomain = domain || process.env.FS_QINIU_DOMAIN || "";
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const putPolicy = new qiniu.rs.PutPolicy({ scope: bucket });
const uploadToken = putPolicy.uploadToken(mac);
const config = new qiniu.conf.Config();
config.zone = qiniu.zone.Zone_z0;
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra();
const uploadResult = await new Promise((resolve, reject) => {
formUploader.putFile(
uploadToken,
key,
filePath,
putExtra,
(respErr, respBody) => {
if (respErr) {
reject(respErr);
return;
}
resolve(respBody);
},
);
});
return resolvedDomain ? `${resolvedDomain}/${uploadResult.key}` : uploadResult.key;
};
const LIBRARY_TYPE_META = {
patent: { label: "专利证书库", type: "patentImages" },
software: { label: "软著证书库", type: "softwareImages" },
patentImages: { label: "专利证书库", type: "patentImages" },
softwareImages: { label: "软著证书库", type: "softwareImages" },
};
const deriveBaseNameFromUrl = (url) => {
if (!url) return "";
const cleanUrl = decodeURIComponent(String(url).split("#")[0].split("?")[0]);
const base = path.basename(cleanUrl);
const ext = path.extname(base);
return base.slice(0, base.length - ext.length);
};
const ensureUserLibraryByType = async (ctx, { userId, libraryType, departmentId }) => {
const { models } = ctx.app.fs.dc;
const uid = parseUserId(userId);
const creatorValue = String(userId);
if (!uid) {
throw new Error("无效的userId");
}
const meta = LIBRARY_TYPE_META[libraryType];
if (!meta) {
throw new Error(
"libraryType仅支持 patent/software 或 patentImages/softwareImages",
);
}
const { label, type: normalizedType } = meta;
const libraryName = `${label}-${uid}`;
// 优先使用 libraryType + creator 定位库,避免名称变更影响判断;兼容旧数据再回退到 name。
let existing = await models.TenderImageLibrary.findOne({
where: { creator: creatorValue, libraryType: normalizedType },
});
if (!existing) {
existing = await models.TenderImageLibrary.findOne({
where: { name: libraryName },
});
}
if (existing) {
return existing;
}
if (!departmentId) {
throw new Error("创建专利/软著图库需要departmentId");
}
const visibleUserIds = ensureCreatorVisible([], userId);
return models.TenderImageLibrary.create(
{
name: libraryName,
description: `${label}(用户${uid}`,
creator: creatorValue,
libraryType: normalizedType,
departmentId,
visibilityScope: "custom",
visibleUserIds,
},
{ returning: true },
);
};
const buildImageTitle = ({ baseName, page, index }) => {
const safeBaseName = baseName || "未命名文件";
const pageNumber = Number(page) || index + 1;
return `${safeBaseName}-page-${pageNumber}`;
};
const deriveGroupFromTitle = (title) => {
const raw = String(title || "").trim();
if (!raw) return { groupKey: "", page: null, isPaged: false };
const match = raw.match(/^(.*?)-page-(\d+)$/i);
if (!match) {
return { groupKey: raw, page: null, isPaged: false };
}
return {
groupKey: match[1].trim() || raw,
page: Number(match[2]) || null,
isPaged: true,
};
};
const buildStableGroupKey = (baseName, sourceUrl) => {
const fromBase = String(baseName || "").trim();
if (fromBase) return fromBase;
const fromUrl = deriveBaseNameFromUrl(sourceUrl);
return fromUrl || "";
};
// ==================== 图片库相关接口 ====================
/**
* 获取图片库列表
*/
module.exports.getLibraryList = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { page, pageSize, keyword, departmentId, userId } =
ctx.request.query;
const where = {};
if (departmentId) {
where.departmentId = departmentId;
}
// 支持按名称模糊搜索
if (keyword) {
const Op = ctx.app.fs.dc.ORM.Op;
where.name = { [Op.like]: `%${keyword}%` };
}
const options = {
where,
order: [["updatedAt", "DESC"]],
raw: true,
};
// 分页处理
if (page && pageSize) {
options.offset = (page - 1) * pageSize;
options.limit = parseInt(pageSize);
}
const libraryList =
await models.TenderImageLibrary.findAndCountAll(options);
const uid = parseUserId(userId);
const rows = (libraryList.rows || []).filter((item) => {
const scope = item.visibilityScope || "all";
const isCreator = uid ? parseUserId(item.creator) === uid : false;
if (scope === "all") {
// 未传部门时,避免“公开库”误返回全量数据,只保留本人创建。
if (!departmentId) return isCreator;
return true;
}
const visibleUsers = Array.isArray(item.visibleUserIds)
? item.visibleUserIds
: [];
// 可见人员为空时,表示“全部门可见”;但未传部门时仅返回本人创建。
if (!visibleUsers.length) {
if (!departmentId) return isCreator;
return true;
}
if (!uid) return false;
if (isCreator) return true;
return visibleUsers.includes(uid);
});
ctx.body = { ...libraryList, rows:rows || [], count: rows.length || 0 };
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "获取图片库列表失败",
};
}
};
/**
* 获取图片库详情
*/
module.exports.getLibraryDetail = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { libraryId } = ctx.params;
if (!libraryId) {
throw "缺少参数";
}
const library = await models.TenderImageLibrary.findOne({
where: { id: libraryId },
include: [
{
model: models.TenderImages,
},
],
});
if (!library) {
throw "图片库不存在";
}
ctx.body = library;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "获取图片库详情失败",
};
}
};
/**
* 获取图片库分组(用于专利/软著库的“管理文件夹”展示)
* 规则:标题形如 xxx-page-N 的图片按 xxx 分组,且仅当分组内数量>1时作为文件夹返回
*/
module.exports.getLibraryGroups = async (ctx, next) => {
try {
const { models, ORM } = ctx.app.fs.dc;
const { libraryId } = ctx.params;
const { keyword, departmentId } = ctx.request.query || {};
if (!libraryId) {
throw new Error("缺少参数:libraryId");
}
const libraryWhere = { id: libraryId };
if (departmentId) {
libraryWhere.departmentId = departmentId;
}
const library = await models.TenderImageLibrary.findOne({
where: libraryWhere,
raw: true,
});
if (!library) {
throw new Error("图片库不存在或无权限");
}
const where = { libraryId };
if (keyword) {
where.title = { [ORM.Op.like]: `%${keyword}%` };
}
const images = await models.TenderImages.findAll({
where,
order: [["createdAt", "DESC"]],
raw: true,
});
const groupMap = new Map();
images.forEach((img) => {
const derived = deriveGroupFromTitle(img.title);
const stableGroupKey =
(img.groupKey && String(img.groupKey).trim()) || derived.groupKey;
const stablePage = img.pageNo ?? derived.page;
if (!stableGroupKey || stablePage == null) return;
if (!groupMap.has(stableGroupKey)) groupMap.set(stableGroupKey, []);
groupMap.get(stableGroupKey).push({
...img,
groupKey: stableGroupKey,
page: Number(stablePage) || null,
});
});
const groups = Array.from(groupMap.entries())
.map(([groupKey, items]) => ({
groupKey,
count: items.length,
items: items.sort((a, b) => (a.page || 0) - (b.page || 0)),
}))
.filter((group) => group.count > 1)
.sort((a, b) => b.count - a.count);
const groupedKeys = new Set(groups.map((g) => g.groupKey));
const singles = images
.filter((img) => {
const derived = deriveGroupFromTitle(img.title);
const stableGroupKey =
(img.groupKey && String(img.groupKey).trim()) || derived.groupKey;
const stablePage = img.pageNo ?? derived.page;
if (!stableGroupKey || stablePage == null) return true;
return !groupedKeys.has(stableGroupKey);
})
.map((img) => {
const derived = deriveGroupFromTitle(img.title);
return {
...img,
groupKey: img.groupKey || derived.groupKey,
page: img.pageNo ?? derived.page,
isPaged: derived.isPaged,
};
});
ctx.status = 200;
ctx.body = {
libraryId: Number(libraryId),
groups,
singles,
};
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: error?.message || "获取图片库分组失败",
};
}
};
/**
* 创建图片库
*/
module.exports.addLibrary = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const {
name,
description,
creator,
departmentId,
visibleUsers,
} = ctx.request.body;
if (!name) {
throw "缺少参数:name";
}
const normalizedVisible = normalizeVisibleUsers(visibleUsers);
const isAllVisible = !normalizedVisible.length;
const library = await models.TenderImageLibrary.create(
{
name,
description,
creator,
departmentId,
visibilityScope: isAllVisible ? "all" : "custom",
visibleUserIds: isAllVisible ? [] : normalizedVisible,
},
{ returning: true },
);
ctx.body = library;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "创建图片库失败",
};
}
};
/**
* 修改图片库
*/
module.exports.updateLibrary = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { libraryId } = ctx.params;
const body = ctx.request.body;
if (!libraryId) {
throw "缺少参数";
}
const library = await models.TenderImageLibrary.findOne({
where: { id: libraryId },
raw: true,
});
if (!library) {
throw "图片库不存在";
}
let payload = { ...body };
if (Array.isArray(body.visibleUsers)) {
const normalizedVisible = normalizeVisibleUsers(body.visibleUsers);
const isAllVisible = !normalizedVisible.length;
payload.visibleUserIds = isAllVisible ? [] : normalizedVisible;
payload.visibilityScope = isAllVisible ? "all" : "custom";
}
await models.TenderImageLibrary.update(
payload,
{ where: { id: libraryId } },
);
ctx.status = 204;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "修改图片库失败",
};
}
};
/**
* 删除图片库(级联删除关联的图片)
*/
module.exports.deleteLibrary = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const {
models,
ORM: { Op },
} = ctx.app.fs.dc;
const { libraryId } = ctx.params;
if (!libraryId) {
throw "缺少参数";
}
// 查询图片库里所有有collectionId的图片
const images = await models.TenderImages.findAll({
attributes: ["id", "datasetCollectionId"],
where: { libraryId, datasetCollectionId: { [Op.ne]: null } },
transaction,
});
// 由于数据库设置了级联删除,直接删除图片库即可
await models.TenderImageLibrary.destroy({
where: { id: libraryId },
transaction,
});
// 从FastGPT知识库删除图片数据
for (const image of images) {
if (image.datasetCollectionId) {
deleteCollectionToFastGpt(ctx, image.datasetCollectionId);
}
}
ctx.status = 204;
await transaction.commit();
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "删除图片库失败",
};
}
};
// ==================== 图片相关接口 ====================
/**
* 获取图片列表
*/
module.exports.getImageList = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { page, pageSize, libraryId, keyword, departmentId, webSearch } = ctx.request.query;
const where = {};
const Op = ctx.app.fs.dc.ORM.Op;
// 按图片库ID筛选
if (libraryId) {
where.libraryId = libraryId;
}
// 支持按标题模糊搜索
if (keyword) {
where.title = { [Op.like]: `%${keyword}%` };
}
const tenderImageLibraryWhere = {};
if (departmentId) {
tenderImageLibraryWhere.departmentId = departmentId;
}
const options = {
where,
order: [["createdAt", "DESC"]],
include: [
{
model: models.TenderImageLibrary,
attributes: ["id", "name"],
where: tenderImageLibraryWhere,
},
{
model: models.TenderImageTagRelation,
include: [
{
model: models.TenderImageTags,
attributes: ["id", "tagName"],
},
],
},
],
};
// 分页处理
if (page && pageSize) {
options.offset = (page - 1) * pageSize;
options.limit = parseInt(pageSize);
}
const imageList = await models.TenderImages.findAndCountAll(options);
let returnList = imageList;
if (webSearch === "true" && keyword) {
try {
const { apiUrl: apiHzUrl, id: apiHzId, key: apiHzKey } = ctx.app.fs.config.apihz;
const webSearchResults = await superagent
.get(`${apiHzUrl}/api/img/apihzimgbaidu.php`)
.query({
id: apiHzId,
key: apiHzKey,
words: keyword, // 搜索关键词
page: 1, // 页码(默认1)
limit: 20, // 返回数量(1-100,默认1)
type: 1 // 返回源类型:1=百度预览图(默认),2=原始图(可能失效)
});
if (webSearchResults
&& webSearchResults.body
&& webSearchResults.body.res
&& webSearchResults.body.res.length > 0
) {
returnList.webSearchRows = webSearchResults.body.res;
} else if (webSearchResults?.body?.code === 400 && webSearchResults?.body?.msg?.startsWith("调用频次过快")) {
returnList.webSearchRows = [];
returnList.webSearchMsg = "联网搜索调用频次过快,请稍后再试";
}
} catch (error) {
console.log("[webSearchImages] Error:", error);
}
}
ctx.body = returnList;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "获取图片列表失败",
};
}
};
/**
* 获取图片详情
*/
module.exports.getImageDetail = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { imageId } = ctx.params;
if (!imageId) {
throw "缺少参数";
}
const image = await models.TenderImages.findOne({
where: { id: imageId },
include: [
{
model: models.TenderImageLibrary,
attributes: ["id", "name"],
},
{
model: models.TenderImageTagRelation,
include: [
{
model: models.TenderImageTags,
attributes: ["id", "tagName"],
},
],
},
],
});
if (!image) {
throw "图片不存在";
}
ctx.body = image;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "获取图片详情失败",
};
}
};
/**
* 添加图片
*/
module.exports.addImage = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const { libraryId, title, imageUrl, images } = ctx.request.body || {};
if (!libraryId) {
throw "缺少必要参数";
}
const sourceImages = Array.isArray(images) && images.length
? images
: [{ title, imageUrl }];
const normalizedImages = sourceImages
.map((item) => {
const itemTitle = item?.title;
const itemImageUrl = item?.imageUrl;
if (!itemTitle || !itemImageUrl) return null;
return {
libraryId,
title: itemTitle,
description: item?.description,
imageUrl: itemImageUrl,
status: "training",
};
})
.filter(Boolean);
if (!normalizedImages.length) {
throw "缺少必要参数";
}
const createdImages = [];
for (const item of normalizedImages) {
const image = await models.TenderImages.create(item, {
returning: true,
transaction,
});
createdImages.push(image);
}
await transaction.commit();
ctx.body = Array.isArray(images) ? createdImages : createdImages[0];
ctx.status = 200;
createdImages.forEach((image) => {
uploadImageToFastGpt(ctx, image);
});
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "添加图片失败",
};
}
};
/**
* 解析图片数据并上传到FastGPT知识库
*/
async function uploadImageToFastGpt(ctx, image) {
const { models } = ctx.app.fs.dc;
try {
const { apiUrl, v2TenderImageAppKey, imageDatasetId } =
ctx.app.fs.config.fastGpt;
// 获取FastGPT Token(使用缓存)
let fastgptToken = await getFastGptToken(ctx.app.fs.config);
// 分析图片特征
const imageFeatureRes = await superagent
.post(`${apiUrl}/api/v1/chat/completions`)
.send({
stream: false,
detail: false,
messages: [
{
role: "user",
content: [
{
type: "image_url",
image_url: { url: image.imageUrl },
},
],
},
],
})
.set({
Authorization: `Bearer ${v2TenderImageAppKey}`,
"Content-Type": "application/json",
});
const imageFeature = imageFeatureRes.body.choices[0].message.content;
// 构建txt文件内容
const txtContent = `图片URL:${image.imageUrl}\n图片标题:${image.title}\n图片描述:${imageFeature}`;
// 调用 FastGPT 上传文件接口(带重试逻辑)
//修改部分
// const fileName = encodeURIComponent(`${image.id}-${image.title}.txt`);
const fileName = `${image.id}-${image.title}`;
let fastgptFileId = null;
let retryCount = 0;
const maxRetries = 1;
// 确认参数开始训练
const qaPrompt = `<Context></Context> 标记中是一段文本,学习和分析它,并整理学习成果:
- 提出问题并给出每个问题的答案。
- 答案需详细完整,尽可能保留原文描述
- 答案可以包含普通文字、链接、代码、表格、公示、媒体链接等 Markdown 元素。
- 生成的问题和答案和源文本语言相同。
- 如果实在找不到问题的答案则留空,不要胡乱填写
- 问题1:图片URL是什么?
- 问题2:请从文本中"图片标题:"后面提取图片标题是什么?
- 问题3:图片描述是什么?`;
const trainRes = await superagent
//修改部分
// .post(`${apiUrl}/api/core/dataset/collection/create/fileId`)
.post(`${apiUrl}/api/core/dataset/collection/create/text`)//修改部分
.set("Content-Type", "application/json")
.set("Accept", "application/json, text/plain, */*")
.set("Cookie", `fastgpt_token=${fastgptToken}`)
//修改部分,传参修改成qa
.send({
datasetId: imageDatasetId,
text: txtContent,
name: fileName,
trainingType: "qa",
chunkSettingMode: "auto",
qaPrompt,
});
const datasetCollectionId = trainRes.body.data.collectionId;
// 更新图片表
await models.TenderImages.update(
{
description: imageFeature,
datasetCollectionId: datasetCollectionId,
status: "trainingSuccess",
},
{ where: { id: image.id } },
);
} catch (error) {
console.error(`[uploadImageToFastGpt imageId: ${image.id}]: ${error}`);
try {
await models.TenderImages.update(
{ status: "trainingFailure" },
{ where: { id: image.id } },
);
} catch (error) {
console.error(
`[uploadImageToFastGpt imageId: ${image.id}]: 更新状态失败`,
);
}
}
}
//修改部分// * 从FastGPT知识库删除图片数据
async function deleteCollectionToFastGpt(ctx, collectionId) {
const { apiUrl } = ctx.app.fs.config.fastGpt;
let fastgptToken = await getFastGptToken(ctx.app.fs.config);
let retryCount = 0;
const maxRetries = 1;
while (retryCount <= maxRetries) {
try {
await superagent
.delete(`${apiUrl}/api/core/dataset/collection/delete?id=${collectionId}`)
.set("Cookie", `fastgpt_token=${fastgptToken}`);
break;
} catch (error) {
const isAuthError =
error?.response?.text?.includes("unAuthorization") ||
error?.response?.statusCode === 401 ||
error?.status === 401;
if (isAuthError && retryCount < maxRetries) {
console.log(`[deleteCollectionToFastGpt] Token认证失败,尝试重新获取token`);
fastgptToken = await getFastGptToken(ctx.app.fs.config, true);
retryCount++;
} else if (error?.response?.body?.code === 501003) {
// collection 不存在,不需要再删
break;
} else {
throw error;
}
}
}
}
/**
* 修改图片
*/
module.exports.updateImage = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { imageId } = ctx.params;
const body = ctx.request.body;
if (!imageId) {
throw "缺少参数";
}
await models.TenderImages.update(
{ ...body, status: "waitSync" },
{ where: { id: imageId } },
);
ctx.status = 204;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "修改图片失败",
};
}
};
/**
* 删除图片
*/
module.exports.deleteImage = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const { imageId } = ctx.params;
if (!imageId) {
throw "缺少参数";
}
const image = await models.TenderImages.findByPk(imageId);
if (!image) {
throw "图片不存在";
}
// 级联删除会自动删除关联的标签关系
await models.TenderImages.destroy({
where: { id: imageId },
transaction,
});
// 从FastGPT知识库删除图片数据
if (image.datasetCollectionId) {
await deleteCollectionToFastGpt(ctx, image.datasetCollectionId);
}
ctx.status = 204;
await transaction.commit();
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "删除图片失败",
};
}
};
// ==================== 标签相关接口 ====================
/**
* 获取标签列表
*/
module.exports.getTagList = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { page, pageSize, keyword } = ctx.request.query;
const where = {};
// 支持按标签名模糊搜索
if (keyword) {
const Op = ctx.app.fs.dc.ORM.Op;
where.tagName = { [Op.like]: `%${keyword}%` };
}
const options = {
where,
order: [["createdAt", "DESC"]],
raw: true,
};
// 分页处理
if (page && pageSize) {
options.offset = (page - 1) * pageSize;
options.limit = parseInt(pageSize);
}
const tagList = await models.TenderImageTags.findAndCountAll(options);
ctx.body = tagList;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "获取标签列表失败",
};
}
};
/**
* 创建标签
*/
module.exports.addTag = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { tagName } = ctx.request.body;
if (!tagName) {
throw "缺少参数:tagName";
}
const tag = await models.TenderImageTags.create(
{ tagName },
{ returning: true },
);
ctx.body = tag;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "创建标签失败",
};
}
};
/**
* 修改标签
*/
module.exports.updateTag = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { tagId } = ctx.params;
const { tagName } = ctx.request.body;
if (!tagId || !tagName) {
throw "缺少参数";
}
await models.TenderImageTags.update(
{ tagName },
{ where: { id: tagId } },
);
ctx.status = 204;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "修改标签失败",
};
}
};
/**
* 删除标签
*/
module.exports.deleteTag = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const { tagId } = ctx.params;
if (!tagId) {
throw "缺少参数";
}
// 级联删除会自动删除关联关系
await models.TenderImageTags.destroy({
where: { id: tagId },
transaction,
});
ctx.status = 204;
await transaction.commit();
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "删除标签失败",
};
}
};
// ==================== 图片标签关联接口 ====================
/**
* 为图片添加标签
*/
module.exports.addImageTag = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const { imageId } = ctx.params;
const { tagId } = ctx.request.body;
if (!imageId || !tagId) {
throw "缺少参数";
}
const relation = await models.TenderImageTagRelation.create(
{ imageId, tagId },
{
returning: true,
transaction,
},
);
await models.TenderImages.update(
{ status: "waitSync" },
{ where: { id: imageId }, transaction },
);
await transaction.commit();
ctx.body = relation;
ctx.status = 200;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "添加图片标签失败",
};
}
};
/**
* 批量为图片添加标签
*/
module.exports.batchAddImageTags = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const { imageId } = ctx.params;
const { tagIds } = ctx.request.body;
if (!imageId || !tagIds || !Array.isArray(tagIds)) {
throw "缺少参数";
}
const relations = tagIds.map((tagId) => ({ imageId, tagId }));
const createdRelations = await models.TenderImageTagRelation.bulkCreate(
relations,
{ transaction, returning: true },
);
await models.TenderImages.update(
{ status: "waitSync" },
{ where: { id: imageId }, transaction },
);
ctx.body = createdRelations;
ctx.status = 200;
await transaction.commit();
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "批量添加图片标签失败",
};
}
};
/**
* 删除图片标签关联
*/
module.exports.deleteImageTag = async (ctx, next) => {
const transaction = await ctx.app.fs.dc.orm.transaction();
try {
const { models } = ctx.app.fs.dc;
const { imageId, tagId } = ctx.params;
if (!imageId || !tagId) {
throw "缺少参数";
}
await models.TenderImageTagRelation.destroy({
where: { imageId: Number(imageId), tagId: Number(tagId) },
transaction,
});
await models.TenderImages.update(
{ status: "waitSync" },
{ where: { id: Number(imageId) }, transaction },
);
await transaction.commit();
ctx.status = 204;
} catch (error) {
await transaction.rollback();
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "删除图片标签关联失败",
};
}
};
/**
* 按标签搜索图片
*/
module.exports.getImagesByTag = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const { tagId } = ctx.params;
const { page, pageSize, departmentId } = ctx.request.query;
if (!tagId) {
throw "缺少参数";
}
const tenderImageLibraryWhere = {};
if (departmentId) {
tenderImageLibraryWhere.departmentId = departmentId;
}
const options = {
where: { tagId },
include: [
{
model: models.TenderImages,
include: [
{
model: models.TenderImageLibrary,
attributes: ["id", "name"],
where: tenderImageLibraryWhere,
},
],
required: true,
},
],
order: [["createdAt", "DESC"]],
};
// 分页处理
if (page && pageSize) {
options.offset = (page - 1) * pageSize;
options.limit = parseInt(pageSize);
}
const relations =
await models.TenderImageTagRelation.findAndCountAll(options);
ctx.body = relations;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: typeof error == "string" ? error : "按标签搜索图片失败",
};
}
};
//获取项企信息+部门
module.exports.getDepartment = async (ctx, next) => {
try {
const { emisApi } = ctx.config.pep;
const { departmentId } = ctx.request.query;
const token = await getPepToken(ctx);
const userRes = await superagent.get(
`${emisApi}/dept/user?token=${token}`,
);
const departments = Array.isArray(userRes._body || userRes.body ) ? userRes._body || userRes.body : [];
// 保留 departmentId 查询的兼容能力;未传时返回完整部门树,供人员选择器使用。
ctx.body = departmentId
? departments.find((item) => item.id === Number(departmentId))
: departments;
ctx.status = 200;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: error?.message || "获取部门失败" };
}
};
//专利接口
module.exports.getPatents = async (ctx, next) => {
try {
const params = ctx.request.query;
const res = await requestKnowledgeApi(ctx, {
method: "get",
path: "/_api/patents",
query: params,
});
const status = res?.status || res?.statusCode || 200;
if (status < 200 || status >= 300) {
ctx.status = status;
ctx.body = res?.body || {
message: "获取知识产权失败",
};
return;
}
ctx.body = res.body;
ctx.status = status;
} catch (error) {
ctx.logger.log(error);
const status = error?.response?.status || 400;
ctx.status = status;
ctx.body =
error?.response?.body || { message: error?.message || "获取知识产权失败" };
}
};
//软著搜索
module.exports.software = async (ctx, next) => {
try {
const params = ctx.request.query;
const res = await requestKnowledgeApi(ctx, {
method: "get",
path: "/_api/software-copyrights",
query: params,
});
const status = res?.status || res?.statusCode || 200;
if (status < 200 || status >= 300) {
ctx.status = status;
ctx.body = res?.body || {
message: "获取知识产权失败",
};
return;
}
ctx.body = res.body;
ctx.status = status;
} catch (error) {
ctx.logger.log(error);
const status = error?.response?.status || 400;
ctx.status = status;
ctx.body =
error?.response?.body || { message: error?.message || "获取知识产权失败" };
}
};
//知识产权文件详情
module.exports.getKnowledgeFiles = async (ctx, next) => {
try {
const params = ctx.request.query;
const res = await requestKnowledgeApi(ctx, {
method: "get",
path: "/_api/files",
query: params,
});
ctx.body = res.body;
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: error?.message || "获取知识产权文件失败" };
}
};
//pdf转图片
module.exports.pdfToImage = async (ctx, next) => {
let tempDir = null;
try {
const { pdfUrl, dpi, startPage, endPage } =
ctx.request.body || ctx.request.query || {};
if (!pdfUrl) {
ctx.status = 400;
ctx.body = { message: "缺少pdfUrl" };
return;
}
tempDir = await fsPromises.mkdtemp(path.join(os.tmpdir(), "pdf2image-"));
const inputPath = path.join(tempDir, `input_${Date.now()}.pdf`);
const outputDir = tempDir;
const token = await getPepToken(ctx);
const res = await superagent
.get(pdfUrl)
.set("Authorization", token)
.buffer(true);
await fsPromises.writeFile(inputPath, res.body);
const images = await convertPdfToImages(
inputPath,
outputDir,
Number(dpi) || 150,
Number(startPage) || 1,
Number(endPage) || undefined,
);
const cleanPdfUrl = decodeURIComponent(
String(pdfUrl).split("#")[0].split("?")[0],
);
const pdfBaseName = path.basename(cleanPdfUrl);
const pdfNameWithoutExt =
pdfBaseName.slice(0, pdfBaseName.length - path.extname(pdfBaseName).length) ||
String(Date.now());
const fileName = pdfNameWithoutExt;
const uuidPrefix = randomUUID();
const uploadedImages = [];
for (let i = 0; i < images.length; i++) {
const imagePath = images[i];
const ext = path.extname(imagePath) || ".png";
const match = path.basename(imagePath).match(/page-(\d+)\.png$/i);
const pageNum = match ? Number(match[1]) : i + 1;
const key = `${uuidPrefix}/${fileName}/page-${pageNum}${ext}`;
const url = await uploadFileToQiniu(ctx, imagePath, key);
uploadedImages.push(url);
}
ctx.status = 200;
ctx.body = {
message: "文件转换成功",
data: {
images: uploadedImages,
startPage: Number(startPage) || 1,
endPage: Number(endPage) || null,
},
};
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = { message: error?.message || "pdf转图片失败" };
} finally {
if (tempDir) {
try {
await fsPromises.rm(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
ctx.logger.log(cleanupError);
}
}
}
};
module.exports.getKnowledgeQualificationPermission = async (ctx, next) => {
try {
const userId = Number(ctx.request.query?.userId);
if (!Number.isInteger(userId) || userId <= 0) {
ctx.status = 400;
ctx.body = { message: "userId无效" };
return;
}
const res = await requestKnowledgeApi(ctx, {
method: "get",
path: `/_api/system/users/${userId}/qualification-permission`,
});
ctx.status = res?.status || 200;
ctx.body = res.body;
} catch (error) {
ctx.logger.log(error);
ctx.status = error?.response?.status || 400;
ctx.body =
error?.response?.body || { message: error?.message || "查询资质权限失败" };
}
};
module.exports.getQualificationCerts = async (ctx, next) => {
try {
const params = {
...ctx.request.query,
};
const res = await requestKnowledgeApi(ctx, {
method: "get",
path: "/_api/qualification/certs",
query: params,
});
ctx.status = res?.status || 200;
ctx.body = res.body;
} catch (error) {
ctx.logger.log(error);
ctx.status = error?.response?.status || 400;
ctx.body =
error?.response?.body || { message: error?.message || "获取资质证照失败" };
}
};
/**
* 将知识产权系统选中的图片入库(用户专属专利/软著库)
*/
module.exports.addKnowledgeImagesToLibrary = async (ctx, next) => {
try {
const { models } = ctx.app.fs.dc;
const {
userId,
libraryType,
departmentId,
baseName,
sourceUrl,
images,
} = ctx.request.body || {};
if (
!userId ||
!libraryType ||
!departmentId ||
!Array.isArray(images) ||
!images.length
) {
throw new Error("缺少必要参数:userId, libraryType, departmentId, images");
}
const library = await ensureUserLibraryByType(ctx, {
userId,
libraryType,
departmentId,
});
const resolvedBaseName = baseName || deriveBaseNameFromUrl(sourceUrl);
const stableGroupKey = buildStableGroupKey(resolvedBaseName, sourceUrl);
const normalizedImages = images
.map((item, index) => {
const url = typeof item === "string" ? item : item?.url;
if (!url) return null;
const page = typeof item === "string" ? undefined : item?.page;
const title = buildImageTitle({ baseName: resolvedBaseName, page, index });
const pageNo = Number(page) || index + 1;
return {
url,
page: pageNo,
pageNo,
title,
groupKey: stableGroupKey,
sourceUrl: sourceUrl || null,
};
})
.filter(Boolean);
if (!normalizedImages.length) {
throw new Error("images中没有可用的图片url");
}
const existingImages = await models.TenderImages.findAll({
where: {
libraryId: library.id,
groupKey: stableGroupKey,
pageNo: normalizedImages.map((item) => item.pageNo),
},
raw: true,
});
const existingPageSet = new Set(existingImages.map((item) => item.pageNo));
const inserted = [];
const skipped = [];
for (const item of normalizedImages) {
if (existingPageSet.has(item.pageNo)) {
skipped.push(item);
continue;
}
const created = await models.TenderImages.create(
{
libraryId: library.id,
title: item.title,
groupKey: item.groupKey,
pageNo: item.pageNo,
sourceUrl: item.sourceUrl,
imageUrl: item.url,
status: "training",
},
{ returning: true },
);
inserted.push(created);
uploadImageToFastGpt(ctx, created);
}
ctx.status = 200;
ctx.body = {
message: "入库完成",
data: {
libraryId: library.id,
libraryName: library.name,
insertedCount: inserted.length,
skippedCount: skipped.length,
inserted,
skipped,
},
};
} catch (error) {
ctx.logger.log(error);
ctx.status = 400;
ctx.body = {
message: error?.message || "知识产权图片入库失败",
};
}
};