├── README.md └── anuneko.py /README.md: -------------------------------------------------------------------------------- 1 | # anuneko.com 域名在国内被DNS污染和SNI阻断,需要科学上网 2 | -------------------------------------------------------------------------------- /anuneko.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import httpx 4 | from nonebot import on_command 5 | from nonebot.adapters.onebot.v11 import Message 6 | from nonebot.params import CommandArg 7 | 8 | # ============================================================ 9 | # API 地址 10 | # ============================================================ 11 | 12 | CHAT_API_URL = "https://anuneko.com/api/v1/chat" 13 | STREAM_API_URL = "https://anuneko.com/api/v1/msg/{uuid}/stream" 14 | SELECT_CHOICE_URL = "https://anuneko.com/api/v1/msg/select-choice" 15 | SELECT_MODEL_URL = "https://anuneko.com/api/v1/user/select_model" 16 | 17 | DEFAULT_TOKEN = ( 18 | "x-token 自己改" 19 | ) 20 | 21 | # 每个 QQ 用户一个独立会话 22 | user_sessions = {} 23 | # 每个 QQ 用户的当前模型 24 | user_models = {} # user_id: "Orange Cat" or "Exotic Shorthair" 25 | 26 | WATERMARK = "\n\n—— 内容由 anuneko.com 提供,该服务只是一个第三方前端" 27 | 28 | 29 | # ============================================================ 30 | # 异步 httpx 请求封装 31 | # ============================================================ 32 | 33 | def build_headers(): 34 | token = os.environ.get("ANUNEKO_TOKEN", DEFAULT_TOKEN) 35 | cookie = os.environ.get("ANUNEKO_COOKIE") 36 | 37 | headers = { 38 | "accept": "*/*", 39 | "content-type": "application/json", 40 | "origin": "https://anuneko.com", 41 | "referer": "https://anuneko.com/", 42 | "user-agent": "Mozilla/5.0", 43 | "x-app_id": "com.anuttacon.neko", 44 | "x-client_type": "4", 45 | "x-device_id": "7b75a432-6b24-48ad-b9d3-3dc57648e3e3", 46 | "x-token": token, 47 | } 48 | 49 | if cookie: 50 | headers["Cookie"] = cookie 51 | 52 | return headers 53 | 54 | 55 | # ============================================================ 56 | # 创建新会话(使用 httpx 异步重写) 57 | # ============================================================ 58 | 59 | async def create_new_session(user_id: str): 60 | headers = build_headers() 61 | model = user_models.get(user_id, "Orange Cat") 62 | data = json.dumps({"model": model}) 63 | 64 | try: 65 | async with httpx.AsyncClient(timeout=10) as client: 66 | resp = await client.post(CHAT_API_URL, headers=headers, content=data) 67 | resp_json = resp.json() 68 | 69 | chat_id = resp_json.get("chat_id") or resp_json.get("id") 70 | if chat_id: 71 | user_sessions[user_id] = chat_id 72 | # 切换模型以确保一致性 73 | await switch_model(user_id, chat_id, model) 74 | return chat_id 75 | 76 | except Exception: 77 | return None 78 | 79 | return None 80 | 81 | 82 | # ============================================================ 83 | # 切换模型(async) 84 | # ============================================================ 85 | 86 | async def switch_model(user_id: str, chat_id: str, model_name: str): 87 | headers = build_headers() 88 | data = json.dumps({"chat_id": chat_id, "model": model_name}) 89 | 90 | try: 91 | async with httpx.AsyncClient(timeout=10) as client: 92 | resp = await client.post(SELECT_MODEL_URL, headers=headers, content=data) 93 | if resp.status_code == 200: 94 | user_models[user_id] = model_name 95 | return True 96 | except: 97 | pass 98 | return False 99 | 100 | 101 | # ============================================================ 102 | # 自动选分支(async) 103 | # ============================================================ 104 | 105 | async def send_choice(msg_id: str): 106 | headers = build_headers() 107 | 108 | data = json.dumps({"msg_id": msg_id, "choice_idx": 0}) 109 | 110 | try: 111 | async with httpx.AsyncClient(timeout=5) as client: 112 | await client.post(SELECT_CHOICE_URL, headers=headers, content=data) 113 | except: 114 | pass 115 | 116 | 117 | # ============================================================ 118 | # 核心:异步流式回复(超级稳定) 119 | # ============================================================ 120 | 121 | async def stream_reply(session_uuid: str, text: str) -> str: 122 | headers = { 123 | "x-token": os.environ.get("ANUNEKO_TOKEN", DEFAULT_TOKEN), 124 | "Content-Type": "text/plain", 125 | } 126 | 127 | cookie = os.environ.get("ANUNEKO_COOKIE") 128 | if cookie: 129 | headers["Cookie"] = cookie 130 | 131 | url = STREAM_API_URL.format(uuid=session_uuid) 132 | data = json.dumps({"contents": [text]}, ensure_ascii=False) 133 | 134 | result = "" 135 | current_msg_id = None 136 | 137 | try: 138 | async with httpx.AsyncClient(timeout=None) as client: 139 | async with client.stream( 140 | "POST", url, headers=headers, content=data 141 | ) as resp: 142 | async for line in resp.aiter_lines(): 143 | if not line: 144 | continue 145 | 146 | # 处理错误响应 147 | if not line.startswith("data: "): 148 | try: 149 | error_json = json.loads(line) 150 | if error_json.get("code") == "chat_choice_shown": 151 | return "⚠️ 检测到对话分支未选择,请重试或新建会话。" 152 | except: 153 | pass 154 | continue 155 | 156 | # 处理 data: {} 157 | try: 158 | raw_json = line[6:] 159 | if not raw_json.strip(): 160 | continue 161 | 162 | j = json.loads(raw_json) 163 | 164 | # 只要出现 msg_id 就更新,流最后一条通常是 assistmsg,也就是我们要的 ID 165 | if "msg_id" in j: 166 | current_msg_id = j["msg_id"] 167 | 168 | # 如果有 'c' 字段,说明是多分支内容 169 | # 格式如: {"c":[{"v":"..."},{"v":"...","c":1}]} 170 | if "c" in j and isinstance(j["c"], list): 171 | for choice in j["c"]: 172 | # 默认选项 idx=0,可能显式 c=0 或隐式(无 c 字段) 173 | idx = choice.get("c", 0) 174 | if idx == 0: 175 | if "v" in choice: 176 | result += choice["v"] 177 | 178 | # 常规内容 (兼容旧格式或无分支情况) 179 | elif "v" in j and isinstance(j["v"], str): 180 | result += j["v"] 181 | 182 | except: 183 | continue 184 | 185 | # 流结束后,如果有 msg_id,自动确认选择第一项,确保下次对话正常 186 | if current_msg_id: 187 | await send_choice(current_msg_id) 188 | 189 | except Exception: 190 | return "请求失败,请稍后再试。" 191 | 192 | return result 193 | 194 | 195 | # ============================================================ 196 | # NoneBot 指令 197 | # ============================================================ 198 | 199 | chat_cmd = on_command("chat", aliases={"对话"}) 200 | new_cmd = on_command("new", aliases={"新会话"}) 201 | switch_cmd = on_command("switch", aliases={"切换"}) 202 | 203 | 204 | # --------------------------- 205 | # /switch 切换模型 206 | # --------------------------- 207 | 208 | @switch_cmd.handle() 209 | async def _(event, args: Message = CommandArg()): 210 | user_id = str(event.user_id) 211 | arg = args.extract_plain_text().strip() 212 | 213 | if "橘猫" in arg or "orange" in arg.lower(): 214 | target_model = "Orange Cat" 215 | target_name = "橘猫" 216 | elif "黑猫" in arg or "exotic" in arg.lower(): 217 | target_model = "Exotic Shorthair" 218 | target_name = "黑猫" 219 | else: 220 | await switch_cmd.finish("请指定要切换的模型:橘猫 / 黑猫") 221 | return 222 | 223 | # 获取当前会话ID,如果没有则新建 224 | if user_id not in user_sessions: 225 | chat_id = await create_new_session(user_id) 226 | if not chat_id: 227 | await switch_cmd.finish("❌ 切换失败:无法创建会话") 228 | return 229 | else: 230 | chat_id = user_sessions[user_id] 231 | 232 | success = await switch_model(user_id, chat_id, target_model) 233 | 234 | if success: 235 | await switch_cmd.finish(f"✨ 已切换为:{target_name}") 236 | else: 237 | await switch_cmd.finish(f"❌ 切换为 {target_name} 失败") 238 | 239 | 240 | # --------------------------- 241 | # /new 创建新会话 242 | # --------------------------- 243 | 244 | @new_cmd.handle() 245 | async def _(event): 246 | user_id = str(event.user_id) 247 | 248 | new_id = await create_new_session(user_id) 249 | 250 | if new_id: 251 | model_name = "橘猫" if user_models.get(user_id) == "Orange Cat" else "黑猫" 252 | await new_cmd.finish(f"✨ 已创建新的会话(当前模型:{model_name})!") 253 | else: 254 | await new_cmd.finish("❌ 创建会话失败,请稍后再试。") 255 | 256 | 257 | # --------------------------- 258 | # /chat 进行对话 259 | # --------------------------- 260 | 261 | @chat_cmd.handle() 262 | async def _(event, args: Message = CommandArg()): 263 | user_id = str(event.user_id) 264 | text = args.extract_plain_text().strip() 265 | 266 | if not text: 267 | await chat_cmd.finish("❗ 请输入内容,例如:/chat 你好") 268 | 269 | # 自动创建会话 270 | if user_id not in user_sessions: 271 | cid = await create_new_session(user_id) 272 | if not cid: 273 | await chat_cmd.finish("❌ 创建会话失败,请稍后再试。") 274 | 275 | session_id = user_sessions[user_id] 276 | reply = await stream_reply(session_id, text) 277 | 278 | reply += WATERMARK 279 | 280 | await chat_cmd.finish(reply) 281 | --------------------------------------------------------------------------------