mirror of
https://github.com/domfelipe/hermes-agent-custom.git
synced 2026-08-07 11:56:46 +00:00
fix: route managed cronjobs and skills
This commit is contained in:
parent
b5bf9dff1b
commit
8c92e14caf
2 changed files with 143 additions and 6 deletions
|
|
@ -1,4 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
"""
|
"""
|
||||||
skills_api.py — Reverse proxy + Runtime Sync API para Hermes Agent.
|
skills_api.py — Reverse proxy + Runtime Sync API para Hermes Agent.
|
||||||
|
|
||||||
|
|
@ -91,10 +93,32 @@ def _write_text(path: str, content: str, mode: int = 0o644) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _managed_skill_dirname(skill: dict) -> str:
|
def _managed_skill_dirname(skill: dict) -> str:
|
||||||
raw_name = str(skill.get("name") or "skill").strip().lower()
|
raw_name = str(skill.get("name") or "skill").strip()
|
||||||
slug = re.sub(r"[^a-z0-9._-]+", "-", raw_name).strip("-") or "skill"
|
# Hermes' skill_view resolves bare names by directory name/path, not by
|
||||||
skill_id = str(skill.get("skill_id") or "manual").strip()[:8] or "manual"
|
# frontmatter. Keep Mika-managed skill dirs aligned with the user-facing
|
||||||
return f"{slug}--{skill_id}"
|
# skill name so `skill_view(name="Minha skill")` can load them immediately.
|
||||||
|
safe_name = re.sub(r"[\x00/\\]+", "-", raw_name)
|
||||||
|
safe_name = re.sub(r"\s+", " ", safe_name).strip(" .")
|
||||||
|
if not safe_name or safe_name in {".", ".."}:
|
||||||
|
return "skill"
|
||||||
|
return safe_name[:120]
|
||||||
|
|
||||||
|
|
||||||
|
def _telegram_home_origin() -> dict | None:
|
||||||
|
chat_id = os.environ.get("TELEGRAM_HOME_CHANNEL", "").strip()
|
||||||
|
if not chat_id:
|
||||||
|
return None
|
||||||
|
thread_id = (
|
||||||
|
os.environ.get("TELEGRAM_CRON_THREAD_ID", "").strip()
|
||||||
|
or os.environ.get("TELEGRAM_HOME_CHANNEL_THREAD_ID", "").strip()
|
||||||
|
)
|
||||||
|
origin = {
|
||||||
|
"platform": "telegram",
|
||||||
|
"chat_id": chat_id,
|
||||||
|
}
|
||||||
|
if thread_id:
|
||||||
|
origin["thread_id"] = thread_id
|
||||||
|
return origin
|
||||||
|
|
||||||
|
|
||||||
def _managed_entry_name(slug_source: str, entry_id: str) -> str:
|
def _managed_entry_name(slug_source: str, entry_id: str) -> str:
|
||||||
|
|
@ -215,8 +239,8 @@ def _sanitize_cron_runtime_job(raw_job: dict, existing_job: dict | None, synced_
|
||||||
"last_status": existing_job.get("last_status") if existing_job else None,
|
"last_status": existing_job.get("last_status") if existing_job else None,
|
||||||
"last_error": existing_job.get("last_error") if existing_job else None,
|
"last_error": existing_job.get("last_error") if existing_job else None,
|
||||||
"last_delivery_error": existing_job.get("last_delivery_error") if existing_job else None,
|
"last_delivery_error": existing_job.get("last_delivery_error") if existing_job else None,
|
||||||
"deliver": "local",
|
"deliver": "origin",
|
||||||
"origin": None,
|
"origin": existing_job.get("origin") if existing_job and existing_job.get("origin") else _telegram_home_origin(),
|
||||||
"enabled_toolsets": None,
|
"enabled_toolsets": None,
|
||||||
"workdir": None,
|
"workdir": None,
|
||||||
"managed_by": "mika",
|
"managed_by": "mika",
|
||||||
|
|
@ -329,6 +353,14 @@ async def skills_sync(request: web.Request) -> web.Response:
|
||||||
}
|
}
|
||||||
_write_json(SKILLS_MANIFEST_PATH, manifest, mode=0o644)
|
_write_json(SKILLS_MANIFEST_PATH, manifest, mode=0o644)
|
||||||
|
|
||||||
|
reload_result = None
|
||||||
|
reload_error = None
|
||||||
|
try:
|
||||||
|
from agent.skill_commands import reload_skills
|
||||||
|
reload_result = reload_skills()
|
||||||
|
except Exception as exc:
|
||||||
|
reload_error = str(exc)
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"received": len(incoming_skills),
|
"received": len(incoming_skills),
|
||||||
|
|
@ -336,6 +368,8 @@ async def skills_sync(request: web.Request) -> web.Response:
|
||||||
"skipped": skipped,
|
"skipped": skipped,
|
||||||
"removed": removed,
|
"removed": removed,
|
||||||
"managed_skills_root": MANAGED_SKILLS_ROOT,
|
"managed_skills_root": MANAGED_SKILLS_ROOT,
|
||||||
|
"reload": reload_result,
|
||||||
|
"reload_error": reload_error,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
103
tests/test_skills_api.py
Normal file
103
tests/test_skills_api.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
class SkillsApiSyncTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
fake_web = types.SimpleNamespace(
|
||||||
|
Request=object,
|
||||||
|
Response=object,
|
||||||
|
Application=object,
|
||||||
|
json_response=lambda payload, status=200: {"payload": payload, "status": status},
|
||||||
|
run_app=lambda *_args, **_kwargs: None,
|
||||||
|
)
|
||||||
|
fake_aiohttp = types.ModuleType("aiohttp")
|
||||||
|
fake_aiohttp.web = fake_web
|
||||||
|
fake_aiohttp.ClientSession = object
|
||||||
|
fake_aiohttp.ClientTimeout = object
|
||||||
|
sys.modules.setdefault("aiohttp", fake_aiohttp)
|
||||||
|
cls.skills_api = importlib.import_module("patches.skills_api")
|
||||||
|
|
||||||
|
def test_managed_skill_dirname_matches_user_facing_skill_name(self) -> None:
|
||||||
|
dirname = self.skills_api._managed_skill_dirname({
|
||||||
|
"skill_id": "ffdcdb9f-a52b-447b-ad47-57b390ce6c5d",
|
||||||
|
"name": "teste fechamento codex",
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(dirname, "teste fechamento codex")
|
||||||
|
|
||||||
|
def test_managed_skill_dirname_removes_path_separators(self) -> None:
|
||||||
|
dirname = self.skills_api._managed_skill_dirname({
|
||||||
|
"name": "../minha/skill\\nova",
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(dirname, "-minha-skill-nova")
|
||||||
|
|
||||||
|
def test_managed_cronjobs_deliver_to_origin_with_telegram_home(self) -> None:
|
||||||
|
def fake_next_run(_schedule, _last_run_at):
|
||||||
|
return "2026-05-30T16:33:00+00:00"
|
||||||
|
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"TELEGRAM_HOME_CHANNEL": "12345",
|
||||||
|
"TELEGRAM_CRON_THREAD_ID": "topic-1",
|
||||||
|
},
|
||||||
|
clear=False,
|
||||||
|
):
|
||||||
|
job = self.skills_api._sanitize_cron_runtime_job(
|
||||||
|
{
|
||||||
|
"job_id": "job-1",
|
||||||
|
"name": "validar fechamento final",
|
||||||
|
"action_prompt": "validar fechamento final",
|
||||||
|
"cron_expression": "*/3 * * * *",
|
||||||
|
"status": "active",
|
||||||
|
"human_readable": "A cada 3 minutos",
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
"2026-05-30T16:30:00Z",
|
||||||
|
fake_next_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(job)
|
||||||
|
self.assertEqual(job["deliver"], "origin")
|
||||||
|
self.assertEqual(
|
||||||
|
job["origin"],
|
||||||
|
{"platform": "telegram", "chat_id": "12345", "thread_id": "topic-1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_existing_origin_is_preserved_on_resync(self) -> None:
|
||||||
|
def fake_next_run(_schedule, _last_run_at):
|
||||||
|
return "2026-05-30T16:33:00+00:00"
|
||||||
|
|
||||||
|
job = self.skills_api._sanitize_cron_runtime_job(
|
||||||
|
{
|
||||||
|
"job_id": "job-1",
|
||||||
|
"name": "validar fechamento final",
|
||||||
|
"action_prompt": "validar fechamento final",
|
||||||
|
"cron_expression": "*/3 * * * *",
|
||||||
|
"status": "active",
|
||||||
|
"human_readable": "A cada 3 minutos",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": {"platform": "telegram", "chat_id": "existing"},
|
||||||
|
"schedule": {"expr": "*/3 * * * *"},
|
||||||
|
"next_run_at": "2026-05-30T16:36:00+00:00",
|
||||||
|
},
|
||||||
|
"2026-05-30T16:30:00Z",
|
||||||
|
fake_next_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(job)
|
||||||
|
self.assertEqual(job["origin"], {"platform": "telegram", "chat_id": "existing"})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue