Add shared Docker network and Miniflux plugin for Hermes
- Create external 'monotrope' Docker network so services can communicate by container name - Add Miniflux to the shared network (db stays on internal network) - Add Hermes Miniflux plugin with list_feeds and get_unread_entries tools - Mount plugin directory and pass Miniflux API key to Hermes container Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
hermes_openrouter_api_key: "{{ lookup('env', 'HERMES_OPENROUTER_API_KEY') }}"
|
hermes_openrouter_api_key: "{{ lookup('env', 'HERMES_OPENROUTER_API_KEY') }}"
|
||||||
hermes_telegram_bot_token: "{{ lookup('env', 'HERMES_TELEGRAM_BOT_TOKEN') }}"
|
hermes_telegram_bot_token: "{{ lookup('env', 'HERMES_TELEGRAM_BOT_TOKEN') }}"
|
||||||
hermes_telegram_allowed_users: "{{ lookup('env', 'HERMES_TELEGRAM_ALLOWED_USERS') }}"
|
hermes_telegram_allowed_users: "{{ lookup('env', 'HERMES_TELEGRAM_ALLOWED_USERS') }}"
|
||||||
|
hermes_miniflux_api_key: "{{ lookup('env', 'HERMES_MINIFLUX_API_KEY') }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
@@ -238,6 +239,15 @@
|
|||||||
enabled: true
|
enabled: true
|
||||||
state: started
|
state: started
|
||||||
|
|
||||||
|
- name: Create shared Docker network
|
||||||
|
command: docker network create monotrope
|
||||||
|
register: docker_net
|
||||||
|
changed_when: docker_net.rc == 0
|
||||||
|
failed_when: docker_net.rc != 0 and 'already exists' not in docker_net.stderr
|
||||||
|
tags:
|
||||||
|
- miniflux
|
||||||
|
- hermes
|
||||||
|
|
||||||
# ── Miniflux ────────────────────────────────────────────────────────────
|
# ── Miniflux ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
- name: Create Miniflux directory
|
- name: Create Miniflux directory
|
||||||
@@ -344,6 +354,17 @@
|
|||||||
notify: Restart Hermes
|
notify: Restart Hermes
|
||||||
tags: hermes
|
tags: hermes
|
||||||
|
|
||||||
|
- name: Copy Hermes plugins
|
||||||
|
copy:
|
||||||
|
src: ../hermes/plugins/
|
||||||
|
dest: /opt/hermes/plugins/
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: '0640'
|
||||||
|
directory_mode: '0750'
|
||||||
|
notify: Restart Hermes
|
||||||
|
tags: hermes
|
||||||
|
|
||||||
- name: Write Hermes .env
|
- name: Write Hermes .env
|
||||||
copy:
|
copy:
|
||||||
dest: /opt/hermes/.env
|
dest: /opt/hermes/.env
|
||||||
@@ -354,6 +375,7 @@
|
|||||||
OPENROUTER_API_KEY={{ hermes_openrouter_api_key }}
|
OPENROUTER_API_KEY={{ hermes_openrouter_api_key }}
|
||||||
TELEGRAM_BOT_TOKEN={{ hermes_telegram_bot_token }}
|
TELEGRAM_BOT_TOKEN={{ hermes_telegram_bot_token }}
|
||||||
TELEGRAM_ALLOWED_USERS={{ hermes_telegram_allowed_users }}
|
TELEGRAM_ALLOWED_USERS={{ hermes_telegram_allowed_users }}
|
||||||
|
MINIFLUX_API_KEY={{ hermes_miniflux_api_key }}
|
||||||
no_log: true
|
no_log: true
|
||||||
tags: hermes
|
tags: hermes
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,24 @@ services:
|
|||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
networks:
|
||||||
|
- monotrope
|
||||||
volumes:
|
volumes:
|
||||||
- hermes_data:/opt/data
|
- hermes_data:/opt/data
|
||||||
- ./config.yaml:/opt/data/config.yaml:ro
|
- ./config.yaml:/opt/data/config.yaml:ro
|
||||||
|
- ./plugins:/opt/data/plugins:ro
|
||||||
environment:
|
environment:
|
||||||
OPENROUTER_API_KEY: "${OPENROUTER_API_KEY}"
|
OPENROUTER_API_KEY: "${OPENROUTER_API_KEY}"
|
||||||
TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN}"
|
TELEGRAM_BOT_TOKEN: "${TELEGRAM_BOT_TOKEN}"
|
||||||
TELEGRAM_ALLOWED_USERS: "${TELEGRAM_ALLOWED_USERS}"
|
TELEGRAM_ALLOWED_USERS: "${TELEGRAM_ALLOWED_USERS}"
|
||||||
|
MINIFLUX_API_KEY: "${MINIFLUX_API_KEY}"
|
||||||
|
MINIFLUX_BASE_URL: "http://miniflux:8080"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
||||||
|
networks:
|
||||||
|
monotrope:
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
hermes_data:
|
hermes_data:
|
||||||
|
|||||||
16
infra/hermes/plugins/miniflux/__init__.py
Normal file
16
infra/hermes/plugins/miniflux/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from . import schemas, tools
|
||||||
|
|
||||||
|
|
||||||
|
def register(ctx):
|
||||||
|
ctx.register_tool(
|
||||||
|
name="list_feeds",
|
||||||
|
toolset="miniflux",
|
||||||
|
schema=schemas.LIST_FEEDS,
|
||||||
|
handler=tools.list_feeds,
|
||||||
|
)
|
||||||
|
ctx.register_tool(
|
||||||
|
name="get_unread_entries",
|
||||||
|
toolset="miniflux",
|
||||||
|
schema=schemas.GET_UNREAD_ENTRIES,
|
||||||
|
handler=tools.get_unread_entries,
|
||||||
|
)
|
||||||
10
infra/hermes/plugins/miniflux/plugin.yaml
Normal file
10
infra/hermes/plugins/miniflux/plugin.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
name: miniflux
|
||||||
|
version: 1.0.0
|
||||||
|
description: Read feeds and entries from the local Miniflux RSS reader
|
||||||
|
requires_env:
|
||||||
|
MINIFLUX_API_KEY:
|
||||||
|
description: Miniflux API key (generate in Miniflux → Settings → API Keys)
|
||||||
|
secret: true
|
||||||
|
provides_tools:
|
||||||
|
- list_feeds
|
||||||
|
- get_unread_entries
|
||||||
34
infra/hermes/plugins/miniflux/schemas.py
Normal file
34
infra/hermes/plugins/miniflux/schemas.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
LIST_FEEDS = {
|
||||||
|
"name": "list_feeds",
|
||||||
|
"description": (
|
||||||
|
"List all subscribed RSS feeds from Miniflux. "
|
||||||
|
"Returns feed titles, URLs, and unread counts."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
GET_UNREAD_ENTRIES = {
|
||||||
|
"name": "get_unread_entries",
|
||||||
|
"description": (
|
||||||
|
"Get unread entries from Miniflux. "
|
||||||
|
"Optionally filter by feed ID and limit the number of results."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"feed_id": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Filter to a specific feed. Omit for all feeds.",
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum number of entries to return. Defaults to 20.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
67
infra/hermes/plugins/miniflux/tools.py
Normal file
67
infra/hermes/plugins/miniflux/tools.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
MINIFLUX_BASE = os.environ.get("MINIFLUX_BASE_URL", "http://miniflux:8080")
|
||||||
|
MINIFLUX_KEY = os.environ.get("MINIFLUX_API_KEY", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _request(path):
|
||||||
|
"""Make an authenticated GET request to the Miniflux API."""
|
||||||
|
url = f"{MINIFLUX_BASE}/v1{path}"
|
||||||
|
req = urllib.request.Request(url, headers={"X-Auth-Token": MINIFLUX_KEY})
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
return json.loads(resp.read())
|
||||||
|
|
||||||
|
|
||||||
|
def list_feeds(args: dict, **kwargs) -> str:
|
||||||
|
try:
|
||||||
|
feeds = _request("/feeds")
|
||||||
|
counters = _request("/feeds/counters")
|
||||||
|
unreads = counters.get("reads", {}) # keyed by feed id
|
||||||
|
unread_map = counters.get("unreads", {})
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for f in feeds:
|
||||||
|
fid = str(f["id"])
|
||||||
|
result.append({
|
||||||
|
"id": f["id"],
|
||||||
|
"title": f["title"],
|
||||||
|
"site_url": f.get("site_url", ""),
|
||||||
|
"unread": unread_map.get(fid, 0),
|
||||||
|
})
|
||||||
|
result.sort(key=lambda x: x["unread"], reverse=True)
|
||||||
|
return json.dumps({"feeds": result, "total": len(result)})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
def get_unread_entries(args: dict, **kwargs) -> str:
|
||||||
|
try:
|
||||||
|
limit = args.get("limit", 20)
|
||||||
|
feed_id = args.get("feed_id")
|
||||||
|
|
||||||
|
if feed_id:
|
||||||
|
path = f"/feeds/{feed_id}/entries?status=unread&limit={limit}&direction=desc&order=published_at"
|
||||||
|
else:
|
||||||
|
path = f"/entries?status=unread&limit={limit}&direction=desc&order=published_at"
|
||||||
|
|
||||||
|
data = _request(path)
|
||||||
|
entries = []
|
||||||
|
for e in data.get("entries", []):
|
||||||
|
entries.append({
|
||||||
|
"id": e["id"],
|
||||||
|
"title": e["title"],
|
||||||
|
"url": e.get("url", ""),
|
||||||
|
"feed": e.get("feed", {}).get("title", ""),
|
||||||
|
"author": e.get("author", ""),
|
||||||
|
"published_at": e.get("published_at", ""),
|
||||||
|
"reading_time": e.get("reading_time", 0),
|
||||||
|
})
|
||||||
|
return json.dumps({
|
||||||
|
"entries": entries,
|
||||||
|
"total": data.get("total", len(entries)),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"error": str(e)})
|
||||||
@@ -12,6 +12,9 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- monotrope
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: "postgres://miniflux:${MINIFLUX_DB_PASSWORD}@db/miniflux?sslmode=disable"
|
DATABASE_URL: "postgres://miniflux:${MINIFLUX_DB_PASSWORD}@db/miniflux?sslmode=disable"
|
||||||
RUN_MIGRATIONS: "1"
|
RUN_MIGRATIONS: "1"
|
||||||
@@ -44,5 +47,10 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
monotrope:
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
miniflux_db:
|
miniflux_db:
|
||||||
|
|||||||
Reference in New Issue
Block a user