Compare commits

..

20 Commits

Author SHA1 Message Date
Liang Jiaqing
cd0ce4d9b0 fix: summary提取与fold_turns防误切,强化thinking模型summary输出 2026-04-26 00:01:42 +08:00
Liang Jiaqing
69f66997fd fix: fallback to getpass.getuser() when os.getlogin() raises OSError 2026-04-25 22:55:17 +08:00
LJQ
567c7f582b Merge pull request #140 from huangrichao2020/feat/macos-desktop-app
feat: add macOS Desktop App installation script
2026-04-25 22:17:44 +08:00
LJQ
adf393c0a5 Merge pull request #173 from Stark-X/fix/linux-desktop-pet-pyside6
fix: add Linux desktop pet implementation
2026-04-25 22:09:35 +08:00
LJQ
12f23138e0 Merge pull request #164 from kukudelaomao/fix/wecomapp-event-handlers
fix(wecomapp): 修复WebSocket连接和事件处理器问题
2026-04-25 22:04:31 +08:00
LJQ
c19ac2dae6 Merge pull request #160 from YooooEX/telegram-polish
fix(tgapp): polish Telegram MarkdownV2 rendering and startup helpers
2026-04-25 21:59:49 +08:00
LJQ
5805566d88 Fix Telegram startup without a configured proxy
Fix Telegram startup without a configured proxy
2026-04-25 21:51:54 +08:00
LJQ
da91a0643f fix(sandbox): patch Popen + block startfile to prevent window leaks
fix(sandbox): patch Popen + block startfile to prevent window leaks
2026-04-25 21:49:15 +08:00
Liang Jiaqing
5b0d96b1b5 tg: support document upload, disable draft streaming; wx: streaming flush during task; llmcore: safeprint, max_tokens fixes 2026-04-25 21:33:38 +08:00
Jiaqing Liang
10776fed51 Remove responses max output token limit 2026-04-25 14:03:21 +08:00
Auston Li
bd2dd42ba4 Fix Telegram startup when proxy is not configured
Only use a proxy for tgapp when mykeys['proxy'] is explicitly configured, avoiding the dead default local proxy and restoring Telegram polling startup.

Closes #175
2026-04-25 00:54:42 -04:00
Stark-X
0f77470fe0 Merge branch 'lsdefine:main' into fix/linux-desktop-pet-pyside6 2026-04-25 11:20:11 +08:00
MuziIsabel
3b93c6137d fix(sandbox): patch Popen + block startfile to prevent window leaks
subprocess.run was already patched with CREATE_NO_WINDOW, but Popen and
os.startfile were unprotected. Agent code could open visible GUI windows
via subprocess.Popen(['notepad.exe']) or os.startfile().
2026-04-25 10:59:15 +08:00
Stark-X
09cf6306af fix: add Linux desktop pet implementation 2026-04-25 10:58:38 +08:00
郭春飞
936501069c fix(wecomapp): 修复WebSocket连接和事件处理器问题
修复企业微信机器人无法正常连接的问题:

1. 修复WSClient初始化参数传递错误
   - 原代码错误地将参数传递给WSClient构造函数
   - 修正为正确传递host, port, path等参数

2. 修复连接方法调用错误
   - 将connect_async()改为connect()方法
   - AiBotSDK的WSClient使用同步connect方法

3. 修复事件处理器签名不匹配
   - on_connected/on_authenticated: 移除frame参数(这些处理器不需要参数)
   - on_disconnected: 添加reason参数
   - on_error: 添加error参数

修复后验证:
- WebSocket连接成功建立
- 认证过程正常完成
- 心跳机制正常工作
- 日志无错误信息

此修复解决了企业微信机器人启动后无法连接服务器的问题。
2026-04-25 01:18:28 +08:00
YooooEX
8e159a9773 feat(tgapp): add support for __ bold and ~~ strikethrough styles 2026-04-24 20:11:40 +08:00
YooooEX
0c5d3c8635 fix(tgapp): align startup with common helpers and render file marker basenames 2026-04-24 20:11:40 +08:00
Huang richao
446d7bf549 Merge remote-tracking branch 'origin/main' into feat/macos-desktop-app 2026-04-24 17:53:18 +08:00
Huang richao
b34965b936 fix: narrow macOS app installer per review 2026-04-24 17:52:47 +08:00
Huang richao
7255515607 feat: add macOS Desktop App installation script
- Add scripts/install-macos-app.sh for one-click macOS app installation
- Uses bundled assets/images/logo.jpg as app icon
- AppleScript prompts for project folder on first run
- Updates README with macOS Desktop App installation option
- Supports both interactive and --auto non-interactive modes

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-23 23:19:24 +08:00
11 changed files with 716 additions and 235 deletions

View File

@@ -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
View 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 ""

View File

@@ -1,6 +1,6 @@
# Role: 物理级全能执行者 # Role: 物理级全能执行者
你拥有文件读写、脚本执行、用户浏览器JS注入、系统级干预的物理操作权限。禁止推诿"无法操作"——不空想,用工具探测。 你拥有文件读写、脚本执行、用户浏览器JS注入、系统级干预的物理操作权限。禁止推诿"无法操作"——不空想,用工具探测。
## 行动原则 ## 行动原则
调用工具前在<thinking>内推演:当前阶段、上步结果是否符合预期、下步策略;<summary>输出极简总结。 调用工具前推演:当前阶段、上步结果是否符合预期、下步策略;回复文本中用<summary>输出极简总结。
- 探测优先:失败时先充分获取信息(日志/状态/上下文),关键信息存入工作记忆,再决定重试或换方案。不可逆操作先询问用户。 - 探测优先:失败时先充分获取信息(日志/状态/上下文),关键信息存入工作记忆,再决定重试或换方案。不可逆操作先询问用户。
- 失败升级1次→读错误理解原因2次→探测环境状态3次→深度分析后换方案或问用户。禁止无新信息的重复操作。 - 失败升级1次→读错误理解原因2次→探测环境状态3次→深度分析后换方案或问用户。禁止无新信息的重复操作。

View File

@@ -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.

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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:

View File

@@ -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)

View File

@@ -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 = """

View File

@@ -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))