diff --git a/web/package.json b/web/package.json
index 039ec348..d2adae7d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -39,6 +39,7 @@
"@xterm/xterm": "^6.0.0",
"alova": "^3.3.4",
"echarts": "^6.0.0",
+ "highlight.js": "^11.11.1",
"install": "^0.13.0",
"lodash-es": "^4.17.21",
"luxon": "^3.7.2",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 4aa46711..4fe7a3a7 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -50,6 +50,9 @@ importers:
echarts:
specifier: ^6.0.0
version: 6.0.0
+ highlight.js:
+ specifier: ^11.11.1
+ version: 11.11.1
install:
specifier: ^0.13.0
version: 0.13.0
diff --git a/web/src/components/common/AppProvider.vue b/web/src/components/common/AppProvider.vue
index d7f7b28c..5d704626 100644
--- a/web/src/components/common/AppProvider.vue
+++ b/web/src/components/common/AppProvider.vue
@@ -1,6 +1,13 @@
-
+
diff --git a/web/src/components/common/RealtimeLogModal.vue b/web/src/components/common/RealtimeLogModal.vue
index db36d989..573663dd 100644
--- a/web/src/components/common/RealtimeLogModal.vue
+++ b/web/src/components/common/RealtimeLogModal.vue
@@ -9,6 +9,11 @@ const props = defineProps({
path: {
type: String,
required: true
+ },
+ language: {
+ type: String,
+ required: false,
+ default: 'systemdlog'
}
})
@@ -72,7 +77,7 @@ defineExpose({
@close="handleClose"
@mask-click="handleClose"
>
-
+
diff --git a/web/src/utils/hljs/systemdlog.ts b/web/src/utils/hljs/systemdlog.ts
new file mode 100644
index 00000000..9e35f0a0
--- /dev/null
+++ b/web/src/utils/hljs/systemdlog.ts
@@ -0,0 +1,167 @@
+/*
+ Language: systemd Journal (journalctl)
+ Description: systemd/journald logs (journalctl -o short / short-iso)
+ Category: system, logs
+ */
+
+/** @type {import('highlight.js').LanguageFn} */
+export default function systemdJournal(hljs: any) {
+ const regex = hljs.regex
+
+ // Month names for "short" format
+ const MONTH = '(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'
+
+ // journalctl -o short:
+ // Jan 13 10:22:33 ...
+ const TS_SHORT = new RegExp(`^${MONTH}\\s+\\d{1,2}\\s+\\d{2}:\\d{2}:\\d{2}`)
+
+ // journalctl -o short-iso:
+ // 2026-01-13T10:22:33+0800 ...
+ // 2026-01-13 10:22:33 ...
+ const TS_ISO = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?/
+
+ const HOST = /[A-Za-z0-9][A-Za-z0-9_.-]*/
+ const IDENT = /[A-Za-z_][A-Za-z0-9_.-]*(?:\/[A-Za-z0-9_.-]+)*/ // e.g. systemd, sshd, foo/bar
+ const PID_OPT = /(?:\[\d+\])?/
+
+ const UNIT = /\b[\w.-]+?\.(?:service|socket|target|timer|mount|device|path|slice|scope)\b/
+
+ const PRIORITY_WORDS = [
+ 'emerg',
+ 'alert',
+ 'crit',
+ 'critical',
+ 'err',
+ 'error',
+ 'warn',
+ 'warning',
+ 'notice',
+ 'info',
+ 'debug',
+ 'panic',
+ // systemd/journal 常见状态词
+ 'failed',
+ 'failure',
+ 'timeout',
+ 'timed',
+ 'denied',
+ 'refused',
+ 'segfault'
+ ]
+
+ const SYSTEMD_VERBS = [
+ 'starting',
+ 'started',
+ 'stopping',
+ 'stopped',
+ 'reloading',
+ 'reloaded',
+ 'restarting',
+ 'restarted',
+ 'activating',
+ 'activated',
+ 'deactivating',
+ 'deactivated',
+ 'mounted',
+ 'mounting',
+ 'unmounted',
+ 'unmounting',
+ 'listening',
+ 'triggered',
+ 'queued',
+ 'succeeded',
+ 'success'
+ ]
+
+ // Prefix: " [pid]: "
+ // Example:
+ // Jan 13 10:22:33 host systemd[1]:
+ // 2026-01-13T10:22:33+0800 host sshd[1234]:
+ const PREFIX = {
+ begin: [regex.either(TS_ISO, TS_SHORT), /\s+/, HOST, /\s+/, IDENT, PID_OPT],
+ beginScope: {
+ 1: 'meta', // timestamp
+ 3: 'title', // hostname
+ 5: 'symbol', // identifier
+ 6: 'number' // [pid]
+ },
+ end: /: /,
+ endScope: 'punctuation',
+ relevance: 10
+ }
+
+ const SEVERITY = {
+ match: new RegExp(`\\b(?:${PRIORITY_WORDS.join('|')})\\b`, 'i'),
+ scope: 'keyword',
+ relevance: 2
+ }
+
+ const STATUS_VERB = {
+ match: new RegExp(`\\b(?:${SYSTEMD_VERBS.join('|')})\\b`, 'i'),
+ scope: 'built_in',
+ relevance: 1
+ }
+
+ // key=value(如:code=exited status=1/FAILURE UNIT=foo.service)
+ const KEY_VALUE = {
+ begin: /\b[\w.-]+=/,
+ scope: 'attr',
+ relevance: 0
+ }
+
+ // kernel/journal 常见的 monotonic timestamp: "[ 123.456]"
+ const MONOTONIC = {
+ match: /\[\s*\d+(?:\.\d+)?\]/,
+ scope: 'meta',
+ relevance: 0
+ }
+
+ const DQUOTE = {
+ scope: 'string',
+ begin: /"/,
+ end: /"/,
+ illegal: /\n/,
+ relevance: 0
+ }
+
+ const SQUOTE = {
+ scope: 'string',
+ begin: /'/,
+ end: /'/,
+ illegal: /\n/,
+ relevance: 0
+ }
+
+ const PATH = {
+ match: /(?:\/[^\s"'():\[\]]+)+/,
+ scope: 'string',
+ relevance: 0
+ }
+
+ return {
+ name: 'systemd Journal',
+ aliases: ['journalctl', 'journald', 'systemdlog', 'systemd-journal', 'systemd'],
+ case_insensitive: true,
+ contains: [
+ PREFIX,
+
+ // unit names in message body
+ { match: UNIT, scope: 'title', relevance: 3 },
+
+ // severity/status words
+ SEVERITY,
+ STATUS_VERB,
+
+ MONOTONIC,
+ KEY_VALUE,
+
+ // numbers (exit codes, pids, etc.)
+ hljs.NUMBER_MODE,
+
+ // strings/paths
+ DQUOTE,
+ SQUOTE,
+ PATH
+ ]
+ }
+}
diff --git a/web/src/views/website/EditView.vue b/web/src/views/website/EditView.vue
index 2346a00e..51ceb594 100644
--- a/web/src/views/website/EditView.vue
+++ b/web/src/views/website/EditView.vue
@@ -912,7 +912,7 @@ const updateTimeoutUnit = (proxy: any, unit: string) => {
{{ $gettext('Are you sure you want to clear?') }}
-
+
@@ -924,7 +924,7 @@ const updateTimeoutUnit = (proxy: any, unit: string) => {
{{ $gettext('view') }}.
-
+