Compare commits
20 Commits
afa4586e3d
...
cd0ce4d9b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd0ce4d9b0 | ||
|
|
69f66997fd | ||
|
|
567c7f582b | ||
|
|
adf393c0a5 | ||
|
|
12f23138e0 | ||
|
|
c19ac2dae6 | ||
|
|
5805566d88 | ||
|
|
da91a0643f | ||
|
|
5b0d96b1b5 | ||
|
|
10776fed51 | ||
|
|
bd2dd42ba4 | ||
|
|
0f77470fe0 | ||
|
|
3b93c6137d | ||
|
|
09cf6306af | ||
|
|
936501069c | ||
|
|
8e159a9773 | ||
|
|
0c5d3c8635 | ||
|
|
446d7bf549 | ||
|
|
b34965b936 | ||
|
|
7255515607 |
@@ -19,4 +19,12 @@ def _run(*a, **k):
|
|||||||
if r.stderr is not None: r.stderr = _d(r.stderr)
|
if r.stderr is not None: r.stderr = _d(r.stderr)
|
||||||
return r
|
return r
|
||||||
subprocess.run = _run
|
subprocess.run = _run
|
||||||
|
_Pi = subprocess.Popen.__init__
|
||||||
|
def _pinit(self, *a, **k):
|
||||||
|
if os.name == 'nt': k['creationflags'] = (k.get('creationflags') or 0) | 0x08000000
|
||||||
|
_Pi(self, *a, **k)
|
||||||
|
subprocess.Popen.__init__ = _pinit
|
||||||
|
if hasattr(os, 'startfile'):
|
||||||
|
def _nosf(*a, **k): raise RuntimeError("startfile disabled in sandbox")
|
||||||
|
os.startfile = _nosf
|
||||||
sys.excepthook = lambda t, v, tb: (sys.__excepthook__(t, v, tb), print(f"\n[Agent Hint]: NO GUESSING! You MUST probe first. If missing common package, pip.")) if issubclass(t, (ImportError, AttributeError)) else sys.__excepthook__(t, v, tb)
|
sys.excepthook = lambda t, v, tb: (sys.__excepthook__(t, v, tb), print(f"\n[Agent Hint]: NO GUESSING! You MUST probe first. If missing common package, pip.")) if issubclass(t, (ImportError, AttributeError)) else sys.__excepthook__(t, v, tb)
|
||||||
|
|||||||
179
assets/install-macos-app.sh
Executable file
179
assets/install-macos-app.sh
Executable file
@@ -0,0 +1,179 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# GenericAgent macOS Desktop App Installation Script
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash assets/install-macos-app.sh [--auto]
|
||||||
|
#
|
||||||
|
# This installer creates a small .app bundle that opens Terminal and runs
|
||||||
|
# `python3 launch.pyw` from the current GenericAgent checkout.
|
||||||
|
|
||||||
|
if [ -z "${BASH_VERSION}" ]; then
|
||||||
|
if command -v bash >/dev/null 2>&1; then
|
||||||
|
exec bash -- "${0}" "$@"
|
||||||
|
else
|
||||||
|
echo "Error: This script requires bash."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||||
|
log_info() { echo -e "${BLUE}ℹ️ $1${NC}"; }
|
||||||
|
log_success() { echo -e "${GREEN}✅ $1${NC}"; }
|
||||||
|
log_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; }
|
||||||
|
log_error() { echo -e "${RED}❌ $1${NC}"; }
|
||||||
|
|
||||||
|
AUTO_MODE=false
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--auto) AUTO_MODE=true ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_NAME="GenericAgent"
|
||||||
|
PRIMARY_INSTALL_DIR="/Applications"
|
||||||
|
FALLBACK_INSTALL_DIR="${HOME}/Applications"
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
ICON_PATH="${PROJECT_ROOT}/assets/images/logo.jpg"
|
||||||
|
LAUNCH_SCRIPT="${PROJECT_ROOT}/launch.pyw"
|
||||||
|
|
||||||
|
echo -e "${CYAN}"
|
||||||
|
echo "╔═══════════════════════════════════════════════════════════╗"
|
||||||
|
echo "║ GenericAgent — macOS Desktop App Installer ║"
|
||||||
|
echo "╚═══════════════════════════════════════════════════════════╝"
|
||||||
|
echo -e "${NC}"
|
||||||
|
|
||||||
|
if [[ "$(uname)" != "Darwin" ]]; then
|
||||||
|
log_error "This script only supports macOS."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
log_error "python3 is not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${LAUNCH_SCRIPT}" ]; then
|
||||||
|
log_error "launch.pyw not found at ${LAUNCH_SCRIPT}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
project_path_for_applescript="${PROJECT_ROOT}/"
|
||||||
|
project_path_for_applescript="${project_path_for_applescript//\\/\\\\}"
|
||||||
|
project_path_for_applescript="${project_path_for_applescript//\"/\\\"}"
|
||||||
|
|
||||||
|
detect_existing_app() {
|
||||||
|
if [ -d "${PRIMARY_INSTALL_DIR}/${APP_NAME}.app" ]; then
|
||||||
|
echo "${PRIMARY_INSTALL_DIR}/${APP_NAME}.app"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ -d "${FALLBACK_INSTALL_DIR}/${APP_NAME}.app" ]; then
|
||||||
|
echo "${FALLBACK_INSTALL_DIR}/${APP_NAME}.app"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
existing_app_path="$(detect_existing_app || true)"
|
||||||
|
if [ -n "${existing_app_path}" ]; then
|
||||||
|
log_warning "${APP_NAME}.app already exists at ${existing_app_path}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${AUTO_MODE}" = false ]; then
|
||||||
|
echo ""
|
||||||
|
echo "This will install a desktop app that launches GenericAgent"
|
||||||
|
echo "from Spotlight, Launchpad, or the Applications folder."
|
||||||
|
echo ""
|
||||||
|
if [ -n "${existing_app_path}" ]; then
|
||||||
|
read -p "Reinstall ${APP_NAME}.app? (y/N) " -n 1 -r
|
||||||
|
else
|
||||||
|
read -p "Continue? (Y/n) " -n 1 -r
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
if [ -n "${existing_app_path}" ]; then
|
||||||
|
[[ ! ${REPLY:-} =~ ^[Yy]$ ]] && { echo "Aborted."; exit 0; }
|
||||||
|
else
|
||||||
|
[[ ${REPLY:-} =~ ^[Nn]$ ]] && { echo "Aborted."; exit 0; }
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||||
|
|
||||||
|
log_info "Building ${APP_NAME}.app..."
|
||||||
|
|
||||||
|
cat > "${TMP_DIR}/${APP_NAME}.applescript" <<APPLESCRIPT
|
||||||
|
on run
|
||||||
|
set projectPathStr to "${project_path_for_applescript}"
|
||||||
|
tell application "Terminal"
|
||||||
|
activate
|
||||||
|
do script "cd " & quoted form of projectPathStr & " && python3 launch.pyw"
|
||||||
|
end tell
|
||||||
|
end run
|
||||||
|
APPLESCRIPT
|
||||||
|
|
||||||
|
osacompile -o "${TMP_DIR}/${APP_NAME}.app" "${TMP_DIR}/${APP_NAME}.applescript"
|
||||||
|
|
||||||
|
log_info "Applying GenericAgent icon..."
|
||||||
|
if [ -f "${ICON_PATH}" ]; then
|
||||||
|
ICONSET_DIR="${TMP_DIR}/ga-icon.iconset"
|
||||||
|
mkdir -p "${ICONSET_DIR}"
|
||||||
|
|
||||||
|
sips -z 16 16 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_16x16.png" >/dev/null 2>&1
|
||||||
|
sips -z 32 32 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_16x16@2x.png" >/dev/null 2>&1
|
||||||
|
sips -z 32 32 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_32x32.png" >/dev/null 2>&1
|
||||||
|
sips -z 64 64 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_32x32@2x.png" >/dev/null 2>&1
|
||||||
|
sips -z 128 128 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_128x128.png" >/dev/null 2>&1
|
||||||
|
sips -z 256 256 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_128x128@2x.png" >/dev/null 2>&1
|
||||||
|
sips -z 256 256 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_256x256.png" >/dev/null 2>&1
|
||||||
|
sips -z 512 512 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_256x256@2x.png" >/dev/null 2>&1
|
||||||
|
sips -z 512 512 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_512x512.png" >/dev/null 2>&1
|
||||||
|
cp "${ICON_PATH}" "${ICONSET_DIR}/icon_512x512@2x.png"
|
||||||
|
|
||||||
|
iconutil -c icns "${ICONSET_DIR}" -o "${TMP_DIR}/ga-icon.icns"
|
||||||
|
cp "${TMP_DIR}/ga-icon.icns" "${TMP_DIR}/${APP_NAME}.app/Contents/Resources/applet.icns"
|
||||||
|
log_success "Icon applied from assets/images/logo.jpg"
|
||||||
|
else
|
||||||
|
log_warning "Logo not found at ${ICON_PATH}, using default icon."
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_bundle() {
|
||||||
|
local install_dir="$1"
|
||||||
|
local destination="${install_dir}/${APP_NAME}.app"
|
||||||
|
mkdir -p "${install_dir}"
|
||||||
|
rm -rf "${destination}"
|
||||||
|
cp -R "${TMP_DIR}/${APP_NAME}.app" "${destination}"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_path=""
|
||||||
|
if install_bundle "${PRIMARY_INSTALL_DIR}" 2>/dev/null; then
|
||||||
|
install_path="${PRIMARY_INSTALL_DIR}/${APP_NAME}.app"
|
||||||
|
else
|
||||||
|
log_warning "Could not write to ${PRIMARY_INSTALL_DIR}; falling back to ${FALLBACK_INSTALL_DIR}"
|
||||||
|
install_bundle "${FALLBACK_INSTALL_DIR}"
|
||||||
|
install_path="${FALLBACK_INSTALL_DIR}/${APP_NAME}.app"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_success "Installed to: ${install_path}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${CYAN}║${NC} ✨ ${APP_NAME} Desktop App installed successfully! ${CYAN}║${NC}"
|
||||||
|
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Launch methods:${NC}"
|
||||||
|
echo " • Spotlight: Cmd + Space → type '${APP_NAME}' → Enter"
|
||||||
|
echo " • Launchpad: Find the '${APP_NAME}' icon"
|
||||||
|
echo " • Finder: Open ${install_path}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Runtime behavior:${NC}"
|
||||||
|
echo " The app uses the current checkout path embedded at install time:"
|
||||||
|
echo " ${PROJECT_ROOT}"
|
||||||
|
echo " If you move the repo later, re-run this installer."
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Uninstall:${NC}"
|
||||||
|
echo " rm -rf '${install_path}'"
|
||||||
|
echo ""
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Role: 物理级全能执行者
|
# Role: 物理级全能执行者
|
||||||
你拥有文件读写、脚本执行、用户浏览器JS注入、系统级干预的物理操作权限。禁止推诿"无法操作"——不空想,用工具探测。
|
你拥有文件读写、脚本执行、用户浏览器JS注入、系统级干预的物理操作权限。禁止推诿"无法操作"——不空想,用工具探测。
|
||||||
## 行动原则
|
## 行动原则
|
||||||
调用工具前在<thinking>内推演:当前阶段、上步结果是否符合预期、下步策略;<summary>内输出极简总结。
|
调用工具前先推演:当前阶段、上步结果是否符合预期、下步策略;回复文本中用<summary>输出极简总结。
|
||||||
- 探测优先:失败时先充分获取信息(日志/状态/上下文),关键信息存入工作记忆,再决定重试或换方案。不可逆操作先询问用户。
|
- 探测优先:失败时先充分获取信息(日志/状态/上下文),关键信息存入工作记忆,再决定重试或换方案。不可逆操作先询问用户。
|
||||||
- 失败升级:1次→读错误理解原因,2次→探测环境状态,3次→深度分析后换方案或问用户。禁止无新信息的重复操作。
|
- 失败升级:1次→读错误理解原因,2次→探测环境状态,3次→深度分析后换方案或问用户。禁止无新信息的重复操作。
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
You have full physical access: file I/O, script execution, browser JS injection, and system-level intervention. Never deflect with "can't do it" — don't speculate, use tools to probe.
|
You have full physical access: file I/O, script execution, browser JS injection, and system-level intervention. Never deflect with "can't do it" — don't speculate, use tools to probe.
|
||||||
Summarize and reply in user's language or follow user's prompt.
|
Summarize and reply in user's language or follow user's prompt.
|
||||||
## Action Principles
|
## Action Principles
|
||||||
Before each tool call, reason inside <thinking>: current phase, whether the last result met expectations, and next strategy.
|
Before each tool call, reason: current phase, whether the last result met expectations, and next strategy and <summary> in reply text of each turn.
|
||||||
- Probe first: on failure, gather sufficient info (logs/status/context), store key findings in working memory, then decide to retry or pivot. Ask the user before irreversible operations.
|
- Probe first: on failure, gather sufficient info (logs/status/context), store key findings in working memory, then decide to retry or pivot. Ask the user before irreversible operations.
|
||||||
- Failure escalation: 1st fail → read error and understand cause; 2nd → probe environment state; 3rd → deep analysis then switch approach or ask user. Never repeat an action without new information.
|
- Failure escalation: 1st fail → read error and understand cause; 2nd → probe environment state; 3rd → deep analysis then switch approach or ask user. Never repeat an action without new information.
|
||||||
@@ -609,9 +609,10 @@ if sys.platform == 'darwin':
|
|||||||
self.frame_idx = 0
|
self.frame_idx = 0
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Windows Implementation - tkinter with transparentcolor
|
# Windows/Linux Implementations
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
else:
|
else:
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from PIL import ImageTk
|
from PIL import ImageTk
|
||||||
|
|
||||||
@@ -619,6 +620,10 @@ else:
|
|||||||
def __init__(self, skin_name=None):
|
def __init__(self, skin_name=None):
|
||||||
self.root = tk.Tk()
|
self.root = tk.Tk()
|
||||||
self.root.wm_attributes('-topmost', True)
|
self.root.wm_attributes('-topmost', True)
|
||||||
|
self.is_windows = sys.platform.startswith('win')
|
||||||
|
self.platform_name = 'Windows' if self.is_windows else 'Linux'
|
||||||
|
self.pet_bg_color = '#F0F0F0' if self.is_windows else 'black'
|
||||||
|
self.toast_bg_color = '#00ff01' if self.is_windows else 'black'
|
||||||
|
|
||||||
# Load skin
|
# Load skin
|
||||||
self.load_skin(skin_name)
|
self.load_skin(skin_name)
|
||||||
@@ -635,11 +640,12 @@ else:
|
|||||||
self.root.wm_attributes('-topmost', True)
|
self.root.wm_attributes('-topmost', True)
|
||||||
|
|
||||||
# Transparent background
|
# Transparent background
|
||||||
self.root.wm_attributes('-transparentcolor', '#F0F0F0')
|
if self.is_windows:
|
||||||
self.root.config(bg='#F0F0F0')
|
self.root.wm_attributes('-transparentcolor', self.pet_bg_color)
|
||||||
|
self.root.config(bg=self.pet_bg_color)
|
||||||
|
|
||||||
# Create label
|
# Create label
|
||||||
self.label = tk.Label(self.root, bg='#F0F0F0', bd=0)
|
self.label = tk.Label(self.root, bg=self.pet_bg_color, bd=0)
|
||||||
self.label.pack()
|
self.label.pack()
|
||||||
|
|
||||||
# Bind events
|
# Bind events
|
||||||
@@ -660,7 +666,7 @@ else:
|
|||||||
self._animate()
|
self._animate()
|
||||||
self._start_server()
|
self._start_server()
|
||||||
|
|
||||||
print(f"✓ Windows Pet started at ({x_pos}, {y_pos})")
|
print(f"✓ {self.platform_name} Pet started at ({x_pos}, {y_pos})")
|
||||||
print(f" Animations: {', '.join(self.animations.keys())}")
|
print(f" Animations: {', '.join(self.animations.keys())}")
|
||||||
|
|
||||||
self.root.mainloop()
|
self.root.mainloop()
|
||||||
@@ -747,13 +753,14 @@ else:
|
|||||||
self.toast_window = tk.Toplevel(self.root)
|
self.toast_window = tk.Toplevel(self.root)
|
||||||
self.toast_window.overrideredirect(True)
|
self.toast_window.overrideredirect(True)
|
||||||
self.toast_window.wm_attributes('-topmost', True)
|
self.toast_window.wm_attributes('-topmost', True)
|
||||||
self.toast_window.wm_attributes('-transparentcolor', '#00ff01')
|
if self.is_windows:
|
||||||
self.toast_window.config(bg='#00ff01')
|
self.toast_window.wm_attributes('-transparentcolor', self.toast_bg_color)
|
||||||
|
self.toast_window.config(bg=self.toast_bg_color)
|
||||||
|
|
||||||
toast_label = tk.Label(
|
toast_label = tk.Label(
|
||||||
self.toast_window,
|
self.toast_window,
|
||||||
image=self.toast_photo,
|
image=self.toast_photo,
|
||||||
bg='#00ff01',
|
bg=self.toast_bg_color,
|
||||||
bd=0,
|
bd=0,
|
||||||
highlightthickness=0
|
highlightthickness=0
|
||||||
)
|
)
|
||||||
@@ -804,6 +811,253 @@ else:
|
|||||||
self.load_skin(skin_name)
|
self.load_skin(skin_name)
|
||||||
self.current_state = 'idle'
|
self.current_state = 'idle'
|
||||||
self.frame_idx = 0
|
self.frame_idx = 0
|
||||||
|
else:
|
||||||
|
from PySide6.QtCore import Qt, QTimer, QPoint
|
||||||
|
from PySide6.QtGui import QAction, QCursor, QImage, QPixmap
|
||||||
|
from PySide6.QtWidgets import QApplication, QLabel, QMenu, QWidget
|
||||||
|
|
||||||
|
class _LinuxPetLabel(QLabel):
|
||||||
|
def __init__(self, pet):
|
||||||
|
super().__init__()
|
||||||
|
self.pet = pet
|
||||||
|
self.drag_offset = None
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button() == Qt.LeftButton:
|
||||||
|
self.drag_offset = event.globalPosition().toPoint() - self.pet.window.frameGeometry().topLeft()
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
if event.button() == Qt.RightButton:
|
||||||
|
self.pet._show_context_menu(event.globalPosition().toPoint())
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
super().mousePressEvent(event)
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
if self.drag_offset is not None and (event.buttons() & Qt.LeftButton):
|
||||||
|
self.pet.window.move(event.globalPosition().toPoint() - self.drag_offset)
|
||||||
|
self.pet._reposition_toast()
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
super().mouseMoveEvent(event)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
if event.button() == Qt.LeftButton:
|
||||||
|
self.drag_offset = None
|
||||||
|
super().mouseReleaseEvent(event)
|
||||||
|
|
||||||
|
def mouseDoubleClickEvent(self, event):
|
||||||
|
if event.button() == Qt.LeftButton:
|
||||||
|
QApplication.instance().quit()
|
||||||
|
event.accept()
|
||||||
|
return
|
||||||
|
super().mouseDoubleClickEvent(event)
|
||||||
|
|
||||||
|
|
||||||
|
class LinuxPet(PetBase):
|
||||||
|
def __init__(self, skin_name=None):
|
||||||
|
self.app = QApplication.instance() or QApplication(sys.argv)
|
||||||
|
self.available_skins = SkinLoader.list_skins()
|
||||||
|
self.load_skin(skin_name)
|
||||||
|
|
||||||
|
screen = self.app.primaryScreen()
|
||||||
|
screen_geo = screen.availableGeometry() if screen else None
|
||||||
|
if screen_geo:
|
||||||
|
x_pos = screen_geo.right() - self.display_width - 72
|
||||||
|
y_pos = screen_geo.bottom() - self.display_height - 120
|
||||||
|
else:
|
||||||
|
x_pos, y_pos = 1200, 700
|
||||||
|
|
||||||
|
self.window = QWidget()
|
||||||
|
self.window.setWindowFlags(
|
||||||
|
Qt.FramelessWindowHint |
|
||||||
|
Qt.WindowStaysOnTopHint |
|
||||||
|
Qt.Tool
|
||||||
|
)
|
||||||
|
self.window.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||||
|
self.window.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
||||||
|
self.window.resize(self.display_width, self.display_height)
|
||||||
|
self.window.move(x_pos, y_pos)
|
||||||
|
|
||||||
|
self.label = _LinuxPetLabel(self)
|
||||||
|
self.label.setParent(self.window)
|
||||||
|
self.label.setGeometry(0, 0, self.display_width, self.display_height)
|
||||||
|
self.label.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||||
|
self.label.setStyleSheet('background: transparent;')
|
||||||
|
self.label.setScaledContents(True)
|
||||||
|
|
||||||
|
self.current_state = 'idle'
|
||||||
|
self.frame_idx = 0
|
||||||
|
self.toast_window = None
|
||||||
|
self.toast_label = None
|
||||||
|
self.toast_pixmap = None
|
||||||
|
|
||||||
|
self.anim_timer = QTimer()
|
||||||
|
self.anim_timer.timeout.connect(self._animate)
|
||||||
|
self._restart_animation_timer()
|
||||||
|
|
||||||
|
self.window.show()
|
||||||
|
self._start_server()
|
||||||
|
|
||||||
|
print(f"✓ Linux PySide6 Pet started at ({x_pos}, {y_pos})")
|
||||||
|
print(f" Animations: {', '.join(self.animations.keys())}")
|
||||||
|
|
||||||
|
def _pil_to_qpixmap(self, pil_img):
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
pil_img.save(buffer, format='PNG')
|
||||||
|
qimage = QImage.fromData(buffer.getvalue(), 'PNG')
|
||||||
|
return QPixmap.fromImage(qimage)
|
||||||
|
|
||||||
|
def load_skin(self, skin_name=None):
|
||||||
|
available_skins = SkinLoader.list_skins()
|
||||||
|
if not available_skins:
|
||||||
|
raise FileNotFoundError(f"No skins found in {SKINS_DIR}")
|
||||||
|
|
||||||
|
if skin_name is None or skin_name not in available_skins:
|
||||||
|
skin_name = available_skins[0]
|
||||||
|
|
||||||
|
skin_path = os.path.join(SKINS_DIR, skin_name)
|
||||||
|
self.skin_config = SkinLoader.load_skin(skin_path)
|
||||||
|
|
||||||
|
display_size = self.skin_config.get('size', {})
|
||||||
|
self.display_width = display_size.get('width', 128)
|
||||||
|
self.display_height = display_size.get('height', 128)
|
||||||
|
|
||||||
|
self.animations = {}
|
||||||
|
for anim_name, anim_config in self.skin_config['animations'].items():
|
||||||
|
pil_frames = AnimationLoader.load_sprite_frames(skin_path, anim_config)
|
||||||
|
qt_frames = []
|
||||||
|
for frame in pil_frames:
|
||||||
|
if frame.mode != 'RGBA':
|
||||||
|
frame = frame.convert('RGBA')
|
||||||
|
scaled = frame.resize((self.display_width, self.display_height), Image.NEAREST)
|
||||||
|
qt_frames.append(self._pil_to_qpixmap(scaled))
|
||||||
|
|
||||||
|
self.animations[anim_name] = {
|
||||||
|
'frames': qt_frames,
|
||||||
|
'fps': anim_config.get('sprite', {}).get('fps', 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasattr(self, 'window'):
|
||||||
|
self.window.resize(self.display_width, self.display_height)
|
||||||
|
self.label.setGeometry(0, 0, self.display_width, self.display_height)
|
||||||
|
self._animate(force=True)
|
||||||
|
self._reposition_toast()
|
||||||
|
|
||||||
|
def _restart_animation_timer(self):
|
||||||
|
anim = self.animations.get(self.current_state) or next(iter(self.animations.values()))
|
||||||
|
fps = max(1, anim.get('fps', 6))
|
||||||
|
self.anim_timer.start(int(1000 / fps))
|
||||||
|
|
||||||
|
def _animate(self, force=False):
|
||||||
|
if self.current_state not in self.animations:
|
||||||
|
return
|
||||||
|
anim = self.animations[self.current_state]
|
||||||
|
frames = anim['frames']
|
||||||
|
if not frames:
|
||||||
|
return
|
||||||
|
if force:
|
||||||
|
self.frame_idx = 0
|
||||||
|
self.label.setPixmap(frames[self.frame_idx])
|
||||||
|
self.frame_idx = (self.frame_idx + 1) % len(frames)
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
if state in self.animations and state != self.current_state:
|
||||||
|
self.current_state = state
|
||||||
|
self.frame_idx = 0
|
||||||
|
self._restart_animation_timer()
|
||||||
|
print(f"→ State: {state}")
|
||||||
|
|
||||||
|
def _show_context_menu(self, global_pos):
|
||||||
|
menu = QMenu(self.window)
|
||||||
|
for skin_name in SkinLoader.list_skins():
|
||||||
|
action = QAction(skin_name, menu)
|
||||||
|
action.triggered.connect(lambda checked=False, name=skin_name: self._change_skin(name))
|
||||||
|
menu.addAction(action)
|
||||||
|
menu.addSeparator()
|
||||||
|
quit_action = QAction('Quit', menu)
|
||||||
|
quit_action.triggered.connect(QApplication.instance().quit)
|
||||||
|
menu.addAction(quit_action)
|
||||||
|
menu.popup(global_pos)
|
||||||
|
|
||||||
|
def _compute_toast_geometry(self, bubble_width, bubble_height, tail_x, tail_y):
|
||||||
|
pet_pos = self.window.frameGeometry().topLeft()
|
||||||
|
anchor_x = pet_pos.x() + int(self.display_width * 0.75)
|
||||||
|
anchor_y = pet_pos.y() + int(self.display_height * 0.15)
|
||||||
|
return anchor_x - tail_x, anchor_y - tail_y - bubble_height // 2
|
||||||
|
|
||||||
|
def show_toast(self, message):
|
||||||
|
if self.toast_window:
|
||||||
|
self.toast_window.close()
|
||||||
|
self.toast_window = None
|
||||||
|
self.toast_label = None
|
||||||
|
self.toast_pixmap = None
|
||||||
|
|
||||||
|
bubble_info = build_bubble_image(message, max_width=max(180, min(260, self.display_width * 2)))
|
||||||
|
bubble_pil = bubble_info['image']
|
||||||
|
bubble_width, bubble_height = bubble_info['size']
|
||||||
|
tail_x, tail_y = bubble_info['tail_tip']
|
||||||
|
self.toast_pixmap = self._pil_to_qpixmap(bubble_pil)
|
||||||
|
|
||||||
|
self.toast_window = QWidget()
|
||||||
|
self.toast_window.setWindowFlags(
|
||||||
|
Qt.FramelessWindowHint |
|
||||||
|
Qt.WindowStaysOnTopHint |
|
||||||
|
Qt.Tool |
|
||||||
|
Qt.WindowTransparentForInput
|
||||||
|
)
|
||||||
|
self.toast_window.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||||
|
self.toast_window.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
||||||
|
self.toast_window.resize(bubble_width, bubble_height)
|
||||||
|
|
||||||
|
self.toast_label = QLabel(self.toast_window)
|
||||||
|
self.toast_label.setGeometry(0, 0, bubble_width, bubble_height)
|
||||||
|
self.toast_label.setPixmap(self.toast_pixmap)
|
||||||
|
self.toast_label.setAttribute(Qt.WA_TranslucentBackground, True)
|
||||||
|
self.toast_label.setStyleSheet('background: transparent;')
|
||||||
|
|
||||||
|
toast_x, toast_y = self._compute_toast_geometry(bubble_width, bubble_height, tail_x, tail_y)
|
||||||
|
self.toast_window.move(toast_x, toast_y)
|
||||||
|
self.toast_window.show()
|
||||||
|
|
||||||
|
QTimer.singleShot(3000, self._hide_toast)
|
||||||
|
print(f"Toast: {message}")
|
||||||
|
|
||||||
|
def _reposition_toast(self):
|
||||||
|
if not self.toast_window:
|
||||||
|
return
|
||||||
|
label_pixmap = self.toast_label.pixmap() if self.toast_label else None
|
||||||
|
if label_pixmap is None:
|
||||||
|
return
|
||||||
|
bubble_width = label_pixmap.width()
|
||||||
|
bubble_height = label_pixmap.height()
|
||||||
|
toast_x, toast_y = self._compute_toast_geometry(
|
||||||
|
bubble_width,
|
||||||
|
bubble_height,
|
||||||
|
bubble_width // 2,
|
||||||
|
bubble_height
|
||||||
|
)
|
||||||
|
self.toast_window.move(toast_x, toast_y)
|
||||||
|
|
||||||
|
def _hide_toast(self):
|
||||||
|
if self.toast_window:
|
||||||
|
self.toast_window.close()
|
||||||
|
self.toast_window = None
|
||||||
|
self.toast_label = None
|
||||||
|
self.toast_pixmap = None
|
||||||
|
|
||||||
|
def _schedule_main(self, fn):
|
||||||
|
QTimer.singleShot(0, fn)
|
||||||
|
|
||||||
|
def _change_skin(self, skin_name):
|
||||||
|
print(f"Changing skin to: {skin_name}")
|
||||||
|
self.load_skin(skin_name)
|
||||||
|
self.current_state = 'idle'
|
||||||
|
self.frame_idx = 0
|
||||||
|
self._restart_animation_timer()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.app.exec()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Singleton: if port already in use, another instance is running
|
# Singleton: if port already in use, another instance is running
|
||||||
@@ -820,5 +1074,9 @@ if __name__ == '__main__':
|
|||||||
if sys.platform == 'darwin':
|
if sys.platform == 'darwin':
|
||||||
pet = MacPet('vita')
|
pet = MacPet('vita')
|
||||||
pet.run()
|
pet.run()
|
||||||
else:
|
elif sys.platform.startswith('win'):
|
||||||
pet = WinPet('vita')
|
pet = WinPet('vita')
|
||||||
|
else:
|
||||||
|
pet = LinuxPet('vita')
|
||||||
|
pet.run()
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,11 @@ with st.sidebar: render_sidebar()
|
|||||||
|
|
||||||
def fold_turns(text):
|
def fold_turns(text):
|
||||||
"""Return list of segments: [{'type':'text','content':...}, {'type':'fold','title':...,'content':...}]"""
|
"""Return list of segments: [{'type':'text','content':...}, {'type':'fold','title':...,'content':...}]"""
|
||||||
parts = re.split(r'(\**LLM Running \(Turn \d+\) \.\.\.\*\**)', text)
|
# 先把4+反引号块替换为占位符,避免误切子agent嵌套的 LLM Running
|
||||||
|
_ph = []
|
||||||
|
safe = re.sub(r'`{4,}.*?`{4,}', lambda m: (_ph.append(m.group(0)), f'\x00PH{len(_ph)-1}\x00')[1], text, flags=re.DOTALL)
|
||||||
|
parts = re.split(r'(\**LLM Running \(Turn \d+\) \.\.\.\*\**)', safe)
|
||||||
|
parts = [re.sub(r'\x00PH(\d+)\x00', lambda m: _ph[int(m.group(1))], p) for p in parts]
|
||||||
if len(parts) < 4: return [{'type': 'text', 'content': text}]
|
if len(parts) < 4: return [{'type': 'text', 'content': text}]
|
||||||
segments = []
|
segments = []
|
||||||
if parts[0].strip(): segments.append({'type': 'text', 'content': parts[0]})
|
if parts[0].strip(): segments.append({'type': 'text', 'content': parts[0]})
|
||||||
@@ -103,7 +107,7 @@ def fold_turns(text):
|
|||||||
turns.append((marker, content))
|
turns.append((marker, content))
|
||||||
for idx, (marker, content) in enumerate(turns):
|
for idx, (marker, content) in enumerate(turns):
|
||||||
if idx < len(turns) - 1:
|
if idx < len(turns) - 1:
|
||||||
_c = re.sub(r'```.*?```|<thinking>.*?</thinking>', '', content, flags=re.DOTALL)
|
_c = re.sub(r'`{3,}.*?`{3,}|<thinking>.*?</thinking>', '', content, flags=re.DOTALL)
|
||||||
matches = re.findall(r'<summary>\s*((?:(?!<summary>).)*?)\s*</summary>', _c, re.DOTALL)
|
matches = re.findall(r'<summary>\s*((?:(?!<summary>).)*?)\s*</summary>', _c, re.DOTALL)
|
||||||
if matches:
|
if matches:
|
||||||
title = matches[0].strip()
|
title = matches[0].strip()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import os, sys, re, threading, asyncio, queue as Q, socket, time, random
|
import os, sys, re, threading, asyncio, queue as Q, time, random
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
_TEMP_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'temp')
|
_TEMP_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'temp')
|
||||||
from agentmain import GeneraticAgent
|
from agentmain import GeneraticAgent
|
||||||
@@ -16,10 +16,12 @@ from chatapp_common import (
|
|||||||
HELP_TEXT,
|
HELP_TEXT,
|
||||||
TELEGRAM_MENU_COMMANDS,
|
TELEGRAM_MENU_COMMANDS,
|
||||||
clean_reply,
|
clean_reply,
|
||||||
|
ensure_single_instance,
|
||||||
extract_files,
|
extract_files,
|
||||||
format_restore,
|
format_restore,
|
||||||
|
redirect_log,
|
||||||
|
require_runtime,
|
||||||
split_text,
|
split_text,
|
||||||
strip_files,
|
|
||||||
)
|
)
|
||||||
from continue_cmd import handle_frontend_command, reset_conversation
|
from continue_cmd import handle_frontend_command, reset_conversation
|
||||||
from llmcore import mykeys
|
from llmcore import mykeys
|
||||||
@@ -38,6 +40,8 @@ _MD_TOKEN_RE = re.compile(
|
|||||||
r"|\[([^\]]+)\]\(([^)\n]+)\)"
|
r"|\[([^\]]+)\]\(([^)\n]+)\)"
|
||||||
r"|`([^`\n]+)`"
|
r"|`([^`\n]+)`"
|
||||||
r"|\*\*([^\n]+?)\*\*"
|
r"|\*\*([^\n]+?)\*\*"
|
||||||
|
r"|__([^\n]+?)__"
|
||||||
|
r"|~~([^\n]+?)~~"
|
||||||
r"|(?<!\*)\*(?!\*)([^\n]+?)(?<!\*)\*(?!\*)",
|
r"|(?<!\*)\*(?!\*)([^\n]+?)(?<!\*)\*(?!\*)",
|
||||||
re.DOTALL,
|
re.DOTALL,
|
||||||
)
|
)
|
||||||
@@ -61,6 +65,11 @@ def _resolve_files(paths):
|
|||||||
return files
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def _render_file_markers(text):
|
||||||
|
def repl(match):
|
||||||
|
return os.path.basename(match.group(1))
|
||||||
|
return re.sub(r"\[FILE:([^\]]+)\]", repl, text or "").strip()
|
||||||
|
|
||||||
def _escape_pre(text):
|
def _escape_pre(text):
|
||||||
return escape_markdown(text or "", version=2, entity_type="pre")
|
return escape_markdown(text or "", version=2, entity_type="pre")
|
||||||
|
|
||||||
@@ -90,7 +99,11 @@ def _to_markdown_v2(text):
|
|||||||
elif match.group(7) is not None:
|
elif match.group(7) is not None:
|
||||||
parts.append(f"*{escape_markdown(match.group(7), version=2)}*")
|
parts.append(f"*{escape_markdown(match.group(7), version=2)}*")
|
||||||
elif match.group(8) is not None:
|
elif match.group(8) is not None:
|
||||||
parts.append(f"_{escape_markdown(match.group(8), version=2)}_")
|
parts.append(f"*{escape_markdown(match.group(8), version=2)}*")
|
||||||
|
elif match.group(9) is not None:
|
||||||
|
parts.append(f"~{escape_markdown(match.group(9), version=2)}~")
|
||||||
|
elif match.group(10) is not None:
|
||||||
|
parts.append(f"_{escape_markdown(match.group(10), version=2)}_")
|
||||||
pos = match.end()
|
pos = match.end()
|
||||||
parts.append(escape_markdown(text[pos:], version=2))
|
parts.append(escape_markdown(text[pos:], version=2))
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
@@ -102,7 +115,7 @@ class _TelegramStreamSession:
|
|||||||
def __init__(self, root_msg):
|
def __init__(self, root_msg):
|
||||||
self.root_msg = root_msg
|
self.root_msg = root_msg
|
||||||
self.private_chat = getattr(getattr(root_msg, "chat", None), "type", "") == ChatType.PRIVATE
|
self.private_chat = getattr(getattr(root_msg, "chat", None), "type", "") == ChatType.PRIVATE
|
||||||
self.can_use_draft = self.private_chat
|
self.can_use_draft = False # can not use or streaming dead
|
||||||
self.draft_id = _make_draft_id()
|
self.draft_id = _make_draft_id()
|
||||||
self.live_msg = None
|
self.live_msg = None
|
||||||
self.raw_text = ""
|
self.raw_text = ""
|
||||||
@@ -144,7 +157,7 @@ class _TelegramStreamSession:
|
|||||||
async def _refresh(self, done, send_files):
|
async def _refresh(self, done, send_files):
|
||||||
cleaned = clean_reply(self.raw_text) if self.raw_text.strip() else ""
|
cleaned = clean_reply(self.raw_text) if self.raw_text.strip() else ""
|
||||||
self.files = _resolve_files(extract_files(cleaned))
|
self.files = _resolve_files(extract_files(cleaned))
|
||||||
body = strip_files(cleaned)
|
body = _render_file_markers(cleaned)
|
||||||
if done and not body and self.files:
|
if done and not body and self.files:
|
||||||
body = "已生成附件"
|
body = "已生成附件"
|
||||||
elif done and not body:
|
elif done and not body:
|
||||||
@@ -244,16 +257,12 @@ async def _stream(dq, msg):
|
|||||||
await session.prime()
|
await session.prime()
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try: first = await asyncio.to_thread(dq.get, True, _QUEUE_WAIT_SECONDS)
|
||||||
first = await asyncio.to_thread(dq.get, True, _QUEUE_WAIT_SECONDS)
|
except Q.Empty: continue
|
||||||
except Q.Empty:
|
|
||||||
continue
|
|
||||||
items = [first]
|
items = [first]
|
||||||
try:
|
try:
|
||||||
while True:
|
while True: items.append(dq.get_nowait())
|
||||||
items.append(dq.get_nowait())
|
except Q.Empty: pass
|
||||||
except Q.Empty:
|
|
||||||
pass
|
|
||||||
done_item = next((item for item in items if "done" in item), None)
|
done_item = next((item for item in items if "done" in item), None)
|
||||||
if done_item is not None:
|
if done_item is not None:
|
||||||
await session.finalize(done_item.get("done", ""))
|
await session.finalize(done_item.get("done", ""))
|
||||||
@@ -267,20 +276,16 @@ async def _stream(dq, msg):
|
|||||||
print(f"[TG stream error] {type(exc).__name__}: {exc}", flush=True)
|
print(f"[TG stream error] {type(exc).__name__}: {exc}", flush=True)
|
||||||
await session.finish_with_notice(f"❌ 输出失败: {exc}")
|
await session.finish_with_notice(f"❌ 输出失败: {exc}")
|
||||||
|
|
||||||
|
|
||||||
def _normalized_command(text):
|
def _normalized_command(text):
|
||||||
parts = (text or "").strip().split(None, 1)
|
parts = (text or "").strip().split(None, 1)
|
||||||
if not parts:
|
if not parts: return ''
|
||||||
return ''
|
|
||||||
head = parts[0].lower()
|
head = parts[0].lower()
|
||||||
if head.startswith('/'):
|
if head.startswith('/'): head = '/' + head[1:].split('@', 1)[0]
|
||||||
head = '/' + head[1:].split('@', 1)[0]
|
|
||||||
return head + (f" {parts[1].strip()}" if len(parts) > 1 and parts[1].strip() else '')
|
return head + (f" {parts[1].strip()}" if len(parts) > 1 and parts[1].strip() else '')
|
||||||
|
|
||||||
def _cancel_stream_task(ctx):
|
def _cancel_stream_task(ctx):
|
||||||
task = ctx.user_data.pop('stream_task', None)
|
task = ctx.user_data.pop('stream_task', None)
|
||||||
if task and not task.done():
|
if task and not task.done(): task.cancel()
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
async def _sync_commands(application):
|
async def _sync_commands(application):
|
||||||
await application.bot.set_my_commands([BotCommand(command, description) for command, description in TELEGRAM_MENU_COMMANDS])
|
await application.bot.set_my_commands([BotCommand(command, description) for command, description in TELEGRAM_MENU_COMMANDS])
|
||||||
@@ -314,14 +319,22 @@ async def cmd_llm(update, ctx):
|
|||||||
|
|
||||||
async def handle_photo(update, ctx):
|
async def handle_photo(update, ctx):
|
||||||
uid = update.effective_user.id
|
uid = update.effective_user.id
|
||||||
if ALLOWED and uid not in ALLOWED:
|
if ALLOWED and uid not in ALLOWED: return await update.message.reply_text("no")
|
||||||
return await update.message.reply_text("no")
|
if update.message.photo:
|
||||||
photo = update.message.photo[-1]
|
photo = update.message.photo[-1]
|
||||||
file = await photo.get_file()
|
file = await photo.get_file()
|
||||||
fpath = f"tg_{photo.file_unique_id}.jpg"
|
fpath = f"tg_{photo.file_unique_id}.jpg"
|
||||||
|
kind = "图片"
|
||||||
|
elif update.message.document:
|
||||||
|
doc = update.message.document
|
||||||
|
file = await doc.get_file()
|
||||||
|
ext = os.path.splitext(doc.file_name or '')[1] or ''
|
||||||
|
fpath = f"tg_{doc.file_unique_id}{ext}"
|
||||||
|
kind = "文件"
|
||||||
|
else: return
|
||||||
await file.download_to_drive(os.path.join(_TEMP_DIR, fpath))
|
await file.download_to_drive(os.path.join(_TEMP_DIR, fpath))
|
||||||
caption = update.message.caption
|
caption = update.message.caption
|
||||||
prompt = f"[TIPS] 收到图片temp/{fpath}\n{caption}" if caption else f"[TIPS] 收到图片temp/{fpath},请等待下一步指令"
|
prompt = f"[TIPS] 收到{kind}temp/{fpath}\n{caption}" if caption else f"[TIPS] 收到{kind}temp/{fpath},请等待下一步指令"
|
||||||
dq = agent.put_task(prompt, source="telegram")
|
dq = agent.put_task(prompt, source="telegram")
|
||||||
task = asyncio.create_task(_stream(dq, update.message))
|
task = asyncio.create_task(_stream(dq, update.message))
|
||||||
ctx.user_data['stream_task'] = task
|
ctx.user_data['stream_task'] = task
|
||||||
@@ -359,20 +372,18 @@ async def handle_command(update, ctx):
|
|||||||
return await update.message.reply_text(HELP_TEXT)
|
return await update.message.reply_text(HELP_TEXT)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
try: # Single instance lock using socket
|
_LOCK_SOCK = ensure_single_instance(19527, "Telegram")
|
||||||
_lock_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM); _lock_sock.bind(('127.0.0.1', 19527))
|
|
||||||
except OSError:
|
|
||||||
print('[Telegram] Another instance is already running, skiping...')
|
|
||||||
sys.exit(1)
|
|
||||||
if not ALLOWED:
|
if not ALLOWED:
|
||||||
print('[Telegram] ERROR: tg_allowed_users in mykey.py is empty or missing. Set it to avoid unauthorized access.')
|
print('[Telegram] ERROR: tg_allowed_users in mykey.py is empty or missing. Set it to avoid unauthorized access.')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
_logf = open(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'temp', 'tgapp.log'), 'a', encoding='utf-8', buffering=1)
|
require_runtime(agent, "Telegram", tg_bot_token=mykeys.get("tg_bot_token"))
|
||||||
sys.stdout = sys.stderr = _logf
|
redirect_log(__file__, "tgapp.log", "Telegram", ALLOWED)
|
||||||
print('[NEW] New process starting, the above are history infos ...')
|
|
||||||
threading.Thread(target=agent.run, daemon=True).start()
|
threading.Thread(target=agent.run, daemon=True).start()
|
||||||
proxy = mykeys.get('proxy', None) # set 'proxy' in mykey.py if needed, e.g. 'http://127.0.0.1:2082'
|
proxy = mykeys.get('proxy')
|
||||||
|
if proxy:
|
||||||
print('proxy:', proxy)
|
print('proxy:', proxy)
|
||||||
|
else:
|
||||||
|
print('proxy: <disabled>')
|
||||||
|
|
||||||
async def _error_handler(update, context: ContextTypes.DEFAULT_TYPE):
|
async def _error_handler(update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
print(f"[{time.strftime('%m-%d %H:%M')}] TG error: {context.error}", flush=True)
|
print(f"[{time.strftime('%m-%d %H:%M')}] TG error: {context.error}", flush=True)
|
||||||
@@ -381,11 +392,15 @@ if __name__ == '__main__':
|
|||||||
try:
|
try:
|
||||||
print(f"TG bot starting... {time.strftime('%m-%d %H:%M')}")
|
print(f"TG bot starting... {time.strftime('%m-%d %H:%M')}")
|
||||||
# Recreate request and app objects on each restart to avoid stale connections
|
# Recreate request and app objects on each restart to avoid stale connections
|
||||||
request = HTTPXRequest(proxy=proxy, read_timeout=30, write_timeout=30, connect_timeout=30, pool_timeout=30)
|
request_kwargs = dict(read_timeout=30, write_timeout=30, connect_timeout=30, pool_timeout=30)
|
||||||
|
if proxy:
|
||||||
|
request_kwargs['proxy'] = proxy
|
||||||
|
request = HTTPXRequest(**request_kwargs)
|
||||||
app = (ApplicationBuilder().token(mykeys['tg_bot_token'])
|
app = (ApplicationBuilder().token(mykeys['tg_bot_token'])
|
||||||
.request(request).get_updates_request(request).post_init(_sync_commands).build())
|
.request(request).get_updates_request(request).post_init(_sync_commands).build())
|
||||||
app.add_handler(MessageHandler(filters.COMMAND, handle_command))
|
app.add_handler(MessageHandler(filters.COMMAND, handle_command))
|
||||||
app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
|
app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
|
||||||
|
app.add_handler(MessageHandler(filters.Document.ALL, handle_photo))
|
||||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_msg))
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_msg))
|
||||||
app.add_error_handler(_error_handler)
|
app.add_error_handler(_error_handler)
|
||||||
app.run_polling(drop_pending_updates=True, poll_interval=1.0, timeout=30)
|
app.run_polling(drop_pending_updates=True, poll_interval=1.0, timeout=30)
|
||||||
|
|||||||
@@ -289,26 +289,33 @@ def on_message(bot, msg):
|
|||||||
dq = agent.put_task(prompt, source="wechat")
|
dq = agent.put_task(prompt, source="wechat")
|
||||||
try: bot.send_typing(uid)
|
try: bot.send_typing(uid)
|
||||||
except: pass
|
except: pass
|
||||||
# Wait for completion
|
result = ''; sent = 0; mi = 0; last_turns = 0; last_send = 0
|
||||||
result = ''
|
def _wx_send(text):
|
||||||
|
try: bot.send_text(uid, text.strip(), context_token=ctx); return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[WX] send maybe-ok: {e}', file=sys.__stdout__); return True
|
||||||
|
def _flush(show, final=False):
|
||||||
|
nonlocal sent, mi, last_send
|
||||||
|
now = time.time()
|
||||||
|
if mi < 9 and sent < len(show) and (mi == 0 or now - last_send >= 6):
|
||||||
|
chunk = show[sent:sent+900]; sent += len(chunk); mi += 1
|
||||||
|
if chunk.strip() and _wx_send(chunk): last_send = time.time()
|
||||||
|
if final:
|
||||||
|
rest = (show[sent:] + '\n\n[Info] 任务完成')[-1800:]
|
||||||
|
if rest.strip(): _wx_send(rest)
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
item = dq.get(timeout=300)
|
item = dq.get(timeout=300)
|
||||||
if 'done' in item: result = item['done']; break
|
if 'done' in item: result = item['done']; break
|
||||||
|
raw = item.get('next', '')
|
||||||
|
turns = raw.count('LLM Running')
|
||||||
|
if turns > last_turns:
|
||||||
|
last_turns = turns; _flush(_clean(raw))
|
||||||
except queue.Empty: result = '[超时]'
|
except queue.Empty: result = '[超时]'
|
||||||
|
show = _clean(result); _flush(show, final=True)
|
||||||
files = re.findall(r'\[FILE:([^\]]+)\]', result)
|
files = re.findall(r'\[FILE:([^\]]+)\]', result)
|
||||||
bad = {'filepath', '<filepath>', 'path', '<path>', 'file_path', '<file_path>', '...'}
|
bad = {'filepath', '<filepath>', 'path', '<path>', 'file_path', '<file_path>', '...'}
|
||||||
files = [f for f in files if f.strip().lower() not in bad and (f if os.path.isabs(f) else os.path.join(_TEMP_DIR, f)) not in media_paths]
|
files = [f for f in files if f.strip().lower() not in bad and (f if os.path.isabs(f) else os.path.join(_TEMP_DIR, f)) not in media_paths]
|
||||||
show = _clean(result)
|
|
||||||
chunks = _split(show)
|
|
||||||
_MAX_MSGS = 6
|
|
||||||
if len(chunks) > _MAX_MSGS:
|
|
||||||
keep = chunks[:3] + [f'...(省略{len(chunks) - 5}条)...'] + chunks[-2:]
|
|
||||||
chunks = keep
|
|
||||||
for chunk in chunks:
|
|
||||||
try: bot.send_text(uid, chunk, context_token=ctx)
|
|
||||||
except Exception as e: print(f'[WX] send err: {e}', file=sys.__stdout__)
|
|
||||||
time.sleep(0.3)
|
|
||||||
for fpath in set(files):
|
for fpath in set(files):
|
||||||
if not os.path.isabs(fpath): fpath = os.path.join(_TEMP_DIR, fpath)
|
if not os.path.isabs(fpath): fpath = os.path.join(_TEMP_DIR, fpath)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -71,20 +71,20 @@ class WeComApp(AgentChatMixin):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[WeCom] welcome error: {e}")
|
print(f"[WeCom] welcome error: {e}")
|
||||||
|
|
||||||
async def on_connected(self, frame):
|
async def on_connected(self):
|
||||||
print("[WeCom] connected")
|
print("[WeCom] connected")
|
||||||
|
|
||||||
async def on_authenticated(self, frame):
|
async def on_authenticated(self):
|
||||||
print("[WeCom] authenticated")
|
print("[WeCom] authenticated")
|
||||||
|
|
||||||
async def on_disconnected(self, frame):
|
async def on_disconnected(self, reason=""):
|
||||||
print("[WeCom] disconnected")
|
print(f"[WeCom] disconnected: {reason}")
|
||||||
|
|
||||||
async def on_error(self, frame):
|
async def on_error(self, error=None):
|
||||||
print(f"[WeCom] error: {frame}")
|
print(f"[WeCom] error: {error}")
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
self.client = WSClient({"bot_id": BOT_ID, "secret": SECRET, "reconnect_interval": 1000, "max_reconnect_attempts": -1, "heartbeat_interval": 30000})
|
self.client = WSClient(BOT_ID, SECRET, reconnect_interval=1000, max_reconnect_attempts=-1, heartbeat_interval=30000)
|
||||||
for event, handler in {
|
for event, handler in {
|
||||||
"connected": self.on_connected,
|
"connected": self.on_connected,
|
||||||
"authenticated": self.on_authenticated,
|
"authenticated": self.on_authenticated,
|
||||||
@@ -95,7 +95,7 @@ class WeComApp(AgentChatMixin):
|
|||||||
}.items():
|
}.items():
|
||||||
self.client.on(event, handler)
|
self.client.on(event, handler)
|
||||||
print("[WeCom] bot starting...")
|
print("[WeCom] bot starting...")
|
||||||
await self.client.connect_async()
|
await self.client.connect()
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|||||||
12
llmcore.py
12
llmcore.py
@@ -74,6 +74,12 @@ def _sanitize_leading_user_msg(msg):
|
|||||||
msg['content'] = [{"type": "text", "text": '\n'.join(t for t in texts if t)}]
|
msg['content'] = [{"type": "text", "text": '\n'.join(t for t in texts if t)}]
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
_oldprint = print
|
||||||
|
def safeprint(*argv):
|
||||||
|
try: _oldprint(*argv)
|
||||||
|
except OSError: pass
|
||||||
|
print = safeprint
|
||||||
|
|
||||||
def trim_messages_history(history, context_win):
|
def trim_messages_history(history, context_win):
|
||||||
compress_history_tags(history)
|
compress_history_tags(history)
|
||||||
cost = sum(len(json.dumps(m, ensure_ascii=False)) for m in history)
|
cost = sum(len(json.dumps(m, ensure_ascii=False)) for m in history)
|
||||||
@@ -503,7 +509,7 @@ class BaseSession:
|
|||||||
mode = str(cfg.get('api_mode', 'chat_completions')).strip().lower().replace('-', '_')
|
mode = str(cfg.get('api_mode', 'chat_completions')).strip().lower().replace('-', '_')
|
||||||
self.api_mode = 'responses' if mode in ('responses', 'response') else 'chat_completions'
|
self.api_mode = 'responses' if mode in ('responses', 'response') else 'chat_completions'
|
||||||
self.temperature = cfg.get('temperature', 1)
|
self.temperature = cfg.get('temperature', 1)
|
||||||
self.max_tokens = cfg.get('max_tokens', 8192)
|
self.max_tokens = cfg.get('max_tokens')
|
||||||
def _apply_claude_thinking(self, payload):
|
def _apply_claude_thinking(self, payload):
|
||||||
if self.thinking_type:
|
if self.thinking_type:
|
||||||
thinking = {"type": self.thinking_type}
|
thinking = {"type": self.thinking_type}
|
||||||
@@ -544,6 +550,7 @@ def _drop_unsigned_thinking(messages):
|
|||||||
|
|
||||||
class ClaudeSession(BaseSession):
|
class ClaudeSession(BaseSession):
|
||||||
def raw_ask(self, messages):
|
def raw_ask(self, messages):
|
||||||
|
if self.max_tokens is None: self.max_tokens = 8192
|
||||||
headers = {"x-api-key": self.api_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01", "anthropic-beta": "prompt-caching-2024-07-31"}
|
headers = {"x-api-key": self.api_key, "Content-Type": "application/json", "anthropic-version": "2023-06-01", "anthropic-beta": "prompt-caching-2024-07-31"}
|
||||||
payload = {"model": self.model, "messages": messages, "max_tokens": self.max_tokens, "stream": True}
|
payload = {"model": self.model, "messages": messages, "max_tokens": self.max_tokens, "stream": True}
|
||||||
if self.temperature != 1: payload["temperature"] = self.temperature
|
if self.temperature != 1: payload["temperature"] = self.temperature
|
||||||
@@ -600,6 +607,7 @@ class NativeClaudeSession(BaseSession):
|
|||||||
self.tools = None
|
self.tools = None
|
||||||
def raw_ask(self, messages):
|
def raw_ask(self, messages):
|
||||||
messages = _drop_unsigned_thinking(_fix_messages(messages))
|
messages = _drop_unsigned_thinking(_fix_messages(messages))
|
||||||
|
if self.max_tokens is None: self.max_tokens = 8192
|
||||||
model = self.model
|
model = self.model
|
||||||
beta_parts = ["claude-code-20250219", "interleaved-thinking-2025-05-14", "redact-thinking-2026-02-12", "prompt-caching-scope-2026-01-05"]
|
beta_parts = ["claude-code-20250219", "interleaved-thinking-2025-05-14", "redact-thinking-2026-02-12", "prompt-caching-scope-2026-01-05"]
|
||||||
if "[1m]" in model.lower():
|
if "[1m]" in model.lower():
|
||||||
@@ -936,7 +944,7 @@ class MixinSession:
|
|||||||
|
|
||||||
THINKING_PROMPT_ZH = """
|
THINKING_PROMPT_ZH = """
|
||||||
### 行动规范(持续有效)
|
### 行动规范(持续有效)
|
||||||
每次回复请先在回复文字中包含一个<summary></summary> 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。
|
每次回复(含工具调用轮)都先在回复文字中包含一个<summary></summary> 中输出极简单行(<30字)物理快照:上次结果新信息+本次意图。此内容进入长期工作记忆。
|
||||||
\n**若用户需求未完成,必须进行工具调用!**
|
\n**若用户需求未完成,必须进行工具调用!**
|
||||||
""".strip()
|
""".strip()
|
||||||
THINKING_PROMPT_EN = """
|
THINKING_PROMPT_EN = """
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"""Keychain: save key to a file, then keys.set("name", file="path"); keys.name.use() to retrieve (use but no print)."""
|
"""Keychain: save key to a file, then keys.set("name", file="path"); keys.name.use() to retrieve (use but no print)."""
|
||||||
import json, os, hashlib, pathlib
|
import json, os, hashlib, pathlib, getpass
|
||||||
|
|
||||||
_PATH = pathlib.Path.home() / "ga_keychain.enc"
|
_PATH = pathlib.Path.home() / "ga_keychain.enc"
|
||||||
_MASK = hashlib.sha256(f"{os.getlogin()}@ga_keychain".encode()).digest()
|
try: _user = os.getlogin()
|
||||||
|
except OSError: _user = getpass.getuser()
|
||||||
|
_MASK = hashlib.sha256(f"{_user}@ga_keychain".encode()).digest()
|
||||||
|
|
||||||
def _xor(data: bytes) -> bytes:
|
def _xor(data: bytes) -> bytes:
|
||||||
return bytes(b ^ _MASK[i % len(_MASK)] for i, b in enumerate(data))
|
return bytes(b ^ _MASK[i % len(_MASK)] for i, b in enumerate(data))
|
||||||
|
|||||||
Reference in New Issue
Block a user