mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-20 21:57:14 +08:00
Merge pull request #2965 from AaronZ345/agent/cross-channel-tasks
feat: make /tasks available across chat channels
This commit is contained in:
@@ -36,6 +36,7 @@ TELEGRAM_BOT_COMMANDS = [
|
|||||||
("help", "Show command help"),
|
("help", "Show command help"),
|
||||||
("status", "Show running status"),
|
("status", "Show running status"),
|
||||||
("context", "View/clear conversation context (sub: clear)"),
|
("context", "View/clear conversation context (sub: clear)"),
|
||||||
|
("tasks", "List scheduled tasks for this chat"),
|
||||||
("skill", "Manage skills (list/search/install/...)"),
|
("skill", "Manage skills (list/search/install/...)"),
|
||||||
("memory", "Manage memory (sub: dream)"),
|
("memory", "Manage memory (sub: dream)"),
|
||||||
("knowledge", "Manage knowledge base (list/on/off)"),
|
("knowledge", "Manage knowledge base (list/on/off)"),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ KNOWN_COMMANDS = {
|
|||||||
"help", "version", "status", "logs",
|
"help", "version", "status", "logs",
|
||||||
"start", "stop", "restart",
|
"start", "stop", "restart",
|
||||||
"cancel",
|
"cancel",
|
||||||
"skill", "context", "config",
|
"skill", "context", "config", "tasks",
|
||||||
"knowledge", "memory",
|
"knowledge", "memory",
|
||||||
"install-browser",
|
"install-browser",
|
||||||
}
|
}
|
||||||
@@ -357,6 +357,7 @@ class CowCliPlugin(Plugin):
|
|||||||
"/logs [N]: Show the last N log lines (default 20)",
|
"/logs [N]: Show the last N log lines (default 20)",
|
||||||
"/context: Show current conversation context",
|
"/context: Show current conversation context",
|
||||||
"/context clear: Clear current conversation context",
|
"/context clear: Clear current conversation context",
|
||||||
|
"/tasks: List scheduled tasks for this chat",
|
||||||
"/skill list: List installed skills",
|
"/skill list: List installed skills",
|
||||||
"/skill list --remote: Browse Skill Hub",
|
"/skill list --remote: Browse Skill Hub",
|
||||||
"/skill search <keyword>: Search skills",
|
"/skill search <keyword>: Search skills",
|
||||||
@@ -385,6 +386,7 @@ class CowCliPlugin(Plugin):
|
|||||||
"/logs [N]: 查看最近N条日志 (默认20)",
|
"/logs [N]: 查看最近N条日志 (默认20)",
|
||||||
"/context: 查看当前对话上下文信息",
|
"/context: 查看当前对话上下文信息",
|
||||||
"/context clear: 清除当前对话上下文",
|
"/context clear: 清除当前对话上下文",
|
||||||
|
"/tasks: 查看当前会话的定时任务",
|
||||||
"/skill list: 查看已安装的技能",
|
"/skill list: 查看已安装的技能",
|
||||||
"/skill list --remote: 浏览技能广场",
|
"/skill list --remote: 浏览技能广场",
|
||||||
"/skill search <关键词>: 搜索技能",
|
"/skill search <关键词>: 搜索技能",
|
||||||
@@ -407,6 +409,85 @@ class CowCliPlugin(Plugin):
|
|||||||
def _cmd_version(self, args: str, e_context, **_) -> str:
|
def _cmd_version(self, args: str, e_context, **_) -> str:
|
||||||
return f"CowAgent v{__version__}"
|
return f"CowAgent v{__version__}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# tasks — read-only scheduler list scoped to the current chat.
|
||||||
|
# Feishu intercepts /tasks before this plugin to keep its interactive card;
|
||||||
|
# every other channel receives this plain-text view.
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _cmd_tasks(self, args: str, e_context, session_id: str = "", **_) -> str:
|
||||||
|
from agent.tools.scheduler.integration import get_task_store
|
||||||
|
|
||||||
|
task_store = get_task_store()
|
||||||
|
if task_store is None:
|
||||||
|
from agent.tools.scheduler.task_store import TaskStore
|
||||||
|
from common.utils import expand_path
|
||||||
|
|
||||||
|
workspace = expand_path(conf().get("agent_workspace", "~/cow"))
|
||||||
|
task_store = TaskStore(os.path.join(workspace, "scheduler", "tasks.json"))
|
||||||
|
|
||||||
|
channel_type = ""
|
||||||
|
receiver = ""
|
||||||
|
if e_context is not None:
|
||||||
|
context = e_context["context"]
|
||||||
|
channel_type = context.get("channel_type", "") or ""
|
||||||
|
receiver = context.get("receiver", "") or ""
|
||||||
|
session_id = context.get("session_id", "") or session_id
|
||||||
|
|
||||||
|
visible = []
|
||||||
|
for task in task_store.list_tasks():
|
||||||
|
action = task.get("action") or {}
|
||||||
|
if receiver:
|
||||||
|
if action.get("receiver") != receiver:
|
||||||
|
continue
|
||||||
|
if channel_type and action.get("channel_type") != channel_type:
|
||||||
|
continue
|
||||||
|
elif session_id not in {
|
||||||
|
action.get("receiver"),
|
||||||
|
action.get("notify_session_id"),
|
||||||
|
}:
|
||||||
|
continue
|
||||||
|
visible.append(task)
|
||||||
|
|
||||||
|
return self._format_tasks(visible)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_tasks(tasks) -> str:
|
||||||
|
if not tasks:
|
||||||
|
return _t(
|
||||||
|
"📅 当前会话暂无定时任务。",
|
||||||
|
"📅 No scheduled tasks in this chat.",
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = [_t("📅 当前会话的定时任务", "📅 Scheduled tasks in this chat"), ""]
|
||||||
|
for index, task in enumerate(tasks[:20], 1):
|
||||||
|
enabled = task.get("enabled", True)
|
||||||
|
status = "✅" if enabled else "⏸️"
|
||||||
|
schedule = task.get("schedule") or {}
|
||||||
|
schedule_type = schedule.get("type")
|
||||||
|
if schedule_type == "cron":
|
||||||
|
schedule_text = "cron {}".format(schedule.get("expression") or "?")
|
||||||
|
elif schedule_type == "interval":
|
||||||
|
schedule_text = "every {}s".format(schedule.get("seconds") or "?")
|
||||||
|
elif schedule_type == "once":
|
||||||
|
schedule_text = "once at {}".format(schedule.get("run_at") or "?")
|
||||||
|
else:
|
||||||
|
schedule_text = str(schedule_type or "unknown")
|
||||||
|
|
||||||
|
next_run = str(task.get("next_run_at") or "-").replace("T", " ")
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"{}. {} {}".format(index, status, task.get("name") or "Unnamed task"),
|
||||||
|
" ID: {}".format(task.get("id") or "-"),
|
||||||
|
" {} · Next: {}".format(schedule_text, next_run),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
hidden = len(tasks) - 20
|
||||||
|
if hidden > 0:
|
||||||
|
lines.append(_t("\n另有 {} 个任务未显示。", "\n{} more tasks are hidden.").format(hidden))
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# cancel — abort the in-flight agent run for the current session.
|
# cancel — abort the in-flight agent run for the current session.
|
||||||
# Fallback handler; in practice chat_channel/web_channel intercept
|
# Fallback handler; in practice chat_channel/web_channel intercept
|
||||||
|
|||||||
114
tests/test_cow_cli_tasks.py
Normal file
114
tests/test_cow_cli_tasks.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import os
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import plugins
|
||||||
|
|
||||||
|
|
||||||
|
_old_plugin_path = plugins.instance.current_plugin_path
|
||||||
|
plugins.instance.current_plugin_path = os.path.join(os.getcwd(), "plugins", "cow_cli")
|
||||||
|
try:
|
||||||
|
from plugins.cow_cli.cow_cli import KNOWN_COMMANDS
|
||||||
|
finally:
|
||||||
|
plugins.instance.current_plugin_path = _old_plugin_path
|
||||||
|
|
||||||
|
CowCliPlugin = plugins.instance.plugins["COW_CLI"]
|
||||||
|
|
||||||
|
|
||||||
|
class FakeTaskStore:
|
||||||
|
def __init__(self, tasks):
|
||||||
|
self.tasks = tasks
|
||||||
|
|
||||||
|
def list_tasks(self):
|
||||||
|
return list(self.tasks)
|
||||||
|
|
||||||
|
|
||||||
|
def _task(task_id, receiver, channel_type, enabled=True, notify_session_id=None):
|
||||||
|
action = {
|
||||||
|
"receiver": receiver,
|
||||||
|
"channel_type": channel_type,
|
||||||
|
}
|
||||||
|
if notify_session_id:
|
||||||
|
action["notify_session_id"] = notify_session_id
|
||||||
|
return {
|
||||||
|
"id": task_id,
|
||||||
|
"name": f"Task {task_id}",
|
||||||
|
"enabled": enabled,
|
||||||
|
"schedule": {"type": "cron", "expression": "0 9 * * *"},
|
||||||
|
"next_run_at": "2026-07-18T09:00:00",
|
||||||
|
"action": action,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _event_context(channel_type, receiver, session_id):
|
||||||
|
context = SimpleNamespace(
|
||||||
|
kwargs={
|
||||||
|
"channel_type": channel_type,
|
||||||
|
"receiver": receiver,
|
||||||
|
"session_id": session_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context.get = context.kwargs.get
|
||||||
|
return {"context": context}
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_is_a_known_command_and_is_listed_in_help():
|
||||||
|
plugin = CowCliPlugin()
|
||||||
|
|
||||||
|
assert "tasks" in KNOWN_COMMANDS
|
||||||
|
assert "/tasks" in plugin._cmd_help("", None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_command_only_lists_tasks_owned_by_current_channel_and_receiver(monkeypatch):
|
||||||
|
store = FakeTaskStore(
|
||||||
|
[
|
||||||
|
_task("mine", "user-1", "telegram"),
|
||||||
|
_task("other-channel", "user-1", "feishu"),
|
||||||
|
_task("other-user", "user-2", "telegram"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"agent.tools.scheduler.integration.get_task_store", lambda: store
|
||||||
|
)
|
||||||
|
plugin = CowCliPlugin()
|
||||||
|
|
||||||
|
result = plugin._cmd_tasks(
|
||||||
|
"", _event_context("telegram", "user-1", "session-1")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "mine" in result
|
||||||
|
assert "other-channel" not in result
|
||||||
|
assert "other-user" not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_command_uses_session_identity_without_channel_context(monkeypatch):
|
||||||
|
store = FakeTaskStore(
|
||||||
|
[
|
||||||
|
_task("direct", "session-1", "web"),
|
||||||
|
_task("notified", "chat-1", "feishu", notify_session_id="session-1"),
|
||||||
|
_task("hidden", "session-2", "web"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"agent.tools.scheduler.integration.get_task_store", lambda: store
|
||||||
|
)
|
||||||
|
plugin = CowCliPlugin()
|
||||||
|
|
||||||
|
result = plugin._cmd_tasks("", None, session_id="session-1")
|
||||||
|
|
||||||
|
assert "direct" in result
|
||||||
|
assert "notified" in result
|
||||||
|
assert "hidden" not in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_tasks_command_formats_empty_state(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"agent.tools.scheduler.integration.get_task_store",
|
||||||
|
lambda: FakeTaskStore([]),
|
||||||
|
)
|
||||||
|
plugin = CowCliPlugin()
|
||||||
|
|
||||||
|
result = plugin._cmd_tasks(
|
||||||
|
"", _event_context("slack", "channel-1", "session-1")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "task" in result.lower() or "任务" in result
|
||||||
Reference in New Issue
Block a user