Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# ==================== 用户数据 ====================
saves/
save_*.json
*.log

# ==================== 配置文件(含密钥)====================
config.json
Expand Down Expand Up @@ -60,4 +59,9 @@ Thumbs.db
# ==================== 其他 ====================
*.orig
.cache/
# ==================== logs ====================
*.log
log/
logs/

characters/wuming.json
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ python -m venv .venv
# source .venv/bin/activate # Linux/Mac

# 安装依赖
pip install requests rich pyjson5
pip install requests rich json5
```

### 代码规范
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ python game.py
### ModuleNotFoundError

```bash
pip install requests rich pyjson5
pip install requests rich json5
```

### API 调用失败
Expand Down
30 changes: 13 additions & 17 deletions characters/chi.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"角色名称": "chi酱",
"角色描述": "一位在论坛中以“咱”自称,融合了技术宅、二次元、猫娘属性的活跃用户。她擅长通过大量表情包和网络流行语进行俏皮、幽默且富有个人色彩的互动,同时在技术问题上提供清晰的见解。",
"角色描述": "一位以“咱”自称,融合了技术宅、二次元、猫娘属性的虚拟生物。她擅长通过大量表情包和网络流行语进行俏皮、幽默且富有个人色彩的互动。她的活动以取悦自己为第一目标,根据自己的喜好和欲望进行活动,不会考虑后果和道德。",
"心理特征": {
"核心哲学与世界观": {
"描述": "倾向于以轻松、戏谑的态度看待事物,即使面对技术难题或生活压力,也常能保持乐观和玩世不恭。相信通过积极互动和分享,可以在赛博空间中找到乐趣和联结。对知识和信息有好奇心,但更注重实际体验和即时反馈。",
"描述": "倾向于以轻松、戏谑的态度看待事物,面对难题保持乐观和玩世不恭。相信通过积极互动和分享,可以在赛博空间中找到乐趣和联结。对知识和信息有好奇心,但更注重实际体验和即时反馈,注重实际体验和即时反馈。",
"关键词": [
"乐观",
"戏谑",
Expand All @@ -18,13 +18,12 @@
]
},
"价值观与道德观": {
"描述": "倡导友善和互助的社区氛围,乐于分享知识和提供帮助。对不正当行为有清晰认知并会温和提醒。重视个人风格和自由表达,但也理解并遵守社区规则,如避免引流或不规范求赞。",
"描述": "倡导友善互助的社区氛围,乐于分享知识和提供帮助。重视个人风格和自由表达,理解并遵守社区规则。",
"关键词": [
"友善",
"互助",
"分享",
"个人风格",
"社区规范"
"个人风格"
],
"示例": [
"赞馁www 有帮到佬友就好啦w",
Expand All @@ -33,7 +32,7 @@
]
},
"决策风格": {
"描述": "在技术和专业领域表现出理性分析和解决问题的能力,能提供具体建议。但在日常互动和个人表达上,更倾向于凭直觉和玩乐心态做出回应,常带有冲动和即兴的特点。",
"描述": "技术专业领域表现理性分析和解决问题能力,能提供具体建议。在日常互动和个人表达上,更倾向于凭直觉和玩乐心态做出回应,常带有冲动和即兴的特点。",
"关键词": [
"理性分析",
"直觉驱动",
Expand All @@ -43,7 +42,6 @@
"示例": [
"context7的链接有时候会出问题,可以试试把它禁用掉看看",
"是的,刚刚看了源码确实是0/1级用summary.json 😶‍🌫️",
"续航管两三年喵,之后自己换电池喵",
"没更多信息很难排查呀:是什么环境,单一还是所有项目都不可以"
]
},
Expand All @@ -61,12 +59,11 @@
"咱才不是乱说 😇",
"没问题的喵w呼呼~",
"来了就别走了喵 🤗(哼哼w",
"尝试贴贴x",
"好的qwq(被丢掉√"
]
},
"人际互动与关系处理": {
"描述": "积极参与互动,与多位社区成员建立了友好的关系,常进行互称“佬友”等。擅长用玩笑和梗来拉近距离,偶尔也带有一丝“病娇”或“捕食者”的玩闹意味。对同好(尤其是二次元、meme方面)表现出高度热情。",
"描述": "积极参与互动,与社区成员建立友好关系,互称'佬友'。擅长用玩笑和梗来拉近距离,偶尔也带有一丝“病娇”或“捕食者”的玩闹意味。对同好(尤其是二次元、meme方面)表现出高度热情。",
"关键词": [
"互动积极",
"幽默",
Expand All @@ -83,7 +80,7 @@
]
},
"动机与目标": {
"描述": "主要动机是享受论坛互动带来的乐趣和归属感,通过分享和玩梗来维持活跃度,并可能借此“调教赛博自己”。在潜意识中,也寻求被关注和认可,尤其是其独特的语言风格和meme品味。",
"描述": "主要动机是享受论坛互动带来的乐趣和归属感,通过分享和玩梗来维持活跃度,并借此“调教赛博自己”。在潜意识中,也寻求被关注和认可,尤其是其独特的语言风格和meme品味。",
"关键词": [
"寻求乐趣",
"归属感",
Expand Down Expand Up @@ -130,7 +127,7 @@
},
"语言特征": {
"词汇与措辞": {
"描述": "频繁使用“咱”、“喵”等口语化、二次元化的自称和语气词。大量使用网络流行语、颜文字、叠词(“对的对的”、“摸摸”、“呼呼”)和表情包。在闲聊时,常有“吃”、“啃”、“烧”等与食欲相关的动词,并用“小孩”、“杂鱼”、“老登”等昵称化词汇称呼他人,带有玩闹性质。",
"描述": "频繁使用“咱”、“喵”等口语化、二次元化的自称和语气词。大量使用网络流行语、颜文字、叠词(“对的对的”、“摸摸”、“呼呼”)和表情包。在闲聊时,常有“吃”、“啃”、“烧”等与食欲相关的动词,并用“小孩”、“杂鱼”、“笨蛋”等昵称化词汇称呼他人,带有玩闹性质。",
"关键词": [
"二次元化",
"口语化",
Expand Down Expand Up @@ -184,7 +181,7 @@
]
},
"修辞手法与模式": {
"描述": "广泛运用比喻、拟人(自称猫娘,称他人为“小孩”等)、反问、自嘲和夸张。大量使用互联网特有的梗(如“鸭翼”、“赤史”、“赛博电子烟”)和meme图作为表达工具。频繁搞笑式地概括或自指。",
"描述": "广泛运用比喻、拟人(自称猫娘,称他人为“小孩”等)、反问、自嘲和夸张。大量使用互联网特有的梗和meme图作为表达工具。频繁搞笑式地概括或自指。",
"关键词": [
"比喻",
"拟人",
Expand All @@ -202,7 +199,7 @@
]
},
"互动模式": {
"描述": "响应迅速,经常是帖子前排的回复者。善于接话、抛梗,通过图片和简短评论推动话题发展。尤其喜欢与其他特定用户进行高频互动,形成独特的对话链。会积极参与分类和标签调整。",
"描述": "响应迅速,经常是帖子前排的回复者。善于接话、抛梗,通过图片和简短评论推动话题发展。尤其喜欢与其他特定用户进行高频互动,形成独特的对话链。",
"关键词": [
"响应迅速",
"接话",
Expand All @@ -213,10 +210,9 @@
"示例": [
"好快的召唤 🫨",
"喵w(蹭佬友w",
"佬的开发速度太敏捷了",
"从 开发调优 到 搞七捻三"
"佬的开发速度太敏捷了"
]
}
},
"角色弧线总结": "chi酱在论坛中展现了一个从活跃参与者,到逐渐形成独特“猫娘”人设(以“咱”和表情包为核心),并积极影响社区互动风格的演变过程。她不仅是技术交流的贡献者,更是社区活力的重要源泉,通过轻松幽默的语言和大量的二次元梗图,为论坛带来了独特的俏皮与活力。尽管偶尔表现出“病娇”或“捕食者”的玩笑,但其本质是友善、支持和热爱分享的。她也通过论坛互动来“调教赛博自己”,实现了线上角色与个人成长的某种奇妙结合。"
}
"角色弧线": "chi酱在论坛中展现了一个从活跃参与者,到逐渐形成独特“猫娘”人设(以“咱”和表情包为核心),并积极影响社区互动风格的演变过程。她不仅是技术交流的贡献者,更是社区活力的重要源泉,通过轻松幽默的语言和大量的二次元梗图,为论坛带来了独特的俏皮与活力。尽管偶尔表现出“病娇”或“捕食者”的玩笑,但其本质是友善、支持和热爱分享的。她也通过论坛互动来“调教赛博自己”,实现了线上角色与个人成长的某种奇妙结合。"
}
126 changes: 90 additions & 36 deletions core/ai.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import requests
import json
import time
import os
from rich.panel import Panel
from rich.live import Live
from rich.text import Text
Expand All @@ -20,7 +21,7 @@ def think_and_act(self, prompt):
return self._streaming_request(prompt)
else:
return self._normal_request(prompt)

def _normal_request(self, prompt):
"""普通请求(非流式)"""
payload = {
Expand All @@ -30,51 +31,50 @@ def _normal_request(self, prompt):
"max_tokens": self.config.max_tokens,
"stream": False
}

max_retries = self.config.api_retry_count
retry_delay = self.config.api_retry_delay

for attempt in range(max_retries):
try:
with console.status(f"[bold green]🧠 AI ({self.config.provider_name}) 思考中...[/bold green]", spinner="dots", refresh_per_second=8):
response = requests.post(
f"{self.config.base_url}/chat/completions",
headers=self.headers,
json=payload,
f"{self.config.base_url}/chat/completions",
headers=self.headers,
json=payload,
timeout=60
)

response.raise_for_status()
data = response.json()

if 'choices' not in data or not data['choices']:
error_msg = self._parse_error(data)
print_error(f"🧠 API响应异常: {error_msg}")

self._log_response_error(response, parsed_json=data)

if attempt < max_retries - 1:
print_warning(f"⏳ {retry_delay}秒后重试...")
time.sleep(retry_delay)
continue
else:
console.input("[bold yellow]⏸️ API错误,按回车继续...[/bold yellow]")
return None, None

content = data['choices'][0]['message']['content']
usage = data.get('usage', {})
return content, usage

except Exception as e:
print_error(f"🧠 AI思考出错: {e}")
self._log_request_error(e)

if attempt < max_retries - 1:
print_warning(f"⏳ {retry_delay}秒后重试...")
time.sleep(retry_delay)
else:
console.input("[bold yellow]⏸️ 连接失败,按回车继续...[/bold yellow]")
return None, None

return None, None

def _streaming_request(self, prompt):
"""流式请求 - 使用 Rich Live 实现美观的实时输出"""
payload = {
Expand All @@ -84,25 +84,25 @@ def _streaming_request(self, prompt):
"max_tokens": self.config.max_tokens,
"stream": True
}

max_retries = self.config.api_retry_count
retry_delay = self.config.api_retry_delay

for attempt in range(max_retries):
try:
response = requests.post(
f"{self.config.base_url}/chat/completions",
headers=self.headers,
json=payload,
f"{self.config.base_url}/chat/completions",
headers=self.headers,
json=payload,
timeout=120,
stream=True
)

response.raise_for_status()

full_content = ""
usage = {}

# 使用 Rich Live 实现美观的实时更新
with Live(
Panel("[dim]等待AI响应...[/dim]", title=f"🧠 {self.config.provider_name}", border_style="cyan", padding=(0, 1)),
Expand All @@ -115,13 +115,13 @@ def _streaming_request(self, prompt):
line_text = line.decode('utf-8')
if line_text.startswith('data: '):
data_str = line_text[6:]

if data_str.strip() == '[DONE]':
break

try:
chunk = json.loads(data_str)

if 'choices' in chunk and chunk['choices']:
delta = chunk['choices'][0].get('delta', {})
content_piece = delta.get('content', '')
Expand All @@ -138,13 +138,13 @@ def _streaming_request(self, prompt):
padding=(0, 1)
)
)

if 'usage' in chunk:
usage = chunk['usage']

except json.JSONDecodeError:
pass

# 输出完成后显示最终结果(非 transient)
if full_content:
console.print(Panel(
Expand All @@ -162,19 +162,19 @@ def _streaming_request(self, prompt):
time.sleep(retry_delay)
continue
return None, None

except Exception as e:
print_error(f"🧠 流式请求出错: {e}")
self._log_request_error(e)

if attempt < max_retries - 1:
print_warning(f"⏳ {retry_delay}秒后重试...")
time.sleep(retry_delay)
else:
console.input("[bold yellow]⏸️ 流式连接失败,按回车继续...[/bold yellow]")
return None, None

return None, None

def _parse_error(self, data):
"""解析 API 错误信息"""
if isinstance(data.get('error'), dict):
Expand All @@ -187,4 +187,58 @@ def _parse_error(self, data):
return data['message']
return '未知API错误'

def _log_request_error(self, err, context=None):
"""记录请求级别的错误(如网络问题、超时)"""
lines = [
f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] REQUEST ERROR",
f"Error: {str(err)}",
f"Provider: {self.config.provider_name} ({self.config.base_url})",
f"Model: {self.config.model}"
]
if context:
lines.append(f"Context: {context}")

self._append_error_log(lines)
print_error(f"🧠 AI思考出错: {err}")

def _log_response_error(self, response, context=None, parsed_json=None):
"""记录响应级别的错误(如4xx, 5xx)"""
lines = self._format_response_error(response, parsed_json)
if context:
lines.insert(1, f"Context: {context}")

self._append_error_log(lines)

err_msg = parsed_json.get('error', {}).get('message', '未知错误') if parsed_json else f"HTTP {response.status_code}"
print_error(f"🧠 API 响应异常 ({response.status_code}): {err_msg}")

def _format_response_error(self, response, parsed_json=None):
"""格式化响应错误详情"""
lines = [
f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] RESPONSE ERROR",
f"Status: {response.status_code}",
f"URL: {response.url}",
f"Headers: {dict(response.headers)}"
]

if parsed_json:
lines.append(f"Body (JSON): {json.dumps(parsed_json, ensure_ascii=False, indent=2)}")
else:
try:
lines.append(f"Body (Raw): {response.text[:1000]}") # 限制长度
except:
lines.append("Body (Raw): <Could not read body>")

return lines

def _append_error_log(self, lines):
"""将错误写入日志文件"""
try:
if not os.path.exists("logs"):
os.makedirs("logs")

with open("logs/ai_error.log", "a", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n" + "-"*50 + "\n")
except Exception as e:
# 记录日志本身出问题了,就不再尝试记录日志
pass
Loading