mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -462,13 +462,12 @@ class MemoryFlushManager:
|
||||
daily_content=daily_content or "(no recent daily records)",
|
||||
)
|
||||
from agent.protocol.models import LLMRequest
|
||||
# Scale max_tokens based on input size to avoid truncating large MEMORY.md
|
||||
input_chars = len(memory_content) + len(daily_content)
|
||||
dream_max_tokens = max(2000, min(input_chars, 8000))
|
||||
# No output cap: the prompt already keeps MEMORY.md concise (~50
|
||||
# items), so a hard max_tokens would only risk truncating a large
|
||||
# rewrite. Let the model use its default output budget.
|
||||
request = LLMRequest(
|
||||
messages=[{"role": "user", "content": user_msg}],
|
||||
temperature=0.3,
|
||||
max_tokens=dream_max_tokens,
|
||||
stream=False,
|
||||
system=_dream_system_prompt(),
|
||||
)
|
||||
|
||||
@@ -34,6 +34,27 @@ class SkillService:
|
||||
"""
|
||||
self.manager = skill_manager
|
||||
|
||||
def _safe_skill_dir(self, name: str) -> str:
|
||||
"""Derive and validate the skill directory path.
|
||||
|
||||
Ensures the resolved path stays within the custom_dir root,
|
||||
preventing path traversal via names like ``../escaped``.
|
||||
|
||||
:raises ValueError: if the name would escape the skills root.
|
||||
"""
|
||||
if not name or not name.strip():
|
||||
raise ValueError("skill name is required")
|
||||
# Reject obvious traversal components.
|
||||
if ".." in name or name.startswith("/") or name.startswith("\\"):
|
||||
raise ValueError(f"invalid skill name (path traversal detected): {name!r}")
|
||||
skill_dir = os.path.realpath(os.path.join(self.manager.custom_dir, name))
|
||||
root = os.path.realpath(self.manager.custom_dir)
|
||||
if not skill_dir.startswith(root + os.sep) and skill_dir != root:
|
||||
raise ValueError(
|
||||
f"skill name {name!r} resolves outside the skills directory"
|
||||
)
|
||||
return skill_dir
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# query
|
||||
# ------------------------------------------------------------------
|
||||
@@ -107,7 +128,7 @@ class SkillService:
|
||||
if not files:
|
||||
raise ValueError("skill files list is empty")
|
||||
|
||||
skill_dir = os.path.join(self.manager.custom_dir, name)
|
||||
skill_dir = self._safe_skill_dir(name)
|
||||
|
||||
tmp_dir = skill_dir + ".tmp"
|
||||
if os.path.exists(tmp_dir):
|
||||
@@ -146,7 +167,7 @@ class SkillService:
|
||||
raise ValueError("package url is required")
|
||||
|
||||
url = files[0]["url"]
|
||||
skill_dir = os.path.join(self.manager.custom_dir, name)
|
||||
skill_dir = self._safe_skill_dir(name)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
zip_path = os.path.join(tmp_dir, "package.zip")
|
||||
@@ -217,7 +238,7 @@ class SkillService:
|
||||
if not name:
|
||||
raise ValueError("skill name is required")
|
||||
|
||||
skill_dir = os.path.join(self.manager.custom_dir, name)
|
||||
skill_dir = self._safe_skill_dir(name)
|
||||
if os.path.exists(skill_dir):
|
||||
shutil.rmtree(skill_dir)
|
||||
logger.info(f"[SkillService] delete: removed directory {skill_dir}")
|
||||
|
||||
@@ -17,11 +17,14 @@ Provider resolution:
|
||||
"""
|
||||
|
||||
import base64
|
||||
import ipaddress
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
@@ -654,6 +657,40 @@ class Vision(BaseTool):
|
||||
return api_base
|
||||
return api_base.rstrip("/") + "/v1"
|
||||
|
||||
@staticmethod
|
||||
def _validate_url_safe(url: str) -> None:
|
||||
"""Reject URLs that target private/loopback/link-local addresses (SSRF guard).
|
||||
|
||||
Resolves the hostname to its IP address(es) and blocks any that fall
|
||||
into non-public ranges. Also rejects URLs with no host, non-HTTP(S)
|
||||
schemes, or hosts that fail DNS resolution.
|
||||
|
||||
Raises:
|
||||
ValueError: if the URL targets a disallowed address.
|
||||
"""
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
raise ValueError(f"Unsupported URL scheme: {parsed.scheme}")
|
||||
|
||||
hostname = parsed.hostname
|
||||
if not hostname:
|
||||
raise ValueError("URL has no hostname")
|
||||
|
||||
try:
|
||||
# Resolve all addresses for the hostname.
|
||||
addr_infos = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
except socket.gaierror:
|
||||
raise ValueError(f"Cannot resolve hostname: {hostname}")
|
||||
|
||||
for family, _, _, _, sockaddr in addr_infos:
|
||||
ip_str = sockaddr[0]
|
||||
ip = ipaddress.ip_address(ip_str)
|
||||
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
|
||||
raise ValueError(
|
||||
f"URL resolves to a non-public address ({ip_str}), "
|
||||
f"request blocked for security"
|
||||
)
|
||||
|
||||
def _build_image_content(self, image: str) -> dict:
|
||||
"""
|
||||
Build the image_url content block.
|
||||
@@ -661,6 +698,7 @@ class Vision(BaseTool):
|
||||
so every bot backend can consume them without extra downloads.
|
||||
"""
|
||||
if image.startswith(("http://", "https://")):
|
||||
self._validate_url_safe(image)
|
||||
return self._download_to_data_url(image)
|
||||
|
||||
if not os.path.isfile(image):
|
||||
|
||||
Reference in New Issue
Block a user