fix: proxy unstable native image media

This commit is contained in:
winchen
2026-08-01 11:57:16 +08:00
parent 0d3d410bd2
commit e6d5dfbfc1
9 changed files with 549 additions and 42 deletions
+205
View File
@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
require __DIR__ . '/im_web_bootstrap.php';
const WEB_MEDIA_PROXY_MAX_BYTES = 15 * 1024 * 1024;
const WEB_MEDIA_PROXY_MAX_SECONDS = 25;
function failMediaProxy(int $status, string $message): never
{
http_response_code($status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
echo $message;
exit;
}
function mediaProxyAllowedHosts(): array
{
$configured = webImEnv('JC_WEB_IM_MEDIA_PROXY_HOSTS');
$hosts = array_map(
static fn(string $host): string => strtolower(rtrim(trim($host), '.')),
preg_split('/[,\s]+/', $configured) ?: []
);
return array_values(array_unique(array_filter($hosts)));
}
function validatedMediaProxyUrl(string $url): array
{
if ($url === '' || strlen($url) > 2048 || filter_var($url, FILTER_VALIDATE_URL) === false) {
failMediaProxy(400, '媒体地址不合法');
}
$parts = parse_url($url);
$scheme = strtolower((string)($parts['scheme'] ?? ''));
$host = strtolower(rtrim((string)($parts['host'] ?? ''), '.'));
$port = isset($parts['port']) ? (int)$parts['port'] : 443;
if (
$scheme !== 'https'
|| $host === ''
|| $port !== 443
|| isset($parts['user'])
|| isset($parts['pass'])
|| !in_array($host, mediaProxyAllowedHosts(), true)
) {
failMediaProxy(403, '媒体来源不允许代理');
}
return [$url, $host];
}
function mediaProxyCacheRoot(): string
{
$root = rtrim(webImEnv('JC_WEB_IM_MEDIA_CACHE_ROOT', '/var/cache/jc-web-im-media'), '/');
if ($root === '' || !is_dir($root) || !is_writable($root)) {
failMediaProxy(503, '媒体缓存目录不可用');
}
return $root;
}
function mediaProxyMime(string $path): string
{
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = $finfo !== false ? (string)(finfo_file($finfo, $path) ?: '') : '';
if ($finfo !== false) {
finfo_close($finfo);
}
return $mime;
}
function fetchMediaProxyImage(string $url, string $host, string $cacheFile): void
{
$deadline = microtime(true) + WEB_MEDIA_PROXY_MAX_SECONDS;
$lastError = '媒体源无响应';
for ($attempt = 1; $attempt <= 6 && microtime(true) < $deadline; $attempt++) {
$temporaryFile = $cacheFile . '.tmp.' . getmypid() . '.' . $attempt;
$stream = @fopen($temporaryFile, 'x+b');
if ($stream === false) {
$lastError = '无法创建媒体缓存文件';
break;
}
$bytes = 0;
$tooLarge = false;
$curl = curl_init($url);
if ($curl === false) {
fclose($stream);
@unlink($temporaryFile);
$lastError = '无法初始化媒体请求';
break;
}
$remainingMs = max(1000, (int)floor(($deadline - microtime(true)) * 1000));
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT_MS => min(2500, $remainingMs),
CURLOPT_TIMEOUT_MS => min(12000, $remainingMs),
CURLOPT_NOSIGNAL => true,
CURLOPT_DNS_CACHE_TIMEOUT => 0,
CURLOPT_FRESH_CONNECT => true,
CURLOPT_FORBID_REUSE => true,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'JC-Web-IM-Media-Proxy/1.0',
CURLOPT_HTTPHEADER => ['Accept: image/avif,image/webp,image/*,*/*;q=0.8'],
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use ($stream, &$bytes, &$tooLarge): int {
$length = strlen($chunk);
if ($bytes + $length > WEB_MEDIA_PROXY_MAX_BYTES) {
$tooLarge = true;
return 0;
}
$written = fwrite($stream, $chunk);
if ($written === false) {
return 0;
}
$bytes += $written;
return $written;
},
]);
$result = curl_exec($curl);
$status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$curlError = curl_error($curl);
curl_close($curl);
fflush($stream);
fclose($stream);
$mime = $bytes > 0 && is_file($temporaryFile) ? mediaProxyMime($temporaryFile) : '';
if ($result !== false && $status >= 200 && $status < 300 && !$tooLarge && str_starts_with($mime, 'image/')) {
if (!@rename($temporaryFile, $cacheFile)) {
@unlink($temporaryFile);
$lastError = '媒体缓存保存失败';
break;
}
@chmod($cacheFile, 0644);
return;
}
@unlink($temporaryFile);
$lastError = $tooLarge
? '图片超过代理大小限制'
: ($curlError !== '' ? $curlError : ($status > 0 ? '媒体源返回 HTTP ' . $status : '媒体源无响应'));
if ($attempt < 6 && microtime(true) < $deadline) {
usleep(150000);
}
}
error_log(sprintf('media_proxy fetch failed host=%s error=%s', $host, $lastError));
failMediaProxy(502, '图片源暂时不可用,请稍后重试');
}
function serveMediaProxyImage(string $cacheFile, string $etag): never
{
if (!is_file($cacheFile)) {
failMediaProxy(404, '媒体缓存不存在');
}
$mime = mediaProxyMime($cacheFile);
if (!str_starts_with($mime, 'image/')) {
failMediaProxy(415, '媒体内容不是图片');
}
header('ETag: "' . $etag . '"');
header('Cache-Control: public, max-age=86400, immutable');
header('Content-Type: ' . $mime);
header('Content-Length: ' . (string)filesize($cacheFile));
header('Content-Disposition: inline');
header('X-Content-Type-Options: nosniff');
if (trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? ''), ' W/') === '"' . $etag . '"') {
http_response_code(304);
exit;
}
readfile($cacheFile);
exit;
}
[$url, $host] = validatedMediaProxyUrl(trim((string)($_GET['url'] ?? '')));
$cacheRoot = mediaProxyCacheRoot();
$cacheKey = hash('sha256', $url);
$cacheFile = $cacheRoot . '/' . $cacheKey . '.image';
$lockFile = $cacheRoot . '/' . $cacheKey . '.lock';
if (!is_file($cacheFile)) {
$lock = @fopen($lockFile, 'c');
if ($lock === false || !flock($lock, LOCK_EX)) {
if (is_resource($lock)) {
fclose($lock);
}
failMediaProxy(503, '媒体缓存暂时忙碌');
}
if (!is_file($cacheFile)) {
fetchMediaProxyImage($url, $host, $cacheFile);
}
flock($lock, LOCK_UN);
fclose($lock);
}
serveMediaProxyImage($cacheFile, $cacheKey);
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Web IM</title><link rel="icon" href="img/5IuOieu9PF8MJtp43kD2kCkdLVjVwDp7.ico"><script src="config/runtime-config.js"></script><script defer="defer" src="js/chunk-vendors.86756863.js"></script><script defer="defer" src="js/app.84630d8b.js"></script><link href="css/app.2c52dabb.css" rel="stylesheet"></head><body><noscript>请启用 JavaScript 后使用 Web IM。</noscript><div id="app"></div></body></html>
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Web IM</title><link rel="icon" href="img/5IuOieu9PF8MJtp43kD2kCkdLVjVwDp7.ico"><script src="config/runtime-config.js"></script><script defer="defer" src="js/chunk-vendors.86756863.js"></script><script defer="defer" src="js/app.a89973f7.js"></script><link href="css/app.2c52dabb.css" rel="stylesheet"></head><body><noscript>请启用 JavaScript 后使用 Web IM。</noscript><div id="app"></div></body></html>
-1
View File
File diff suppressed because one or more lines are too long
+101 -36
View File
@@ -2879,9 +2879,10 @@ const _hoisted_14 = ["disabled"];
/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4__);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(5130);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(6768);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(5187);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(144);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(144);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(5187);
/* harmony import */ var _stores_contacts__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(9045);
/* harmony import */ var _services_mediaProxy__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(446);
@@ -2981,6 +2982,7 @@ const _hoisted_36 = ["disabled"];
const _hoisted_37 = ["disabled"];
const IMAGE_MIN_SCALE = 0.5;
const IMAGE_MAX_SCALE = 6;
/* harmony default export */ __webpack_exports__.A = ({
@@ -3018,9 +3020,9 @@ const IMAGE_MAX_SCALE = 6;
const props = __props;
const emit = __emit;
const contacts = (0,_stores_contacts__WEBPACK_IMPORTED_MODULE_9__/* .useContactStore */ .p)();
const listEl = (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .ref */ .KR)(null);
const viewerStage = (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .ref */ .KR)(null);
const contextMenu = (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .reactive */ .Kh)({
const listEl = (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .ref */ .KR)(null);
const viewerStage = (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .ref */ .KR)(null);
const contextMenu = (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .reactive */ .Kh)({
visible: false,
x: 0,
y: 0,
@@ -3032,7 +3034,7 @@ const IMAGE_MAX_SCALE = 6;
hour12: false,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
});
const imageViewer = (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .reactive */ .Kh)({
const imageViewer = (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .reactive */ .Kh)({
visible: false,
url: '',
name: '',
@@ -3046,7 +3048,7 @@ const IMAGE_MAX_SCALE = 6;
dragOffsetX: 0,
dragOffsetY: 0
});
const senderProfile = (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .reactive */ .Kh)({
const senderProfile = (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .reactive */ .Kh)({
visible: false,
uid: '',
displayName: '',
@@ -3565,7 +3567,7 @@ const IMAGE_MAX_SCALE = 6;
}, [((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)(vue__WEBPACK_IMPORTED_MODULE_6__/* .Fragment */ .FK, null, (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .renderList */ .pI)(__props.messages, message => {
return (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("article", {
key: message.id,
class: (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .normalizeClass */ .C4)(["message-row", [message.direction, {
class: (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .normalizeClass */ .C4)(["message-row", [message.direction, {
selected: isSelected(message),
attention: isAttention(message),
recalled: message.recalled || message.status === 'recalled'
@@ -3587,13 +3589,13 @@ const IMAGE_MAX_SCALE = 6;
key: 0,
src: message.senderPortrait,
alt: ""
}, null, 8, _hoisted_5)) : ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_6, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(groupSenderAvatarText(message)), 1))], 8, _hoisted_4)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", _hoisted_7, [showGroupSender(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("button", {
}, null, 8, _hoisted_5)) : ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_6, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(groupSenderAvatarText(message)), 1))], 8, _hoisted_4)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", _hoisted_7, [showGroupSender(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("button", {
key: 0,
type: "button",
class: "message-sender-name",
onClick: (0,vue__WEBPACK_IMPORTED_MODULE_5__/* .withModifiers */ .D$)($event => openSenderProfile(message), ["stop"])
}, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createTextVNode */ .eW)((0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(groupSenderName(message)) + " ", 1), senderRoleLabel(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_9, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderRoleLabel(message)), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)], 8, _hoisted_8)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", {
class: (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .normalizeClass */ .C4)(["message-bubble", roleBubbleClass(message)]),
}, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createTextVNode */ .eW)((0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(groupSenderName(message)) + " ", 1), senderRoleLabel(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_9, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderRoleLabel(message)), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)], 8, _hoisted_8)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", {
class: (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .normalizeClass */ .C4)(["message-bubble", roleBubbleClass(message)]),
onContextmenu: (0,vue__WEBPACK_IMPORTED_MODULE_5__/* .withModifiers */ .D$)($event => openMenu($event, message), ["prevent"])
}, [isImageMessage(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("button", {
key: 0,
@@ -3601,7 +3603,7 @@ const IMAGE_MAX_SCALE = 6;
class: "message-image",
onClick: (0,vue__WEBPACK_IMPORTED_MODULE_5__/* .withModifiers */ .D$)($event => handleImageClick(message), ["stop"])
}, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("img", {
src: message.attachment.url,
src: (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .unref */ .R1)(_services_mediaProxy__WEBPACK_IMPORTED_MODULE_10__/* .imageDisplayUrl */ .B)(message.attachment.url),
alt: message.attachment.name || '图片',
loading: "lazy",
decoding: "async",
@@ -3615,18 +3617,18 @@ const IMAGE_MAX_SCALE = 6;
download: ""
}, [_cache[7] || (_cache[7] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", {
class: "message-file-icon"
}, "▤", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", _hoisted_14, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("strong", null, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(message.attachment.name || '文件'), 1), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("small", null, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(formatFileSize(message.attachment.size)), 1)])], 8, _hoisted_13)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), displayMessageText(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("p", _hoisted_15, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(displayMessageText(message)), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("time", null, [isRecallNotice(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_16, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(message.recallStatus || '已撤回'), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", null, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(formatTime(message.displayTimestamp || message.timestamp)), 1), message.editedAt ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_17, "【edit " + (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(formatTime(message.editedAt)) + "】", 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), isFavorite(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_18, "★")) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), deliveryState(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", {
}, "▤", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", _hoisted_14, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("strong", null, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(message.attachment.name || '文件'), 1), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("small", null, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(formatFileSize(message.attachment.size)), 1)])], 8, _hoisted_13)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), displayMessageText(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("p", _hoisted_15, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(displayMessageText(message)), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("time", null, [isRecallNotice(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_16, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(message.recallStatus || '已撤回'), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", null, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(formatTime(message.displayTimestamp || message.timestamp)), 1), message.editedAt ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_17, "【edit " + (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(formatTime(message.editedAt)) + "】", 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), isFavorite(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_18, "★")) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), deliveryState(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", {
key: 3,
class: (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .normalizeClass */ .C4)(["delivery-mark", `delivery-mark-${deliveryState(message)}`]),
class: (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .normalizeClass */ .C4)(["delivery-mark", `delivery-mark-${deliveryState(message)}`]),
title: deliveryTitle(message),
"aria-label": deliveryTitle(message)
}, [_cache[8] || (_cache[8] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", {
class: "delivery-check"
}, "✓", -1)), deliveryState(message) !== 'sent' ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_20, "✓")) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)], 10, _hoisted_19)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), message.status === 'failed' ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_21, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(failureText(message)), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)])], 42, _hoisted_10)])], 10, _hoisted_1);
}, "✓", -1)), deliveryState(message) !== 'sent' ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_20, "✓")) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)], 10, _hoisted_19)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), message.status === 'failed' ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_21, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(failureText(message)), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)])], 42, _hoisted_10)])], 10, _hoisted_1);
}), 128)), contextMenu.visible ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("div", {
key: 0,
class: "message-context-menu",
style: (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .normalizeStyle */ .Tr)({
style: (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .normalizeStyle */ .Tr)({
left: `${contextMenu.x}px`,
top: `${contextMenu.y}px`
}),
@@ -3636,7 +3638,7 @@ const IMAGE_MAX_SCALE = 6;
key: item.action,
type: "button",
onClick: $event => chooseMenuAction(item.action)
}, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(item.label), 9, _hoisted_22);
}, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(item.label), 9, _hoisted_22);
}), 128))], 4)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createBlock */ .Wv)(vue__WEBPACK_IMPORTED_MODULE_6__/* .Teleport */ .Im, {
to: "body"
}, [imageViewer.visible ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("div", {
@@ -3646,7 +3648,7 @@ const IMAGE_MAX_SCALE = 6;
}, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", {
class: "image-viewer-dialog",
onClick: _cache[4] || (_cache[4] = (0,vue__WEBPACK_IMPORTED_MODULE_5__/* .withModifiers */ .D$)(() => {}, ["stop"]))
}, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", _hoisted_23, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", _hoisted_24, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(imageViewer.name || '图片'), 1), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("button", {
}, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", _hoisted_23, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", _hoisted_24, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(imageViewer.name || '图片'), 1), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("button", {
type: "button",
title: "缩小",
"aria-label": "缩小",
@@ -3679,7 +3681,7 @@ const IMAGE_MAX_SCALE = 6;
}, "×")]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", {
ref_key: "viewerStage",
ref: viewerStage,
class: (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .normalizeClass */ .C4)(["image-viewer-stage", {
class: (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .normalizeClass */ .C4)(["image-viewer-stage", {
dragging: imageViewer.dragging,
zoomed: imageViewer.scale > 1
}]),
@@ -3687,9 +3689,9 @@ const IMAGE_MAX_SCALE = 6;
onPointerdown: (0,vue__WEBPACK_IMPORTED_MODULE_5__/* .withModifiers */ .D$)(startImageDrag, ["prevent"])
}, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("img", {
class: "image-viewer-img",
src: imageViewer.url,
src: (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .unref */ .R1)(_services_mediaProxy__WEBPACK_IMPORTED_MODULE_10__/* .imageDisplayUrl */ .B)(imageViewer.url),
alt: imageViewer.name || '图片',
style: (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .normalizeStyle */ .Tr)(imageViewerStyle.value),
style: (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .normalizeStyle */ .Tr)(imageViewerStyle.value),
decoding: "async",
onError: retryImageLoad,
draggable: "false"
@@ -3712,7 +3714,7 @@ const IMAGE_MAX_SCALE = 6;
class: "profile-avatar image-avatar",
src: senderProfile.portrait,
alt: ""
}, null, 8, _hoisted_27)) : ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("div", _hoisted_28, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.avatarText), 1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("strong", null, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.displayName || senderProfile.uid), 1), senderProfile.roleLabel ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_29, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.roleLabel), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dl", _hoisted_30, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", null, [_cache[10] || (_cache[10] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dt", null, "用户ID", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dd", null, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.uid || '-'), 1)]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", null, [_cache[11] || (_cache[11] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dt", null, "昵称", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dd", null, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.rawName || senderProfile.displayName || '-'), 1)]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", null, [_cache[12] || (_cache[12] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dt", null, "手机号", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dd", null, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.mobile || '未知'), 1)]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", null, [_cache[13] || (_cache[13] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dt", null, "地区", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dd", null, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.address || '未知'), 1)])]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("label", _hoisted_31, [_cache[14] || (_cache[14] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", null, "备注名称", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .withDirectives */ .bo)((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("input", {
}, null, 8, _hoisted_27)) : ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("div", _hoisted_28, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.avatarText), 1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("strong", null, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.displayName || senderProfile.uid), 1), senderProfile.roleLabel ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_29, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.roleLabel), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dl", _hoisted_30, [(0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", null, [_cache[10] || (_cache[10] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dt", null, "用户ID", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dd", null, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.uid || '-'), 1)]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", null, [_cache[11] || (_cache[11] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dt", null, "昵称", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dd", null, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.rawName || senderProfile.displayName || '-'), 1)]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", null, [_cache[12] || (_cache[12] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dt", null, "手机号", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dd", null, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.mobile || '未知'), 1)]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("div", null, [_cache[13] || (_cache[13] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dt", null, "地区", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("dd", null, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.address || '未知'), 1)])]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("label", _hoisted_31, [_cache[14] || (_cache[14] = (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("span", null, "备注名称", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .withDirectives */ .bo)((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("input", {
"onUpdate:modelValue": _cache[5] || (_cache[5] = $event => senderProfile.aliasDraft = $event),
maxlength: "32",
autocomplete: "off",
@@ -3720,18 +3722,18 @@ const IMAGE_MAX_SCALE = 6;
disabled: senderProfile.saving || senderProfile.adding
}, null, 8, _hoisted_32), [[vue__WEBPACK_IMPORTED_MODULE_5__/* .vModelText */ .Jo, senderProfile.aliasDraft, void 0, {
trim: true
}]])]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("footer", _hoisted_33, [senderProfile.notice ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_34, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.notice), 1)) : senderProfile.error ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_35, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.error), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), !senderProfile.isContact ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("button", {
}]])]), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("footer", _hoisted_33, [senderProfile.notice ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_34, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.notice), 1)) : senderProfile.error ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("span", _hoisted_35, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.error), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), !senderProfile.isContact ? ((0,vue__WEBPACK_IMPORTED_MODULE_6__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementBlock */ .CE)("button", {
key: 2,
type: "button",
class: "ghost-inline-action",
disabled: senderProfile.adding || senderProfile.saving || !senderProfile.uid,
onClick: addSenderAsContact
}, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.adding ? '添加中...' : '添加为好友'), 9, _hoisted_36)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("button", {
}, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.adding ? '添加中...' : '添加为好友'), 9, _hoisted_36)) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createElementVNode */ .Lk)("button", {
type: "button",
class: "primary-inline-action",
disabled: senderProfile.saving || senderProfile.adding || !senderProfile.uid,
onClick: saveSenderAlias
}, (0,vue__WEBPACK_IMPORTED_MODULE_7__/* .toDisplayString */ .v_)(senderProfile.saving ? '保存中...' : '保存备注'), 9, _hoisted_37)])])])) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)]))], 512);
}, (0,vue__WEBPACK_IMPORTED_MODULE_8__/* .toDisplayString */ .v_)(senderProfile.saving ? '保存中...' : '保存备注'), 9, _hoisted_37)])])])) : (0,vue__WEBPACK_IMPORTED_MODULE_6__/* .createCommentVNode */ .Q3)("", true)]))], 512);
};
}
});
@@ -3982,10 +3984,11 @@ const _hoisted_6 = {
/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(116);
/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6768);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5187);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(144);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(144);
/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5187);
/* harmony import */ var _stores_conversations__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(7566);
/* harmony import */ var _stores_navigation__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(1414);
/* harmony import */ var _services_mediaProxy__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(446);
@@ -4032,14 +4035,15 @@ const _hoisted_14 = {
/* harmony default export */ __webpack_exports__.A = ({
__name: 'FavoritePanel',
setup(__props) {
const conversations = (0,_stores_conversations__WEBPACK_IMPORTED_MODULE_5__/* .useConversationStore */ .w)();
const navigation = (0,_stores_navigation__WEBPACK_IMPORTED_MODULE_6__/* .useNavigationStore */ .l)();
const favorite = (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .computed */ .EW)(() => conversations.activeFavorite);
const notice = (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .ref */ .KR)('');
const error = (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .ref */ .KR)('');
const notice = (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .ref */ .KR)('');
const error = (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .ref */ .KR)('');
function clearFeedback() {
notice.value = '';
error.value = '';
@@ -4165,20 +4169,20 @@ const _hoisted_14 = {
return (_ctx, _cache) => {
return (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("section", _hoisted_1, [favorite.value ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)(vue__WEBPACK_IMPORTED_MODULE_2__/* .Fragment */ .FK, {
key: 0
}, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("header", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", null, [_cache[0] || (_cache[0] = (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("h2", null, "收藏详情", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("p", null, (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .toDisplayString */ .v_)(favoriteTitle(favorite.value)), 1)])]), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", {
class: (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .normalizeClass */ .C4)(["favorite-detail-message", {
}, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("header", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", null, [_cache[0] || (_cache[0] = (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("h2", null, "收藏详情", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("p", null, (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .toDisplayString */ .v_)(favoriteTitle(favorite.value)), 1)])]), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", {
class: (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .normalizeClass */ .C4)(["favorite-detail-message", {
recalled: favorite.value.recalled
}])
}, [_cache[2] || (_cache[2] = (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("span", {
class: "favorite-detail-star"
}, "★", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", _hoisted_4, [favorite.value.recalled ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("span", _hoisted_5, (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .toDisplayString */ .v_)(favorite.value.recallStatus || '已撤回'), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createCommentVNode */ .Q3)("", true), isImageFavorite(favorite.value) ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("a", {
}, "★", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", _hoisted_4, [favorite.value.recalled ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("span", _hoisted_5, (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .toDisplayString */ .v_)(favorite.value.recallStatus || '已撤回'), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createCommentVNode */ .Q3)("", true), isImageFavorite(favorite.value) ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("a", {
key: 1,
class: "favorite-detail-image",
href: favorite.value.attachment.url,
href: (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .unref */ .R1)(_services_mediaProxy__WEBPACK_IMPORTED_MODULE_7__/* .imageDisplayUrl */ .B)(favorite.value.attachment.url),
target: "_blank",
rel: "noreferrer"
}, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("img", {
src: favorite.value.attachment.url,
src: (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .unref */ .R1)(_services_mediaProxy__WEBPACK_IMPORTED_MODULE_7__/* .imageDisplayUrl */ .B)(favorite.value.attachment.url),
alt: favorite.value.attachment.name || '图片'
}, null, 8, _hoisted_7)], 8, _hoisted_6)) : isFileFavorite(favorite.value) ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("a", {
key: 2,
@@ -4189,7 +4193,7 @@ const _hoisted_14 = {
download: ""
}, [_cache[1] || (_cache[1] = (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("span", {
class: "message-file-icon"
}, "▤", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("span", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("strong", null, (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .toDisplayString */ .v_)(favorite.value.attachment.name || '文件'), 1), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("small", null, (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .toDisplayString */ .v_)(formatFileSize(favorite.value.attachment.size)), 1)])], 8, _hoisted_8)) : (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createCommentVNode */ .Q3)("", true), favoriteText(favorite.value) ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("p", _hoisted_10, (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .toDisplayString */ .v_)(favoriteText(favorite.value)), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createCommentVNode */ .Q3)("", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("time", null, (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .toDisplayString */ .v_)(formatFavoriteTime(favorite.value.timestamp)), 1)], 2), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", _hoisted_11, [notice.value ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("span", _hoisted_12, (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .toDisplayString */ .v_)(notice.value), 1)) : error.value ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("span", _hoisted_13, (0,vue__WEBPACK_IMPORTED_MODULE_3__/* .toDisplayString */ .v_)(error.value), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("button", {
}, "▤", -1)), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("span", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("strong", null, (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .toDisplayString */ .v_)(favorite.value.attachment.name || '文件'), 1), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("small", null, (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .toDisplayString */ .v_)(formatFileSize(favorite.value.attachment.size)), 1)])], 8, _hoisted_8)) : (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createCommentVNode */ .Q3)("", true), favoriteText(favorite.value) ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("p", _hoisted_10, (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .toDisplayString */ .v_)(favoriteText(favorite.value)), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createCommentVNode */ .Q3)("", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("time", null, (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .toDisplayString */ .v_)(formatFavoriteTime(favorite.value.timestamp)), 1)], 2), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("div", _hoisted_11, [notice.value ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("span", _hoisted_12, (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .toDisplayString */ .v_)(notice.value), 1)) : error.value ? ((0,vue__WEBPACK_IMPORTED_MODULE_2__/* .openBlock */ .uX)(), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementBlock */ .CE)("span", _hoisted_13, (0,vue__WEBPACK_IMPORTED_MODULE_4__/* .toDisplayString */ .v_)(error.value), 1)) : (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createCommentVNode */ .Q3)("", true), (0,vue__WEBPACK_IMPORTED_MODULE_2__/* .createElementVNode */ .Lk)("button", {
type: "button",
class: "ghost-inline-action",
onClick: copyFavorite
@@ -7225,6 +7229,67 @@ class LongConnectionImClient extends _httpImClient__WEBPACK_IMPORTED_MODULE_5__/
/***/ }),
/***/ 446:
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ B: function() { return /* binding */ imageDisplayUrl; }
/* harmony export */ });
/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8111);
/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2489);
/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1701);
/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(7642);
/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(8004);
/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_4__);
/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(3853);
/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_5__);
/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5876);
/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_6__);
/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(2475);
/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_7__);
/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(5024);
/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_8__);
/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(1698);
/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_9__);
function configuredProxyHosts() {
const configured = window.JC_WEB_IM_CONFIG?.mediaProxyHosts;
const values = Array.isArray(configured) ? configured : String(configured || '').split(/[\s,]+/);
return new Set(values.map(value => String(value || '').trim().toLowerCase().replace(/\.$/, '')).filter(Boolean));
}
function imageDisplayUrl(value) {
const source = String(value || '').trim();
if (!source || source.startsWith('blob:') || source.startsWith('data:')) {
return source;
}
try {
const url = new URL(source, window.location.origin);
if (url.protocol !== 'https:' || !configuredProxyHosts().has(url.hostname.toLowerCase())) {
return source;
}
const proxyUrl = new URL('/api/media_proxy.php', window.location.origin);
proxyUrl.searchParams.set('url', url.toString());
return proxyUrl.toString();
} catch (error) {
return source;
}
}
/***/ }),
/***/ 9687:
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
@@ -9809,4 +9874,4 @@ const __exports__ = _NavigationRail_vue_vue_type_script_setup_true_lang_js__WEBP
/******/
/******/ })()
;
//# sourceMappingURL=app.84630d8b.js.map
//# sourceMappingURL=app.a89973f7.js.map
+1
View File
File diff suppressed because one or more lines are too long
+205
View File
@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
require __DIR__ . '/im_web_bootstrap.php';
const WEB_MEDIA_PROXY_MAX_BYTES = 15 * 1024 * 1024;
const WEB_MEDIA_PROXY_MAX_SECONDS = 25;
function failMediaProxy(int $status, string $message): never
{
http_response_code($status);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
echo $message;
exit;
}
function mediaProxyAllowedHosts(): array
{
$configured = webImEnv('JC_WEB_IM_MEDIA_PROXY_HOSTS');
$hosts = array_map(
static fn(string $host): string => strtolower(rtrim(trim($host), '.')),
preg_split('/[,\s]+/', $configured) ?: []
);
return array_values(array_unique(array_filter($hosts)));
}
function validatedMediaProxyUrl(string $url): array
{
if ($url === '' || strlen($url) > 2048 || filter_var($url, FILTER_VALIDATE_URL) === false) {
failMediaProxy(400, '媒体地址不合法');
}
$parts = parse_url($url);
$scheme = strtolower((string)($parts['scheme'] ?? ''));
$host = strtolower(rtrim((string)($parts['host'] ?? ''), '.'));
$port = isset($parts['port']) ? (int)$parts['port'] : 443;
if (
$scheme !== 'https'
|| $host === ''
|| $port !== 443
|| isset($parts['user'])
|| isset($parts['pass'])
|| !in_array($host, mediaProxyAllowedHosts(), true)
) {
failMediaProxy(403, '媒体来源不允许代理');
}
return [$url, $host];
}
function mediaProxyCacheRoot(): string
{
$root = rtrim(webImEnv('JC_WEB_IM_MEDIA_CACHE_ROOT', '/var/cache/jc-web-im-media'), '/');
if ($root === '' || !is_dir($root) || !is_writable($root)) {
failMediaProxy(503, '媒体缓存目录不可用');
}
return $root;
}
function mediaProxyMime(string $path): string
{
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = $finfo !== false ? (string)(finfo_file($finfo, $path) ?: '') : '';
if ($finfo !== false) {
finfo_close($finfo);
}
return $mime;
}
function fetchMediaProxyImage(string $url, string $host, string $cacheFile): void
{
$deadline = microtime(true) + WEB_MEDIA_PROXY_MAX_SECONDS;
$lastError = '媒体源无响应';
for ($attempt = 1; $attempt <= 6 && microtime(true) < $deadline; $attempt++) {
$temporaryFile = $cacheFile . '.tmp.' . getmypid() . '.' . $attempt;
$stream = @fopen($temporaryFile, 'x+b');
if ($stream === false) {
$lastError = '无法创建媒体缓存文件';
break;
}
$bytes = 0;
$tooLarge = false;
$curl = curl_init($url);
if ($curl === false) {
fclose($stream);
@unlink($temporaryFile);
$lastError = '无法初始化媒体请求';
break;
}
$remainingMs = max(1000, (int)floor(($deadline - microtime(true)) * 1000));
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => false,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_CONNECTTIMEOUT_MS => min(2500, $remainingMs),
CURLOPT_TIMEOUT_MS => min(12000, $remainingMs),
CURLOPT_NOSIGNAL => true,
CURLOPT_DNS_CACHE_TIMEOUT => 0,
CURLOPT_FRESH_CONNECT => true,
CURLOPT_FORBID_REUSE => true,
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'JC-Web-IM-Media-Proxy/1.0',
CURLOPT_HTTPHEADER => ['Accept: image/avif,image/webp,image/*,*/*;q=0.8'],
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use ($stream, &$bytes, &$tooLarge): int {
$length = strlen($chunk);
if ($bytes + $length > WEB_MEDIA_PROXY_MAX_BYTES) {
$tooLarge = true;
return 0;
}
$written = fwrite($stream, $chunk);
if ($written === false) {
return 0;
}
$bytes += $written;
return $written;
},
]);
$result = curl_exec($curl);
$status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$curlError = curl_error($curl);
curl_close($curl);
fflush($stream);
fclose($stream);
$mime = $bytes > 0 && is_file($temporaryFile) ? mediaProxyMime($temporaryFile) : '';
if ($result !== false && $status >= 200 && $status < 300 && !$tooLarge && str_starts_with($mime, 'image/')) {
if (!@rename($temporaryFile, $cacheFile)) {
@unlink($temporaryFile);
$lastError = '媒体缓存保存失败';
break;
}
@chmod($cacheFile, 0644);
return;
}
@unlink($temporaryFile);
$lastError = $tooLarge
? '图片超过代理大小限制'
: ($curlError !== '' ? $curlError : ($status > 0 ? '媒体源返回 HTTP ' . $status : '媒体源无响应'));
if ($attempt < 6 && microtime(true) < $deadline) {
usleep(150000);
}
}
error_log(sprintf('media_proxy fetch failed host=%s error=%s', $host, $lastError));
failMediaProxy(502, '图片源暂时不可用,请稍后重试');
}
function serveMediaProxyImage(string $cacheFile, string $etag): never
{
if (!is_file($cacheFile)) {
failMediaProxy(404, '媒体缓存不存在');
}
$mime = mediaProxyMime($cacheFile);
if (!str_starts_with($mime, 'image/')) {
failMediaProxy(415, '媒体内容不是图片');
}
header('ETag: "' . $etag . '"');
header('Cache-Control: public, max-age=86400, immutable');
header('Content-Type: ' . $mime);
header('Content-Length: ' . (string)filesize($cacheFile));
header('Content-Disposition: inline');
header('X-Content-Type-Options: nosniff');
if (trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? ''), ' W/') === '"' . $etag . '"') {
http_response_code(304);
exit;
}
readfile($cacheFile);
exit;
}
[$url, $host] = validatedMediaProxyUrl(trim((string)($_GET['url'] ?? '')));
$cacheRoot = mediaProxyCacheRoot();
$cacheKey = hash('sha256', $url);
$cacheFile = $cacheRoot . '/' . $cacheKey . '.image';
$lockFile = $cacheRoot . '/' . $cacheKey . '.lock';
if (!is_file($cacheFile)) {
$lock = @fopen($lockFile, 'c');
if ($lock === false || !flock($lock, LOCK_EX)) {
if (is_resource($lock)) {
fclose($lock);
}
failMediaProxy(503, '媒体缓存暂时忙碌');
}
if (!is_file($cacheFile)) {
fetchMediaProxyImage($url, $host, $cacheFile);
}
flock($lock, LOCK_UN);
fclose($lock);
}
serveMediaProxyImage($cacheFile, $cacheKey);
+3 -2
View File
@@ -48,7 +48,7 @@
@click.stop="handleImageClick(message)"
>
<img
:src="message.attachment.url"
:src="imageDisplayUrl(message.attachment.url)"
:alt="message.attachment.name || '图片'"
loading="lazy"
decoding="async"
@@ -133,7 +133,7 @@
>
<img
class="image-viewer-img"
:src="imageViewer.url"
:src="imageDisplayUrl(imageViewer.url)"
:alt="imageViewer.name || '图片'"
:style="imageViewerStyle"
decoding="async"
@@ -219,6 +219,7 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { useContactStore } from '@/stores/contacts';
import { imageDisplayUrl } from '@/services/mediaProxy';
const props = defineProps({
messages: {
+3 -2
View File
@@ -15,11 +15,11 @@
<a
v-if="isImageFavorite(favorite)"
class="favorite-detail-image"
:href="favorite.attachment.url"
:href="imageDisplayUrl(favorite.attachment.url)"
target="_blank"
rel="noreferrer"
>
<img :src="favorite.attachment.url" :alt="favorite.attachment.name || '图片'">
<img :src="imageDisplayUrl(favorite.attachment.url)" :alt="favorite.attachment.name || '图片'">
</a>
<a
v-else-if="isFileFavorite(favorite)"
@@ -59,6 +59,7 @@
import { computed, ref } from 'vue';
import { useConversationStore } from '@/stores/conversations';
import { useNavigationStore } from '@/stores/navigation';
import { imageDisplayUrl } from '@/services/mediaProxy';
const conversations = useConversationStore();
const navigation = useNavigationStore();
+30
View File
@@ -0,0 +1,30 @@
function configuredProxyHosts() {
const configured = window.JC_WEB_IM_CONFIG?.mediaProxyHosts;
const values = Array.isArray(configured)
? configured
: String(configured || '').split(/[\s,]+/);
return new Set(
values
.map(value => String(value || '').trim().toLowerCase().replace(/\.$/, ''))
.filter(Boolean)
);
}
export function imageDisplayUrl(value) {
const source = String(value || '').trim();
if (!source || source.startsWith('blob:') || source.startsWith('data:')) {
return source;
}
try {
const url = new URL(source, window.location.origin);
if (url.protocol !== 'https:' || !configuredProxyHosts().has(url.hostname.toLowerCase())) {
return source;
}
const proxyUrl = new URL('/api/media_proxy.php', window.location.origin);
proxyUrl.searchParams.set('url', url.toString());
return proxyUrl.toString();
} catch (error) {
return source;
}
}