import { ALLOW_EXTERNAL_LINK_DOMAIN } from "@/constants/config";
import { TENANT_CONFIG_INFO } from "@/constants/home.constants";
import { SUCCESS_CODE } from "@/constants/codes.constants";
import { apiTenantConfig } from "@/servers/account";
import { apiGetStaticFileList } from "@/servers/agentDev.uts";
import { t } from "@/utils/i18n";
const SYSTEM_INFO = uni.getSystemInfoSync();
export const getStatusBarHeight = () => SYSTEM_INFO.statusBarHeight || 15;
export const formatDate = (
input: string | number | Date,
format:
| "YYYY-MM-DD"
| "MM-DD"
| "HH:mm"
| "YYYY-MM-DD HH:mm:ss"
| "YYYY-MM-DD HH:mm" = "MM-DD",
): string => {
const date = new Date(input);
const YYYY = date.getFullYear();
const MM = String(date.getMonth() + 1).padStart(2, "0");
const DD = String(date.getDate()).padStart(2, "0");
const HH = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
switch (format) {
case "YYYY-MM-DD":
return `${YYYY}-${MM}-${DD}`;
case "YYYY-MM-DD HH:mm:ss":
const ss = String(date.getSeconds()).padStart(2, "0");
return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`;
case "YYYY-MM-DD HH:mm":
return `${YYYY}-${MM}-${DD} ${HH}:${mm}`;
case "HH:mm":
return `${HH}:${mm}`;
default:
return `${MM}-${DD}`;
}
};
// 判断是否是允许外部链接跳转的域名
export const isAllowExternalLinkDomain = (url: string) => {
return ALLOW_EXTERNAL_LINK_DOMAIN.some((domain) => url.includes(domain));
};
/**
* 根据链接地址判断不允许调整的页面需要复制链接到剪切板
* @param url 链接地址
* @returns void
*/
export const handleExternalLink = (url: string) => {
// 如果链接地址为空,则提示链接地址配置错误
if (!url) {
uni.showToast({
title: t("Mobile.Link.invalidConfig"),
icon: "none",
});
return;
}
// #ifdef H5 || WEB
// H5 环境直接打开外部链接
try {
// 判断是否为有效的 URL
const isValidUrl =
url.startsWith("http://") ||
url.startsWith("https://") ||
url.startsWith("//");
if (isValidUrl) {
// 尝试在新标签页中打开链接
const newWindow = window.open(url, "_blank");
// 检查是否被浏览器拦截(某些浏览器会阻止弹窗)
if (
!newWindow ||
newWindow.closed ||
typeof newWindow.closed === "undefined"
) {
// 如果被拦截,提示用户并复制链接到剪贴板
console.warn("链接被浏览器拦截,使用备用方案");
// 尝试复制到剪贴板
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(url)
.then(() => {
uni.showToast({
title: t("Mobile.Link.copyAndOpen"),
icon: "none",
duration: 3000,
});
})
.catch(() => {
// 如果复制失败,直接在当前页面打开
window.location.href = url;
});
} else {
// 如果不支持剪贴板 API,直接在当前页面打开
window.location.href = url;
}
}
} else {
// 如果不是完整的 URL,使用 navigateTo 跳转到 webview 页面
uni.navigateTo({
url: `/subpackages/pages/webview/webview?url=${encodeURIComponent(url)}`,
});
}
} catch (error) {
console.error("打开链接失败:", error);
uni.showToast({
title: t("Mobile.Link.openFailed"),
icon: "none",
});
}
// #endif
// #ifdef MP-WEIXIN
// 如果允许外部链接跳转,则打开外部链接
if (isAllowExternalLinkDomain(url)) {
uni.navigateTo({
url: `/subpackages/pages/webview/webview?url=${encodeURIComponent(url)}`,
});
} else {
uni.setClipboardData({
data: url,
success: () => {
uni.showToast({
title: t("Mobile.Link.copied"),
icon: "none",
});
},
});
}
// #endif
};
// 获取用户配置并设置页面标题和缓存租户信息
export const fetchTenantConfig = async () => {
const { code, data } = await apiTenantConfig();
const siteName = data?.siteName;
if (code === SUCCESS_CODE && siteName) {
// 设置当前页面导航栏标题
uni.setNavigationBarTitle({ title: siteName });
// 缓存租户信息
uni.setStorageSync(TENANT_CONFIG_INFO, JSON.stringify(data));
}
};
// 设置当前页面导航栏标题
export const setCurrentPageNavigationBarTitle = async () => {
// 获取本地用户配置
const tenantConfigInfoString = await uni.getStorageSync(TENANT_CONFIG_INFO);
if (tenantConfigInfoString) {
try {
// JSON.parse 返回 unknown,这里显式约束为仅关心 siteName 的结构
const tenantConfig = JSON.parse(tenantConfigInfoString) as {
siteName?: string;
};
const { siteName } = tenantConfig;
if (siteName) {
// 设置当前页面导航栏标题
uni.setNavigationBarTitle({ title: siteName });
return;
}
} catch (error) {
console.error("获取本地用户配置失败:", error);
}
}
fetchTenantConfig(); // 获取用户配置并设置页面标题和缓存租户信息
};
// 根据会话id和文件路径 跳转至预览页面
export const jumpToFilePreviewPage = (
conversationId: string,
fileProxyUrl: string,
) => {
if (!conversationId || !fileProxyUrl) {
return;
}
const path = "/subpackages/pages/file-preview-page/file-preview-page";
const url = `${path}?cId=${conversationId}&fileProxyUrl=${encodeURIComponent(fileProxyUrl)}`;
uni.navigateTo({ url });
};
// 根据会话id查询文件列表并根据文件路径获取到文件代理url
export const getFileProxyUrlByConversationIdAndFilePath = async (
conversationId: string,
filePath: string,
) => {
if (!conversationId || !filePath) {
return null;
}
// 接口要求 number 类型,先做显式转换并校验,避免把非法字符串传入服务层
const normalizedConversationId = Number(conversationId);
if (Number.isNaN(normalizedConversationId)) {
return null;
}
const result = await apiGetStaticFileList(normalizedConversationId);
if (result.code === SUCCESS_CODE) {
const { files } = result.data || {};
const file = files.find((item) => item.name === filePath);
return file?.fileProxyUrl;
}
return null;
};
/**
* 任务结果数据
*/
interface TaskResultData {
hasTaskResult: boolean;
description?: string;
file?: string;
}
/**
* 提取任务结果数据
* @param text 文本
* @returns 任务结果数据
*/
export const extractTaskResult = (text: string): TaskResultData => {
const result: TaskResultData = {
hasTaskResult: false,
};
if (!text) return result;
// 1️⃣ 匹配 ...
const taskResultMatch = text.match(/([\s\S]*?)<\/task-result>/);
if (!taskResultMatch) {
return result;
}
result.hasTaskResult = true;
const taskResultContent = taskResultMatch[1];
// 2️⃣ 提取 description
const descriptionMatch = taskResultContent.match(
/([\s\S]*?)<\/description>/,
);
if (descriptionMatch) {
result.description = descriptionMatch[1].trim();
}
// 3️⃣ 提取 file
const fileMatch = taskResultContent.match(/([\s\S]*?)<\/file>/);
if (fileMatch) {
result.file = fileMatch[1].trim();
}
return result;
};
/**
* 提取字符串中最后一个 内的 内容
* @param text 原始字符串
* @returns file 内容或 null
*/
export const extractLastTaskResultFile = (text: string): string | null => {
if (!text) return null;
// 匹配所有 ...
const taskResultMatches = text.match(/[\s\S]*?<\/task-result>/g);
if (!taskResultMatches || taskResultMatches.length === 0) {
return null;
}
// 取最后一个
const lastTaskResult = taskResultMatches[taskResultMatches.length - 1];
// 在其中提取 内容
const fileMatch = lastTaskResult.match(/([\s\S]*?)<\/file>/);
return fileMatch?.[1]?.trim() ?? null;
};
/**
* 健壮的千分位数字格式化工具函数(支持整数、浮点数以及负数,带有强健的边界容错降级机制)
* @param value 输入数值或字符
* @returns 格式化后的千分位字符串
*/
export const formatNumber = (value: any, precision: number = -1): string => {
if (value == null) {
return "0";
}
let str = String(value).trim();
if (str === "" || str === "null" || str === "undefined") {
return "0";
}
const num = Number(str);
if (Number.isNaN(num) || !Number.isFinite(num)) {
return "0";
}
if (precision >= 0) {
str = num.toFixed(precision);
} else {
str = num.toString();
}
const isNegative = str.startsWith("-");
if (isNegative) {
str = str.substring(1);
}
const parts = str.split(".");
const integerPart = parts[0];
const decimalPart = parts.length > 1 ? "." + parts[1] : "";
let result = "";
let counter = 0;
for (let i = integerPart.length - 1; i >= 0; i--) {
result = integerPart.charAt(i) + result;
counter++;
if (counter === 3 && i !== 0) {
result = "," + result;
counter = 0;
}
}
return (isNegative ? "-" : "") + result + decimalPart;
};