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
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 || "知识产权图片入库失败",
|
|
};
|
|
}
|
|
};
|
|
|