Skip to content

Commit 3fa1122

Browse files
authored
Plugin:[QQ] 猫娘代发言追回 (Project-N-E-K-O#1312)
* Plugin:[QQ] 猫娘代发言追回 * Plugin:[QQ] 猫娘代发言追回 * Plugin:[QQ] 猫娘代发言追回 * Plugin:[QQ] 猫娘代发言追回
1 parent c6944fd commit 3fa1122

21 files changed

Lines changed: 265 additions & 28 deletions

plugin/plugins/qq_auto_reply/__init__.py

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .prompting import QQAutoReplyPromptingMixin
2626
from .qq_client import QQClient
2727
from .session import QQAutoReplySessionMixin
28-
from .targets import QQAutoReplyTargetsMixin
28+
from .targets import QQAutoReplyTargetsMixin, QQAutoReplyValidationError
2929

3030

3131
def build_open_ui_payload(*, plugin_id: str, available: bool, i18n=None) -> dict[str, Any]:
@@ -388,7 +388,7 @@ async def init_config(self, guide_step_config_done: Optional[bool] = None, **_):
388388
self._refresh_admin_qq()
389389
return Ok(await self._build_dashboard_state())
390390

391-
@plugin_entry(id="qq_auto_reply_get_dashboard_state", name=tr("entries.get_dashboard_state.name", default="获取控制面板状态"), description=tr("entries.get_dashboard_state.description", default="返回 QQ 自动回复前端控制面板所需的完整状态。"), input_schema={"type": "object", "properties": {}})
391+
@plugin_entry(id="get_dashboard_state", name=tr("entries.get_dashboard_state.name", default="获取控制面板状态"), description=tr("entries.get_dashboard_state.description", default="返回 QQ 自动回复前端控制面板所需的完整状态。"), input_schema={"type": "object", "properties": {}})
392392
async def get_dashboard_state(self, **_):
393393
return Ok(await self._build_dashboard_state())
394394

@@ -523,7 +523,7 @@ async def remove_trusted_group(self, group_id: str, **_):
523523
payload["persisted"] = success
524524
return Ok(payload)
525525

526-
@plugin_entry(id="qq_auto_reply_sync_qrcode", name=tr("entries.sync_qrcode.name", default="刷新二维码"), description=tr("entries.sync_qrcode.description", default="重新复制 NapCat 登录二维码到插件静态目录并返回最新状态。"), input_schema={"type": "object", "properties": {}})
526+
@plugin_entry(id="sync_qrcode", name=tr("entries.sync_qrcode.name", default="刷新二维码"), description=tr("entries.sync_qrcode.description", default="重新复制 NapCat 登录二维码到插件静态目录并返回最新状态。"), input_schema={"type": "object", "properties": {}})
527527
async def sync_qrcode(self, **_):
528528
await self._sync_napcat_qrcode_into_static()
529529
return Ok(await self._build_dashboard_state())
@@ -550,6 +550,92 @@ async def stop_auto_reply(self, **_):
550550
await self._stop_auto_reply_runtime(stop_napcat=False)
551551
return Ok({"status": "stopped"})
552552

553+
@plugin_entry(id="send_private_proactive_message", name=tr("entries.send_private_proactive_message.name", default="主动发送私聊消息"), description=tr("entries.send_private_proactive_message.description", default="让 AI 生成一条私聊消息并主动发送给指定 QQ 用户。"), input_schema={"type": "object", "properties": {"target": {"type": "string"}, "message": {"type": "string"}}, "required": ["target", "message"], "additionalProperties": False}, metadata={"timeout": 90})
554+
async def send_private_proactive_message(self, target: str, message: str, **_):
555+
try:
556+
self._ensure_qq_client_connected()
557+
resolved_qq, matched_nickname = self._resolve_private_message_target(target)
558+
prompt_message = self._validate_outbound_message(message)
559+
permission_level = "admin" if resolved_qq == self._admin_qq else (self.permission_mgr.get_permission_level(resolved_qq) if self.permission_mgr else "trusted")
560+
if permission_level == "none":
561+
permission_level = "trusted"
562+
reply_text = await self._generate_reply(
563+
prompt_message,
564+
permission_level,
565+
resolved_qq,
566+
is_group=False,
567+
user_nickname=matched_nickname,
568+
use_memory_context=permission_level == "admin",
569+
persist_memory=False,
570+
ephemeral_session=True,
571+
)
572+
if not reply_text:
573+
return Err(SdkError(f"GENERATE_FAILED: {self.i18n.t('errors.proactive_private_generate_failed', default='AI 未生成可发送的私聊内容')}"))
574+
await self.qq_client.send_message(resolved_qq, reply_text)
575+
return Ok({
576+
"status": "sent",
577+
"target": str(target or "").strip(),
578+
"resolved_qq": resolved_qq,
579+
"resolved_nickname": matched_nickname,
580+
"message_prompt": prompt_message,
581+
"generated_message": reply_text,
582+
})
583+
except QQAutoReplyValidationError as e:
584+
code = e.code
585+
message_text = str(e)
586+
if code in ("NICKNAME_NOT_FOUND", "NICKNAME_AMBIGUOUS"):
587+
return Err(SdkError(f"{code}: {message_text}"))
588+
if code == "INVALID_TARGET":
589+
return Err(SdkError(f"INVALID_TARGET: {self.i18n.t('errors.proactive_invalid_target', default=message_text)}"))
590+
if code == "INVALID_MESSAGE":
591+
return Err(SdkError(f"INVALID_MESSAGE: {self.i18n.t('errors.proactive_invalid_message', default=message_text)}"))
592+
return Err(SdkError(f"INVALID_TARGET: {message_text}"))
593+
except RuntimeError as e:
594+
return Err(SdkError(f"NOT_READY: {self.i18n.t('errors.proactive_not_ready', default='{error}', error=str(e))}"))
595+
except Exception as e:
596+
self.logger.exception("Failed to send proactive private QQ message")
597+
return Err(SdkError(f"SEND_FAILED: {self.i18n.t('errors.proactive_send_failed', default='{error}', error=str(e))}"))
598+
599+
@plugin_entry(id="send_group_proactive_message", name=tr("entries.send_group_proactive_message.name", default="主动发送群聊消息"), description=tr("entries.send_group_proactive_message.description", default="让 AI 生成一条群聊消息并主动发送给指定 QQ 群。"), input_schema={"type": "object", "properties": {"group_id": {"type": "string"}, "message": {"type": "string"}}, "required": ["group_id", "message"], "additionalProperties": False}, metadata={"timeout": 90})
600+
async def send_group_proactive_message(self, group_id: str, message: str, **_):
601+
try:
602+
self._ensure_qq_client_connected()
603+
normalized_group_id = self._validate_group_id(group_id)
604+
prompt_message = self._validate_outbound_message(message)
605+
reply_text = await self._generate_reply(
606+
prompt_message,
607+
"open",
608+
self._admin_qq or "0",
609+
is_group=True,
610+
group_id=normalized_group_id,
611+
use_memory_context=False,
612+
persist_memory=False,
613+
ephemeral_session=True,
614+
group_facing=True,
615+
)
616+
if not reply_text:
617+
return Err(SdkError(f"GENERATE_FAILED: {self.i18n.t('errors.proactive_group_generate_failed', default='AI 未生成可发送的群聊内容')}"))
618+
await self.qq_client.send_group_message(normalized_group_id, reply_text)
619+
return Ok({
620+
"status": "sent",
621+
"group_id": normalized_group_id,
622+
"message_prompt": prompt_message,
623+
"generated_message": reply_text,
624+
})
625+
except QQAutoReplyValidationError as e:
626+
code = e.code
627+
message_text = str(e)
628+
if code == "INVALID_GROUP_ID":
629+
return Err(SdkError(f"INVALID_GROUP_ID: {self.i18n.t('errors.proactive_invalid_group_id', default=message_text)}"))
630+
if code == "INVALID_MESSAGE":
631+
return Err(SdkError(f"INVALID_MESSAGE: {self.i18n.t('errors.proactive_invalid_message', default=message_text)}"))
632+
return Err(SdkError(f"INVALID_GROUP_ID: {message_text}"))
633+
except RuntimeError as e:
634+
return Err(SdkError(f"NOT_READY: {self.i18n.t('errors.proactive_not_ready', default='{error}', error=str(e))}"))
635+
except Exception as e:
636+
self.logger.exception("Failed to send proactive group QQ message")
637+
return Err(SdkError(f"SEND_FAILED: {self.i18n.t('errors.proactive_send_failed', default='{error}', error=str(e))}"))
638+
553639
async def _stop_auto_reply_runtime(self, *, stop_napcat: bool):
554640
self._running = False
555641
if self._message_task:
@@ -574,6 +660,19 @@ async def _stop_auto_reply_runtime(self, *, stop_napcat: bool):
574660
await self._stop_managed_napcat()
575661
self._session_locks.clear()
576662

663+
def _track_handler_task(self, task: asyncio.Task) -> None:
664+
self._handler_tasks.add(task)
665+
task.add_done_callback(self._on_handler_task_done)
666+
667+
def _on_handler_task_done(self, task: asyncio.Task) -> None:
668+
self._handler_tasks.discard(task)
669+
try:
670+
task.result()
671+
except asyncio.CancelledError:
672+
pass
673+
except Exception as exc:
674+
self.logger.error(f"Message handler task failed: {exc}")
675+
577676
async def _process_messages(self):
578677
while self._running:
579678
try:
@@ -651,10 +750,6 @@ async def _handle_normal_relay(self, message_text: str, sender_id: str, source_t
651750
await self.qq_client.send_message(self._admin_qq, relay_text)
652751
return None
653752

654-
def _track_handler_task(self, task: asyncio.Task) -> None:
655-
self._handler_tasks.add(task)
656-
task.add_done_callback(self._handler_tasks.discard)
657-
658753
async def _run_message_handler(self, message: Dict[str, Any]) -> None:
659754
session_key = self._message_session_key(message)
660755
async with self._message_concurrency:

plugin/plugins/qq_auto_reply/doc/README.en.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ Notes:
9595
- Existing character persona and model configuration are reused.
9696
- Proactive private sending may read memory context for generation, but this action itself is not written back into memory.
9797
- Proactive group sending is not written into memory.
98-
- `qq_number` / `group_id` must be numeric strings.
98+
- The private entry uses `target`: it can be either a numeric QQ ID or a nickname already configured in the trusted-user list.
99+
- `group_id` must be a numeric string.
99100
- `message` cannot be empty.
100101
- Auto-reply must already be started and OneBot must be connected; otherwise the entry fails immediately.
101102

plugin/plugins/qq_auto_reply/doc/README.es.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ Notas:
9595
- Se reutilizan la personalidad del personaje y la configuración del modelo existentes.
9696
- En mensajes privados proactivos puede leerse el contexto de memoria, pero este envío no se vuelve a escribir en la memoria.
9797
- Los envíos proactivos a grupos no se escriben en memoria.
98-
- `qq_number` / `group_id` deben ser cadenas numéricas puras.
98+
- La entrada privada usa `target`: puede ser un ID numérico de QQ o un apodo ya configurado en la lista de usuarios de confianza.
99+
- `group_id` debe ser una cadena numérica.
99100
- `message` no puede estar vacío.
100101
- La respuesta automática debe estar ya iniciada y OneBot debe estar conectado; de lo contrario, la entrada fallará inmediatamente.
101102

plugin/plugins/qq_auto_reply/doc/README.ja.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ OneBot プロトコル経由で QQ に接続し、権限に応じたインテリ
9595
- 既存のキャラ設定とモデル設定をそのまま使います。
9696
- 個人チャットへの能動送信では記憶コンテキストを参照する場合がありますが、この送信自体は記憶へ書き戻しません。
9797
- グループへの能動送信も記憶へは書き込みません。
98-
- `qq_number` / `group_id` は数字だけの文字列である必要があります。
98+
- 個人向け entry は `target` を使います。数値の QQ ID、または信頼ユーザー一覧に設定済みのニックネームを指定できます。
99+
- `group_id` は数字だけの文字列である必要があります。
99100
- `message` は空にできません。
100101
- あらかじめ自動返信が起動していて、OneBot が接続済みである必要があります。
101102

plugin/plugins/qq_auto_reply/doc/README.ko.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ OneBot 프로토콜을 통해 QQ에 연결하고, 권한에 따라 지능형 자
9797
- 기존 캐릭터 설정과 모델 설정을 그대로 사용합니다.
9898
- 개인 채팅 능동 발신은 메모리 문맥을 읽어 생성할 수 있지만, 이 발신 자체는 메모리에 다시 기록되지 않습니다.
9999
- 그룹 능동 발신도 메모리에 기록되지 않습니다.
100-
- `qq_number` / `group_id` 는 숫자로만 이루어진 문자열이어야 합니다.
100+
- 개인 발신 entry 는 `target` 을 사용하며, 숫자 QQ ID 또는 신뢰 사용자 목록에 등록된 별명을 지정할 수 있습니다.
101+
- `group_id` 는 숫자로만 이루어진 문자열이어야 합니다.
101102
- `message` 는 비워둘 수 없습니다.
102103
- 먼저 자동 응답이 시작되어 있고 OneBot이 연결되어 있어야 합니다.
103104

plugin/plugins/qq_auto_reply/doc/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ https://github.com/NapNeko/NapCatQQ/releases
9696
- 会复用现有角色人设与模型配置
9797
- 私聊主动发送会读取记忆库上下文辅助生成,但不会把这次主动发送写回记忆库
9898
- 群聊主动发送不会写入记忆库
99-
- `qq_number` / `group_id` 必须是纯数字字符串
99+
- 私聊入口使用 `target` 参数:可以填写纯数字 QQ 号,或填写已配置在信任用户列表中的昵称
100+
- `group_id` 必须是纯数字字符串
100101
- `message` 不能为空
101102
- 使用前需先启动自动回复并确保 OneBot 已连接,否则入口会直接报错
102103

plugin/plugins/qq_auto_reply/doc/README.pt.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ Observações:
9595
- A personalidade do personagem e a configuração do modelo existentes são reutilizadas.
9696
- Em envios proativos privados, o contexto de memória pode ser lido, mas esse envio não é escrito de volta na memória.
9797
- Envios proativos para grupos não são escritos na memória.
98-
- `qq_number` / `group_id` devem ser cadeias numéricas puras.
98+
- A entrada privada usa `target`: ele pode ser um ID numérico do QQ ou um apelido já configurado na lista de usuários confiáveis.
99+
- `group_id` deve ser uma cadeia numérica.
99100
- `message` não pode ser vazio.
100101
- A resposta automática deve estar iniciada e o OneBot conectado; caso contrário, a entrada falha imediatamente.
101102

plugin/plugins/qq_auto_reply/doc/README.ru.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@
9595
- Используются текущие настройки персонажа и модели.
9696
- При активной отправке в личку может быть прочитан контекст памяти, но этот отправленный текст сам в память не записывается.
9797
- При активной отправке в группу память не обновляется.
98-
- `qq_number` / `group_id` должны быть строками, состоящими только из цифр.
98+
- Личная entry использует `target`: это может быть числовой QQ ID или ник, уже добавленный в список доверенных пользователей.
99+
- `group_id` должен быть строкой, состоящей только из цифр.
99100
- `message` не может быть пустым.
100101
- Автоответчик должен быть уже запущен, а OneBot должен быть подключён, иначе entry завершится ошибкой.
101102

plugin/plugins/qq_auto_reply/doc/README.zh-TW.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@
9595
- 會沿用既有角色設定與模型設定。
9696
- 私聊主動發送時可能會讀取記憶上下文輔助生成,但這次主動發送本身不會寫回記憶庫。
9797
- 群組主動發送也不會寫入記憶庫。
98-
- `qq_number` / `group_id` 必須是純數字字串。
98+
- 私聊 entry 使用 `target` 參數:可填入純數字 QQ 號,或填入已設定於信任使用者清單中的暱稱。
99+
- `group_id` 必須是純數字字串。
99100
- `message` 不能為空。
100101
- 需先啟動自動回覆並確認 OneBot 已連線,否則 entry 會直接失敗。
101102

plugin/plugins/qq_auto_reply/i18n/en-US.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
"entries.add_trusted_group.description": "Add a trusted QQ group to the allowlist.",
2020
"entries.remove_trusted_group.name": "Remove trusted group",
2121
"entries.remove_trusted_group.description": "Remove a QQ group from the allowlist.",
22+
"entries.send_private_proactive_message.name": "Send proactive private message",
23+
"entries.send_private_proactive_message.description": "Generate a private QQ message with AI and proactively send it to the target user.",
24+
"entries.send_group_proactive_message.name": "Send proactive group message",
25+
"entries.send_group_proactive_message.description": "Generate a group QQ message with AI and proactively send it to the target group.",
2226
"entries.start_auto_reply.name": "Start auto-reply",
2327
"entries.start_auto_reply.description": "Start listening for QQ messages and auto-replying.",
2428
"entries.stop_auto_reply.name": "Stop auto-reply",
@@ -37,6 +41,13 @@
3741
"errors.admin_no_nickname": "Admins are always called Master, so a nickname can’t be set",
3842
"errors.set_nickname_failed": "Couldn’t save the nickname",
3943
"errors.start_connect_failed": "Can’t connect to the OneBot service at {url}. Please wake up external NapCat/OneBot first: {error}",
44+
"errors.proactive_private_generate_failed": "AI did not generate a private message that can be sent",
45+
"errors.proactive_group_generate_failed": "AI did not generate a group message that can be sent",
46+
"errors.proactive_invalid_target": "target cannot be empty, and nicknames must exist in the trusted user list",
47+
"errors.proactive_invalid_group_id": "group_id cannot be empty and must contain digits only",
48+
"errors.proactive_invalid_message": "message cannot be empty",
49+
"errors.proactive_not_ready": "{error}",
50+
"errors.proactive_send_failed": "{error}",
4051
"prompts.default_master": "Master",
4152
"prompts.default_qq_user": "QQ User {sender_id}",
4253
"prompts.default_ai_assistant": "You are a friendly AI assistant",
@@ -124,6 +135,9 @@
124135
"ui.tabs.users": "Trusted Users",
125136
"ui.tabs.groups": "Trusted Groups",
126137
"ui.actions.add": "Add",
138+
"ui.actions.open": "Open UI",
139+
"ui.open_path.message": "UI is available",
140+
"ui.unavailable.message": "UI is unavailable",
127141
"ui.actions.save_settings": "Save Settings",
128142
"ui.empty.no_items": "There’s nothing here yet~",
129143
"ui.defaults.user": "Trusted Friend",

0 commit comments

Comments
 (0)