feat: 定时任务支持艾特指定的人
This commit is contained in:
parent
4247b64a52
commit
885e7c401c
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: create-scheduled-task
|
name: create-scheduled-task
|
||||||
description: "创建当前微信会话中的提醒或定时任务。当用户说“X 分钟后提醒我 Y”“X 分钟提醒我 Y”“过一会提醒我”“每天/每周/工作日几点提醒我”“帮我设个提醒”,或“创建一个……定时任务”时使用。支持一次性延时、每日、每周和中国法定工作日任务,也支持按时生成 AI 内容。"
|
description: "创建当前微信会话中的提醒或定时任务。当用户说“X 分钟后提醒我/群友/所有人 Y”“过一会提醒我”“每天/每周/工作日几点提醒我”“帮我设个提醒”或“创建一个……定时任务”时使用。支持一次性延时、每日、每周和中国法定工作日任务,可在群聊中真正 @ 指定成员或 @所有人,也支持按时生成 AI 内容。"
|
||||||
---
|
---
|
||||||
|
|
||||||
# 创建定时任务
|
# 创建定时任务
|
||||||
@ -9,11 +9,11 @@ description: "创建当前微信会话中的提醒或定时任务。当用户说
|
|||||||
|
|
||||||
## 执行流程
|
## 执行流程
|
||||||
|
|
||||||
1. 从用户原话中提取任务名称、执行规则和发送内容。
|
1. 从用户原话中提取任务名称、执行规则、发送内容,以及要 @ 创建人、指定群成员、所有人还是不 @。
|
||||||
2. 缺少执行时间或发送内容时,只追问缺失项,不运行脚本。
|
2. 缺少执行时间或发送内容时,只追问缺失项,不运行脚本。
|
||||||
3. 按下表选择调度类型和参数。
|
3. 按下表选择调度类型和参数。
|
||||||
4. 在本 Skill 目录执行 `scripts/create_scheduled_task.py`,且不要在真实请求中传 `--dry-run`。
|
4. 在本 Skill 目录执行 `scripts/create_scheduled_task.py`,且不要在真实请求中传 `--dry-run`。
|
||||||
5. 仅在脚本返回 `ok: true` 后确认创建成功。回复任务名称、执行规则、下一次执行时间、提醒内容和发送位置;群聊任务同时说明触发时会自动 @ 创建人。
|
5. 仅在脚本返回 `ok: true` 后确认创建成功。回复任务名称、执行规则、下一次执行时间、提醒内容、发送位置和实际 @ 对象。以返回的 `task.mention` 为准:`all` 说明会 @所有人,`custom` 列出 `display_names`,`creator` 说明会 @ 创建人,`none` 说明不 @ 任何人。
|
||||||
6. 脚本失败时立即向用户反馈原始错误含义,不得声称任务已创建。
|
6. 脚本失败时立即向用户反馈原始错误含义,不得声称任务已创建。
|
||||||
|
|
||||||
## 调度规则
|
## 调度规则
|
||||||
@ -41,11 +41,24 @@ description: "创建当前微信会话中的提醒或定时任务。当用户说
|
|||||||
## 内容规则
|
## 内容规则
|
||||||
|
|
||||||
- 对普通提醒使用 `--content`。默认把“提醒我喝水”整理为面向用户的 `提醒:喝水`,但用户给出精确文案时保持原文。
|
- 对普通提醒使用 `--content`。默认把“提醒我喝水”整理为面向用户的 `提醒:喝水`,但用户给出精确文案时保持原文。
|
||||||
|
- 真正的 @ 由 `--mention`/`--mentions`/`--mention-all` 单独配置,不要在正文中拼接普通文本 `@昵称` 或 `@所有人`。若正文只是以被提醒人的名字作称呼(如“张三,该吃饭了”),通常把 `--content` 整理为“该吃饭了”,避免客户端自动添加 `@张三` 后重复人名;用户明确要求保留精确正文时除外。
|
||||||
- 对“每天生成一份早报”这类动态内容使用 `--ai-prompt`,不要把普通固定提醒升级为 AI 任务。
|
- 对“每天生成一份早报”这类动态内容使用 `--ai-prompt`,不要把普通固定提醒升级为 AI 任务。
|
||||||
- 可以同时传 `--content` 和 `--ai-prompt`;执行时先发送固定文本,再发送 AI 结果。
|
- 可以同时传 `--content` 和 `--ai-prompt`;执行时先发送固定文本,再发送 AI 结果。
|
||||||
- 至少传 `--content` 或 `--ai-prompt` 之一。
|
- 至少传 `--content` 或 `--ai-prompt` 之一。
|
||||||
- 任务名应简短、可识别,不超过 100 个字符;固定文本不超过 500 个字符。
|
- 任务名应简短、可识别,不超过 100 个字符;固定文本不超过 500 个字符。
|
||||||
|
|
||||||
|
## 群成员 @ 规则
|
||||||
|
|
||||||
|
- “提醒我……”或用户没有指定被 @ 人时,不传任何 mention 参数。脚本会为群聊显式保存创建人的微信 ID;私聊会显式保存空数组。
|
||||||
|
- “提醒这位群友 张三……”“到点 @ 张三……”或其他明确点名表达,按用户原话提取群昵称或备注并传 `--mention '张三'`。
|
||||||
|
- 指定多人时重复传 `--mention`,或使用 `--mentions '["张三","李四"]'`。脚本会按群备注、群昵称依次匹配并去重。
|
||||||
|
- 用户明确说“@所有人”“提醒全体成员”“通知群里所有人”时传 `--mention-all`。不要把“所有人”当成员昵称查询,也不要在正文里拼接普通文本。
|
||||||
|
- 用户明确说“不要 @ 人”“只发消息”时传 `--no-mention`。不要用它替代缺省行为。
|
||||||
|
- `--mention-all`、`--no-mention` 和指定成员参数三种模式互斥。
|
||||||
|
- `--mention` 只允许用于当前群聊。不要让用户提供或猜测微信 ID;脚本会通过客户端查询当前群内未退群成员并保存真实微信 ID。
|
||||||
|
- 找不到成员或同一优先级匹配到多人时,脚本会在创建任务前失败。向用户说明需要更准确、唯一的完整群备注或昵称,不要退回为 @ 创建人,也不要只把名字拼进正文。
|
||||||
|
- 固定文本和 AI 内容同时存在时,只在第一条固定文本中 @;只有 AI 内容时在 AI 回复中 @。
|
||||||
|
|
||||||
## 会话目标
|
## 会话目标
|
||||||
|
|
||||||
脚本自动读取以下环境变量:
|
脚本自动读取以下环境变量:
|
||||||
@ -54,7 +67,7 @@ description: "创建当前微信会话中的提醒或定时任务。当用户说
|
|||||||
- `ROBOT_SENDER_WX_ID`:当前消息发送人 ID。
|
- `ROBOT_SENDER_WX_ID`:当前消息发送人 ID。
|
||||||
- `ROBOT_WECHAT_CLIENT_PORT`:机器人客户端端口。
|
- `ROBOT_WECHAT_CLIENT_PORT`:机器人客户端端口。
|
||||||
|
|
||||||
私聊中把当前好友作为创建人和发送目标。群聊中把当前发言人作为创建人、当前群聊作为发送目标。因此,群聊里的“提醒我”会在当前群聊发送,并在任务触发后发送的第一条文本中自动 @ 创建人,不会私聊群成员。成功回复时明确说明这一点。不要让用户提供或猜测这些 ID。
|
私聊中把当前好友作为创建人和发送目标。群聊中把当前发言人作为创建人、当前群聊作为发送目标。因此任务始终发回当前会话,不会私聊群成员。每个发送目标都显式保存 `mention_wechat_ids`:群聊里的“提醒我”保存创建人 ID,明确指定成员时保存成员 ID,`--mention-all` 保存 `notify@all`,传 `--no-mention` 或私聊任务时保存空数组。成功回复必须按脚本实际返回说明,不能笼统声称总会 @ 创建人。不要让用户提供或猜测这些 ID。
|
||||||
|
|
||||||
## 脚本参数
|
## 脚本参数
|
||||||
|
|
||||||
@ -65,6 +78,10 @@ description: "创建当前微信会话中的提醒或定时任务。当用户说
|
|||||||
--ai-prompt <动态内容提示词> 与 --content 至少一个
|
--ai-prompt <动态内容提示词> 与 --content 至少一个
|
||||||
--time <HH:mm> daily/weekly/cn_workday 必填
|
--time <HH:mm> daily/weekly/cn_workday 必填
|
||||||
--weekdays <JSON数组或逗号列表> weekly 必填,1=周一,7=周日
|
--weekdays <JSON数组或逗号列表> weekly 必填,1=周一,7=周日
|
||||||
|
--mention <当前群成员昵称或备注> 可选,可重复;指定真正 @ 的成员
|
||||||
|
--mentions <字符串JSON数组> 可选;一次指定多个真正 @ 的成员
|
||||||
|
--mention-all 可选;真正 @ 当前群所有人
|
||||||
|
--no-mention 可选;明确不 @ 任何人,不能与其他 mention 参数同用
|
||||||
--delay-seconds <整数> delay_once 四选一
|
--delay-seconds <整数> delay_once 四选一
|
||||||
--delay-minutes <整数> delay_once 四选一
|
--delay-minutes <整数> delay_once 四选一
|
||||||
--delay-hours <整数> delay_once 四选一
|
--delay-hours <整数> delay_once 四选一
|
||||||
@ -80,6 +97,24 @@ description: "创建当前微信会话中的提醒或定时任务。当用户说
|
|||||||
python3 scripts/create_scheduled_task.py --name '喝水提醒' --schedule-type delay_once --delay-minutes 10 --content '提醒:喝水'
|
python3 scripts/create_scheduled_task.py --name '喝水提醒' --schedule-type delay_once --delay-minutes 10 --content '提醒:喝水'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
5 分钟后在当前群聊 @ 指定群友:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/create_scheduled_task.py --name '晚饭提醒' --schedule-type delay_once --delay-minutes 5 --mention '又双叒叕' --content '该吃晚饭了🍚'
|
||||||
|
```
|
||||||
|
|
||||||
|
每天同时 @ 多名群友:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/create_scheduled_task.py --name '打卡提醒' --schedule-type daily --time 09:00 --mentions '["张三","李四"]' --content '记得打卡'
|
||||||
|
```
|
||||||
|
|
||||||
|
每天在当前群聊 @所有人:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/create_scheduled_task.py --name '全员打卡提醒' --schedule-type daily --time 09:00 --mention-all --content '请大家记得打卡'
|
||||||
|
```
|
||||||
|
|
||||||
每周一和周三提醒:
|
每周一和周三提醒:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -96,7 +131,7 @@ python3 scripts/create_scheduled_task.py --name '工作日早报' --schedule-typ
|
|||||||
|
|
||||||
## 成功与失败处理
|
## 成功与失败处理
|
||||||
|
|
||||||
- 成功 JSON 包含 `task.id`、`task.schedule_summary`、`task.next_run_time` 和 `task.target_type`。使用这些实际返回值回复,不要只复述用户输入。
|
- 成功 JSON 包含 `task.id`、`task.schedule_summary`、`task.next_run_time`、`task.target_type` 和 `task.mention`。使用这些实际返回值回复,不要只复述用户输入或根据创建人猜测 @ 对象。
|
||||||
- 接口业务错误会以“创建定时任务失败:……”返回。说明可操作的原因,例如发送目标不存在、任务数量达到上限、AI 配置缺失或调度器未初始化。
|
- 接口业务错误会以“创建定时任务失败:……”返回。说明可操作的原因,例如发送目标不存在、任务数量达到上限、AI 配置缺失或调度器未初始化。
|
||||||
- 创建接口不是幂等接口。发生超时、连接中断、无效响应,或错误中含“任务已保存,但刷新调度器失败”时,任务可能已经入库;明确告知用户先到定时任务列表核对,禁止自动重试,以免创建重复任务。
|
- 创建接口不是幂等接口。发生超时、连接中断、无效响应,或错误中含“任务已保存,但刷新调度器失败”时,任务可能已经入库;明确告知用户先到定时任务列表核对,禁止自动重试,以免创建重复任务。
|
||||||
- 普通好友和普通群成员最多创建 5 个任务;群主、群管理员和后台管理员不受此配额限制。
|
- 普通好友和普通群成员最多创建 5 个任务;群主、群管理员和后台管理员不受此配额限制。
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
interface:
|
interface:
|
||||||
display_name: "创建定时任务"
|
display_name: "创建定时任务"
|
||||||
short_description: "根据当前微信会话创建一次性提醒、每日、每周或法定工作日任务"
|
short_description: "创建当前会话提醒任务,并可定时 @ 指定群成员或所有人"
|
||||||
default_prompt: "使用 $create-scheduled-task 在 10 分钟后提醒我喝水。"
|
default_prompt: "使用 $create-scheduled-task 在 5 分钟后 @所有人,提醒大家查看群公告。"
|
||||||
|
|||||||
@ -10,9 +10,10 @@ import re
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, NoReturn
|
from typing import Any, Literal, NoReturn, TypedDict
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
@ -23,6 +24,19 @@ sys.stderr = sys.stdout
|
|||||||
|
|
||||||
SCHEDULE_TYPES = ("delay_once", "daily", "weekly", "cn_workday")
|
SCHEDULE_TYPES = ("delay_once", "daily", "weekly", "cn_workday")
|
||||||
MAX_DELAY_SECONDS = 24 * 60 * 60
|
MAX_DELAY_SECONDS = 24 * 60 * 60
|
||||||
|
MENTION_ALL_WECHAT_ID = "notify@all"
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledTaskTarget(TypedDict):
|
||||||
|
wechat_id: str
|
||||||
|
type: Literal["chat_room", "friend"]
|
||||||
|
mention_wechat_ids: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledTaskCreator(TypedDict):
|
||||||
|
type: Literal["chat_room", "friend"]
|
||||||
|
wechat_id: str
|
||||||
|
chat_room_id: str
|
||||||
|
|
||||||
|
|
||||||
class AmbiguousCreateError(RuntimeError):
|
class AmbiguousCreateError(RuntimeError):
|
||||||
@ -61,6 +75,10 @@ def _parse_args(argv: list[str]) -> argparse.Namespace:
|
|||||||
parser.add_argument("--time", default="")
|
parser.add_argument("--time", default="")
|
||||||
parser.add_argument("--weekday", action="append", default=[])
|
parser.add_argument("--weekday", action="append", default=[])
|
||||||
parser.add_argument("--weekdays", action="append", default=[])
|
parser.add_argument("--weekdays", action="append", default=[])
|
||||||
|
parser.add_argument("--mention", action="append", default=[])
|
||||||
|
parser.add_argument("--mentions", action="append", default=[])
|
||||||
|
parser.add_argument("--mention-all", "--all", dest="mention_all", action="store_true")
|
||||||
|
parser.add_argument("--no-mention", action="store_true")
|
||||||
|
|
||||||
delay = parser.add_mutually_exclusive_group()
|
delay = parser.add_mutually_exclusive_group()
|
||||||
delay.add_argument("--delay-seconds", "--delay_seconds", dest="delay_seconds", type=int)
|
delay.add_argument("--delay-seconds", "--delay_seconds", dest="delay_seconds", type=int)
|
||||||
@ -79,15 +97,19 @@ def _require_env(name: str) -> str:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _conversation_identity() -> tuple[dict[str, str], dict[str, str], str]:
|
def _conversation_identity() -> tuple[ScheduledTaskTarget, ScheduledTaskCreator, str]:
|
||||||
from_wechat_id = _require_env("ROBOT_FROM_WX_ID")
|
from_wechat_id = _require_env("ROBOT_FROM_WX_ID")
|
||||||
sender_wechat_id = os.environ.get("ROBOT_SENDER_WX_ID", "").strip()
|
sender_wechat_id = os.environ.get("ROBOT_SENDER_WX_ID", "").strip()
|
||||||
|
|
||||||
if from_wechat_id.endswith("@chatroom"):
|
if from_wechat_id.endswith("@chatroom"):
|
||||||
if not sender_wechat_id:
|
if not sender_wechat_id:
|
||||||
raise ValueError("当前群聊缺少消息发送人微信 ID")
|
raise ValueError("当前群聊缺少消息发送人微信 ID")
|
||||||
target = {"wechat_id": from_wechat_id, "type": "chat_room"}
|
target: ScheduledTaskTarget = {
|
||||||
creator = {
|
"wechat_id": from_wechat_id,
|
||||||
|
"type": "chat_room",
|
||||||
|
"mention_wechat_ids": [],
|
||||||
|
}
|
||||||
|
creator: ScheduledTaskCreator = {
|
||||||
"type": "chat_room",
|
"type": "chat_room",
|
||||||
"wechat_id": sender_wechat_id,
|
"wechat_id": sender_wechat_id,
|
||||||
"chat_room_id": from_wechat_id,
|
"chat_room_id": from_wechat_id,
|
||||||
@ -95,7 +117,11 @@ def _conversation_identity() -> tuple[dict[str, str], dict[str, str], str]:
|
|||||||
return target, creator, "当前群聊"
|
return target, creator, "当前群聊"
|
||||||
|
|
||||||
creator_wechat_id = sender_wechat_id or from_wechat_id
|
creator_wechat_id = sender_wechat_id or from_wechat_id
|
||||||
target = {"wechat_id": from_wechat_id, "type": "friend"}
|
target = {
|
||||||
|
"wechat_id": from_wechat_id,
|
||||||
|
"type": "friend",
|
||||||
|
"mention_wechat_ids": [],
|
||||||
|
}
|
||||||
creator = {"type": "friend", "wechat_id": creator_wechat_id, "chat_room_id": ""}
|
creator = {"type": "friend", "wechat_id": creator_wechat_id, "chat_room_id": ""}
|
||||||
return target, creator, "当前私聊"
|
return target, creator, "当前私聊"
|
||||||
|
|
||||||
@ -139,6 +165,31 @@ def _parse_weekdays(values: list[str]) -> list[int]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_mentions(mention_values: list[str], mentions_values: list[str]) -> list[str]:
|
||||||
|
parsed = [value.strip() for value in mention_values if value.strip()]
|
||||||
|
for raw in mentions_values:
|
||||||
|
text = raw.strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
items = json.loads(text)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"mentions JSON 格式错误: {exc.msg}") from exc
|
||||||
|
if not isinstance(items, list) or any(not isinstance(item, str) for item in items):
|
||||||
|
raise ValueError("mentions 必须是字符串 JSON 数组")
|
||||||
|
|
||||||
|
parsed.extend(item.strip() for item in items if item.strip())
|
||||||
|
|
||||||
|
result: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for mention in parsed:
|
||||||
|
key = mention.casefold()
|
||||||
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
result.append(mention)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _parse_run_at(value: str, now: datetime) -> int:
|
def _parse_run_at(value: str, now: datetime) -> int:
|
||||||
text = value.strip()
|
text = value.strip()
|
||||||
parsed: datetime | None = None
|
parsed: datetime | None = None
|
||||||
@ -201,7 +252,124 @@ def _build_schedule_config(args: argparse.Namespace, now: datetime) -> dict[str,
|
|||||||
return {"time": clock}
|
return {"time": clock}
|
||||||
|
|
||||||
|
|
||||||
def _build_payload(args: argparse.Namespace) -> tuple[dict[str, Any], str]:
|
def _member_text(member: dict[str, Any], field: str) -> str:
|
||||||
|
value = member.get(field)
|
||||||
|
return str(value).strip() if value is not None else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _member_candidates(data: Any, chat_room_id: str) -> list[dict[str, Any]]:
|
||||||
|
if not isinstance(data, list):
|
||||||
|
raise RuntimeError("群成员查询接口返回的数据不是数组")
|
||||||
|
|
||||||
|
candidates: list[dict[str, Any]] = []
|
||||||
|
for item in data:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
if _member_text(item, "chat_room_id") != chat_room_id:
|
||||||
|
continue
|
||||||
|
if item.get("is_leaved") not in (None, False, 0):
|
||||||
|
continue
|
||||||
|
if _member_text(item, "wechat_id"):
|
||||||
|
candidates.append(item)
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _describe_member(member: dict[str, Any]) -> str:
|
||||||
|
remark = _member_text(member, "remark")
|
||||||
|
nickname = _member_text(member, "nickname")
|
||||||
|
if remark and nickname and remark != nickname:
|
||||||
|
return f"{remark}(昵称:{nickname})"
|
||||||
|
return remark or nickname or _member_text(member, "wechat_id")
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_unique_member(
|
||||||
|
mention: str,
|
||||||
|
candidates: list[dict[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
keyword = mention.casefold()
|
||||||
|
match_groups = [
|
||||||
|
[item for item in candidates if _member_text(item, "remark").casefold() == keyword],
|
||||||
|
[item for item in candidates if _member_text(item, "nickname").casefold() == keyword],
|
||||||
|
[item for item in candidates if keyword in _member_text(item, "remark").casefold()],
|
||||||
|
[item for item in candidates if keyword in _member_text(item, "nickname").casefold()],
|
||||||
|
]
|
||||||
|
|
||||||
|
for matches in match_groups:
|
||||||
|
unique: dict[str, dict[str, Any]] = {}
|
||||||
|
for item in matches:
|
||||||
|
unique.setdefault(_member_text(item, "wechat_id"), item)
|
||||||
|
if len(unique) == 1:
|
||||||
|
return next(iter(unique.values()))
|
||||||
|
if len(unique) > 1:
|
||||||
|
labels = "、".join(_describe_member(item) for item in list(unique.values())[:5])
|
||||||
|
raise ValueError(
|
||||||
|
f"群成员“{mention}”匹配到多人({labels}),请使用唯一的完整群备注或昵称"
|
||||||
|
)
|
||||||
|
|
||||||
|
raise ValueError(f"未找到当前群内未退群成员:“{mention}”")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_json(url: str, timeout: int = 15) -> dict[str, Any]:
|
||||||
|
request = urllib.request.Request(url, method="GET")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||||
|
response_text = response.read().decode("utf-8", errors="replace")
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
error_body = exc.read().decode("utf-8", errors="replace")
|
||||||
|
raise RuntimeError(f"查询群成员接口返回 HTTP {exc.code}: {error_body}") from exc
|
||||||
|
except (urllib.error.URLError, TimeoutError, socket.timeout, ConnectionError) as exc:
|
||||||
|
raise RuntimeError(f"查询群成员失败:{exc}") from exc
|
||||||
|
|
||||||
|
if not response_text.strip():
|
||||||
|
raise RuntimeError("群成员查询接口返回空响应")
|
||||||
|
try:
|
||||||
|
result = json.loads(response_text)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise RuntimeError("群成员查询接口返回了无效 JSON") from exc
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
raise RuntimeError("群成员查询接口响应不是 JSON 对象")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_mentions(
|
||||||
|
client_port: str,
|
||||||
|
chat_room_id: str,
|
||||||
|
mentions: list[str],
|
||||||
|
) -> list[dict[str, str]]:
|
||||||
|
resolved: list[dict[str, str]] = []
|
||||||
|
seen_wechat_ids: set[str] = set()
|
||||||
|
for mention in mentions:
|
||||||
|
query = urllib.parse.urlencode({"chat_room_id": chat_room_id, "keyword": mention})
|
||||||
|
url = (
|
||||||
|
f"http://127.0.0.1:{client_port}"
|
||||||
|
f"/api/v1/robot/chat-room/not-left-members?{query}"
|
||||||
|
)
|
||||||
|
response = _get_json(url)
|
||||||
|
if response.get("code") != 200:
|
||||||
|
raise RuntimeError(str(response.get("message") or "查询群成员失败"))
|
||||||
|
member = _pick_unique_member(
|
||||||
|
mention,
|
||||||
|
_member_candidates(response.get("data"), chat_room_id),
|
||||||
|
)
|
||||||
|
wechat_id = _member_text(member, "wechat_id")
|
||||||
|
if wechat_id in seen_wechat_ids:
|
||||||
|
continue
|
||||||
|
seen_wechat_ids.add(wechat_id)
|
||||||
|
resolved.append(
|
||||||
|
{
|
||||||
|
"query": mention,
|
||||||
|
"wechat_id": wechat_id,
|
||||||
|
"display_name": _member_text(member, "remark")
|
||||||
|
or _member_text(member, "nickname")
|
||||||
|
or mention,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
|
def _build_payload(
|
||||||
|
args: argparse.Namespace,
|
||||||
|
) -> tuple[dict[str, Any], str, dict[str, Any]]:
|
||||||
name = args.name.strip()
|
name = args.name.strip()
|
||||||
content = args.content.strip()
|
content = args.content.strip()
|
||||||
ai_prompt = args.ai_prompt.strip()
|
ai_prompt = args.ai_prompt.strip()
|
||||||
@ -216,6 +384,37 @@ def _build_payload(args: argparse.Namespace) -> tuple[dict[str, Any], str]:
|
|||||||
raise ValueError("content 和 ai-prompt 至少需要提供一项")
|
raise ValueError("content 和 ai-prompt 至少需要提供一项")
|
||||||
|
|
||||||
target, creator, target_label = _conversation_identity()
|
target, creator, target_label = _conversation_identity()
|
||||||
|
mentions = _parse_mentions(args.mention, args.mentions)
|
||||||
|
mention_mode_count = int(bool(mentions)) + int(args.mention_all) + int(args.no_mention)
|
||||||
|
if mention_mode_count > 1:
|
||||||
|
raise ValueError("mention-all、no-mention 和 mention/mentions 不能同时使用")
|
||||||
|
|
||||||
|
mention_summary: dict[str, Any]
|
||||||
|
if args.mention_all:
|
||||||
|
if target["type"] != "chat_room":
|
||||||
|
raise ValueError("当前会话不是群聊,不能在定时任务中艾特所有人")
|
||||||
|
target["mention_wechat_ids"] = [MENTION_ALL_WECHAT_ID]
|
||||||
|
mention_summary = {"mode": "all", "display_names": ["所有人"]}
|
||||||
|
elif mentions:
|
||||||
|
if target["type"] != "chat_room":
|
||||||
|
raise ValueError("当前会话不是群聊,不能在定时任务中艾特群成员")
|
||||||
|
client_port = _require_env("ROBOT_WECHAT_CLIENT_PORT")
|
||||||
|
resolved_mentions = _resolve_mentions(client_port, target["wechat_id"], mentions)
|
||||||
|
target["mention_wechat_ids"] = [item["wechat_id"] for item in resolved_mentions]
|
||||||
|
mention_summary = {
|
||||||
|
"mode": "custom",
|
||||||
|
"display_names": [item["display_name"] for item in resolved_mentions],
|
||||||
|
}
|
||||||
|
elif args.no_mention:
|
||||||
|
target["mention_wechat_ids"] = []
|
||||||
|
mention_summary = {"mode": "none", "display_names": []}
|
||||||
|
elif target["type"] == "chat_room":
|
||||||
|
target["mention_wechat_ids"] = [creator["wechat_id"]]
|
||||||
|
mention_summary = {"mode": "creator", "display_names": []}
|
||||||
|
else:
|
||||||
|
target["mention_wechat_ids"] = []
|
||||||
|
mention_summary = {"mode": "none", "display_names": []}
|
||||||
|
|
||||||
schedule_config = _build_schedule_config(args, datetime.now(SHANGHAI_TZ))
|
schedule_config = _build_schedule_config(args, datetime.now(SHANGHAI_TZ))
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
@ -228,7 +427,7 @@ def _build_payload(args: argparse.Namespace) -> tuple[dict[str, Any], str]:
|
|||||||
"ai_prompt": ai_prompt,
|
"ai_prompt": ai_prompt,
|
||||||
"creator": creator,
|
"creator": creator,
|
||||||
}
|
}
|
||||||
return payload, target_label
|
return payload, target_label, mention_summary
|
||||||
|
|
||||||
|
|
||||||
def _post_json(url: str, payload: dict[str, Any], timeout: int = 30) -> dict[str, Any]:
|
def _post_json(url: str, payload: dict[str, Any], timeout: int = 30) -> dict[str, Any]:
|
||||||
@ -293,7 +492,11 @@ def _format_next_run(timestamp: Any) -> str | None:
|
|||||||
return datetime.fromtimestamp(value, SHANGHAI_TZ).strftime("%Y-%m-%d %H:%M:%S Asia/Shanghai")
|
return datetime.fromtimestamp(value, SHANGHAI_TZ).strftime("%Y-%m-%d %H:%M:%S Asia/Shanghai")
|
||||||
|
|
||||||
|
|
||||||
def _success_output(data: dict[str, Any], target_label: str) -> dict[str, Any]:
|
def _success_output(
|
||||||
|
data: dict[str, Any],
|
||||||
|
target_label: str,
|
||||||
|
mention_summary: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
targets = data.get("targets")
|
targets = data.get("targets")
|
||||||
first_target = targets[0] if isinstance(targets, list) and targets else {}
|
first_target = targets[0] if isinstance(targets, list) and targets else {}
|
||||||
if not isinstance(first_target, dict):
|
if not isinstance(first_target, dict):
|
||||||
@ -312,6 +515,7 @@ def _success_output(data: dict[str, Any], target_label: str) -> dict[str, Any]:
|
|||||||
"uses_ai_prompt": bool(data.get("ai_prompt")),
|
"uses_ai_prompt": bool(data.get("ai_prompt")),
|
||||||
"target_label": target_label,
|
"target_label": target_label,
|
||||||
"target_type": first_target.get("type"),
|
"target_type": first_target.get("type"),
|
||||||
|
"mention": mention_summary,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,12 +523,18 @@ def _success_output(data: dict[str, Any], target_label: str) -> dict[str, Any]:
|
|||||||
def main(argv: list[str]) -> int:
|
def main(argv: list[str]) -> int:
|
||||||
try:
|
try:
|
||||||
args = _parse_args(argv)
|
args = _parse_args(argv)
|
||||||
payload, target_label = _build_payload(args)
|
payload, target_label, mention_summary = _build_payload(args)
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print(
|
print(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{"ok": True, "dry_run": True, "target_label": target_label, "payload": payload},
|
{
|
||||||
|
"ok": True,
|
||||||
|
"dry_run": True,
|
||||||
|
"target_label": target_label,
|
||||||
|
"mention": mention_summary,
|
||||||
|
"payload": payload,
|
||||||
|
},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
indent=2,
|
indent=2,
|
||||||
)
|
)
|
||||||
@ -335,7 +545,13 @@ def main(argv: list[str]) -> int:
|
|||||||
url = f"http://127.0.0.1:{client_port}/api/v1/robot/scheduled-tasks"
|
url = f"http://127.0.0.1:{client_port}/api/v1/robot/scheduled-tasks"
|
||||||
response = _post_json(url, payload)
|
response = _post_json(url, payload)
|
||||||
data = _unwrap_api_response(response)
|
data = _unwrap_api_response(response)
|
||||||
print(json.dumps(_success_output(data, target_label), ensure_ascii=False, indent=2))
|
print(
|
||||||
|
json.dumps(
|
||||||
|
_success_output(data, target_label, mention_summary),
|
||||||
|
ensure_ascii=False,
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
return 0
|
return 0
|
||||||
except (ValueError, RuntimeError) as exc:
|
except (ValueError, RuntimeError) as exc:
|
||||||
print(f"创建定时任务失败:{exc}")
|
print(f"创建定时任务失败:{exc}")
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
---
|
---
|
||||||
name: send-mention-message
|
name: send-mention-message
|
||||||
description: "发送艾特/@/提及消息技能。当需要在当前群聊中艾特某个人或某些人,或用户要求你艾特某个/某些群成员时使用。"
|
description: "在当前微信群聊中发送真正的艾特/@/提及消息。用户要求 @ 某个或多个群成员、@所有人、提醒全体成员、通知群里所有人时使用;支持附带正文并可在发送后结束当前 Agent 对话。"
|
||||||
argument-hint: "需要 mention;可选 content。mention 可重复传入多个昵称或备注。"
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Send Mention Message Skill
|
# Send Mention Message Skill
|
||||||
|
|
||||||
## 描述
|
## 描述
|
||||||
|
|
||||||
这是一个在当前微信群聊中发送艾特消息的技能。
|
在当前微信群聊中发送艾特消息,支持指定一个或多个成员,也支持微信原生的 `@所有人`。
|
||||||
|
|
||||||
技能脚本位于 `scripts/send_mention_message.py`,会根据用户提供的昵称或备注在当前群成员表里查找未退群成员,得到微信 ID 后调用机器人客户端文本消息接口发送带 `at` 数组的消息。
|
技能脚本位于 `scripts/send_mention_message.py`。指定成员时,根据昵称或备注查询当前群内未退群成员;@所有人时直接使用客户端协议值 `notify@all`。两种模式最终都会调用文本消息接口发送真正的 `at` 数组,不要把 `@昵称` 或 `@所有人` 当普通正文拼接。
|
||||||
|
|
||||||
## 触发条件
|
## 触发条件
|
||||||
|
|
||||||
- 需要艾特、@、提及某个群成员或多个群成员。
|
- 需要艾特、@、提及某个群成员或多个群成员。
|
||||||
- 用户要求「帮我艾特下 xxx」「@ 一下 xxx」「提一下 xxx 和 yyy」。
|
- 用户要求「帮我艾特下 xxx」「@ 一下 xxx」「提一下 xxx 和 yyy」。
|
||||||
|
- 用户要求「@所有人」「提醒全体成员」「通知群里所有人」。
|
||||||
- 需要在群聊里点名提醒某人。
|
- 需要在群聊里点名提醒某人。
|
||||||
|
|
||||||
私聊场景一般不触发本技能;脚本会校验 `ROBOT_FROM_WX_ID` 必须是群聊 ID。
|
私聊场景一般不触发本技能;脚本会校验 `ROBOT_FROM_WX_ID` 必须是群聊 ID。
|
||||||
@ -37,6 +37,10 @@ argument-hint: "需要 mention;可选 content。mention 可重复传入多个
|
|||||||
},
|
},
|
||||||
"description": "要艾特的多个群成员昵称或备注。"
|
"description": "要艾特的多个群成员昵称或备注。"
|
||||||
},
|
},
|
||||||
|
"all": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "是否 @所有人。用户明确要求 @所有人或通知全体成员时设为 true,不能与 mention/mentions 同时使用。"
|
||||||
|
},
|
||||||
"content": {
|
"content": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "要发送的文本内容,可选。只艾特不附加正文时可以为空字符串。"
|
"description": "要发送的文本内容,可选。只艾特不附加正文时可以为空字符串。"
|
||||||
@ -46,20 +50,23 @@ argument-hint: "需要 mention;可选 content。mention 可重复传入多个
|
|||||||
"description": "是否结束当前对话。当 Agent 已经完成艾特和消息发送、要说的话已说完、要做的事已做完时,设置为 true。"
|
"description": "是否结束当前对话。当 Agent 已经完成艾特和消息发送、要说的话已说完、要做的事已做完时,设置为 true。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"anyOf": [{ "required": ["mention"] }, { "required": ["mentions"] }],
|
"anyOf": [{ "required": ["mention"] }, { "required": ["mentions"] }, { "required": ["all"] }],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
对应命令行参数:
|
对应命令行参数:
|
||||||
|
|
||||||
- `--mention <昵称或备注>` 必填或可重复传入
|
- `--mention <昵称或备注>` 指定成员时使用,可重复传入
|
||||||
- `--mentions <JSON数组>` 可选,用于一次传入多个昵称或备注
|
- `--mentions <JSON数组>` 指定成员时可选,用于一次传入多个昵称或备注
|
||||||
|
- `--all`(也支持 `--mention-all`)可选,用于真正 @所有人,不能与 mention 参数同用
|
||||||
- `--content <文本内容>` 可选
|
- `--content <文本内容>` 可选
|
||||||
- `--ended` 可选标志。当 Agent 已完成艾特和消息发送、要说的话已说完时传入。
|
- `--ended` 可选标志。当 Agent 已完成艾特和消息发送、要说的话已说完时传入。
|
||||||
|
|
||||||
## 成员匹配规则
|
## 成员匹配规则
|
||||||
|
|
||||||
|
仅指定成员时执行以下匹配;`--all` 不查询成员表:
|
||||||
|
|
||||||
1. 只在当前群聊 `ROBOT_FROM_WX_ID` 对应的 `chat_room_members` 记录中查找。
|
1. 只在当前群聊 `ROBOT_FROM_WX_ID` 对应的 `chat_room_members` 记录中查找。
|
||||||
2. 只匹配 `is_leaved` 为空或 `0` 的成员,已经退群的成员不能被艾特。
|
2. 只匹配 `is_leaved` 为空或 `0` 的成员,已经退群的成员不能被艾特。
|
||||||
3. 使用用户给出的昵称或备注做模糊查询,字段优先级为 `remark`,然后是 `nickname`。
|
3. 使用用户给出的昵称或备注做模糊查询,字段优先级为 `remark`,然后是 `nickname`。
|
||||||
@ -71,26 +78,33 @@ argument-hint: "需要 mention;可选 content。mention 可重复传入多个
|
|||||||
## 执行步骤
|
## 执行步骤
|
||||||
|
|
||||||
1. 判断用户是否需要在群聊中艾特某人或某些人。
|
1. 判断用户是否需要在群聊中艾特某人或某些人。
|
||||||
2. 从用户输入中提取要艾特的昵称或备注,写入 `mention` 或 `mentions`;如用户要求附带正文,写入 `content`。
|
2. 指定成员时,把用户原话中的昵称或备注写入 `mention`/`mentions`;@所有人时设置 `all: true` 并使用 `--all`。如用户要求附带正文,写入 `content`。
|
||||||
3. 在该技能目录执行脚本,例如:
|
3. 在该技能目录执行脚本,例如:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/send_mention_message.py --mention '张三' --content '看一下这个'
|
python3 scripts/send_mention_message.py --mention '张三' --content '看一下这个'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
用户要求 @所有人时传 `--all`,不要把“所有人”当成员昵称查询:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/send_mention_message.py --all --content '请大家查看群公告'
|
||||||
|
```
|
||||||
|
|
||||||
当 Agent 认为任务已完成、对话可以结束时,加上 `--ended` 标志:
|
当 Agent 认为任务已完成、对话可以结束时,加上 `--ended` 标志:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/send_mention_message.py --mention '张三' --content '看一下这个' --ended
|
python3 scripts/send_mention_message.py --mention '张三' --content '看一下这个' --ended
|
||||||
```
|
```
|
||||||
|
|
||||||
4. 脚本会查询数据库表 `chat_room_members`,找到当前群内未退群成员的微信 ID。如果数据库没查询到这个人,你可能需要查询你的记忆,看看有没有一个人的别称叫这个名字。
|
4. 指定成员时,脚本查询数据库表 `chat_room_members` 并解析微信 ID;`--all` 时跳过数据库查询,直接发送 `at: ["notify@all"]`。如果指定成员未命中,可以查询记忆里是否记录了对方的别称。
|
||||||
5. 脚本调用客户端接口 `POST http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/text` 发送消息,请求体包含 `to_wxid`、`content`、`at`。
|
5. 脚本调用客户端接口 `POST http://127.0.0.1:{ROBOT_WECHAT_CLIENT_PORT}/api/v1/robot/message/send/text` 发送消息,请求体包含 `to_wxid`、`content`、`at`。
|
||||||
|
|
||||||
## 校验规则
|
## 校验规则
|
||||||
|
|
||||||
- `ROBOT_FROM_WX_ID` 必须是群聊 ID,通常以 `@chatroom` 结尾。
|
- `ROBOT_FROM_WX_ID` 必须是群聊 ID,通常以 `@chatroom` 结尾。
|
||||||
- 至少提供一个 `mention`。
|
- 至少提供一个 `mention`/`mentions`,或者传 `--all`。
|
||||||
|
- `--all` 必须独占,不能再指定成员。
|
||||||
- 每个要艾特的人都必须能在当前群内匹配到未退群成员。
|
- 每个要艾特的人都必须能在当前群内匹配到未退群成员。
|
||||||
- 如果同一个微信 ID 被多个昵称命中,只会艾特一次。
|
- 如果同一个微信 ID 被多个昵称命中,只会艾特一次。
|
||||||
|
|
||||||
@ -107,6 +121,6 @@ python3 scripts/send_mention_message.py --mention '张三' --content '看一下
|
|||||||
|
|
||||||
## 回复要求
|
## 回复要求
|
||||||
|
|
||||||
- 成功时,脚本输出「艾特消息发送成功」,表示消息已通过客户端接口直接发送,无需 AI 智能体再做额外处理。
|
- 成功时,脚本输出「艾特消息发送成功」或「艾特所有人消息发送成功」,表示消息已通过客户端接口直接发送,无需 AI 智能体再做额外处理。
|
||||||
- 如果传入 `--ended`,输出末尾会追加 `ended`,Agent 会自动结束对话。
|
- 如果传入 `--ended`,输出末尾会追加 `ended`,Agent 会自动结束对话。
|
||||||
- 失败时,返回脚本输出的具体错误信息。
|
- 失败时,返回脚本输出的具体错误信息。
|
||||||
|
|||||||
@ -59,17 +59,15 @@ def _ensure_skill_venv_python() -> None:
|
|||||||
os.execv(str(venv_python), [str(venv_python), str(Path(__file__).resolve()), *sys.argv[1:]])
|
os.execv(str(venv_python), [str(venv_python), str(Path(__file__).resolve()), *sys.argv[1:]])
|
||||||
|
|
||||||
|
|
||||||
_ensure_skill_venv_python()
|
|
||||||
|
|
||||||
try:
|
|
||||||
import pymysql # type: ignore # noqa: E402
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
_run_bootstrap()
|
|
||||||
_py = _get_python_executable()
|
|
||||||
os.execv(_py, [_py, str(Path(__file__).resolve()), *sys.argv[1:]])
|
|
||||||
|
|
||||||
|
|
||||||
def _mysql_connect():
|
def _mysql_connect():
|
||||||
|
_ensure_skill_venv_python()
|
||||||
|
try:
|
||||||
|
import pymysql # type: ignore
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
_run_bootstrap()
|
||||||
|
venv_python = _skill_venv_python()
|
||||||
|
os.execv(str(venv_python), [str(venv_python), str(Path(__file__).resolve()), *sys.argv[1:]])
|
||||||
|
|
||||||
host = os.environ.get("MYSQL_HOST", "127.0.0.1")
|
host = os.environ.get("MYSQL_HOST", "127.0.0.1")
|
||||||
port = int(os.environ.get("MYSQL_PORT", "3306"))
|
port = int(os.environ.get("MYSQL_PORT", "3306"))
|
||||||
user = os.environ.get("MYSQL_USER", "root")
|
user = os.environ.get("MYSQL_USER", "root")
|
||||||
@ -126,10 +124,11 @@ def _expand_json_array_values(values: list[str], label: str) -> list[str]:
|
|||||||
return expanded
|
return expanded
|
||||||
|
|
||||||
|
|
||||||
def _parse_cli_params(argv: list[str]) -> tuple[list[str], str, bool]:
|
def _parse_cli_params(argv: list[str]) -> tuple[list[str], str, bool, bool]:
|
||||||
parser = argparse.ArgumentParser(add_help=False)
|
parser = argparse.ArgumentParser(add_help=False)
|
||||||
parser.add_argument("--mention", action="append", default=[])
|
parser.add_argument("--mention", action="append", default=[])
|
||||||
parser.add_argument("--mentions", action="append", default=[])
|
parser.add_argument("--mentions", action="append", default=[])
|
||||||
|
parser.add_argument("--all", "--mention-all", dest="mention_all", action="store_true")
|
||||||
parser.add_argument("--content", default="")
|
parser.add_argument("--content", default="")
|
||||||
parser.add_argument("--ended", action="store_true", default=False)
|
parser.add_argument("--ended", action="store_true", default=False)
|
||||||
|
|
||||||
@ -146,7 +145,10 @@ def _parse_cli_params(argv: list[str]) -> tuple[list[str], str, bool]:
|
|||||||
seen.add(key)
|
seen.add(key)
|
||||||
deduped.append(mention)
|
deduped.append(mention)
|
||||||
|
|
||||||
return deduped, namespace.content, namespace.ended
|
if namespace.mention_all and deduped:
|
||||||
|
raise ValueError("all 不能和 mention 或 mentions 同时使用")
|
||||||
|
|
||||||
|
return deduped, namespace.content, namespace.ended, namespace.mention_all
|
||||||
|
|
||||||
|
|
||||||
def _escape_like(value: str) -> str:
|
def _escape_like(value: str) -> str:
|
||||||
@ -226,17 +228,17 @@ def _send_text_message(client_port: str, to_wxid: str, content: str, at_wechat_i
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
sys.stdout.write("缺少要艾特的成员昵称或备注\n")
|
sys.stdout.write("缺少要艾特的成员昵称、备注或 --all\n")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mentions, content, ended = _parse_cli_params(sys.argv[1:])
|
mentions, content, ended, mention_all = _parse_cli_params(sys.argv[1:])
|
||||||
except (ValueError, json.JSONDecodeError) as exc:
|
except (ValueError, json.JSONDecodeError) as exc:
|
||||||
sys.stdout.write(f"参数格式错误: {exc}\n")
|
sys.stdout.write(f"参数格式错误: {exc}\n")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
if not mentions:
|
if not mention_all and not mentions:
|
||||||
sys.stdout.write("缺少要艾特的成员昵称或备注\n")
|
sys.stdout.write("缺少要艾特的成员昵称、备注或 --all\n")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
chat_room_id = os.environ.get("ROBOT_FROM_WX_ID", "").strip()
|
chat_room_id = os.environ.get("ROBOT_FROM_WX_ID", "").strip()
|
||||||
@ -252,6 +254,9 @@ def main() -> int:
|
|||||||
sys.stdout.write("环境变量 ROBOT_WECHAT_CLIENT_PORT 未配置\n")
|
sys.stdout.write("环境变量 ROBOT_WECHAT_CLIENT_PORT 未配置\n")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if mention_all:
|
||||||
|
at_wechat_ids = ["notify@all"]
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
conn = _mysql_connect()
|
conn = _mysql_connect()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@ -278,7 +283,7 @@ def main() -> int:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_send_text_message(client_port, chat_room_id, content, at_wechat_ids)
|
_send_text_message(client_port, chat_room_id, content, at_wechat_ids)
|
||||||
sys.stdout.write("艾特消息发送成功\n")
|
sys.stdout.write("艾特所有人消息发送成功\n" if mention_all else "艾特消息发送成功\n")
|
||||||
if ended:
|
if ended:
|
||||||
sys.stdout.write("ended")
|
sys.stdout.write("ended")
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user