release: v0.50.102–v0.50.108 batch (code blocks, utf-8, image URLs, deletion warning, PermissionError, Docker docs, kimi-k2.5) (#755)
## Batch release: v0.50.102 – v0.50.108 Seven self-built PRs reviewed and approved by @nesquena, now consolidated into a single release branch. ### Included fixes | Version | PR | What it fixes | |---|---|---| | v0.50.102 | #746 | Code blocks lose newlines when not preceded by blank line (fixes #745) | | v0.50.103 | #743 | `encoding='utf-8'` on `write_text()` in `api/profiles.py` — Windows `.env` detection (fixes #741) | | v0.50.104 | #735 | Agent `MEDIA:localhost:*` image URLs rewritten to `document.baseURI` — remote users get working images (fixes #642) | | v0.50.105 | #736 | Profile deletion warning strengthened: "permanently deleted, cannot be undone" across all 6 locales (fixes #637) | | v0.50.106 | #738 | Catch `PermissionError` in `_signing_key()` — three-container Docker UID mismatch no longer crashes all HTTP requests | | v0.50.107 | #737 | Docs: three-container UID/GID alignment guide in README + `HERMES_UID`/`HERMES_GID` forwarded in compose (fixes #645) | | v0.50.108 | #742 | Add `kimi-k2.5` to Kimi/Moonshot provider model list (fixes #740) | ### Testing - **pytest**: 1510 passed, 1 warning (1 pre-existing unrelated failure excluded) - **QA harness**: 20/20 passed (`~/WebUI/scripts/run-browser-tests.sh`) - **Browser**: layout, slash autocomplete width, edit button, image URL rewrite, profile deletion dialog all verified All PRs reviewed and approved by @nesquena. Ready to merge and tag **v0.50.108**.
This commit is contained in:
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,9 +1,42 @@
|
|||||||
# Hermes Web UI -- Changelog
|
# Hermes Web UI -- Changelog
|
||||||
|
|
||||||
|
## [v0.50.108] — 2026-04-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Kimi K2.5 added to Kimi/Moonshot provider model list** — `kimi-k2.5` was present in `hermes_cli` but missing from the WebUI's `api/config.py` kimi-coding provider, making it unavailable in the model selector. (Fixes #740)
|
||||||
|
|
||||||
|
## [v0.50.107] — 2026-04-20
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Three-container UID/GID alignment guide in README** — new subsection explains why UIDs must match across containers sharing a bind-mounted volume, documents the variable name asymmetry (`HERMES_UID`/`HERMES_GID` for the agent image vs `WANTED_UID`/`WANTED_GID` for the WebUI image), gives the recommended `.env` setup for standard Linux and NAS/Unraid deployments, provides the one-time `chown` fix for existing installs, and notes that the dashboard volume must be read-write. (Fixes #645)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`HERMES_UID`/`HERMES_GID` forwarded to agent and dashboard containers** — `docker-compose.three-container.yml` now declares `HERMES_UID=${HERMES_UID:-10000}` and `HERMES_GID=${HERMES_GID:-10000}` in the environment blocks for `hermes-agent` and `hermes-dashboard`, making the documented `.env` recipe functional.
|
||||||
|
|
||||||
|
## [v0.50.106] — 2026-04-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`PermissionError` in auth signing key no longer crashes every HTTP request** — `key_file.exists()` in `api/auth.py`'s `_signing_key()` was called outside the try/except block. In three-container bind-mount setups where the agent container initialises the state directory under a different UID, `pathlib.Path.exists()` raises `PermissionError`, which escaped up through `is_auth_enabled()` → `check_auth()` and crashed every HTTP request with HTTP 500. The `exists()` call is now inside the try block so `PermissionError` is caught and falls back to an in-memory key. (PR #625)
|
||||||
|
|
||||||
|
## [v0.50.105] — 2026-04-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Profile deletion warning now leads with destructive impact** — the confirmation dialog now reads: "All sessions, config, skills, and memory for this profile will be permanently deleted. This cannot be undone." Updated across all 6 supported locales. (Fixes #637)
|
||||||
|
|
||||||
|
## [v0.50.104] — 2026-04-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Agent image URLs rewritten to actual server base** — when an agent emits a `MEDIA:http://localhost:8787/...` URL, the WebUI now rewrites the `localhost`/`127.0.0.1` host to the page's `document.baseURI` before inserting it as an `<img src>`. Fixes broken images for remote users (VPN, Docker, deployed servers) and preserves subpath mounts (e.g. `/hermes/`). (Fixes #642)
|
||||||
|
|
||||||
|
## [v0.50.103] — 2026-04-20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Windows `.env` encoding fix** — `write_text()` calls in `api/profiles.py` were missing `encoding='utf-8'`, causing failures on Windows systems with non-UTF-8 locale encodings. All file I/O in `api/` now explicitly specifies `encoding='utf-8'`. (Fixes #741)
|
||||||
|
|
||||||
## [v0.50.102] — 2026-04-20
|
## [v0.50.102] — 2026-04-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Code blocks no longer lose newlines when not preceded by a blank line** — `renderMd()` in `static/ui.js` now stashes `<pre>` blocks (including language-labelled blocks with `<div class="pre-header">` wrappers), mermaid diagrams, and katex blocks before the paragraph-splitting pass, then restores them. Previously, if a fenced code block was not separated from surrounding text by a double newline, all `\n` inside it were replaced with `<br>`, causing Prism.js to collapse the entire block to one line and misidentify everything after a `//` comment as a comment. (Fixes #745, reported by @qqxpee)
|
- **Code blocks no longer lose newlines when not preceded by a blank line** — `renderMd()` now stashes `<pre>` blocks (including language-labelled wrappers), mermaid diagrams, and katex blocks before the paragraph-splitting pass, then restores them. Previously, if a fenced code block was not separated from surrounding text by a blank line, all `\n` inside it were replaced with `<br>`, collapsing the entire block to one line. (Fixes #745)
|
||||||
|
|
||||||
## [v0.50.101] — 2026-04-20
|
## [v0.50.101] — 2026-04-20
|
||||||
|
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -200,6 +200,75 @@ first boot. Subsequent restarts reuse the installed packages.
|
|||||||
|
|
||||||
See `docker-compose.two-container.yml` for the full configuration.
|
See `docker-compose.two-container.yml` for the full configuration.
|
||||||
|
|
||||||
|
### Running alongside hermes-dashboard (three-container setup)
|
||||||
|
|
||||||
|
To run the Hermes Agent, Hermes Dashboard, and the WebUI together on a
|
||||||
|
shared volume, use the three-container Compose file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.three-container.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
This brings up:
|
||||||
|
- **`hermes-agent`** — gateway API on port 8642
|
||||||
|
- **`hermes-dashboard`** — monitoring UI on port 9119
|
||||||
|
- **`hermes-webui`** — browser chat interface on port 8787
|
||||||
|
|
||||||
|
All three services share the same `hermes-home` named volume so config,
|
||||||
|
sessions, skills, and memory are consistent across all surfaces.
|
||||||
|
|
||||||
|
#### Why UIDs must match
|
||||||
|
|
||||||
|
The `hermes-home` volume is a bind-mount in practice — all three containers
|
||||||
|
write to the same filesystem tree under `~/.hermes`. If the containers run
|
||||||
|
as different UIDs, whichever container creates a file first becomes its
|
||||||
|
owner, and the others hit `PermissionError` on subsequent writes.
|
||||||
|
|
||||||
|
The fix is to make all containers run as **your host user's UID and GID**.
|
||||||
|
|
||||||
|
#### Variable name asymmetry
|
||||||
|
|
||||||
|
> ⚠️ **The two image families use different environment variable names** for
|
||||||
|
> the UID/GID setting:
|
||||||
|
>
|
||||||
|
> | Image | Variable |
|
||||||
|
> |---|---|
|
||||||
|
> | `nousresearch/hermes-agent` (agent + dashboard) | `HERMES_UID` / `HERMES_GID` |
|
||||||
|
> | `ghcr.io/nesquena/hermes-webui` | `WANTED_UID` / `WANTED_GID` |
|
||||||
|
>
|
||||||
|
> You must set **both pairs** when using a `.env` file.
|
||||||
|
|
||||||
|
#### Recommended setup
|
||||||
|
|
||||||
|
For a standard Linux user (UID ≥ 1000):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a .env file with your host UID/GID
|
||||||
|
echo "UID=$(id -u)" >> .env
|
||||||
|
echo "GID=$(id -g)" >> .env
|
||||||
|
# hermes-agent / hermes-dashboard
|
||||||
|
echo "HERMES_UID=$(id -u)" >> .env
|
||||||
|
echo "HERMES_GID=$(id -g)" >> .env
|
||||||
|
```
|
||||||
|
|
||||||
|
For NAS/Unraid deployments where a fixed service account is preferred, use
|
||||||
|
`10000:10000` (or your NAS service UID) instead of `$(id -u)`.
|
||||||
|
|
||||||
|
If you get `PermissionError` on an **existing** `~/.hermes` directory, run
|
||||||
|
the one-time ownership fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chown -R $(id -u):$(id -g) ~/.hermes
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Volume mount mode
|
||||||
|
|
||||||
|
The dashboard container needs **read-write** access to the shared volume
|
||||||
|
(it writes session logs and dashboard state). Do **not** add `:ro` to the
|
||||||
|
`hermes-home` volume in `hermes-dashboard`'s `volumes:` entry.
|
||||||
|
|
||||||
|
See `docker-compose.three-container.yml` for the full reference configuration.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What start.sh discovers automatically
|
## What start.sh discovers automatically
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ def _record_login_attempt(ip: str) -> None:
|
|||||||
def _signing_key():
|
def _signing_key():
|
||||||
"""Return a random signing key, generating and persisting one on first call."""
|
"""Return a random signing key, generating and persisting one on first call."""
|
||||||
key_file = STATE_DIR / '.signing_key'
|
key_file = STATE_DIR / '.signing_key'
|
||||||
if key_file.exists():
|
|
||||||
try:
|
try:
|
||||||
|
if key_file.exists():
|
||||||
raw = key_file.read_bytes()
|
raw = key_file.read_bytes()
|
||||||
if len(raw) >= 32:
|
if len(raw) >= 32:
|
||||||
return raw[:32]
|
return raw[:32]
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("Failed to read signing key from file, generating new key")
|
logger.debug("Failed to read or access signing key file, using in-memory key")
|
||||||
# Generate a new random key
|
# Generate a new random key
|
||||||
key = secrets.token_bytes(32)
|
key = secrets.token_bytes(32)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -533,6 +533,7 @@ _PROVIDER_MODELS = {
|
|||||||
{"id": "moonshot-v1-32k", "label": "Moonshot v1 32k"},
|
{"id": "moonshot-v1-32k", "label": "Moonshot v1 32k"},
|
||||||
{"id": "moonshot-v1-128k", "label": "Moonshot v1 128k"},
|
{"id": "moonshot-v1-128k", "label": "Moonshot v1 128k"},
|
||||||
{"id": "kimi-latest", "label": "Kimi Latest"},
|
{"id": "kimi-latest", "label": "Kimi Latest"},
|
||||||
|
{"id": "kimi-k2.5", "label": "Kimi K2.5"},
|
||||||
],
|
],
|
||||||
"minimax": [
|
"minimax": [
|
||||||
{"id": "MiniMax-M2.7", "label": "MiniMax M2.7"},
|
{"id": "MiniMax-M2.7", "label": "MiniMax M2.7"},
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ def switch_profile(name: str) -> dict:
|
|||||||
# Write sticky default for CLI consistency
|
# Write sticky default for CLI consistency
|
||||||
try:
|
try:
|
||||||
ap_file = _DEFAULT_HERMES_HOME / 'active_profile'
|
ap_file = _DEFAULT_HERMES_HOME / 'active_profile'
|
||||||
ap_file.write_text(name if name != 'default' else '')
|
ap_file.write_text(name if name != 'default' else '', encoding='utf-8')
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("Failed to write active profile file")
|
logger.debug("Failed to write active profile file")
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ def _write_endpoint_to_config(profile_dir: Path, base_url: str = None, api_key:
|
|||||||
if api_key:
|
if api_key:
|
||||||
model_section['api_key'] = api_key
|
model_section['api_key'] = api_key
|
||||||
cfg['model'] = model_section
|
cfg['model'] = model_section
|
||||||
config_path.write_text(_yaml.dump(cfg, default_flow_style=False, allow_unicode=True))
|
config_path.write_text(_yaml.dump(cfg, default_flow_style=False, allow_unicode=True), encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
def create_profile_api(name: str, clone_from: str = None,
|
def create_profile_api(name: str, clone_from: str = None,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ services:
|
|||||||
- hermes-agent-src:/opt/hermes
|
- hermes-agent-src:/opt/hermes
|
||||||
environment:
|
environment:
|
||||||
- HERMES_HOME=/root/.hermes
|
- HERMES_HOME=/root/.hermes
|
||||||
|
- HERMES_UID=${HERMES_UID:-10000}
|
||||||
|
- HERMES_GID=${HERMES_GID:-10000}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
@@ -47,6 +49,8 @@ services:
|
|||||||
- hermes-home:/root/.hermes
|
- hermes-home:/root/.hermes
|
||||||
environment:
|
environment:
|
||||||
- HERMES_HOME=/root/.hermes
|
- HERMES_HOME=/root/.hermes
|
||||||
|
- HERMES_UID=${HERMES_UID:-10000}
|
||||||
|
- HERMES_GID=${HERMES_GID:-10000}
|
||||||
# Dashboard connects to the gateway for health/session data
|
# Dashboard connects to the gateway for health/session data
|
||||||
- GATEWAY_HEALTH_URL=http://hermes-agent:8642
|
- GATEWAY_HEALTH_URL=http://hermes-agent:8642
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -90,6 +94,12 @@ services:
|
|||||||
# echo "UID=$(id -u)" >> .env && echo "GID=$(id -g)" >> .env
|
# echo "UID=$(id -u)" >> .env && echo "GID=$(id -g)" >> .env
|
||||||
- WANTED_UID=${UID:-1000}
|
- WANTED_UID=${UID:-1000}
|
||||||
- WANTED_GID=${GID:-1000}
|
- WANTED_GID=${GID:-1000}
|
||||||
|
# NOTE: When using bind-mount volumes shared across containers, ALL containers
|
||||||
|
# that write to the same host directory must run as the same UID/GID.
|
||||||
|
# If hermes-agent initialises the state dir as root (UID 0), hermes-webui
|
||||||
|
# will get a PermissionError accessing those paths — including a crash on every
|
||||||
|
# HTTP request if the auth signing-key file is unreadable. Either set WANTED_UID
|
||||||
|
# to match the agent container's UID, or use a named Docker volume (preferred).
|
||||||
# Optional: set a password for remote access
|
# Optional: set a password for remote access
|
||||||
# - HERMES_WEBUI_PASSWORD=your-secret-password
|
# - HERMES_WEBUI_PASSWORD=your-secret-password
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -464,7 +464,7 @@ const LOCALES = {
|
|||||||
profile_base_url_rule: 'Base URL must start with http:// or https://',
|
profile_base_url_rule: 'Base URL must start with http:// or https://',
|
||||||
profile_created: (name) => `Profile created: ${name}`,
|
profile_created: (name) => `Profile created: ${name}`,
|
||||||
profile_delete_confirm_title: (name) => `Delete profile "${name}"?`,
|
profile_delete_confirm_title: (name) => `Delete profile "${name}"?`,
|
||||||
profile_delete_confirm_message: 'This removes all config, skills, memory, and sessions for this profile.',
|
profile_delete_confirm_message: 'All sessions, config, skills, and memory for this profile will be permanently deleted. This cannot be undone.',
|
||||||
profile_deleted: (name) => `Profile deleted: ${name}`,
|
profile_deleted: (name) => `Profile deleted: ${name}`,
|
||||||
active_conversation_none: 'No active conversation selected.',
|
active_conversation_none: 'No active conversation selected.',
|
||||||
active_conversation_meta: (title, count) => `${title} · ${count} message${count === 1 ? '' : 's'}`,
|
active_conversation_meta: (title, count) => `${title} · ${count} message${count === 1 ? '' : 's'}`,
|
||||||
@@ -914,7 +914,7 @@ const LOCALES = {
|
|||||||
profile_base_url_rule: 'Базовый URL должен начинаться с http:// или https://',
|
profile_base_url_rule: 'Базовый URL должен начинаться с http:// или https://',
|
||||||
profile_created: (name) => `Профиль создан: ${name}`,
|
profile_created: (name) => `Профиль создан: ${name}`,
|
||||||
profile_delete_confirm_title: (name) => `Удалить профиль «${name}»?`,
|
profile_delete_confirm_title: (name) => `Удалить профиль «${name}»?`,
|
||||||
profile_delete_confirm_message: 'Это удалит всю конфигурацию, навыки, память и сеансы этого профиля.',
|
profile_delete_confirm_message: 'Все сеансы, конфигурация, навыки и память этого профиля будут удалены безвозвратно. Это действие невозможно отменить.',
|
||||||
profile_deleted: (name) => `Профиль удалён: ${name}`,
|
profile_deleted: (name) => `Профиль удалён: ${name}`,
|
||||||
active_conversation_none: 'Активная беседа не выбрана.',
|
active_conversation_none: 'Активная беседа не выбрана.',
|
||||||
active_conversation_meta: (title, count) => {
|
active_conversation_meta: (title, count) => {
|
||||||
@@ -1348,7 +1348,7 @@ const LOCALES = {
|
|||||||
profile_base_url_rule: 'Base URL must start with http:// or https://',
|
profile_base_url_rule: 'Base URL must start with http:// or https://',
|
||||||
profile_created: (name) => `Profile created: ${name}`,
|
profile_created: (name) => `Profile created: ${name}`,
|
||||||
profile_delete_confirm_title: (name) => `Delete profile "${name}"?`,
|
profile_delete_confirm_title: (name) => `Delete profile "${name}"?`,
|
||||||
profile_delete_confirm_message: 'This removes all config, skills, memory, and sessions for this profile.',
|
profile_delete_confirm_message: 'Todas las sesiones, configuración, habilidades y memoria de este perfil se eliminarán de forma permanente. Esta acción no se puede deshacer.',
|
||||||
profile_deleted: (name) => `Profile deleted: ${name}`,
|
profile_deleted: (name) => `Profile deleted: ${name}`,
|
||||||
active_conversation_none: 'No active conversation selected.',
|
active_conversation_none: 'No active conversation selected.',
|
||||||
active_conversation_meta: (title, count) => `${title} · ${count} message${count === 1 ? '' : 's'}`,
|
active_conversation_meta: (title, count) => `${title} · ${count} message${count === 1 ? '' : 's'}`,
|
||||||
@@ -1581,6 +1581,7 @@ const LOCALES = {
|
|||||||
onboarding_password_will_replace: 'Wird ersetzt',
|
onboarding_password_will_replace: 'Wird ersetzt',
|
||||||
onboarding_password_keep_existing: 'Aktuelles Passwort beibehalten',
|
onboarding_password_keep_existing: 'Aktuelles Passwort beibehalten',
|
||||||
onboarding_password_remains_disabled: 'Bleibt deaktiviert',
|
onboarding_password_remains_disabled: 'Bleibt deaktiviert',
|
||||||
|
profile_delete_confirm_message: 'Alle Sitzungen, Konfigurationen, Fähigkeiten und Erinnerungen dieses Profils werden dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.',
|
||||||
},
|
},
|
||||||
|
|
||||||
zh: {
|
zh: {
|
||||||
@@ -1992,7 +1993,7 @@ const LOCALES = {
|
|||||||
profile_base_url_rule: 'Base URL 必须以 http:// 或 https:// 开头',
|
profile_base_url_rule: 'Base URL 必须以 http:// 或 https:// 开头',
|
||||||
profile_created: (name) => `配置档已创建:${name}`,
|
profile_created: (name) => `配置档已创建:${name}`,
|
||||||
profile_delete_confirm_title: (name) => `删除配置档“${name}”?`,
|
profile_delete_confirm_title: (name) => `删除配置档“${name}”?`,
|
||||||
profile_delete_confirm_message: '这将删除该配置档的所有配置、技能、记忆和会话。',
|
profile_delete_confirm_message: '该配置档的所有会话、配置、技能和记忆将被永久删除。此操作无法撤销。',
|
||||||
profile_deleted: (name) => `配置档已删除:${name}`,
|
profile_deleted: (name) => `配置档已删除:${name}`,
|
||||||
active_conversation_none: '当前未选择活动会话。',
|
active_conversation_none: '当前未选择活动会话。',
|
||||||
active_conversation_meta: (title, count) => `${title} · ${count} 条消息`,
|
active_conversation_meta: (title, count) => `${title} · ${count} 条消息`,
|
||||||
@@ -2235,6 +2236,7 @@ const LOCALES = {
|
|||||||
// ui.js
|
// ui.js
|
||||||
workspace_desc: '\u8acb\u9078\u64c7\u5de5\u4f5c\u5340\uff0c\u6216\u8f09\u5165\u65b0\u540d\u7a31\u5beb\u4e00\u500b',
|
workspace_desc: '\u8acb\u9078\u64c7\u5de5\u4f5c\u5340\uff0c\u6216\u8f09\u5165\u65b0\u540d\u7a31\u5beb\u4e00\u500b',
|
||||||
tab_profiles: '\u914d\u7f6e',
|
tab_profiles: '\u914d\u7f6e',
|
||||||
|
profile_delete_confirm_message: '\u6b64\u914d\u7f6e\u6a94\u7684\u6240\u6709\u6703\u8a71\u3001\u8a2d\u5b9a\u3001\u6280\u80fd\u548c\u8a18\u61b6\u5c07\u88ab\u6c38\u4e45\u522a\u9664\u3002\u6b64\u64cd\u4f5c\u7121\u6cd5\u64a4\u92b7\u3002',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
15
static/ui.js
15
static/ui.js
@@ -663,10 +663,19 @@ function renderMd(raw){
|
|||||||
const ref=media_stash[+i];
|
const ref=media_stash[+i];
|
||||||
// HTTP(S) URL
|
// HTTP(S) URL
|
||||||
if(/^https?:\/\//i.test(ref)){
|
if(/^https?:\/\//i.test(ref)){
|
||||||
if(_IMAGE_EXTS.test(ref.split('?')[0])){
|
// Rewrite localhost/127.0.0.1 to the actual server base URL so remote
|
||||||
return `<img class="msg-media-img" src="${esc(ref)}" alt="image" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`;
|
// users (VPN, Docker, deployed) can load agent-generated images (#642).
|
||||||
|
// Strip the trailing slash from document.baseURI so the URL's own path
|
||||||
|
// joins cleanly — this preserves any subpath mount (e.g. /hermes/).
|
||||||
|
let src=ref;
|
||||||
|
if(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/i.test(src)){
|
||||||
|
const base=document.baseURI.replace(/\/$/,'');
|
||||||
|
src=src.replace(/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?/i,base);
|
||||||
}
|
}
|
||||||
return `<a href="${esc(ref)}" target="_blank" rel="noopener">${esc(ref)}</a>`;
|
if(_IMAGE_EXTS.test(src.split('?')[0])){
|
||||||
|
return `<img class="msg-media-img" src="${esc(src)}" alt="image" loading="lazy" onclick="this.classList.toggle('msg-media-img--full')">`;
|
||||||
|
}
|
||||||
|
return `<a href="${esc(src)}" target="_blank" rel="noopener">${esc(src)}</a>`;
|
||||||
}
|
}
|
||||||
// Local file path
|
// Local file path
|
||||||
const apiUrl='api/media?path='+encodeURIComponent(ref);
|
const apiUrl='api/media?path='+encodeURIComponent(ref);
|
||||||
|
|||||||
Reference in New Issue
Block a user