From 362b7d601c117c4d2415adf5ec8bfbccadd0794a Mon Sep 17 00:00:00 2001 From: dd3xp Date: Mon, 25 May 2026 22:03:19 +0800 Subject: [PATCH] chore: sync framework to upstream lsdefine/GenericAgent @ 1eb8c4c Content-overlay update of all GA framework files from upstream base b4393af (2026-05-18) to 1eb8c4c (2026-05-25). Applied as an overlay because GADesktop and upstream share no common git history. - 26 framework files brought to upstream: tui_v3 (new), fsapp rework, llmcore hardening, plugins/hooks lifecycle hooks, goal_mode prompt rewrite, supergrok_proxy, qq/tg/wechat/st apps, cost_tracker py3.9 compat, docs; removed obsolete install scripts. - Desktop frontend (frontends/desktop/*) and desktop_bridge.py left untouched. Upstream's only overlapping change (bridge mykey.txt-> mykey.py) is already present in our version, so it is a no-op. --- .gitignore | 3 + README.md | 5 +- agent_loop.py | 14 +- agentmain.py | 5 +- assets/configure_mykey.py | 145 +- assets/images/wechat_group19.jpg | Bin 0 -> 190040 bytes assets/install-macos-app.sh | 179 - assets/install_python_windows.bat | 111 - assets/supergrok_proxy.py | 390 +++ docs/installation_zh.md | 2 +- docs/macos_desktop_installation_zh.md | 85 + frontends/cost_tracker.py | 1 + frontends/fsapp.py | 602 ++-- frontends/qqapp.py | 6 +- frontends/stapp.py | 37 +- frontends/tgapp.py | 160 +- frontends/tui_v3.py | 4609 +++++++++++++++++++++++++ frontends/tuiapp_v2.py | 185 +- frontends/wechatapp.py | 35 +- ga_cli/cli.py | 7 + llmcore.py | 31 +- memory/keychain.py | 21 +- plugins/hooks.py | 67 + plugins/langfuse_tracing.py | 148 +- pyproject.toml | 5 + reflect/goal_mode.py | 11 +- 26 files changed, 6125 insertions(+), 739 deletions(-) create mode 100644 assets/images/wechat_group19.jpg delete mode 100755 assets/install-macos-app.sh delete mode 100644 assets/install_python_windows.bat create mode 100644 assets/supergrok_proxy.py create mode 100644 docs/macos_desktop_installation_zh.md create mode 100644 frontends/tui_v3.py create mode 100644 plugins/hooks.py diff --git a/.gitignore b/.gitignore index 8d7f9e8c..9282e4ff 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,9 @@ memory/L4_raw_sessions/* # Subagent SOP !memory/subagent_sop.md +# Incubator SOP +!memory/incubator_sop.md + # Supervisor SOP !memory/supervisor_sop.md diff --git a/README.md b/README.md index cc90fe5e..8ab44455 100644 --- a/README.md +++ b/README.md @@ -373,7 +373,7 @@ You're also welcome to join the **GenericAgent Community Group** for discussion,
- +
WeChat Group 18
WeChat Group 18 QR
WeChat Group 19
WeChat Group 19 QR
@@ -742,7 +742,7 @@ GenericAgent 通过 **分层记忆 × 最小工具集 × 自主执行循环**
- +
微信群 18
微信群 18 二维码
微信群 19
微信群 19 二维码
@@ -757,6 +757,7 @@ GenericAgent 通过 **分层记忆 × 最小工具集 × 自主执行循环** - [chilishark27/ga-manager](https://github.com/chilishark27/ga-manager) - [wangjc683/galley](https://github.com/wangjc683/galley) +- https://github.com/FroStorM/A3Agent/tree/workbench --- diff --git a/agent_loop.py b/agent_loop.py index 3011735f..7afb5850 100644 --- a/agent_loop.py +++ b/agent_loop.py @@ -1,6 +1,8 @@ import json, re, os from dataclasses import dataclass from typing import Any, Optional +try: from plugins.hooks import trigger as _hook +except ImportError: _hook = lambda *a, **k: None @dataclass class StepOutcome: data: Any @@ -12,16 +14,14 @@ def try_call_generator(func, *args, **kwargs): return ret class BaseHandler: - def tool_before_callback(self, tool_name, args, response): pass - def tool_after_callback(self, tool_name, args, response, ret): pass def turn_end_callback(self, response, tool_calls, tool_results, turn, next_prompt, exit_reason): return next_prompt def dispatch(self, tool_name, args, response, index=0, tool_num=1): method_name = f"do_{tool_name}" if hasattr(self, method_name): args['_index'] = index; args['_tool_num'] = tool_num - prer = yield from try_call_generator(self.tool_before_callback, tool_name, args, response) + _hook('tool_before', locals()) ret = yield from try_call_generator(getattr(self, method_name), args, response) - _ = yield from try_call_generator(self.tool_after_callback, tool_name, args, response, ret) + _hook('tool_after', locals()) return ret elif tool_name == 'bad_json': return StepOutcome(None, next_prompt=args.get('msg', 'bad_json'), should_exit=False) else: @@ -46,6 +46,7 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, {"role": "user", "content": initial_user_content if initial_user_content is not None else user_input} ] turn = 0; handler.max_turns = max_turns + _hook('agent_before', locals()) while turn < handler.max_turns: turn += 1; turnstr = f'LLM Running (Turn {turn}) ...' if handler.parent.task_dir: turnstr = f'Turn {turn} ...' @@ -53,6 +54,8 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, if yield_info: yield {'turn': turn} yield f"\n\n{turnstr}\n\n" if turn%10 == 0: client.last_tools = '' # 每10轮重置一次工具描述 + _hook('turn_before', locals()) + _hook('llm_before', locals()) response_gen = client.chat(messages=messages, tools=tools_schema) if verbose: response = yield from response_gen @@ -61,6 +64,7 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, response = exhaust(response_gen) cleaned = _clean_content(response.content) if cleaned: yield cleaned + '\n' + _hook('llm_after', locals()) if not response.tool_calls: tool_calls = [{'tool_name': 'no_tool', 'args': {}}] else: tool_calls = [{'tool_name': tc.function.name, 'args': json.loads(tc.function.arguments), 'id': tc.id} @@ -96,8 +100,10 @@ def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, if len(handler._done_hooks) == 0 or exit_reason.get('result', '') == 'EXITED': break next_prompts.add(handler._done_hooks.pop(0)) next_prompt = handler.turn_end_callback(response, tool_calls, tool_results, turn, '\n'.join(next_prompts), exit_reason) + _hook('turn_after', locals()) messages = [{"role": "user", "content": next_prompt, "tool_results": tool_results}] # just new message, history is kept in *Session if exit_reason: handler.turn_end_callback(response, tool_calls, tool_results, turn, '', exit_reason) + _hook('agent_after', locals()) return exit_reason or {'result': 'MAX_TURNS_EXCEEDED'} def _clean_content(text): diff --git a/agentmain.py b/agentmain.py index 1403f4b8..cf1d47c0 100644 --- a/agentmain.py +++ b/agentmain.py @@ -6,8 +6,11 @@ if sys.stderr is None: sys.stderr = open(os.devnull, "w") elif hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(errors='replace') sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from llmcore import reload_mykeys, LLMSession, ToolClient, ClaudeSession, MixinSession, NativeToolClient, NativeClaudeSession, NativeOAISession, resolve_client +from llmcore import reload_mykeys, ToolClient, MixinSession, NativeToolClient, NativeClaudeSession, NativeOAISession, resolve_client from agent_loop import agent_runner_loop +try: + from plugins.hooks import discover_and_load; discover_and_load() +except Exception: pass from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file script_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/assets/configure_mykey.py b/assets/configure_mykey.py index 6055a627..175d7199 100644 --- a/assets/configure_mykey.py +++ b/assets/configure_mykey.py @@ -7,6 +7,7 @@ GenericAgent — 交互式初始化向导 (configure.py) python configure.py """ +import ast import os import sys import re @@ -857,6 +858,8 @@ def configure_platforms(): if pid == 'feishu' and ask_yesno("使用一键扫码创建应用?(推荐)", default=True): env_vals = _feishu_scan(platform) + if pid == 'wechat' and ask_yesno("扫码登录微信 iLink?(推荐)", default=True): + env_vals = _wechat_scan() for var in platform['env_vars']: if var['key'] not in env_vals: @@ -946,6 +949,39 @@ def _feishu_scan(platform): return {} +def _wechat_scan(): + """微信 iLink 扫码登录,保存 token 到 ~/.wxbot/token.json,返回 env_vals""" + print(f"\n {C['cyan']}📱 正在启动微信 iLink 扫码登录...{C['reset']}") + print(f" {C['dim']} 请用微信扫描终端二维码,完成授权后自动获取凭据。{C['reset']}\n") + + # 确保项目根在路径中,以便导入 frontends/wechatapp + if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + try: + from frontends.wechatapp import WxBotClient + except ImportError as e: + print(f"\n {C['yellow']}⚠ 无法导入 WxBotClient: {e}{C['reset']}") + return {} + + try: + bot = WxBotClient() + if bot.token: + print(f" {C['green']}✅ 已有有效 token (bot_id={bot.bot_id}){C['reset']}") + if ask_yesno("重新扫码登录?", default=False): + bot.token = '' + else: + return {} + bot.login_qr() + print(f"\n {C['green']}✅ 微信 iLink 扫码登录成功!{C['reset']}") + print(f" Bot ID: {C['bold']}{bot.bot_id}{C['reset']}") + print(f" Token 已保存到: {C['dim']}{bot._tf}{C['reset']}") + except Exception as e: + print(f"\n {C['red']}✗ 扫码登录失败: {e}{C['reset']}") + return {} + + return {} + + # ═══════════════════════════════════════════════════════════════════════════ # 生成 mykey.py @@ -1033,7 +1069,7 @@ def generate_mykey(llm_cfgs, platform_configs): def _write_config_fields(lines, cfg): """写入配置字典的键值对(缩进的 'key': value, 格式)""" - for key in ['name', 'apikey', 'apibase', 'model', 'api_mode', + for key in ['name', 'type', 'apikey', 'apibase', 'model', 'api_mode', 'fake_cc_system_prompt', 'thinking_type', 'thinking_budget_tokens', 'reasoning_effort', 'max_tokens', 'max_retries', 'connect_timeout', 'read_timeout', 'temperature', 'context_win', @@ -1066,7 +1102,7 @@ def _write_platform_value(lines, key, val): def _parse_existing_mykey(): """解析已有 mykey.py,返回 (model_names, platform_infos) - llm_cfgs: [{'name': str, 'type': str, ...}] — 模型配置字典列表 + model_names: [str] — 模型名列表 platform_infos: [{'id': str, 'vars': [{'key': str, 'val': ...}]}] — 平台信息 解析失败时返回 ([], []) """ @@ -1082,21 +1118,81 @@ def _parse_existing_mykey(): if m: model_names = re.findall(r"'([^']+)'", m.group(1)) - # 解析平台变量 → 平台 ID - platform_id_map = { - 'tg_bot_token': 'telegram', 'qq_app_id': 'qq', - 'fs_app_id': 'feishu', 'wecom_bot_id': 'wecom', - 'dingtalk_client_id': 'dingtalk', 'dc_bot_token': 'discord', - } + # 先收集所有已知平台 env var key → 判断值类型 + all_env_var_keys = {} + platform_env_keys = {} # pid -> [var_key] + for p in PLATFORMS: + pid = p['id'] + platform_env_keys.setdefault(pid, []) + for var in p.get('env_vars', []): + vkey = var['key'] + all_env_var_keys[vkey] = var + platform_env_keys[pid].append(vkey) + + # 逐平台解析所有已知变量 platform_infos = [] - for var_key, pid in platform_id_map.items(): - m_var = re.search(rf"^{var_key}\s*=\s*'([^']*)'", content, re.MULTILINE) - if m_var: - platform_infos.append({'id': pid, 'vars': [{'key': var_key, 'val': m_var.group(1)}]}) + for pid, env_keys in platform_env_keys.items(): + vars_found = [] + for vkey in env_keys: + var_def = all_env_var_keys[vkey] + val = None + if var_def.get('is_list'): + # 匹配 `xxx = [...]` + m_var = re.search(rf"^{vkey}\s*=\s*(\[[^\]]*\])", content, re.MULTILINE) + if m_var: + try: + val = ast.literal_eval(m_var.group(1)) + except (ValueError, SyntaxError): + pass + else: + # 匹配 `xxx = '...'` + m_var = re.search(rf"^{vkey}\s*=\s*'([^']*)'", content, re.MULTILINE) + if m_var: + val = m_var.group(1) + if val is not None: + vars_found.append({'key': vkey, 'val': val}) + if vars_found: + platform_infos.append({'id': pid, 'vars': vars_found}) return model_names, platform_infos +def _parse_existing_llm_cfgs(): + """解析已有 mykey.py,返回完整 LLM 配置字典列表 [{name, apikey, ...}] + 解析失败时返回 [] + """ + if not os.path.exists(MYKPY_PATH): + return [] + + with open(MYKPY_PATH, 'r', encoding='utf-8') as f: + content = f.read() + + cfgs = [] + # 匹配所有 `xxx = { ... }` 顶层字典赋值 + # 用简单状态机: 找 `\w+ = {` 然后匹配花括号 + pattern = re.compile(r'^(\w+)\s*=\s*\{', re.MULTILINE) + for m in pattern.finditer(content): + brace_start = m.end() - 1 # '{' 的位置 + depth = 1 + i = brace_start + 1 + while i < len(content) and depth > 0: + if content[i] == '{': + depth += 1 + elif content[i] == '}': + depth -= 1 + i += 1 + if depth == 0: + dict_text = content[m.end():i - 1] + try: + d = ast.literal_eval('{' + dict_text + '}') + if isinstance(d, dict) and 'name' in d: + cfgs.append(d) + except (ValueError, SyntaxError): + continue + + return cfgs + + def _backup_with_name(model_names, platform_ids): """按 mykey+模型名+机器人名 格式备份旧 mykey.py""" parts = ['mykey'] @@ -1107,6 +1203,8 @@ def _backup_with_name(model_names, platform_ids): if pid_clean not in parts: parts.append(pid_clean) safe_name = '_'.join(parts) + if safe_name == 'mykey': + safe_name = 'mykey_backup' # 避免和源文件同名 if len(safe_name) > 100: safe_name = safe_name[:100] backup_path = os.path.join(PROJECT_ROOT, f'{safe_name}.py') @@ -1154,7 +1252,7 @@ def main(): if mode == 'new': backup_path = _backup_with_name(model_names, [p['id'] for p in platform_infos]) - print(f" {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup}{C['reset']}") + print(f" {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup_path}{C['reset']}") is_new = True else: is_modify = True @@ -1177,8 +1275,12 @@ def main(): config_dict = {v['key']: v['val'] for v in pi['vars']} platform_configs.append({'platform': p, 'config': config_dict}) elif scope == 'platform' and model_names: - print(f"\n {C['yellow']}⚠ 只修改平台时若未提供 LLM 配置将无法使用。{C['reset']}") - cprint(f" 建议两项都重新配置。", 'dim') + old_cfgs = _parse_existing_llm_cfgs() + if old_cfgs: + llm_cfgs = old_cfgs + print(f"\n {C['green']}✓ 已保留现有 LLM 配置: {', '.join(c['name'] for c in old_cfgs)}{C['reset']}") + else: + print(f"\n {C['yellow']}⚠ 保留 LLM 配置失败,将生成空配置。建议两项都重新配置。{C['reset']}") if not is_modify: if is_new: @@ -1210,6 +1312,12 @@ def main(): platform_configs, platform_deps = configure_platforms() if ask_yesno("是否继续配置 LLM 模型?", default=True): llm_cfgs = _do_llm() + elif os.path.exists(MYKPY_PATH): + # 新建+仅平台:从备份保留旧 LLM 配置 + old_cfgs = _parse_existing_llm_cfgs() + if old_cfgs: + llm_cfgs = old_cfgs + print(f"\n {C['green']}✓ 已保留备份中的 LLM 配置: {', '.join(c['name'] for c in old_cfgs)}{C['reset']}") # ── 生成 mykey.py ── if not llm_cfgs and not platform_configs: @@ -1218,10 +1326,9 @@ def main(): content = generate_mykey(llm_cfgs, platform_configs) - # 备份旧文件 - if os.path.exists(MYKPY_PATH): - backup = os.path.join(PROJECT_ROOT, f'mykey.py.bak.{datetime.now().strftime("%Y%m%d_%H%M%S")}') - shutil.copy2(MYKPY_PATH, backup) + # 备份旧文件(修改模式不备份,直接在原文件修改) + if os.path.exists(MYKPY_PATH) and not is_modify and not is_new: + backup = _backup_with_name(model_names, [p['id'] for p in platform_infos]) print(f"\n {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup}{C['reset']}") # 写入 diff --git a/assets/images/wechat_group19.jpg b/assets/images/wechat_group19.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f02e694d447a9b2e10015b59215280269713e992 GIT binary patch literal 190040 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 $GVkI*{bI6I;`Z1 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 Wms3bm=x/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" </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 "" diff --git a/assets/install_python_windows.bat b/assets/install_python_windows.bat deleted file mode 100644 index b2873705..00000000 --- a/assets/install_python_windows.bat +++ /dev/null @@ -1,111 +0,0 @@ -@echo off -setlocal enabledelayedexpansion -title Python One-Click Installer -color 0A - -echo. -echo ======================================== -echo Python One-Click Installer (Windows) -echo ======================================== -echo. - -net session >nul 2>&1 -if %errorlevel% neq 0 ( - echo [!] Administrator privileges required. Restarting with elevation... - powershell -Command "Start-Process '%~f0' -Verb RunAs" - exit /b -) - -echo [OK] Administrator privileges confirmed -echo. - -python --version >nul 2>&1 -if %errorlevel% equ 0 ( - echo [OK] Python already installed: - python --version - echo. - choice /C YN /M "Install latest version anyway? (Y=Yes / N=Exit)" - if errorlevel 2 goto :end -) - -set PYTHON_VERSION=3.12.9 -set MIRROR_URL=https://npmmirror.com/mirrors/python/3.12.9/python-3.12.9-amd64.exe -set OFFICIAL_URL=https://www.python.org/ftp/python/3.12.9/python-3.12.9-amd64.exe -set INSTALLER=%TEMP%\python_installer.exe - -echo [*] Preparing to download Python %PYTHON_VERSION% -echo [*] Trying mirror source first... -echo. - -powershell -NoProfile -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '%MIRROR_URL%' -OutFile '%INSTALLER%' -UseBasicParsing" - -if not exist "%INSTALLER%" goto :official -for %%A in ("%INSTALLER%") do if %%~zA lss 1000000 goto :official -echo [OK] Mirror download complete -goto :install - -:official -echo [!] Mirror failed, switching to official source... -powershell -NoProfile -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '%OFFICIAL_URL%' -OutFile '%INSTALLER%' -UseBasicParsing" - -if not exist "%INSTALLER%" ( - echo [x] Download failed. Please check your network connection and retry. - pause - goto :end -) -for %%A in ("%INSTALLER%") do if %%~zA lss 1000000 ( - echo [x] Downloaded file is incomplete. Please check your network and retry. - pause - goto :end -) -echo [OK] Official source download complete - -:install -echo. -echo [*] Installing Python %PYTHON_VERSION% (this may take 2-5 minutes^)... -echo. - -start /wait "" "%INSTALLER%" /passive InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 - -set INSTALL_CODE=%errorlevel% -del /f /q "%INSTALLER%" >nul 2>&1 - -if %INSTALL_CODE% neq 0 ( - echo [x] Installation failed with error code: %INSTALL_CODE% - pause - goto :end -) - -echo [+] Installation complete! -echo. - -timeout /t 3 /nobreak >nul - -set "PATH=C:\Program Files\Python312;C:\Program Files\Python312\Scripts;%PATH%" - -python --version >nul 2>&1 -if %errorlevel% equ 0 ( - echo [OK] Python installed successfully: - python --version - echo. - echo [OK] pip version: - pip --version - echo. - echo [*] Configuring pip mirror (Tsinghua^)... - pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple - pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn - echo. - echo [*] Installing requests... - pip install requests - echo. - echo ======================================== - echo All done! Open a new terminal to use - echo python and pip commands. - echo ======================================== -) else ( - echo [!] PATH not yet refreshed. Please close this window and open a new terminal. -) - -:end -echo. -pause diff --git a/assets/supergrok_proxy.py b/assets/supergrok_proxy.py new file mode 100644 index 00000000..41e411be --- /dev/null +++ b/assets/supergrok_proxy.py @@ -0,0 +1,390 @@ +""" +SuperGrok Local Proxy - CLI +本地 OpenAI 兼容代理,通过 xAI OAuth (PKCE) 登录 SuperGrok,自动管理 token 并转发请求。 + +用法: + python assets/supergrok_proxy.py login + python assets/supergrok_proxy.py serve --port 15433 + python assets/supergrok_proxy.py models + python assets/supergrok_proxy.py test --model grok-4.3 + +GenericAgent/mykey 配置示例: + native_oai_config_supergrok_proxy = { + 'name': 'supergrok', + 'apikey': 'dummy', + 'apibase': 'http://127.0.0.1:15433/v1', + 'model': 'grok-4.3', + 'max_retries': 3, + 'read_timeout': 600, + 'stream': False, + } +""" +from __future__ import annotations +import argparse +import base64 +import hashlib +import json +import os +import secrets +import threading +import time +import uuid +import webbrowser +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer, HTTPServer +from urllib.parse import urlencode, urlparse, parse_qs + +import requests + +XAI_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828' +XAI_AUTHORIZE_URL = 'https://auth.x.ai/oauth2/authorize' +XAI_TOKEN_URL = 'https://auth.x.ai/oauth2/token' +XAI_SCOPE = 'openid profile email offline_access grok-cli:access api:access' +XAI_CALLBACK_PORT = 56121 +XAI_CALLBACK_URI = f'http://127.0.0.1:{XAI_CALLBACK_PORT}/callback' +XAI_API_BASE = 'https://api.x.ai/v1' +DEFAULT_STORE = os.path.join(os.path.expanduser('~'), '.genericagent', 'xai_oauth.json') +DEFAULT_PROXY_PORT = 15433 +REFRESH_MARGIN = 120 + + +def log(msg): + print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True) + + +def b64url(raw: bytes) -> str: + return base64.urlsafe_b64encode(raw).decode().rstrip('=') + + +def make_proxies(proxy: str | None): + if not proxy: + return None + return {'http': proxy, 'https': proxy} + + +class TokenManager: + def __init__(self, store_path=DEFAULT_STORE, proxy='http://127.0.0.1:2082'): + self.store_path = os.path.expanduser(store_path) + self.proxy = proxy or None + self.proxies = make_proxies(self.proxy) + self.lock = threading.Lock() + self.access_token = None + self.refresh_token = None + self.expires_at = 0.0 + self.load() + + def load(self): + try: + with open(self.store_path, encoding='utf-8') as f: + data = json.load(f) + self.access_token = data.get('access_token') + self.refresh_token = data.get('refresh_token') + self.expires_at = float(data.get('expires_at') or 0) + if self.access_token: + remain = int(self.expires_at - time.time()) + log(f"Token loaded: ***{self.access_token[-6:]} expires_in={remain}s") + except FileNotFoundError: + log(f"No saved token: {self.store_path}") + except Exception as e: + log(f"Failed to load token: {e}") + + def save(self, data): + os.makedirs(os.path.dirname(self.store_path), exist_ok=True) + with open(self.store_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + try: + os.chmod(self.store_path, 0o600) + except OSError: + pass + log(f"Token saved: {self.store_path}") + + def login(self, open_browser=True, timeout=300): + verifier = b64url(secrets.token_bytes(32)) + challenge = b64url(hashlib.sha256(verifier.encode()).digest()) + state = secrets.token_urlsafe(24) + nonce = secrets.token_urlsafe(24) + params = { + 'client_id': XAI_CLIENT_ID, + 'redirect_uri': XAI_CALLBACK_URI, + 'response_type': 'code', + 'scope': XAI_SCOPE, + 'state': state, + 'nonce': nonce, + 'code_challenge': challenge, + 'code_challenge_method': 'S256', + 'plan': 'generic', + 'referrer': 'generic-agent', + } + auth_url = f'{XAI_AUTHORIZE_URL}?{urlencode(params)}' + result = {} + + class CallbackHandler(BaseHTTPRequestHandler): + def log_message(self, fmt, *args): + pass + + def do_GET(self): + qs = parse_qs(urlparse(self.path).query) + result.update({k: v[0] for k, v in qs.items() if v}) + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.end_headers() + self.wfile.write('SuperGrok login complete. You can close this tab.'.encode('utf-8')) + + httpd = HTTPServer(('127.0.0.1', XAI_CALLBACK_PORT), CallbackHandler) + httpd.timeout = 1 + log(f"Callback listening: {XAI_CALLBACK_URI}") + log("Open this URL to login xAI/SuperGrok:") + print(auth_url, flush=True) + if open_browser: + try: + webbrowser.open(auth_url) + except Exception as e: + log(f"Browser open failed: {e}") + + deadline = time.time() + timeout + while not result.get('code') and time.time() < deadline: + httpd.handle_request() + httpd.server_close() + + if not result.get('code'): + raise RuntimeError(f'OAuth timeout after {timeout}s') + if result.get('state') and result['state'] != state: + raise RuntimeError('OAuth state mismatch') + + log("Exchanging authorization code for token...") + resp = requests.post(XAI_TOKEN_URL, data={ + 'grant_type': 'authorization_code', + 'client_id': XAI_CLIENT_ID, + 'code': result['code'], + 'redirect_uri': XAI_CALLBACK_URI, + 'code_verifier': verifier, + 'code_challenge': challenge, + 'code_challenge_method': 'S256', + }, proxies=self.proxies, timeout=30) + resp.raise_for_status() + data = resp.json() + data['expires_at'] = time.time() + int(data.get('expires_in', 3600)) + self.access_token = data.get('access_token') + self.refresh_token = data.get('refresh_token') + self.expires_at = data['expires_at'] + self.save(data) + log("Login OK") + return self.access_token + + def get_token(self): + with self.lock: + if self.access_token and time.time() < self.expires_at - REFRESH_MARGIN: + return self.access_token + return self.refresh() + + def refresh(self): + if not self.refresh_token: + raise RuntimeError('No refresh_token. Run login first.') + log("Refreshing token...") + resp = requests.post(XAI_TOKEN_URL, data={ + 'grant_type': 'refresh_token', + 'client_id': XAI_CLIENT_ID, + 'refresh_token': self.refresh_token, + }, proxies=self.proxies, timeout=30) + resp.raise_for_status() + data = resp.json() + data['expires_at'] = time.time() + int(data.get('expires_in', 3600)) + if 'refresh_token' not in data: + data['refresh_token'] = self.refresh_token + self.access_token = data.get('access_token') + self.refresh_token = data.get('refresh_token') + self.expires_at = data['expires_at'] + self.save(data) + log(f"Refresh OK expires_in={int(self.expires_at - time.time())}s") + return self.access_token + + +def make_proxy_handler(token_mgr: TokenManager): + class ProxyHandler(BaseHTTPRequestHandler): + protocol_version = 'HTTP/1.1' + + def log_message(self, fmt, *args): + pass + + def _send_json_error(self, code, msg): + body = json.dumps({'error': {'message': str(msg), 'type': 'supergrok_proxy_error'}}).encode('utf-8') + self.send_response(code) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _target_url(self): + path = self.path + if path.startswith('/v1/'): + path = path[3:] + elif path == '/v1': + path = '' + return XAI_API_BASE + path + + def do_GET(self): + try: + token = token_mgr.get_token() + target = self._target_url() + log(f"GET {self.path} -> {target}") + resp = requests.get(target, headers={ + 'Authorization': f'Bearer {token}', + 'Accept': 'application/json', + 'User-Agent': 'GenericAgent-SuperGrok-Proxy/1.0', + }, proxies=token_mgr.proxies, timeout=60) + content = resp.content + self.send_response(resp.status_code) + self.send_header('Content-Type', resp.headers.get('content-type', 'application/json')) + self.send_header('Content-Length', str(len(content))) + self.end_headers() + self.wfile.write(content) + log(f"RESP {resp.status_code}") + except Exception as e: + log(f"GET error: {e}") + self._send_json_error(502, e) + + def do_POST(self): + try: + length = int(self.headers.get('Content-Length', 0)) + raw = self.rfile.read(length) if length else b'{}' + try: + body = json.loads(raw.decode('utf-8')) if raw else {} + except Exception: + body = None + stream = bool(body.get('stream')) if isinstance(body, dict) else False + model = body.get('model', '?') if isinstance(body, dict) else '?' + token = token_mgr.get_token() + target = self._target_url() + log(f"POST {self.path} model={model} stream={stream}") + resp = requests.post(target, headers={ + 'Authorization': f'Bearer {token}', + 'Content-Type': self.headers.get('Content-Type', 'application/json'), + 'Accept': 'text/event-stream' if stream else 'application/json', + 'User-Agent': 'GenericAgent-SuperGrok-Proxy/1.0', + 'x-request-id': str(uuid.uuid4()), + }, data=raw, proxies=token_mgr.proxies, timeout=180, stream=stream) + + ctype = resp.headers.get('content-type', '') + if stream or 'text/event-stream' in ctype: + self.send_response(resp.status_code) + self.send_header('Content-Type', 'text/event-stream') + self.send_header('Cache-Control', 'no-cache') + self.end_headers() + for chunk in resp.iter_content(chunk_size=None): + if chunk: + self.wfile.write(chunk) + self.wfile.flush() + else: + content = resp.content + self.send_response(resp.status_code) + self.send_header('Content-Type', ctype or 'application/json') + self.send_header('Content-Length', str(len(content))) + self.end_headers() + self.wfile.write(content) + + if resp.status_code >= 400: + log(f"RESP {resp.status_code} {'' if stream else resp.text[:500]}") + else: + log(f"RESP {resp.status_code}") + except Exception as e: + log(f"POST error: {e}") + self._send_json_error(502, e) + + return ProxyHandler + + +def cmd_login(args): + TokenManager(args.store, args.upstream_proxy).login(open_browser=not args.no_browser, timeout=args.timeout) + + +def cmd_refresh(args): + TokenManager(args.store, args.upstream_proxy).refresh() + + +def cmd_serve(args): + mgr = TokenManager(args.store, args.upstream_proxy) + if not mgr.access_token: + log("No token found. Starting login first...") + mgr.login(open_browser=not args.no_browser, timeout=args.timeout) + else: + mgr.get_token() + server = ThreadingHTTPServer((args.host, args.port), make_proxy_handler(mgr)) + log(f"Serving OpenAI-compatible proxy at http://{args.host}:{args.port}/v1") + log("Press Ctrl+C to stop.") + try: + server.serve_forever() + except KeyboardInterrupt: + log("Stopping...") + finally: + server.server_close() + + +def cmd_models(args): + mgr = TokenManager(args.store, args.upstream_proxy) + token = mgr.get_token() + resp = requests.get(f'{XAI_API_BASE}/models', headers={ + 'Authorization': f'Bearer {token}', + 'Accept': 'application/json', + 'User-Agent': 'GenericAgent-SuperGrok-Proxy/1.0', + }, proxies=mgr.proxies, timeout=60) + print(resp.status_code) + print(resp.text) + resp.raise_for_status() + + +def cmd_test(args): + mgr = TokenManager(args.store, args.upstream_proxy) + token = mgr.get_token() + payload = {'model': args.model, 'messages': [{'role': 'user', 'content': args.prompt}], 'stream': False} + resp = requests.post(f'{XAI_API_BASE}/chat/completions', headers={ + 'Authorization': f'Bearer {token}', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'GenericAgent-SuperGrok-Proxy/1.0', + }, json=payload, proxies=mgr.proxies, timeout=120) + print(resp.status_code) + print(resp.text) + resp.raise_for_status() + + +def build_parser(): + p = argparse.ArgumentParser(description='SuperGrok xAI OAuth local OpenAI-compatible proxy') + p.add_argument('--store', default=DEFAULT_STORE, help=f'token store path, default: {DEFAULT_STORE}') + p.add_argument('--upstream-proxy', default='http://127.0.0.1:2082', help='proxy for xAI auth/api; empty disables') + # Defaults for zero-argument mode: run once, login if needed, then serve. + p.set_defaults(func=cmd_serve, host='127.0.0.1', port=DEFAULT_PROXY_PORT, no_browser=False, timeout=300) + sub = p.add_subparsers(dest='cmd') + + sp = sub.add_parser('login', help='open browser and login xAI OAuth') + sp.add_argument('--no-browser', action='store_true') + sp.add_argument('--timeout', type=int, default=300) + sp.set_defaults(func=cmd_login) + + sp = sub.add_parser('refresh', help='refresh saved token') + sp.set_defaults(func=cmd_refresh) + + sp = sub.add_parser('serve', help='serve local OpenAI-compatible proxy') + sp.add_argument('--host', default='127.0.0.1') + sp.add_argument('--port', type=int, default=DEFAULT_PROXY_PORT) + sp.add_argument('--no-browser', action='store_true') + sp.add_argument('--timeout', type=int, default=300) + sp.set_defaults(func=cmd_serve) + + sp = sub.add_parser('models', help='call /v1/models directly') + sp.set_defaults(func=cmd_models) + + sp = sub.add_parser('test', help='send one chat completion request directly') + sp.add_argument('--model', default='grok-4.3') + sp.add_argument('--prompt', default='Say OK in one word.') + sp.set_defaults(func=cmd_test) + return p + + +def main(argv=None): + args = build_parser().parse_args(argv) + if args.upstream_proxy == '': + args.upstream_proxy = None + args.func(args) + + +if __name__ == '__main__': + main() diff --git a/docs/installation_zh.md b/docs/installation_zh.md index 1994798c..c124cc7b 100644 --- a/docs/installation_zh.md +++ b/docs/installation_zh.md @@ -204,7 +204,7 @@ uv pip install -e ".[ui]" > **重要:用 `curl` 获取本文,不要依赖会摘要网页的读取器。** 摘要常会丢掉环境变量、命令引号和安全规则。 > > ```bash -> curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/installation.zh.md +> curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/docs/installation.md > ``` 你正在替人类用户安装 **GenericAgent**。请直接执行、逐步验证;除非用户明确授权,不要做破坏性清理。 diff --git a/docs/macos_desktop_installation_zh.md b/docs/macos_desktop_installation_zh.md new file mode 100644 index 00000000..71b13fc8 --- /dev/null +++ b/docs/macos_desktop_installation_zh.md @@ -0,0 +1,85 @@ +# GenericAgent 桌面版安装指南 + +## 📦 安装步骤 + +### 第一步:打开安装包 + +双击下载的 `GenericAgent_x.x.x_aarch64.dmg` 文件,会弹出一个安装窗口。 + +将左边的 **GenericAgent** 图标拖到右边的 **Applications** 文件夹图标上,等待拷贝完成。 + +拷贝完成后,可以右键点击桌面上的 DMG 图标,选择「推出」来关闭安装包。 + +--- + +### 第二步:首次打开前的准备(重要) + +由于本应用暂未通过 Apple 官方签名认证,macOS 会阻止首次打开。这是正常的安全提示,不代表应用有问题。 + +请按以下步骤解除限制: + +#### 1. 打开「终端」应用 + +不知道终端是什么?别担心,它就是一个可以输入命令的工具。打开方式: + +- 按下键盘上的 `Command(⌘) + 空格键`,会弹出搜索框(Spotlight) +- 输入 `终端` 或 `Terminal`,按回车键打开 + +你会看到一个黑色或白色的文字窗口,里面有一个闪烁的光标,这就是终端。 + +#### 2. 输入解除限制的命令 + +在终端窗口中,复制粘贴以下这行命令(整行复制,一个字都不要漏��: + +``` +xattr -cr /Applications/GenericAgent.app +``` + +粘贴方法:在终端窗口里按 `Command(⌘) + V` + +然后按 `回车键(Enter)` 执行。 + +> 如果终端要求输入密码,输入你的 Mac 开机密码(输入时不会显示任何字符,这是正常的),然后按回车。 + +执行完毕后,终端不会有任何提示,这代表成功了。 + +#### 3. 打开 GenericAgent + +现在可以正常打开应用了: + +- 打开 Finder → 侧边栏点击「应用程序」 +- 找到 **GenericAgent**,双击打开 + +首次打开可能还会弹出一个确认框,点击「打开」即可。之后就不会再弹出了。 + +--- + +## ❓ 常见问题 + +### Q: 提示「GenericAgent 已损坏,无法打开」怎么办? + +这不是真的损坏,是 macOS 的安全机制。请回到第二步,确保在终端中执行了 `xattr -cr` 命令。 + +### Q: 提示「无法打开,因为无法验证开发者」怎么办? + +方法一(推荐):执行第二步的终端命令。 + +方法二:右键点击 GenericAgent.app → 选择「打开」→ 在弹出的对话框中点击「打开」。 + +### Q: 我的 Mac 是 Intel 芯片的,能用吗? + +当前版本仅支持 Apple Silicon(M1/M2/M3/M4)芯片的 Mac。如果你的 Mac 是 2020 年之前购买的,大概率是 Intel 芯片,暂时无法使用本安装包。 + +查看方法:点击左上角 → 「关于本机」,如果芯片一栏显示 Apple M1/M2/M3/M4,就可以使用。 + +### Q: 终端命令执行后没有任何反应? + +没有反应就是成功了。Unix/macOS 的设计哲学是「没有消息就是好消息」。 + +--- + +## 🔧 系统要求 + +- macOS 12 (Monterey) 或更高版本 +- Apple Silicon 芯片(M1/M2/M3/M4) +- 约 50MB 可用磁盘空间 diff --git a/frontends/cost_tracker.py b/frontends/cost_tracker.py index 75c7396a..74425898 100644 --- a/frontends/cost_tracker.py +++ b/frontends/cost_tracker.py @@ -8,6 +8,7 @@ runs its agent on `ga-tui-agent-`, so `/cost` is a thread lookup. Subagent processes are out-of-process, so `scan_subagent_logs` parses the same `[Cache]` / `[Output]` print lines from `temp/*/stdout.log`. """ +from __future__ import annotations import glob, os, re, threading, time from dataclasses import dataclass, field diff --git a/frontends/fsapp.py b/frontends/fsapp.py index 8fa97bee..ad959f66 100644 --- a/frontends/fsapp.py +++ b/frontends/fsapp.py @@ -1,22 +1,86 @@ -import glob, json, os, queue as Q, re, sys, threading, time -import atexit, hashlib -try: - import msvcrt -except Exception: - msvcrt = None +import argparse, asyncio, importlib.util, json, os, queue as Q, re, sys, threading, time, uuid +from pathlib import Path PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, PROJECT_ROOT) os.chdir(PROJECT_ROOT) -from agentmain import GeneraticAgent -from frontends.chatapp_common import format_restore -from frontends.continue_cmd import handle_frontend_command as handle_continue_frontend, reset_conversation -from llmcore import mykeys import traceback import lark_oapi as lark from lark_oapi.api.im.v1 import * + +def _ensure_dir(path): + path = Path(path) + path.mkdir(parents=True, exist_ok=True) + return path + + +def _workspace_root_dir(): + root = os.environ.get("GA_WORKSPACE_ROOT") + if root: + return _ensure_dir(Path(root).expanduser().resolve()) + return _ensure_dir(Path(PROJECT_ROOT).resolve()) + + +def _workspace_config_dir(root=None): + base = Path(root).expanduser().resolve() if root else _workspace_root_dir() + if base.name == "ga_config": + return _ensure_dir(base) + return _ensure_dir(base / "ga_config") + + +def _load_dict_config(path): + path = Path(path) + if not path.exists(): + return None + try: + if path.suffix == ".py": + mod_name = f"_fs_mykey_{uuid.uuid4().hex}" + spec = importlib.util.spec_from_file_location(mod_name, path) + if not spec or not spec.loader: + return None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + data = {k: v for k, v in vars(module).items() if not k.startswith("_")} + else: + with open(path, encoding="utf-8") as f: + data = json.load(f) + return data if isinstance(data, dict) else None + except Exception as e: + print(f"[ERROR] load config failed {path}: {e}") + return None + + +def _resolve_mykey_path(): + workspace_root = _workspace_root_dir() + config_root = _workspace_config_dir(workspace_root) + candidates = [ + config_root / "mykey.json", + config_root / "mykey.py", + workspace_root / "mykey.json", + workspace_root / "mykey.py", + Path(PROJECT_ROOT) / "mykey.json", + Path(PROJECT_ROOT) / "mykey.py", + ] + for candidate in candidates: + if _load_dict_config(candidate): + return candidate + return candidates[0] + + +def _ensure_runtime_paths(): + workspace_root = _workspace_root_dir() + config_root = _workspace_config_dir(workspace_root) + os.environ.setdefault("GA_WORKSPACE_ROOT", str(workspace_root)) + os.environ.setdefault("GA_USER_DATA_DIR", str(config_root)) + return str(workspace_root), str(config_root) + + +_ensure_runtime_paths() +from agentmain import GeneraticAgent +from frontends.chatapp_common import AgentChatMixin, FILE_HINT, split_text + _TAG_PATS = [r"<" + t + r">.*?" for t in ("thinking", "summary", "tool_use", "file_content")] _IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".ico", ".tiff", ".tif"} _AUDIO_EXTS = {".opus", ".mp3", ".wav", ".m4a", ".aac"} @@ -39,74 +103,29 @@ MEDIA_DIR = os.path.join(TEMP_DIR, "feishu_media") os.makedirs(MEDIA_DIR, exist_ok=True) -def _acquire_fsapp_singleton(): - """Per-agent process lock; prevents duplicate Feishu long-connection clients.""" - if msvcrt is None: - return None - path = os.path.join(TEMP_DIR, "fsapp_singleton.lock") - f = open(path, "a+b") - try: - msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1) - except OSError: - print(f"[INFO] another fsapp is already running for {PROJECT_ROOT}; exiting duplicate pid={os.getpid()}", flush=True) - f.close() - sys.exit(0) - atexit.register(lambda: (msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1), f.close())) - return f - - -_FSAPP_LOCK = _acquire_fsapp_singleton() _TRUNC_TAIL = 300 # 截断兜底时保留原文尾部字符数 -_DEDUP_FILE = os.path.join(TEMP_DIR, "fsapp_seen_message_ids.txt") -_DEDUP_TTL_SEC = 6 * 3600 -_DEDUP_MAX = 500 +_DEDUP_TTL_SEC = 10 * 60 +_DEDUP_MAX = 2000 +_DEDUP_LOCK = threading.Lock() +_SEEN_MESSAGES = {} -def _message_claim_once(message_id): - """Return True only for the first process that claims this Feishu message_id.""" +def _claim_message_once(message_id): + """Best-effort cross-platform dedup for Feishu reconnect redeliveries.""" if not message_id: return True now = time.time() - lock_path = _DEDUP_FILE + ".lock" - lf = None - try: - if msvcrt is not None: - lf = open(lock_path, "a+b") - msvcrt.locking(lf.fileno(), msvcrt.LK_LOCK, 1) - rows = [] - if os.path.exists(_DEDUP_FILE): - with open(_DEDUP_FILE, "r", encoding="utf-8", errors="replace") as f: - for line in f: - parts = line.rstrip("\n").split(" ", 1) - if len(parts) != 2: - continue - try: - ts = float(parts[0]) - except Exception: - continue - mid = parts[1] - if now - ts <= _DEDUP_TTL_SEC: - rows.append((ts, mid)) - if any(mid == message_id for _, mid in rows): + with _DEDUP_LOCK: + expired = [mid for mid, ts in _SEEN_MESSAGES.items() if now - ts > _DEDUP_TTL_SEC] + for mid in expired: + _SEEN_MESSAGES.pop(mid, None) + if len(_SEEN_MESSAGES) > _DEDUP_MAX: + for mid, _ in sorted(_SEEN_MESSAGES.items(), key=lambda item: item[1])[:len(_SEEN_MESSAGES) - _DEDUP_MAX]: + _SEEN_MESSAGES.pop(mid, None) + if message_id in _SEEN_MESSAGES: return False - rows.append((now, message_id)) - rows = rows[-_DEDUP_MAX:] - tmp = _DEDUP_FILE + ".tmp" - with open(tmp, "w", encoding="utf-8") as f: - for ts, mid in rows: - f.write(f"{ts:.3f} {mid}\n") - os.replace(tmp, _DEDUP_FILE) + _SEEN_MESSAGES[message_id] = now return True - except Exception as e: - print(f"[WARN] message dedup failed: {e}") - return True - finally: - if lf is not None: - try: - msvcrt.locking(lf.fileno(), msvcrt.LK_UNLCK, 1) - lf.close() - except Exception: - pass def _clean(text): @@ -128,7 +147,7 @@ def _display_text(text): if cleaned: return cleaned tail = (text or "").strip()[-_TRUNC_TAIL:] - return "(无文本输出)" + (f"\n…{tail}" if tail else "") + return "⚠️ 模型输出被截断或为空" + (f"\n…{tail}" if tail else "") def _to_allowed_set(value): @@ -301,26 +320,89 @@ def _extract_post_content(content_json): return "", [] -APP_ID = str(mykeys.get("fs_app_id", "") or "").strip() -APP_SECRET = str(mykeys.get("fs_app_secret", "") or "").strip() -ALLOWED_USERS = _to_allowed_set(mykeys.get("fs_allowed_users", [])) -PUBLIC_ACCESS = not ALLOWED_USERS or "*" in ALLOWED_USERS AGENT_TIMEOUT_SEC = 900 -agent = GeneraticAgent() -_agent_thread = threading.Thread(target=agent.run, daemon=True, name="GA-core") -_agent_thread.start() -try: - print(f"[INFO] GA core started pid={os.getpid()} thread_alive={_agent_thread.is_alive()} llm={agent.get_llm_name()}") -except Exception as e: - print(f"[WARN] GA core started but status unavailable: {e}") -client, user_tasks = None, {} +agent = None +agent_error = None +agent_thread = None +client, user_tasks, app = None, {}, None +agent_lock = threading.Lock() + + +def _load_config(): + path = _resolve_mykey_path() + if not path or not path.exists(): + return {}, str(path or "") + try: + data = _load_dict_config(path) + return data if isinstance(data, dict) else {}, str(path) + except Exception as e: + print(f"[ERROR] load mykey failed {path}: {e}") + return {}, str(path) + + +def _feishu_config(): + cfg, path = _load_config() + app_id = str(cfg.get("fs_app_id", "") or "").strip() + app_secret = str(cfg.get("fs_app_secret", "") or "").strip() + allowed = _to_allowed_set(cfg.get("fs_allowed_users", [])) + return app_id, app_secret, allowed, (not allowed or "*" in allowed), path + + +APP_ID, APP_SECRET, ALLOWED_USERS, PUBLIC_ACCESS, CONFIG_PATH = _feishu_config() + + +def get_agent(): + global agent, agent_error, agent_thread + with agent_lock: + if agent is not None: + return agent + if agent_error: + raise RuntimeError(agent_error) + try: + agent = GeneraticAgent() + agent_thread = threading.Thread(target=agent.run, daemon=True) + agent_thread.start() + return agent + except Exception as e: + agent_error = str(e) + raise def create_client(): return lark.Client.builder().app_id(APP_ID).app_secret(APP_SECRET).log_level(lark.LogLevel.INFO).build() +def _mask_secret(value): + value = str(value or "") + if len(value) <= 8: + return "*" * len(value) + return value[:4] + "*" * (len(value) - 8) + value[-4:] + + +def check_config(init_agent=False): + app_id, app_secret, allowed, public_access, path = _feishu_config() + result = { + "config_path": path, + "app_id": app_id, + "app_secret": _mask_secret(app_secret), + "app_secret_present": bool(app_secret), + "public_access": public_access, + "allowed_users": sorted(allowed), + "ready": bool(app_id and app_secret), + } + if init_agent: + try: + ga = get_agent() + result["agent_ready"] = True + result["llm_count"] = len(ga.list_llms()) if hasattr(ga, "list_llms") else 0 + result["current_llm"] = ga.get_llm_name() if getattr(ga, "llmclient", None) else "" + except Exception as e: + result["agent_ready"] = False + result["agent_error"] = str(e) + return result + + def _card_raw(elements): return json.dumps({ "schema": "2.0", @@ -343,15 +425,12 @@ def _send_raw(receive_id, payload, msg_type, rtype): return r.data.message_id if r.data else None print(f"发送失败: {r.code}, {r.msg}") except Exception as e: - print(f"[ERROR] _send_raw 网络异常: {e}") + print(f"[ERROR] send_message failed: {e}") + traceback.print_exc() return None def _patch_card(message_id, card_json): - return _patch_card_result(message_id, card_json)[0] - - -def _patch_card_result(message_id, card_json): try: body = PatchMessageRequest.builder().message_id(message_id).request_body( PatchMessageRequestBody.builder().content(card_json).build() @@ -359,11 +438,11 @@ def _patch_card_result(message_id, card_json): r = client.im.v1.message.patch(body) if not r.success(): print(f"[ERROR] patch_card 失败: {r.code}, {r.msg}") - msg = f"{getattr(r, 'code', '')} {getattr(r, 'msg', '')}".lower() - return r.success(), ("230099" in msg or "11310" in msg or "element exceeds the limit" in msg) + return r.success() except Exception as e: - print(f"[ERROR] _patch_card 网络异常: {e}") - return False, False + print(f"[ERROR] patch_card exception: {e}") + traceback.print_exc() + return False def send_message(receive_id, content, msg_type="text", use_card=False, receive_id_type="open_id"): @@ -556,7 +635,6 @@ def _build_step_detail(resp, tool_calls): class _TaskCard: """飞书任务卡片:单卡片持续 patch;每步一个独立折叠面板(header 显示 summary,展开看详情)。""" _DETAIL_LIMIT = 8000 - _FINAL_LIMIT = 6000 def __init__(self, receive_id, rid_type): self.rid, self.rtype = receive_id, rid_type @@ -564,10 +642,8 @@ class _TaskCard: self.status = "🤔 思考中..." self.final = None self.msg_id = None - self.page_no = 1 - self.turn_no = 0 - self.turn_base = 1 - self.note = None + self.start_fallback_sent = False + self.final_fallback_sent = False def _step_panel(self, idx, summary, detail): detail = detail or "_(无输出)_" @@ -580,13 +656,8 @@ class _TaskCard: } def _build(self): - header = f"**{self.status}**" - if self.page_no > 1: - header += f"\n\n📄 工作卡片 {self.page_no}" - els = [{"tag": "markdown", "content": header}] - if self.note: - els.append({"tag": "markdown", "content": self.note}) - for i, (s, d) in enumerate(self.steps, self.turn_base): + els = [{"tag": "markdown", "content": f"**{self.status}**"}] + for i, (s, d) in enumerate(self.steps, 1): els.append(self._step_panel(i, s, d)) if self.final: els += [{"tag": "hr"}, {"tag": "markdown", "content": self.final}] @@ -595,92 +666,53 @@ class _TaskCard: def _push(self): card = self._build() if self.msg_id: - return _patch_card_result(self.msg_id, card) + ok = _patch_card(self.msg_id, card) else: self.msg_id = _send_raw(self.rid, card, "interactive", self.rtype) - return bool(self.msg_id), False + ok = bool(self.msg_id) + return ok - def _rollover(self): - self.page_no += 1 - self.msg_id = None - self.final = None - self.note = "⚠️ 上一张工作卡片达到飞书限制,本页继续展示后续进展。" + def _fallback_text(self, text, *, final=False): + attr = "final_fallback_sent" if final else "start_fallback_sent" + if getattr(self, attr): + return + setattr(self, attr, True) + send_message(self.rid, text, receive_id_type=self.rtype) # ── 公开接口 ── def start(self): - self._push() + if not self._push(): + self._fallback_text("🤔 思考中...") def step(self, summary, detail=""): - self.turn_no += 1 - step = (summary, detail) - self.steps.append(step) - self.status = f"⏳ 工作中 · Turn {self.turn_no}" - ok, limit = self._push() - if limit: - self.steps.pop() - self._rollover() - self.turn_base = self.turn_no - self.steps = [step] - self._push() + self.steps.append((summary, detail)) + self.status = f"⏳ 工作中 · Turn {len(self.steps)}" + self._push() def done(self, text): self.status = "✅ 已完成" - self.final = (text or "_(无文本输出)_")[:self._FINAL_LIMIT] - ok, limit = self._push() - if limit: - self._rollover() - self.steps = [] - self.turn_base = self.turn_no + 1 - self.final = (text or "_(无文本输出)_")[:self._FINAL_LIMIT] - ok, _ = self._push() - # Last-resort delivery: if card creation/patch fails (common after cold boot - # or message-card expiry), still send a plain text reply so the user is not - # left with no response. - if not ok: - try: - send_message(self.rid, _display_text(text), receive_id_type=self.rtype) - except Exception as e: - print(f"[ERROR] done fallback send failed: {e}") + self.final = text or "_(无文本输出)_" + if not self._push(): + self._fallback_text(_display_text(text), final=True) def fail(self, msg): self.status = f"❌ {msg}" - self._push() + if not self._push(): + self._fallback_text(f"❌ {msg}", final=True) -def _make_task_hook(card, done_event, on_final): +def _make_task_hook(card, task_id, on_final): """飞书任务 hook:每轮 patch 卡片状态;结束触发 on_final(raw) 处理附件。""" def hook(ctx): try: + parent = getattr(ctx.get("self"), "parent", None) + if getattr(parent, "_fs_active_task_id", None) != task_id: + return if ctx.get('exit_reason'): resp = ctx.get('response') raw = resp.content if hasattr(resp, 'content') else str(resp) - display = _display_text(raw) - if display.startswith("(无文本输出)"): - # fallback: show thinking + tool_calls when content is empty after cleaning - parts = [] - thinking = getattr(resp, 'thinking', '') or '' - if thinking: - parts.append(f"**[Thinking]**\n{thinking.strip()}") - tool_calls = ctx.get('tool_calls') or [] - for tc in tool_calls: - name = tc.get('tool_name') or tc.get('name', '?') - args = tc.get('args') or tc.get('arguments') or {} - if name == 'ask_user': - q = args.get('question', '') - candidates = args.get('candidates') or [] - if candidates: - q += '\n' + '\n'.join(f'- {c}' for c in candidates) - parts.append(q) - else: - args_s = json.dumps(args, ensure_ascii=False) - if len(args_s) > 200: - args_s = args_s[:200] + '...' - parts.append(f"`{name}`: {args_s}") - display = "\n\n".join(p for p in parts if p) or display - card.done(display) on_final(raw) - done_event.set() elif ctx.get('summary'): detail = _build_step_detail(ctx.get('response'), ctx.get('tool_calls') or []) card.step(ctx['summary'], detail) @@ -689,11 +721,98 @@ def _make_task_hook(card, done_event, on_final): return hook +class FeishuApp(AgentChatMixin): + label, source, split_limit = "Feishu", "feishu", 4000 + + async def send_text(self, chat_id, content, *, receive_id=None, receive_id_type="open_id", **_): + rid = receive_id or chat_id + for part in split_text(content, self.split_limit): + await asyncio.to_thread(send_message, rid, part, "text", False, receive_id_type) + + async def send_done(self, chat_id, raw_text, *, receive_id=None, receive_id_type="open_id", **_): + rid = receive_id or chat_id + text = _display_text(raw_text) + await asyncio.to_thread(send_message, rid, text, "text", False, receive_id_type) + await asyncio.to_thread(_send_generated_files, rid, raw_text, receive_id_type) + + async def run_agent(self, chat_id, text, *, receive_id=None, receive_id_type="open_id", images=None, **_): + if self.user_tasks: + await self.send_text(chat_id, "当前会话已有任务在运行,请等待完成或发送 /stop 后再试。", receive_id=receive_id, receive_id_type=receive_id_type) + return + state = {"running": True} + self.user_tasks[chat_id] = state + rid = receive_id or chat_id + task_id = f"{chat_id}_{uuid.uuid4().hex}" + hook_key = f"fs_{task_id}" + card = _TaskCard(rid, receive_id_type) + result = {"raw": None, "sent": False} + finish_lock = threading.Lock() + + def _finish(raw): + with finish_lock: + if result["sent"]: + return + result["raw"] = raw + result["sent"] = True + card.done(_display_text(raw)) + _send_generated_files(rid, raw, receive_id_type=receive_id_type) + + try: + await asyncio.to_thread(card.start) + if not hasattr(self.agent, '_turn_end_hooks'): + self.agent._turn_end_hooks = {} + self.agent._turn_end_hooks[hook_key] = _make_task_hook(card, task_id, _finish) + self.agent._fs_active_task_id = task_id + dq = self.agent.put_task(f"{FILE_HINT}\n\n{text}", source=self.source, images=images or None) + start = time.time() + while state["running"] and not result["sent"]: + try: + item = await asyncio.to_thread(dq.get, True, 1) + except Q.Empty: + item = None + if item and "done" in item: + await asyncio.to_thread(_finish, item.get("done", "")) + break + if time.time() - start > AGENT_TIMEOUT_SEC: + self.agent.abort() + await asyncio.to_thread(card.fail, "任务超时") + break + if not state["running"] and not result["sent"]: + self.agent.abort() + await asyncio.to_thread(card.fail, "已停止") + except Exception as e: + traceback.print_exc() + await asyncio.to_thread(card.fail, f"错误: {e}") + finally: + if getattr(self.agent, "_fs_active_task_id", None) == task_id: + try: + delattr(self.agent, "_fs_active_task_id") + except AttributeError: + pass + if hasattr(self.agent, '_turn_end_hooks'): + self.agent._turn_end_hooks.pop(hook_key, None) + self.user_tasks.pop(chat_id, None) + + +def get_app(): + global app + if app is None: + app = FeishuApp(get_agent(), user_tasks) + return app + + +def _run_async(coro): + try: + asyncio.run(coro) + except Exception: + traceback.print_exc() + + def handle_message(data): event, message, sender = data.event, data.event.message, data.event.sender - msg_id = getattr(message, "message_id", "") or "" - if msg_id and not _message_claim_once(msg_id): - print(f"[INFO] duplicate Feishu message ignored: {msg_id}") + message_id = getattr(message, "message_id", "") or "" + if not _claim_message_once(message_id): + print(f"忽略重复飞书消息: {message_id}") return open_id = sender.sender_id.open_id chat_id = message.chat_id @@ -708,133 +827,54 @@ def handle_message(data): send_message(open_id, f"⚠️ 暂不支持处理此类飞书消息:{message.message_type}") return print(f"收到消息 [{open_id}] ({message.message_type}, {len(image_paths)} images): {user_input[:200]}") + receive_id = chat_id or open_id + receive_id_type = "chat_id" if chat_id else "open_id" + chat_key = receive_id if message.message_type == "text" and user_input.startswith("/"): - return handle_command(open_id, user_input, chat_id) - - def run_agent(): - user_tasks[open_id] = {"running": True} - receive_id = chat_id or open_id - rid_type = "chat_id" if chat_id else "open_id" - done_event = threading.Event() - hook_key = f"fs_{open_id}" - card = _TaskCard(receive_id, rid_type) - card.start() - on_final = lambda raw: _send_generated_files(receive_id, raw, receive_id_type=rid_type) - if not hasattr(agent, '_turn_end_hooks'): agent._turn_end_hooks = {} - agent._turn_end_hooks[hook_key] = _make_task_hook(card, done_event, on_final) - try: - # Keep the display_queue as the authoritative completion channel. - # The turn_end_hook updates the rich card per turn, but startup/cold-run - # races or hook errors must not leave Feishu waiting forever. - dq = agent.put_task(user_input, source="feishu", images=image_paths) - start = time.time() - while True: - # Drain any agent output first; this is independent of desktop/Streamlit UI. - try: - while True: - item = dq.get_nowait() - if 'done' in item: - raw = item.get('done', '') - if not done_event.is_set(): - card.done(_display_text(raw)) - on_final(raw) - done_event.set() - break - except Q.Empty: - pass - if done_event.is_set(): - break - if not user_tasks.get(open_id, {}).get("running", True): - agent.abort() - card.fail("已停止") - break - if time.time() - start > AGENT_TIMEOUT_SEC: - agent.abort() - card.fail("任务超时") - break - done_event.wait(timeout=0.5) - except Exception as e: - traceback.print_exc() - card.fail(f"错误: {e}") - finally: - agent._turn_end_hooks.pop(hook_key, None) - user_tasks.pop(open_id, None) - - threading.Thread(target=run_agent, daemon=True).start() - - -def handle_command(open_id, cmd, chat_id=None): - def _send_cmd_response(content): - if chat_id: - send_message(chat_id, content, receive_id_type="chat_id") - else: - send_message(open_id, content) - parts = (cmd or "").split() - op = (parts[0] if parts else "").lower() - if op == "/stop": - if open_id in user_tasks: - user_tasks[open_id]["running"] = False - agent.abort() - _send_cmd_response("正在停止...") - elif op == "/new": - _send_cmd_response(reset_conversation(agent)) - elif op == "/help": - _send_cmd_response("命令列表:\n/stop - 停止当前任务\n/status - 查看状态\n/llm - 查看当前模型列表\n/llm [n] - 切换到第 n 个模型\n/restore - 恢复上次对话历史\n/continue - 列出可恢复会话\n/continue [n] - 恢复第 n 个会话\n/new - 开启新对话并清空当前上下文\n/help - 显示帮助") - elif op == "/status": - llm = agent.get_llm_name() if agent.llmclient else "未配置" - _send_cmd_response(f"状态: {'🔴 运行中' if agent.is_running else '🟢 空闲'}\nLLM: [{agent.llm_no}] {llm}") - elif op == "/llm": - if not agent.llmclient: - return _send_cmd_response("❌ 当前没有可用的 LLM 配置") - if len(parts) > 1: - try: - agent.next_llm(int(parts[1])) - return _send_cmd_response(f"✅ 已切换到 [{agent.llm_no}] {agent.get_llm_name()}") - except Exception: - return _send_cmd_response(f"用法: /llm <0-{len(agent.list_llms()) - 1}>") - lines = [f"{'→' if cur else ' '} [{i}] {name}" for i, name, cur in agent.list_llms()] - _send_cmd_response("LLMs:\n" + "\n".join(lines)) - elif op == "/restore": - try: - restored_info, err = format_restore() - if err: - return _send_cmd_response(err.replace("❌ ", "")) - restored, fname, count = restored_info - agent.history.extend(restored) - agent.abort() - _send_cmd_response(f"已恢复 {count} 轮对话\n来源: {fname}\n(仅恢复上下文,请输入新问题继续)") - except Exception as e: - _send_cmd_response(f"恢复失败: {e}") - elif op == "/continue" or cmd.startswith("/continue"): - _send_cmd_response(handle_continue_frontend(agent, cmd)) - else: - _send_cmd_response(f"未知命令: {cmd}") + threading.Thread( + target=_run_async, + args=(get_app().handle_command(chat_key, user_input, receive_id=receive_id, receive_id_type=receive_id_type),), + daemon=True, + ).start() + return + threading.Thread( + target=_run_async, + args=(get_app().run_agent(chat_key, user_input, receive_id=receive_id, receive_id_type=receive_id_type, images=image_paths),), + daemon=True, + ).start() def main(): - global client + global client, APP_ID, APP_SECRET, ALLOWED_USERS, PUBLIC_ACCESS, CONFIG_PATH + APP_ID, APP_SECRET, ALLOWED_USERS, PUBLIC_ACCESS, CONFIG_PATH = _feishu_config() if not APP_ID or not APP_SECRET: - print("错误: 请在 mykey.py 或 mykey.json 中配置 fs_app_id 和 fs_app_secret") + print(f"错误: 请在 mykey 配置中填写 fs_app_id 和 fs_app_secret\n配置文件: {CONFIG_PATH}", flush=True) sys.exit(1) - client = create_client() handler = lark.EventDispatcherHandler.builder("", "").register_p2_im_message_receive_v1(handle_message).build() - print("=" * 50 + "\n飞书 Agent 已启动(长连接模式)\n" + f"App ID: {APP_ID}\n等待消息...\n" + "=" * 50) retry_delay = 5 while True: try: + client = create_client() cli = lark.ws.Client(APP_ID, APP_SECRET, event_handler=handler, log_level=lark.LogLevel.INFO) + print("=" * 50 + "\n飞书 Agent 已启动(长连接模式)\n" + f"App ID: {APP_ID}\n配置: {CONFIG_PATH}\n等待消息...\n" + "=" * 50, flush=True) cli.start() + retry_delay = 5 + except KeyboardInterrupt: + raise except Exception as e: - print(f"[WARN] 飞书长连接断开或启动失败: {e}") - print(f"[INFO] {retry_delay}s 后重连...") + print(f"[WARN] 飞书长连接断开或启动失败: {e}", flush=True) + traceback.print_exc() + print(f"[INFO] {retry_delay}s 后重连飞书长连接...", flush=True) time.sleep(retry_delay) retry_delay = min(retry_delay * 2, 120) - # 重连时刷新 client - try: - client = create_client() - except Exception: - pass if __name__ == "__main__": - main() + parser = argparse.ArgumentParser(description="A3Agent Feishu frontend") + parser.add_argument("--check", action="store_true", help="只检查飞书配置,不启动长连接") + parser.add_argument("--check-agent", action="store_true", help="检查配置并初始化 Agent/LLM") + args = parser.parse_args() + if args.check or args.check_agent: + print(json.dumps(check_config(init_agent=args.check_agent), ensure_ascii=False, indent=2), flush=True) + else: + main() diff --git a/frontends/qqapp.py b/frontends/qqapp.py index 30a66414..2ad088f8 100644 --- a/frontends/qqapp.py +++ b/frontends/qqapp.py @@ -75,7 +75,11 @@ class QQApp(AgentChatMixin): api = self.client.api.post_group_message if is_group else self.client.api.post_c2c_message key = "group_openid" if is_group else "openid" for part in split_text(content, self.split_limit): - await api(**{key: chat_id, "msg_type": 0, "content": part, "msg_id": msg_id, "msg_seq": _next_msg_seq()}) + seq = _next_msg_seq() + try: + await api(**{key: chat_id, "msg_type": 2, "markdown": {"content": part}, "msg_id": msg_id, "msg_seq": seq}) + except Exception: + await api(**{key: chat_id, "msg_type": 0, "content": part, "msg_id": msg_id, "msg_seq": seq}) async def on_message(self, data, is_group=False): try: diff --git a/frontends/stapp.py b/frontends/stapp.py index 0b5e2639..29b8951e 100644 --- a/frontends/stapp.py +++ b/frontends/stapp.py @@ -94,19 +94,23 @@ def render_sidebar(): st.toast("Desktop pet started") if LANG == 'zh': + if st.button('🎯 给我找点事做'): + st.session_state['_inject_prompt'] = '按照自主行动的规划部分,充分分析我的情况,给我生成一批TODO,务必让我感兴趣' + st.rerun(scope="app") st.divider() if st.button("开始空闲自主行动"): st.session_state.last_reply_time = int(time.time()) - 1800 - st.toast("已将上次回复时间设为1800秒前"); st.rerun() + st.session_state.autonomous_enabled = True + st.toast("已将上次回复时间设为1800秒前,自主行动已激活"); st.rerun(scope="app") if st.session_state.autonomous_enabled: if st.button("⏸️ 禁止自主行动"): st.session_state.autonomous_enabled = False - st.toast("⏸️ 已禁止自主行动"); st.rerun() + st.toast("⏸️ 已禁止自主行动"); st.rerun(scope="app") st.caption("🟢 自主行动运行中,会在你离开它30分钟后自动进行") else: if st.button("▶️ 允许自主行动", type="primary"): st.session_state.autonomous_enabled = True - st.toast("✅ 已允许自主行动"); st.rerun() + st.toast("✅ 已允许自主行动"); st.rerun(scope="app") st.caption("🔴 自主行动已停止") with st.sidebar: render_sidebar() @@ -151,11 +155,7 @@ def render_segments(segments, suffix=''): if seg['type'] == 'fold': with st.expander(seg['title'], expanded=False): st.markdown(seg['content']) else: - # Strip meta tags from text segments — folded turns already - # promote them to expander titles; for the first/last segments - # they'd otherwise leak into the chat as raw text (esp. after /continue - # restores a multi-turn body). - st.markdown(_SUMMARY_TAG_RE.sub('', seg['content']) + suffix) + st.markdown(seg['content'] + suffix) def agent_backend_stream(prompt=None): """Drain main task display_queue. @@ -235,14 +235,17 @@ except (ImportError, AttributeError): from streamlit.components.v1 import html as _embed_html # ≤1.55 _js_scroll_fix = ( "!function(){var p=window.parent;if(p.__sfx2)return;p.__sfx2=1;var d=p.document;" - "function f(){var m=d.querySelector('section.main');if(!m)return;" - "var s=m.scrollTop;m.style.minHeight=m.scrollHeight+1+'px';void m.offsetHeight;" - "m.style.minHeight='';void m.offsetHeight;m.scrollTop=s}" + "var pending=0;" + "function f(){pending=0;var m=d.querySelector('section.main');if(!m)return;" + "var s=m.scrollTop,h=m.scrollHeight;" + "m.style.minHeight=h+1+'px';void m.offsetHeight;" + "m.style.minHeight='';void m.offsetHeight;" + "m.scrollTop=s}" + "function schedule(){if(!pending){pending=1;requestAnimationFrame(f)}}" "d.addEventListener('transitionend',function(e){" - "e.target.closest&&e.target.closest('details')&&setTimeout(f,60)},!0);" - "new MutationObserver(function(){setTimeout(f,80)})" - ".observe(d.body,{subtree:1,attributes:1,attributeFilter:['open']});" - "setInterval(f,5000)}()" + "e.target.closest&&e.target.closest('details')&&setTimeout(schedule,60)},!0);" + "new MutationObserver(function(){setTimeout(schedule,80)})" + ".observe(d.body,{subtree:1,attributes:1,attributeFilter:['open']})}()" ) # IME composition fix (macOS only) - prevents Enter from submitting during CJK input _js_ime_fix = ("" if os.name == 'nt' else @@ -257,7 +260,9 @@ _js_ime_fix = ("" if os.name == 'nt' else "f();new MutationObserver(f).observe(d.body,{childList:1,subtree:1})}()") _embed_html(f'', height=0) -if prompt := st.chat_input("any task?"): +_injected = st.session_state.pop('_inject_prompt', None) +prompt = st.chat_input("any task?") or _injected +if prompt: ts = time.strftime("%Y-%m-%d %H:%M:%S") cmd = (prompt or "").strip() def _reset_and_rerun(): diff --git a/frontends/tgapp.py b/frontends/tgapp.py index fddab2f5..0716af94 100644 --- a/frontends/tgapp.py +++ b/frontends/tgapp.py @@ -42,11 +42,19 @@ _RETRY_AFTER_MARGIN_SECONDS = 1.0 _QUEUE_WAIT_SECONDS = 1 _ASK_USER_HOOK_KEY = "telegram_ask_user_menu" _ASK_CALLBACK_PREFIX = "ask:" +_LLM_CALLBACK_PREFIX = "llm:" _ASK_CANCEL_ACTION = "none" +_ASK_MULTI_DONE_ACTION = "done" +_ASK_TOGGLE_ACTION = "toggle" _ASK_CANCEL_LABEL = "none of these above" _ASK_CANCEL_PROMPT = "已取消选择,请直接发送下一步操作。" +_ASK_MULTI_HINT = "可多选:点选项目后点击 Done 提交。" +_ASK_MULTI_EMPTY_HINT = "请至少选择一项,或选择 none of these above。" +_LLM_MENU_PROMPT = "请选择要切换的 LLM:" _ask_menu_events = Q.Queue() _ask_menu_store = {} +_llm_menu_store = {} +_MULTI_SELECT_RE = re.compile(r"\[?(?:多选|multi(?:[-_ ]?select)?|select all)\]?", re.IGNORECASE) _QUOTE_OPEN_TAG = "<_quote_>" _QUOTE_CLOSE_TAG = "" _QUOTE_TOKEN_PATTERN = re.escape(_QUOTE_OPEN_TAG) + r"([\s\S]*?)" + re.escape(_QUOTE_CLOSE_TAG) @@ -265,7 +273,11 @@ def _extract_ask_user_event(ctx): if not candidates: return None question = str(data.get("question") or "请选择下一步操作:").strip() or "请选择下一步操作:" - return {"question": question, "candidates": candidates} + return { + "question": question, + "candidates": candidates, + "multi": bool(_MULTI_SELECT_RE.search(question)), + } def _register_ask_user_hook(): if not hasattr(agent, "_turn_end_hooks"): @@ -285,25 +297,49 @@ def _drain_latest_ask_user_event(): break return latest -def _build_ask_user_markup(menu_id, candidates): - rows = [ - [InlineKeyboardButton(candidate, callback_data=f"{_ASK_CALLBACK_PREFIX}{menu_id}:{idx}")] - for idx, candidate in enumerate(candidates) - ] +def _build_ask_user_markup(menu_id, candidates, multi=False, selected_indexes=None): + selected_indexes = set(selected_indexes or []) + rows = [] + for idx, candidate in enumerate(candidates): + if multi: + label = f"✓ {candidate}" if idx in selected_indexes else candidate + action = f"{_ASK_TOGGLE_ACTION}:{idx}" + else: + label = candidate + action = str(idx) + rows.append([ + InlineKeyboardButton(label, callback_data=f"{_ASK_CALLBACK_PREFIX}{menu_id}:{action}") + ]) + if multi: + rows.append([ + InlineKeyboardButton("Done", callback_data=f"{_ASK_CALLBACK_PREFIX}{menu_id}:{_ASK_MULTI_DONE_ACTION}") + ]) rows.append([ InlineKeyboardButton(_ASK_CANCEL_LABEL, callback_data=f"{_ASK_CALLBACK_PREFIX}{menu_id}:{_ASK_CANCEL_ACTION}") ]) return InlineKeyboardMarkup(rows) -def _parse_ask_callback_data(data): - if not (data or "").startswith(_ASK_CALLBACK_PREFIX): +def _build_llm_markup(menu_id, llms): + rows = [] + for idx, name, current in llms: + label = f"→ [{idx}] {name}" if current else f"[{idx}] {name}" + rows.append([ + InlineKeyboardButton(label, callback_data=f"{_LLM_CALLBACK_PREFIX}{menu_id}:{idx}") + ]) + return InlineKeyboardMarkup(rows) + +def _parse_menu_callback_data(data, prefix): + if not (data or "").startswith(prefix): return None, None - payload = data[len(_ASK_CALLBACK_PREFIX):] + payload = data[len(prefix):] menu_id, sep, action = payload.partition(":") if not sep or not menu_id or not action: return None, None return menu_id, action +def _parse_ask_callback_data(data): + return _parse_menu_callback_data(data, _ASK_CALLBACK_PREFIX) + def _build_text_prompt(text): return f"{FILE_HINT}\n\n{text}" @@ -313,11 +349,15 @@ def _normalize_ask_menu_event(stored): return { "question": str(stored.get("question") or "请选择下一步操作:").strip() or "请选择下一步操作:", "candidates": [str(candidate).strip() for candidate in candidates if str(candidate).strip()], + "multi": bool(stored.get("multi")), + "selected": [int(idx) for idx in stored.get("selected", []) if isinstance(idx, int)], } if isinstance(stored, (list, tuple)): return { "question": "请选择下一步操作:", "candidates": [str(candidate).strip() for candidate in stored if str(candidate).strip()], + "multi": False, + "selected": [], } return None @@ -357,11 +397,18 @@ async def _edit_ask_user_result(query, event, selected=None, cancelled=False): async def _send_ask_user_menu(root_msg, event): menu_id = uuid.uuid4().hex[:16] candidates = event["candidates"] - _ask_menu_store[menu_id] = {"question": event["question"], "candidates": list(candidates)} + multi = bool(event.get("multi")) + _ask_menu_store[menu_id] = { + "question": event["question"], + "candidates": list(candidates), + "multi": multi, + "selected": [], + } + prompt = f"{event['question']}\n\n{_ASK_MULTI_HINT}" if multi else event["question"] try: await root_msg.reply_text( - event["question"], - reply_markup=_build_ask_user_markup(menu_id, candidates), + prompt, + reply_markup=_build_ask_user_markup(menu_id, candidates, multi=multi), ) except Exception as exc: _ask_menu_store.pop(menu_id, None) @@ -845,6 +892,45 @@ async def handle_ask_callback(update, ctx): await query.answer("菜单已过期") return await _clear_ask_reply_markup(query) candidates = event["candidates"] + if event.get("multi") and action.startswith(f"{_ASK_TOGGLE_ACTION}:"): + try: + selected_idx = int(action.split(":", 1)[1]) + if selected_idx < 0 or selected_idx >= len(candidates): + raise ValueError + except ValueError: + return await query.answer("菜单无效") + stored = _ask_menu_store.get(menu_id) + if not isinstance(stored, dict): + return await query.answer("菜单已过期") + selected = set(stored.get("selected", [])) + if selected_idx in selected: + selected.remove(selected_idx) + else: + selected.add(selected_idx) + stored["selected"] = sorted(selected) + await query.answer() + return await query.edit_message_reply_markup( + reply_markup=_build_ask_user_markup( + menu_id, + candidates, + multi=True, + selected_indexes=stored["selected"], + ) + ) + if event.get("multi") and action == _ASK_MULTI_DONE_ACTION: + selected_indexes = event.get("selected") or [] + if not selected_indexes: + return await query.answer(_ASK_MULTI_EMPTY_HINT, show_alert=True) + selected = "; ".join(candidates[idx] for idx in selected_indexes) + _ask_menu_store.pop(menu_id, None) + await query.answer() + await _edit_ask_user_result(query, event, selected=selected) + if query.message is None: + return + dq = agent.put_task(_build_text_prompt(selected), source="telegram") + task = asyncio.create_task(_stream(dq, query.message)) + ctx.user_data['stream_task'] = task + return if action == _ASK_CANCEL_ACTION: _ask_menu_store.pop(menu_id, None) await query.answer() @@ -865,6 +951,52 @@ async def handle_ask_callback(update, ctx): task = asyncio.create_task(_stream(dq, query.message)) ctx.user_data['stream_task'] = task +async def _send_llm_menu(message): + llms = agent.list_llms() + if not llms: + return await message.reply_text("没有可用模型。") + menu_id = uuid.uuid4().hex[:16] + _llm_menu_store[menu_id] = [idx for idx, _, _ in llms] + lines = [f"{'→' if cur else ' '} [{idx}] {name}" for idx, name, cur in llms] + try: + await message.reply_text( + _LLM_MENU_PROMPT, + reply_markup=_build_llm_markup(menu_id, llms), + ) + except Exception as exc: + _llm_menu_store.pop(menu_id, None) + print(f"[TG llm menu error] {type(exc).__name__}: {exc}", flush=True) + await message.reply_text("LLMs:\n" + "\n".join(lines)) + +async def handle_llm_callback(update, ctx): + query = update.callback_query + if query is None: + return + uid = update.effective_user.id if update.effective_user else None + if ALLOWED and uid not in ALLOWED: + return await query.answer("no", show_alert=True) + menu_id, action = _parse_menu_callback_data(query.data, _LLM_CALLBACK_PREFIX) + if not menu_id: + return await query.answer("菜单无效") + valid_indexes = _llm_menu_store.get(menu_id) + if valid_indexes is None: + await query.answer("菜单已过期") + return await _clear_ask_reply_markup(query) + try: + selected_idx = int(action) + except (TypeError, ValueError): + return await query.answer("菜单无效") + if selected_idx not in valid_indexes: + return await query.answer("菜单已过期", show_alert=True) + try: + agent.next_llm(selected_idx) + selected_name = agent.get_llm_name() + except Exception as exc: + return await query.answer(f"切换失败: {exc}", show_alert=True) + _llm_menu_store.pop(menu_id, None) + await query.answer(f"已切换到 [{selected_idx}] {selected_name}") + await query.edit_message_text(f"✅ 已切换到 [{selected_idx}] {selected_name}") + async def cmd_abort(update, ctx): _cancel_stream_task(ctx) agent.abort() @@ -880,8 +1012,7 @@ async def cmd_llm(update, ctx): except (ValueError, IndexError): await update.message.reply_text(f"用法: /llm <0-{len(agent.list_llms())-1}>") else: - lines = [f"{'→' if cur else ' '} [{i}] {name}" for i, name, cur in agent.list_llms()] - await update.message.reply_text("LLMs:\n" + "\n".join(lines)) + await _send_llm_menu(update.message) async def handle_photo(update, ctx): uid = update.effective_user.id @@ -969,6 +1100,7 @@ if __name__ == '__main__': app = (ApplicationBuilder().token(mykeys['tg_bot_token']) .request(request).get_updates_request(request).post_init(_sync_commands).build()) app.add_handler(CallbackQueryHandler(handle_ask_callback, pattern=r"^ask:")) + app.add_handler(CallbackQueryHandler(handle_llm_callback, pattern=r"^llm:")) app.add_handler(MessageHandler(filters.COMMAND, handle_command)) app.add_handler(MessageHandler(filters.PHOTO, handle_photo)) app.add_handler(MessageHandler(filters.Document.ALL, handle_photo)) diff --git a/frontends/tui_v3.py b/frontends/tui_v3.py new file mode 100644 index 00000000..8b011ec9 --- /dev/null +++ b/frontends/tui_v3.py @@ -0,0 +1,4609 @@ +"""tui_v3 — scrollback-first TUI for GenericAgent, consolidated. + +Merged from frontends/tui/ (cjk, clipboard, renderer, protocol, core/sb) +into a single file so the v3 frontend ships as one drop-in module. +Run: `python -m frontends.tui_v3` or `python frontends/tui_v3.py`. +""" +from __future__ import annotations + +import asyncio, atexit, json, locale, logging, os, queue, random, re, select, shutil, signal, subprocess +import sys, tempfile, threading, time + +_IS_WINDOWS = os.name == 'nt' + + +# Make `frontends/` parent (project root) importable so `from agentmain import …` +# works whether this file is run as `python -m frontends.tui_v3` or directly +# via `python frontends/tui_v3.py`. +_proj_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_front_dir = os.path.dirname(os.path.abspath(__file__)) +for _p in (_proj_root, _front_dir): + if _p not in sys.path: + sys.path.insert(0, _p) + +from agentmain import GeneraticAgent +from dataclasses import dataclass +from dataclasses import dataclass, field +from functools import lru_cache +from io import StringIO +from rich.cells import cell_len +from rich.console import Console +from rich.markdown import Markdown +from rich.text import Text +from rich.theme import Theme +from typing import Callable + +# ════════════════════════════════════════════════════════════════════════════ +# i18n — minimal dict-based zh/en translation layer (inlined; was tui_v3_i18n.py) +# +# - Single nested dict `_I18N[lang][key]` → format string. +# - `t(key, **fmt)` returns the formatted string for the current language; +# falls back to English, then to the key itself (so missing keys are visible). +# - Language detection: persisted user choice > system locale > 'en'. +# - Persistence: temp/tui_v3_settings.json (workspace-local, matches v2 pattern). +# +# Strings that intentionally stay single-language (English): +# - Spinner gerunds (Pondering/Brewing/...) — ported from v2. +# - Tech jargon embedded in zh strings: 'tokens', 'ctx', 'model', 'session', … +# ════════════════════════════════════════════════════════════════════════════ + +_SETTINGS_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "..", "temp", "tui_v3_settings.json" +) + +_SUPPORTED = ('zh', 'en') +_DEFAULT = 'en' + +_LANG: str = _DEFAULT + + +# ---------------- spinner gerunds (English only, from v2) ---------------- + +SPINNER_GERUNDS = ( + "Pondering", "Reticulating", "Sleuthing", "Hatching", "Pouncing", + "Brewing", "Sharpening", "Untangling", "Compiling", "Unraveling", + "Distilling", "Calibrating", "Marinating", "Conjuring", "Foraging", + "Spelunking", "Synthesizing", "Refactoring thoughts", "Tracing breadcrumbs", + "Following the rabbit hole", + "Routing", "Threading", "Polling", "Spinning", "Hooking", + "Patching", "Caching", "Yielding", "Hydrating", "Folding", + "Streaming", "Resolving", "Reaping", "Tuning", +) + + +# Language display names (always shown in their own script — never translated, +# so users always see them in a form they recognize). +LANG_LABELS = { + 'zh': '简体中文', + 'en': 'English', +} + + +# Rotating usage tips — one picked per launch, shown in the banner (v2 _TIPS). +# Only covers features v3 has actually adapted; the en/zh lists run parallel. +_TIPS = { + 'en': [ + "Tip: press / to open the command palette — arrow keys to pick, Enter drops it into the input box.", + "Tip: pasted images / files fold into [Image #N] / [File #N] placeholders; backspace deletes the whole block.", + "Tip: /btw lets a side-agent answer without interrupting the main task.", + "Tip: /rewind [n] rewinds the last n turns; /stop aborts the current task.", + "Tip: /continue lists past sessions — arrow keys to pick, Enter to restore.", + "Tip: Ctrl+J / Shift+Enter inserts a newline in multi-line input; Enter sends.", + "Tip: put [multi-select] in an ask_user prompt to switch to a multi-pick picker.", + "Tip: /cost shows token usage; /llm views / switches the model.", + "Tip: /new [name] starts a fresh session; /language switches the interface language.", + "Tip: /export clip copies the last reply to your system clipboard; /export all prints the log path.", + "Tip: Ctrl+O folds / unfolds all completed tool chips — each fold collapses to one line.", + ], + 'zh': [ + "Tip: 按 / 唤起命令面板 —— 方向键选择,Enter 落入输入框。", + "Tip: 粘贴图片 / 文件会折叠成 [Image #N] / [File #N] 占位符,退格可整块删除。", + "Tip: /btw <问题> 让 side-agent 回答而不打断主任务。", + "Tip: /rewind [n] 回退最近 n 轮对话;/stop 中止当前任务。", + "Tip: /continue 列出历史会话 —— 方向键选择,Enter 恢复。", + "Tip: 多行输入用 Ctrl+J / Shift+Enter 换行;Enter 直接发送。", + "Tip: ask_user 题目里写 [多选] 会自动切到多选 picker。", + "Tip: /cost 查看 token 用量;/llm 查看 / 切换模型。", + "Tip: /new [name] 新建会话;/language 切换界面语言。", + "Tip: /export clip 把最后回复复制到系统剪贴板;/export all 打印日志路径。", + "Tip: Ctrl+O 折叠 / 展开所有已完成的工具 chip —— 每个 chip 折叠成一行。", + ], +} + + +# ---------------- translations ---------------- +# Keys are dot-namespaced. Format placeholders use {name}. + +_I18N: dict[str, dict[str, str]] = { + 'en': { + # /help intro & rows — phrasing mirrors v2's COMMANDS table. + 'help.title': 'Commands:', + 'help.help': ' /help Show help', + 'help.status': ' /status View session status', + 'help.sessions': ' /sessions List all sessions', + 'help.llm': ' /llm [n] View / switch model', + 'help.btw': ' /btw Side question — does not interrupt main agent', + 'help.review': ' /review [request] In-session code review (report inline)', + 'help.rewind': ' /rewind [n] Rewind the last n rounds', + 'help.continue': ' /continue [n|name] List / restore historical sessions', + 'help.new': ' /new [name] Start a new session (clears the current one)', + 'help.rename': ' /rename Rename the current session', + 'help.clear': ' /clear Clear display (does not touch LLM history)', + 'help.cost': ' /cost Token usage for the current session', + 'help.verbose': ' /verbose Tool-call audit (↑↓ select · Enter switch · c copy · q quit)', + 'help.export': ' /export [sub] Export last reply: clip / file [name] / all', + 'help.stop': ' /stop Abort current task', + 'help.language': ' /language [code] View / switch interface language', + 'help.quit': ' /quit Quit', + 'help.esc': ' Esc Cancel ask · clear draft · stop task (no exit)', + 'help.cc': ' Ctrl+C × 2 Quit (when idle; only aborts the task while running)', + 'help.cl': ' Ctrl+L Force repaint (recover from sleep/wake)', + 'help.cz': ' Ctrl+Z / Ctrl+Y Undo / redo input edits', + 'help.shift_arrow': ' Shift+←→↑↓ Select text (Ctrl+C copy / Ctrl+X cut / Ctrl+A all)', + + # _CMDS palette entries — same wording as /help, condensed for one-line hint. + 'cmd.help.desc': 'Show help', + 'cmd.status.desc': 'View session status', + 'cmd.llm.desc': 'View / switch model', + 'cmd.btw.desc': 'Side question — does not interrupt main agent', + 'cmd.review.desc': 'In-session code review', + 'cmd.rewind.desc': 'Rewind the last n rounds', + 'cmd.continue.desc': 'List / restore historical sessions', + 'cmd.new.desc': 'Start a new session', + 'cmd.rename.desc': 'Rename the current session', + 'cmd.clear.desc': 'Clear display (LLM history untouched)', + 'cmd.cost.desc': 'Token usage for the current session', + 'cmd.verbose.desc': 'Tool-call audit', + 'cmd.export.desc': 'Export the last reply (clip/file/all)', + 'cmd.stop.desc': 'Abort current task', + 'cmd.language.desc': 'View / switch interface language', + 'cmd.quit.desc': 'Quit', + + # _CMDS arg hints — mirror v2 (lowercase n, full word "question"). + 'cmd.llm.arg': '[n]', + 'cmd.btw.arg': '', + 'cmd.review.arg': '[request]', + 'cmd.rewind.arg': '[n]', + 'cmd.continue.arg': '[n|name]', + 'cmd.new.arg': '[name]', + 'cmd.rename.arg': '', + 'cmd.export.arg': '[clip|file|all]', + 'cmd.language.arg': '[code]', + + # status line (one-liner above input box) + 'status.asking': '◉ waiting · Esc cancel', + 'status.running.tail': ' · Esc stop', + 'status.tps': ' · {rate:.0f} tok/s', + 'status.cc_confirm': 'Press Ctrl+C again to quit', + 'status.ready': '○ ready', + + # /status output rows + 'status.title': ' Session status', + 'status.label.model': 'model:', + 'status.label.state': 'state:', + 'status.label.rounds': 'rounds:', + 'status.label.context': 'context:', + 'status.label.cwd': 'cwd:', + 'status.state.running': 'running · {verb} {elapsed}', + 'status.state.waiting': 'waiting · ask_user pending', + 'status.state.idle': 'idle', + 'status.ctx.unknown': 'n/a', + 'status.ctx.fmt': '{used:,} / {cap:,} ctx', + + # banner + 'banner.label.model': 'model:', + 'banner.label.directory': 'directory:', + 'banner.label.session': 'session:', + 'banner.session.single': 'single · scrollback', + 'banner.llm_hint': '/llm switch', + + # messages — success / status + 'msg.ask_cancelled': '✗ ask cancelled · type freely or ask again', + 'msg.abort_requested': '⏹ abort requested · Esc', + 'msg.abort_done': '⏹ abort requested', + 'msg.idle_no_task': '(idle, no task)', + 'msg.cleared': 'new conversation · context cleared', + 'msg.new_session': 'new session · previous conversation cleared', + 'msg.new_session_named': 'new session "{name}" · previous conversation cleared', + 'msg.renamed': '✎ session renamed to "{name}"', + 'msg.rewind': '↩ rewound {n} turn(s) (removed {removed} history entries; scrollback is not editable)', + 'msg.no_rewindable': 'no rewindable turns', + 'msg.continue_loading': '┄┄ loaded {name}, full context above ┄┄', + 'msg.continue_ready': '┄┄ {msg} · continue typing ┄┄', + 'msg.llm_switched': 'LLM → {name}', + 'msg.export_done': 'exported: {path}', + 'msg.export_clipped': 'copied to clipboard ({n} chars)', + 'msg.export_clip_failed': '❌ copy failed: no clipboard tool found', + 'msg.export_all': 'full log:\n{path}', + 'msg.export_all_missing': 'no log file yet', + 'msg.review_empty': '(review produced no output)', + 'msg.no_export': '(no reply to export)', + 'msg.no_tracker': '(no stats yet)', + 'msg.no_history': ' no restorable historical sessions', + 'msg.no_llms': '(no LLMs available)', + 'msg.no_tools': ' (no tool-call records)', + 'msg.lang_current': 'Current language: {label} ({code})', + 'msg.lang_switched': 'Language → {label}', + 'msg.btw_no_answer': '(no answer)', + 'btw.title': 'side-questions · Esc to clear', + 'btw.querying': ' ⋯ querying…', + + # plan card + 'plan.header': 'Plan ({done}/{total})', + 'plan.complete': '✓ Plan complete ({n}/{n})', + 'plan.placeholder': 'Plan mode activated', + 'plan.waiting': 'waiting for {path} …', + 'plan.overflow': '+{n} more', + + # errors + 'err.running_blocked': 'busy — /stop before using this command', + 'err.continue_usage': 'usage: /continue or /continue N', + 'err.index_oob': '❌ index out of range (valid: 1-{max})', + 'err.btw_usage': 'usage: /btw (does not pollute main context)', + 'err.rewind_usage': 'usage: /rewind ', + 'err.rename_usage': 'usage: /rename ', + 'err.rewind_range': '❌ rewind failed: n must be 1-{max}', + 'err.lang_usage': 'usage: /language [code] (codes: {available})', + 'err.lang_unknown': 'unknown language code: {code} (available: {available})', + 'err.unknown_cmd': 'unknown command /{name} — /help to list', + 'err.multi_session': '/{name}: multi-session backend not wired yet; command reserved', + 'err.menu_cb': '❌ menu callback failed: {err}', + 'err.no_llm': 'No LLM configured — check mykey.py', + 'err.no_tty': 'tui_v3: needs a real TTY (run it in iTerm directly)', + 'err.dep_missing': 'Error: {name} is not installed.', + 'err.dep_install': 'Install with: pip install rich prompt_toolkit', + + # menu / picker / palette + 'menu.default_title': 'Pick', + 'menu.hint': '↑↓ pick · Enter confirm · Esc cancel', + 'ask.default_q': 'answer:', + 'ask.title': '◉ answer', + 'ask.pending': ' +{n} pending', + 'ask.hint.multi': '↳ ↑↓ move · Space toggle · Enter submit · Esc cancel', + 'ask.hint.single': '↳ ↑↓ navigate (options ⇄ input) · Enter confirm · Esc cancel', + 'ask.hint.freetext': '↳ ↑↓ back to options · type to input · Enter submit · Esc cancel', + + # continue picker + 'continue.title': 'Restore historical session', + 'continue.row.fmt': '{rel:>4} {rounds:>3}r {preview}', + 'continue.unit.round': 'r', + + # llm picker + 'llm.title': 'Switch LLM', + + # export picker + 'export.title': 'Export the last reply', + 'export.opt.clip': 'Copy to clipboard', + 'export.opt.file': 'Save to file (temp/)', + 'export.opt.all': 'Show full log path', + + # rewind picker + 'rewind.title': 'Rewind to which turn', + 'rewind.option': 'rewind {n} turn(s) · {preview}', + + # language picker + 'lang.title': 'Switch interface language', + + # verbose + 'verbose.title': ' Tool Trace', + 'verbose.hint': ' ↑↓ pick · PgUp/Dn scroll · Enter switch[{field}] · c copy · e export · q quit', + 'verbose.empty': '(empty)', + + # answer prefix when committing user reply to ask_user + 'msg.answer_prefix': '[ans] {text}', + }, + + 'zh': { + # /help intro & rows — 措辞与 v2 COMMANDS 表对齐。 + 'help.title': '命令:', + 'help.help': ' /help 显示帮助', + 'help.status': ' /status 查看会话状态', + 'help.sessions': ' /sessions 列出所有会话', + 'help.llm': ' /llm [n] 查看 / 切换模型', + 'help.btw': ' /btw 旁问 — 不打断主 agent', + 'help.review': ' /review [request] in-session 代码审查(直接输出报告)', + 'help.rewind': ' /rewind [n] 回退最近 n 轮', + 'help.continue': ' /continue [n|name] 列出 / 恢复历史会话', + 'help.new': ' /new [name] 新建会话(清空当前会话)', + 'help.rename': ' /rename 重命名当前会话', + 'help.clear': ' /clear 清空显示(不动 LLM 历史)', + 'help.cost': ' /cost 显示当前会话 token 用量', + 'help.verbose': ' /verbose 工具调用审计(↑↓ 选 · Enter 切换 · c 复制 · q 退)', + 'help.export': ' /export [sub] 导出最后回复:clip / file [name] / all', + 'help.stop': ' /stop 中止当前任务', + 'help.language': ' /language [code] 查看 / 切换界面语言', + 'help.quit': ' /quit 退出', + 'help.esc': ' Esc 撤回提问 · 清草稿 · 停任务(不退出)', + 'help.cc': ' Ctrl+C × 2 退出(空闲时;运行中只 abort 任务)', + 'help.cl': ' Ctrl+L 强制重画(睡眠唤醒后修复)', + 'help.cz': ' Ctrl+Z / Ctrl+Y 撤销 / 重做 输入框编辑', + 'help.shift_arrow': ' Shift+←→↑↓ 选中文字(Ctrl+C 复制 / Ctrl+X 剪切 / Ctrl+A 全选)', + + # _CMDS palette entries — 与 /help 同源,命令面板单行显示。 + 'cmd.help.desc': '显示帮助', + 'cmd.status.desc': '查看会话状态', + 'cmd.llm.desc': '查看 / 切换模型', + 'cmd.btw.desc': '旁问 — 不打断主 agent', + 'cmd.review.desc': 'in-session 代码审查', + 'cmd.rewind.desc': '回退最近 n 轮', + 'cmd.continue.desc': '列出 / 恢复历史会话', + 'cmd.new.desc': '新建会话', + 'cmd.rename.desc': '重命名当前会话', + 'cmd.clear.desc': '清空显示(不动 LLM 历史)', + 'cmd.cost.desc': '显示当前会话 token 用量', + 'cmd.verbose.desc': '工具调用审计', + 'cmd.export.desc': '导出最后回复(剪贴板/文件/日志路径)', + 'cmd.stop.desc': '中止当前任务', + 'cmd.language.desc': '查看 / 切换界面语言', + 'cmd.quit.desc': '退出', + + # arg hints — 与 v2 对齐:小写 n、完整的 question 等。 + 'cmd.llm.arg': '[n]', + 'cmd.btw.arg': '', + 'cmd.review.arg': '[request]', + 'cmd.rewind.arg': '[n]', + 'cmd.continue.arg': '[n|name]', + 'cmd.new.arg': '[name]', + 'cmd.rename.arg': '', + 'cmd.export.arg': '[clip|file|all]', + 'cmd.language.arg': '[code]', + + # status line + 'status.asking': '◉ 待答 · Esc 撤回提问', + 'status.running.tail': ' · Esc 停', + 'status.tps': ' · {rate:.0f} tok/s', + 'status.cc_confirm': '再按 Ctrl+C 退出', + 'status.ready': '○ 就绪', + + # /status + 'status.title': ' 会话状态', + 'status.label.model': 'model:', + 'status.label.state': 'state:', + 'status.label.rounds': 'rounds:', + 'status.label.context': 'context:', + 'status.label.cwd': 'cwd:', + 'status.state.running': 'running · {verb} {elapsed}', + 'status.state.waiting': 'waiting · ask_user pending', + 'status.state.idle': 'idle', + 'status.ctx.unknown': 'n/a', + 'status.ctx.fmt': '{used:,} / {cap:,} ctx', + + # banner + 'banner.label.model': 'model:', + 'banner.label.directory': 'directory:', + 'banner.label.session': 'session:', + 'banner.session.single': '单会话 · scrollback', + 'banner.llm_hint': '/llm 切换', + + # messages + 'msg.ask_cancelled': '✗ 已撤回提问 · 可直接输入或重新发问', + 'msg.abort_requested': '⏹ 已请求中止 · Esc', + 'msg.abort_done': '⏹ 已请求中止', + 'msg.idle_no_task': '(空闲,无任务)', + 'msg.cleared': '新对话 · 上下文已清空', + 'msg.new_session': '新会话 · 上一段对话已清空', + 'msg.new_session_named': '新会话「{name}」· 上一段对话已清空', + 'msg.renamed': '✎ 会话已重命名为「{name}」', + 'msg.rewind': '↩ 回退 {n} 轮(移除 {removed} 条历史;scrollback 不可改,以此为界)', + 'msg.no_rewindable': '没有可回退的轮次', + 'msg.continue_loading': '┄┄ 载入 {name},以下为完整上文 ┄┄', + 'msg.continue_ready': '┄┄ {msg} · 接着说即可 ┄┄', + 'msg.llm_switched': 'LLM → {name}', + 'msg.export_done': '已导出: {path}', + 'msg.export_clipped': '已复制到剪贴板({n} 字符)', + 'msg.export_clip_failed': '❌ 复制失败:未找到剪贴板工具', + 'msg.export_all': '完整日志:\n{path}', + 'msg.export_all_missing': '尚无日志文件', + 'msg.review_empty': '(review 无输出)', + 'msg.no_export': '(没有可导出的回答)', + 'msg.no_tracker': '(暂无统计)', + 'msg.no_history': ' 没有可恢复的历史会话', + 'msg.no_llms': '(无可用 LLM)', + 'msg.no_tools': ' (暂无工具调用记录)', + 'msg.lang_current': '当前界面语言:{label}({code})', + 'msg.lang_switched': '界面语言 → {label}', + 'msg.btw_no_answer': '(无回答)', + 'btw.title': '旁问 · Esc 清空', + 'btw.querying': ' ⋯ 查询中…', + + # plan card + 'plan.header': '计划 ({done}/{total})', + 'plan.complete': '✓ 计划完成 ({n}/{n})', + 'plan.placeholder': '计划模式已激活', + 'plan.waiting': '等待写入 {path} …', + 'plan.overflow': '还有 {n} 项', + + # errors + 'err.running_blocked': '运行中,先 /stop 再用该命令', + 'err.continue_usage': '用法: /continue 或 /continue N', + 'err.index_oob': '❌ 索引越界(有效 1-{max})', + 'err.btw_usage': '用法: /btw <旁问>(不污染主上下文)', + 'err.rewind_usage': '用法:/rewind ', + 'err.rename_usage': '用法:/rename ', + 'err.rewind_range': '❌ 回退失败:n 应在 1-{max}', + 'err.lang_usage': '用法:/language [code] (可选 code:{available})', + 'err.lang_unknown': '未知语言代码:{code} (可选:{available})', + 'err.unknown_cmd': '未知命令 /{name} — /help 看可用命令', + 'err.multi_session': '/{name}:多会话后端尚未接入,命令已预留但暂未实现', + 'err.menu_cb': '❌ 菜单回调失败: {err}', + 'err.no_llm': 'No LLM configured — check mykey.py', + 'err.no_tty': 'tui_v3: needs a real TTY (run it in iTerm directly)', + 'err.dep_missing': 'Error: {name} is not installed.', + 'err.dep_install': 'Install with: pip install rich prompt_toolkit', + + # menu / picker / palette + 'menu.default_title': '选择', + 'menu.hint': '↑↓ 选 · Enter 确认 · Esc 取消', + 'ask.default_q': '请回答:', + 'ask.title': '◉ 请回答', + 'ask.pending': ' +{n} 待答', + 'ask.hint.multi': '↳ ↑↓ 移动 · Space 标记 · Enter 提交 · Esc 撤回', + 'ask.hint.single': '↳ ↑↓ 切换(选项 ⇄ 输入框)· Enter 确认 · Esc 撤回', + 'ask.hint.freetext': '↳ ↑↓ 回到选项 · 输字符输入 · Enter 提交 · Esc 撤回', + + # continue picker + 'continue.title': '恢复历史会话', + 'continue.row.fmt': '{rel:>4} {rounds:>3}轮 {preview}', + 'continue.unit.round': '轮', + + # llm picker + 'llm.title': '切换 LLM', + + # export picker + 'export.title': '导出最后回复', + 'export.opt.clip': '复制到剪贴板', + 'export.opt.file': '保存到文件(temp/)', + 'export.opt.all': '显示完整日志路径', + + # rewind picker + 'rewind.title': '选择回退到的轮次', + 'rewind.option': '回退 {n} 轮 · {preview}', + + # language picker + 'lang.title': '切换界面语言', + + # verbose + 'verbose.title': ' Tool Trace', + 'verbose.hint': ' ↑↓ 选 · PgUp/Dn 滚 · Enter 切换[{field}] · c 复制 · e 导出 · q 退', + 'verbose.empty': '(空)', + + # answer prefix + 'msg.answer_prefix': '[答] {text}', + }, +} + + +def t(key: str, **fmt) -> str: + """Translate `key` to current language; fall back to en then to key itself. + Missing format fields raise KeyError — caller bug, not i18n bug.""" + val = _I18N.get(_LANG, {}).get(key) + if val is None and _LANG != 'en': + val = _I18N.get('en', {}).get(key) + if val is None: + val = key # visible breadcrumb for missing keys + if fmt: + try: + return val.format(**fmt) + except (KeyError, IndexError, ValueError): + return val + return val + + +def tip_count() -> int: + """Number of rotating banner tips (same for every language).""" + return len(_TIPS['en']) + + +def tip(idx: int) -> str: + """Banner tip at `idx`, resolved in the current language so a /language + switch relabels it on the next banner repaint.""" + pool = _TIPS.get(_LANG) or _TIPS['en'] + return pool[idx % len(pool)] if pool else '' + + +def get_lang() -> str: + return _LANG + + +def set_lang(code: str) -> bool: + """Switch active language and persist. Returns True on success.""" + global _LANG + if code not in _SUPPORTED: + return False + _LANG = code + _save_settings({'lang': code}) + return True + + +def supported() -> tuple[str, ...]: + return _SUPPORTED + + +def _detect_system_lang() -> str: + """System language: check LC_ALL → LC_MESSAGES → LANG → locale.getlocale(). + Prefix `zh` → 'zh', else 'en'.""" + for env in ('LC_ALL', 'LC_MESSAGES', 'LANG'): + v = os.environ.get(env) + if v: + if v.lower().startswith('zh'): + return 'zh' + return 'en' + try: + loc = locale.getlocale()[0] or '' + if loc.lower().startswith('zh'): + return 'zh' + except Exception: + pass + return _DEFAULT + + +def _load_settings() -> dict: + try: + with open(_SETTINGS_PATH, 'r', encoding='utf-8') as f: + data = json.load(f) + return data if isinstance(data, dict) else {} + except Exception: + return {} + + +def _save_settings(patch: dict) -> None: + cur = _load_settings() + cur.update(patch) + try: + os.makedirs(os.path.dirname(_SETTINGS_PATH), exist_ok=True) + with open(_SETTINGS_PATH, 'w', encoding='utf-8') as f: + json.dump(cur, f, ensure_ascii=False, indent=2) + except Exception: + pass + + +def init_lang() -> str: + """Resolve and install initial language: persisted > system > default. + Call once at startup; returns the resolved code.""" + global _LANG + saved = _load_settings().get('lang') + if saved in _SUPPORTED: + _LANG = saved + else: + _LANG = _detect_system_lang() + return _LANG + + +# `_t` alias kept so the hundreds of existing `_t(...)` call sites are untouched. +_t = t + +# Resolve language once on import so any module-level string (banner, _CMDS, +# /help) sees the right locale. +init_lang() +# ════════════════════════════════════════════════════════════════════════════ + + +# Module-level `clip` shim: keep sb.py-style `clip.copy(...)` calls +# working without a separate clipboard module — the underlying funcs +# (copy, paste, paste_image) are defined later in this same file. +class _Clip: + @staticmethod + def copy(text): return copy(text) + @staticmethod + def paste(): return paste() + @staticmethod + def paste_image(): return paste_image() +clip = _Clip() + + +def _enable_windows_vt_mode() -> None: + """Enable UTF-8 + ANSI escape processing on Windows consoles when possible.""" + if not _IS_WINDOWS: + return + try: + import ctypes + kernel32 = ctypes.windll.kernel32 + # Make classic conhost/cmd decode UTF-8 bytes written by _w(). This is + # harmless in mintty/Git Bash where these calls usually fail because the + # std handles are pipes/ptys rather than Win32 console handles. + kernel32.SetConsoleOutputCP(65001) + kernel32.SetConsoleCP(65001) + enable_vt = 0x0004 + for handle_id in (-11, -12): # STD_OUTPUT_HANDLE, STD_ERROR_HANDLE + handle = kernel32.GetStdHandle(handle_id) + mode = ctypes.c_uint32() + if kernel32.GetConsoleMode(handle, ctypes.byref(mode)): + kernel32.SetConsoleMode(handle, mode.value | enable_vt) + except Exception: + # Safe fallback: modern terminals usually already support ANSI/UTF-8; + # older conhost may render escape codes, but the TUI should not crash. + pass + + +def _enter_utf8_charset() -> None: + """Ask VT-compatible terminals to interpret subsequent bytes as UTF-8.""" + # ESC % G is the ISO-2022/VT sequence for selecting UTF-8. It fixes some + # mintty/Git-Bash launches where the child process inherits a legacy locale + # and mojibakes UTF-8 box drawing/CJK into CP936-looking text. + _w('\x1b%G') + + +def _ptk_keypress_to_bytes(kp) -> bytes: + """Map prompt_toolkit KeyPress objects to tui_v3's internal byte protocol. + + prompt_toolkit normalizes platform-specific console input (Win32 console, + ConPTY, mintty/msys pty) into symbolic Keys. Keep the editor core small by + translating those symbols back to the bytes already handled by _feed/_keys. + """ + try: + from prompt_toolkit.keys import Keys + except Exception: + Keys = None # type: ignore[assignment] + + key = getattr(kp, 'key', None) + data = getattr(kp, 'data', '') or '' + + # Printable text and paste chunks. PTK may deliver a multi-character data + # string for bracketed paste/typeahead; forwarding UTF-8 preserves CJK/emoji. + if isinstance(data, str) and data and (len(data) > 1 or (len(data) == 1 and ord(data) >= 0x20 and data != '\x7f')): + return data.encode('utf-8', 'replace') + + name = getattr(key, 'name', str(key)) + key_s = str(key) + norm = name.lower().replace('_', '-').replace('keys.', '') + norm_s = key_s.lower().replace('_', '-').replace('keys.', '') + aliases = {norm, norm_s} + + def has(*needles: str) -> bool: + return any(n in a for a in aliases for n in needles) + + # Enter submits; Ctrl+J / Shift+Enter insert newline when PTK can distinguish. + if has('controlm', 'c-m') or key_s in ('\r', '\n'): + return b'\r' + if has('controlj', 'c-j', 's-enter', 'shift-enter'): + return b'\n' + + # Navigation. Existing _keys() uses these small control bytes. + if has('up') and has('shift'): + return b'\x1c' + if has('down') and has('shift'): + return b'\x1d' + if has('left') and has('shift'): + return b'\x1e' + if has('right') and has('shift'): + return b'\x1f' + if has('up'): + return b'\x10' + if has('down'): + return b'\x0e' + if has('left'): + return b'\x02' + if has('right'): + return b'\x06' + if has('home'): + return b'\x01' + if has('end'): + return b'\x05' + if has('delete'): + return b'\x7f' + if has('backspace') or data == '\x7f' or data == '\x08': + return b'\x7f' + if has('escape') or data == '\x1b': + return b'\x1b' + + ctrl = { + 'controla': b'\x01', 'c-a': b'\x01', + 'controlb': b'\x02', 'c-b': b'\x02', + 'controlc': b'\x03', 'c-c': b'\x03', + 'controld': b'\x04', 'c-d': b'\x04', + 'controle': b'\x05', 'c-e': b'\x05', + 'controlf': b'\x06', 'c-f': b'\x06', + 'controlh': b'\x7f', 'c-h': b'\x7f', + 'controlj': b'\n', 'c-j': b'\n', + 'controlk': b'\x0b', 'c-k': b'\x0b', + 'controll': b'\x0c', 'c-l': b'\x0c', + 'controln': b'\x0e', 'c-n': b'\x0e', + 'controlp': b'\x10', 'c-p': b'\x10', + 'controlu': b'\x15', 'c-u': b'\x15', + 'controlv': b'\x16', 'c-v': b'\x16', + 'controlx': b'\x18', 'c-x': b'\x18', + 'controly': b'\x19', 'c-y': b'\x19', + 'controlz': b'\x1a', 'c-z': b'\x1a', + } + for a in aliases: + if a in ctrl: + return ctrl[a] + + if isinstance(data, str) and data: + return data.encode('utf-8', 'replace') + return b'' + + +# ──────────────────────────────────────────────────────────────────────────── +# cjk: CJK wrap monkey-patch for Rich +# ──────────────────────────────────────────────────────────────────────────── + +log = logging.getLogger(__name__) + +_CJK_RANGES = ( + (0x4E00, 0x9FFF), (0x3400, 0x4DBF), (0x20000, 0x2A6DF), + (0x2A700, 0x2B73F), (0x2B740, 0x2B81F), (0x2B820, 0x2CEAF), + (0x2CEB0, 0x2EBEF), (0xF900, 0xFAFF), (0x2F800, 0x2FA1F), + (0x3000, 0x303F), (0x3040, 0x309F), (0x30A0, 0x30FF), + (0x31F0, 0x31FF), (0xFF00, 0xFFEF), (0xAC00, 0xD7AF), +) + + +def _is_cjk(ch: str) -> bool: + cp = ord(ch) + return any(lo <= cp <= hi for lo, hi in _CJK_RANGES) + + +def _is_wide(ch: str) -> bool: + try: + from rich.cells import cell_len + return cell_len(ch) == 2 + except ImportError: + return _is_cjk(ch) + + +def install_cjk_wrap() -> bool: + """Monkey-patch Rich's word-wrap to handle CJK char-level breaks. + Returns True on success, False on fallback.""" + try: + import rich._wrap as wrap_mod + from rich.cells import cell_len + except (ImportError, AttributeError) as e: + log.warning("CJK patch skipped: %s", e) + return False + + orig_divide = getattr(wrap_mod, 'divide_line', None) + if orig_divide is None: + log.warning("CJK patch skipped: Rich lacks divide_line") + return False + + def _patched_divide_line(text, width, fold=True): + divides = set() + line_width = 0 + for i, ch in enumerate(text._text if hasattr(text, '_text') else str(text)): + char_w = cell_len(ch) if ch != '\n' else 0 + if line_width + char_w > width and line_width > 0: + if _is_wide(ch) or fold: + divides.add(i) + line_width = char_w + continue + line_width += char_w + if ch == '\n': + line_width = 0 + # Merge with original for non-CJK content + try: + orig_divides = orig_divide(text, width, fold) + divides.update(orig_divides) + except Exception: + pass + return sorted(divides) + + try: + wrap_mod.divide_line = _patched_divide_line + log.info("CJK wrap patch installed for Rich %s", _rich_version()) + return True + except Exception as e: + log.warning("CJK patch failed: %s", e) + return False + + +def _rich_version() -> str: + try: + from importlib.metadata import version + return version('rich') + except Exception: + return '?' + + +# ──────────────────────────────────────────────────────────────────────────── +# clipboard: cross-platform copy/paste via native tools +# ──────────────────────────────────────────────────────────────────────────── + +log = logging.getLogger(__name__) + +_TEMP_DIR = os.path.join(tempfile.gettempdir(), 'genericagent_tui') +_platform = sys.platform +_HAS_WAYLAND = bool(os.environ.get('WAYLAND_DISPLAY')) + + +def _run(cmd: list[str], input: bytes | None = None, timeout: float = 3.0) -> bytes | None: + try: + r = subprocess.run(cmd, input=input, capture_output=True, timeout=timeout) + return r.stdout if r.returncode == 0 else None + except (FileNotFoundError, subprocess.TimeoutExpired, OSError) as e: + log.debug("clipboard cmd %s failed: %s", cmd, e) + return None + + +def copy(text: str) -> bool: + data = text.encode('utf-8') + if _platform == 'darwin': + return _run(['pbcopy'], input=data) is not None + if _platform == 'win32': + return _run(['clip.exe'], input=data) is not None + if _HAS_WAYLAND and shutil.which('wl-copy'): + return _run(['wl-copy'], input=data) is not None + if shutil.which('xclip'): + return _run(['xclip', '-selection', 'clipboard'], input=data) is not None + if shutil.which('xsel'): + return _run(['xsel', '--clipboard', '--input'], input=data) is not None + log.warning("No clipboard tool found") + return False + + +def paste() -> str | None: + out: bytes | None = None + if _platform == 'darwin': + out = _run(['pbpaste']) + elif _platform == 'win32': + out = _run(['powershell', '-NoProfile', '-Command', 'Get-Clipboard']) + elif _HAS_WAYLAND and shutil.which('wl-paste'): + out = _run(['wl-paste', '--no-newline']) + elif shutil.which('xclip'): + out = _run(['xclip', '-selection', 'clipboard', '-o']) + elif shutil.which('xsel'): + out = _run(['xsel', '--clipboard', '--output']) + if out is not None: + return out.decode('utf-8', errors='replace') + return None + + +def paste_image() -> str | None: + """Save clipboard image to temp file, return path or None.""" + os.makedirs(_TEMP_DIR, exist_ok=True) + import time + path = os.path.join(_TEMP_DIR, f'clip_{int(time.time()*1000)}.png') + ok = False + if _platform == 'darwin': + script = ( + 'use framework "AppKit"\n' + 'set pb to current application\'s NSPasteboard\'s generalPasteboard()\n' + 'set imgData to pb\'s dataForType:"public.png"\n' + 'if imgData is missing value then error "no image"\n' + 'imgData\'s writeToFile:"' + path + '" atomically:true\n' + ) + ok = _run(['osascript', '-e', script], timeout=5.0) is not None + elif _HAS_WAYLAND and shutil.which('wl-paste'): + data = _run(['wl-paste', '-t', 'image/png']) + if data: + with open(path, 'wb') as f: + f.write(data) + ok = True + elif shutil.which('xclip'): + data = _run(['xclip', '-selection', 'clipboard', '-t', 'image/png', '-o']) + if data and len(data) > 8: + with open(path, 'wb') as f: + f.write(data) + ok = True + return path if ok and os.path.isfile(path) else None + + +def _grab_clipboard_file() -> tuple[str, bool] | None: + """Return (path, is_image) from the clipboard via PIL — ported from v2. + + PIL.ImageGrab.grabclipboard() is the one cross-platform path that also + works on Windows (osascript/xclip/wl-paste below don't). It handles two + shapes: a list of copied file paths, or a raw bitmap Image (saved to a + temp PNG). is_image distinguishes images (→ `[Image #N]`, sent to the + model) from any other file (→ `[File #N]`, expanded to its path).""" + try: + from PIL import ImageGrab, Image + data = ImageGrab.grabclipboard() + except Exception: + return None + if isinstance(data, list): + for item in data: + if isinstance(item, str) and os.path.isfile(item): + return (item, os.path.splitext(item)[1].lower() in _IMAGE_EXTS) + return None + if isinstance(data, Image.Image): + try: + os.makedirs(_TEMP_DIR, exist_ok=True) + path = os.path.join(_TEMP_DIR, f'clip_{int(time.time() * 1000)}.png') + data.save(path, 'PNG') + return (path, True) + except Exception: + return None + return None + + +def _cleanup(): + if os.path.isdir(_TEMP_DIR): + shutil.rmtree(_TEMP_DIR, ignore_errors=True) + +atexit.register(_cleanup) + + +# ──────────────────────────────────────────────────────────────────────────── +# renderer: markdown / ANSI sanitisation / fold +# ──────────────────────────────────────────────────────────────────────────── + +# Comprehensive ANSI sanitization — matches v2's thoroughness +_ANSI_INCOMPLETE_RE = re.compile(r'\x1b\[[0-9;]*$') +_ANSI_DEC_PRIVATE_RE = re.compile(r'\x1b\[\?[0-9;]*[a-zA-Z]') +_ANSI_OSC_RE = re.compile(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?') +_ANSI_MODE_SET_RE = re.compile(r'\x1b[=>][0-9]*') +# Keep SGR (color) codes, strip everything else +_ANSI_SGR_RE = re.compile(r'\x1b\[[0-9;]*m') + +_TURN_MARKER_RE = re.compile(r'\*\*LLM Running \(Turn (\d+)\).*?\*\*') +_META_TAG_RE = re.compile(r'<(?:thinking|summary|tool_use|file_content)>.*?', re.DOTALL) +_TOOL_USE_BLOCK_RE = re.compile(r'```json\s*\{[^}]*"tool_name"[^}]*\}\s*```', re.DOTALL) +_TOOL_USE_TAG_RE = re.compile(r'\s*\{.*?"tool_name"\s*:\s*"([^"]+)".*?\}\s*', re.DOTALL) +_SUMMARY_RE = re.compile(r'\s*(.*?)\s*', re.DOTALL) +_QUAD_BACKTICK_RE = re.compile(r'(`{4,})') +_ASK_USER_RE = re.compile(r'"tool_name"\s*:\s*"ask_user".*?"question"\s*:\s*"([^"]*)"', re.DOTALL) + +# v2 fold_turns helpers (tuiapp_v2.py:240-267). 4-fence stash keeps a tool +# result's `` ``` `` from being misread as a turn boundary; the per-turn +# `` regex uses a negative lookahead so two adjacent summaries don't +# merge; the title cleaner strips fenced code + thinking before extraction. +_FENCE4_STASH_RE = re.compile(r'^`{4,}.*?^`{4,}\n?', re.DOTALL | re.MULTILINE) +_TURN_SPLIT_FOLD_RE = re.compile(r'(\*\*LLM Running \(Turn \d+\) \.\.\.\*\*)') +_SUMMARY_PERTURN_RE = re.compile(r'\s*((?:(?!).)*?)\s*', re.DOTALL) +_TITLE_CLEAN_RE = re.compile(r'`{3,}.*?`{3,}|.*?', re.DOTALL) +_TITLE_ARGS_TAIL_RE = re.compile(r',?\s*args:.*$') + + +@dataclass +class FoldSegment: + title: str + body: str + turn: int + is_last: bool = False + + +@dataclass +class Block: + """A unit of finalized scrollback history, stored as SOURCE (not rendered + lines). Resize replays each block through its renderer at the new width, + so width-baked structures (chip boxes, banner box) reflow correctly. The + actual scrollback bytes above the viewport stay frozen at the old width — + that's a terminal physics constraint — but the viewport and everything + new flows correctly.""" + kind: str # 'user' | 'assistant' | 'plain' | 'banner' + source: str # source text (or '' for banner — regenerated on demand) + tool_n: int = 0 # tool count cached at last render (for stable tids) + + +def sanitize_ansi(text: str) -> str: + """Strip non-SGR ANSI escapes and incomplete sequences from streaming chunks.""" + text = _ANSI_DEC_PRIVATE_RE.sub('', text) + text = _ANSI_OSC_RE.sub('', text) + text = _ANSI_MODE_SET_RE.sub('', text) + text = _ANSI_INCOMPLETE_RE.sub('', text) + return text + + +def _render_checkboxes(text: str) -> str: + """Convert markdown task lists to visual checkboxes.""" + text = re.sub(r'^(\s*[-*+]\s)\[ \]', r'\1☐', text, flags=re.MULTILINE) + text = re.sub(r'^(\s*[-*+]\s)\[x\]', r'\1☑', text, flags=re.MULTILINE | re.IGNORECASE) + return text + + +def strip_meta_tags(text: str) -> str: + """Strip internal tags, render tool_use as readable summaries.""" + def _tool_replace(m): + name = m.group(1) + if name == 'ask_user': + q_match = _ASK_USER_RE.search(m.group(0)) + if q_match: + return f'> {q_match.group(1)}' + return f'🔧 {name}' + text = _TOOL_USE_TAG_RE.sub(_tool_replace, text) + text = _META_TAG_RE.sub('', text) + text = _TOOL_USE_BLOCK_RE.sub('', text) + text = _render_checkboxes(text) + return re.sub(r'\n{3,}', '\n\n', text).strip() + + +def _extract_title(text: str, max_len: int = 72) -> str: + m = _SUMMARY_RE.search(text) + if m: + title = m.group(1).strip() + else: + first = text.strip().split('\n')[0] if text.strip() else '' + title = re.sub(r'^[#*>\-\s]+', '', first).strip() + if len(title) > max_len: + title = title[:max_len - 1] + '…' + return title or '...' + + +def fold_segments(text: str) -> list[FoldSegment]: + """Split agent response into per-turn fold segments.""" + if not text: + return [] + cleaned = _QUAD_BACKTICK_RE.sub(lambda m: '~' * len(m.group(1)), text) + parts = _TURN_MARKER_RE.split(cleaned) + if len(parts) <= 1: + return [FoldSegment(title=_extract_title(text), body=text, turn=1, is_last=True)] + segments: list[FoldSegment] = [] + if parts[0].strip(): + segments.append(FoldSegment(title=_extract_title(parts[0]), body=parts[0], turn=0)) + for i in range(1, len(parts), 2): + turn_num = int(parts[i]) if i < len(parts) else len(segments) + 1 + body = parts[i + 1] if i + 1 < len(parts) else '' + body = body.replace('~' * 4, '````') + segments.append(FoldSegment(title=_extract_title(body), body=body, turn=turn_num)) + if segments: + segments[-1].is_last = True + return segments + + +# Render cache: (content_hash, width) -> rendered object +_render_cache: dict[tuple[int, int], object] = {} +_CACHE_MAX = 200 + + +class HardBreakMarkdown(Markdown): + """Markdown that treats softbreaks as hardbreaks, preserving code blocks.""" + def __init__(self, markup: str, **kwargs): + lines = [] + in_code = False + for line in markup.split('\n'): + stripped = line.strip() + if stripped.startswith('```'): + in_code = not in_code + if in_code: + lines.append(line) + else: + lines.append(line + ' ') + super().__init__('\n'.join(lines), **kwargs) + + +def _markdown_to_text(cleaned: str, width: int) -> Text: + """Render Markdown to a CONCRETE Text (v2 approach). A Textual Static holding + a live rich.markdown.Markdown does not re-composite reliably when scrolled + past the viewport (height measurement is unstable → frozen/blank scroll); + a pre-rendered Text has a fixed line count and scrolls correctly.""" + from io import StringIO + from rich.console import Console + buf = StringIO() + Console(file=buf, width=max(1, width), force_terminal=True, + color_system='truecolor', legacy_windows=False + ).print(HardBreakMarkdown(cleaned), end='') + return Text.from_ansi(buf.getvalue().rstrip('\n')) + + +def render_message(text: str, role: str = 'assistant', width: int = 0) -> Text: + """Render a message to a concrete Text. width<=0 → provisional plain text + (the widget re-renders via on_resize once its real width is known).""" + cleaned = strip_meta_tags(text) if role == 'assistant' else text + if not cleaned.strip(): + cleaned = '...' + if role == 'system': + return Text(cleaned, style='dim') + if width <= 0: + return Text(cleaned) + key = (hash(cleaned), width) + cached = _render_cache.get(key) + if cached is not None: + return cached + try: + result = _markdown_to_text(cleaned, width) + except Exception: + result = Text(cleaned) + if len(_render_cache) >= _CACHE_MAX: + for k in list(_render_cache.keys())[:_CACHE_MAX // 4]: + _render_cache.pop(k, None) + _render_cache[key] = result + return result + + +# ──────────────────────────────────────────────────────────────────────────── +# protocol: AgentBridge + typed events over display_queue +# ──────────────────────────────────────────────────────────────────────────── + +@dataclass(frozen=True) +class StreamEvent: + text: str + turn: int = 0 + source: str = "user" + +@dataclass(frozen=True) +class DoneEvent: + text: str + turn: int = 0 + source: str = "user" + outputs: list[str] = field(default_factory=list) + +@dataclass(frozen=True) +class AskUserEvent: + question: str + candidates: list[str] = field(default_factory=list) + +@dataclass(frozen=True) +class SystemEvent: + text: str + +@dataclass(frozen=True) +class ErrorEvent: + message: str + exception: Exception | None = None + +AgentEvent = StreamEvent | DoneEvent | AskUserEvent | SystemEvent | ErrorEvent + +_HOOK_KEY = '_tui_v3_ask_user' + + +def _extract_ask_user(ctx: dict | None) -> AskUserEvent | None: + er = (ctx or {}).get('exit_reason') or {} + if er.get('result') != 'EXITED': + return None + payload = er.get('data') or {} + if payload.get('status') != 'INTERRUPT' or payload.get('intent') != 'HUMAN_INTERVENTION': + return None + data = payload.get('data') or {} + return AskUserEvent( + question=data.get('question', ''), + candidates=data.get('candidates', []), + ) + + +class AgentBridge: + """Wraps GenericAgent for the TUI. One bridge per session.""" + + def __init__(self, llm_no: int = 0): + self.agent = GeneraticAgent() + self.agent.llm_no = llm_no + if llm_no and hasattr(self.agent, 'llmclients') and self.agent.llmclients: + self.agent.llmclient = self.agent.llmclients[llm_no % len(self.agent.llmclients)] + self.agent.inc_out = True + self.agent.verbose = True + self.ask_user_queue: queue.Queue[AskUserEvent] = queue.Queue() + self._install_hook() + self._healthy = True + self._init_error: str | None = None + if not getattr(self.agent, 'llmclient', None): + self._healthy = False + self._init_error = _t('err.no_llm') + self._runner = threading.Thread(target=self._run_safe, daemon=True, name=f'ga-tui-agent') + self._runner.start() + + def _run_safe(self): + try: + self.agent.run() + except Exception as e: + self._healthy = False + self._init_error = str(e) + + def _install_hook(self): + if not hasattr(self.agent, '_turn_end_hooks'): + self.agent._turn_end_hooks = {} + self.agent._turn_end_hooks[_HOOK_KEY] = self._on_turn_end + + def _on_turn_end(self, ctx: dict): + ev = _extract_ask_user(ctx) + if ev: + self.ask_user_queue.put(ev) + + def submit(self, query: str, images: list | None = None) -> queue.Queue: + return self.agent.put_task(query, source='user', images=images) + + def abort(self): + self.agent.abort() + + @property + def is_running(self) -> bool: + return self.agent.is_running + + @property + def llm_name(self) -> str: + try: + return self.agent.get_llm_name() + except Exception: + return '?' + + def list_llms(self) -> list[tuple[int, str, bool]]: + return self.agent.list_llms() + + def switch_llm(self, n: int): + self.agent.next_llm(n) + + def drain_display_queue(self, dq: queue.Queue, timeout: float = 0.25): + """Generator: yields typed events from a display_queue.""" + while True: + try: + item = dq.get(timeout=timeout) + except queue.Empty: + yield None + continue + if not isinstance(item, dict): + continue + if 'done' in item: + yield DoneEvent( + text=item['done'], + turn=item.get('turn', 0), + source=item.get('source', 'user'), + outputs=item.get('outputs', []), + ) + break + if 'next' in item: + yield StreamEvent( + text=item['next'], + turn=item.get('turn', 0), + source=item.get('source', 'user'), + ) + + +# ──────────────────────────────────────────────────────────────────────────── +# sb: scrollback-first TUI core (input, paint, flow, ask, /verbose, …) +# ──────────────────────────────────────────────────────────────────────────── + +# Prose hierarchy via ATTRIBUTES only (bold/italic/underline) — NO dim for body +# content (dim on white = unreadable grey). Keep normal prose inherited from the +# surrounding tile; avoid Rich's inline-code reverse without pinning prose dark. +_MD_THEME = Theme({ + 'markdown.h1': 'bold underline', 'markdown.h2': 'bold underline', + 'markdown.h3': 'bold', 'markdown.h4': 'bold', + 'markdown.h5': 'bold', 'markdown.h6': 'bold', + 'markdown.strong': 'bold', 'markdown.em': 'italic', + # Rich's default inline-code ``reverse`` can vanish on themed tiles; keep it + # bold but inherited so it stays readable on both light and dark surfaces. + 'markdown.code': 'bold', 'markdown.code_block': 'none', + 'markdown.block_quote': 'italic', 'markdown.hr': 'none', + 'markdown.link': 'underline', 'markdown.link_url': 'underline', + # Bullet inherits the surrounding foreground (a pinned dark hue vanished on + # dark terminals); bold alone keeps it visible on any background. + 'markdown.item.bullet': 'bold', +}, inherit=True) + + +PROMPT = '❯ ' +CONT = ' ' +# macOS Terminal.app quantises ALL truecolor escapes to their nearest 256-color +# slot, and the slot it picks for #5e6ad2 (iTerm lavender) is 62/#5f5fd7 — that's +# the "blue" border the user sees. It also renders \x1b[2m as a heavy 30%-opacity +# multiply instead of the gentle blend iTerm does — that's the "heavy shadow". +# Branching on TERM_PROGRAM lets iTerm keep its truecolor + dim look, while +# Apple_Terminal uses pinned 256-color slots that match iTerm's RENDERED result. +_IS_APPLE_TERMINAL = os.environ.get('TERM_PROGRAM') == 'Apple_Terminal' +_RST = '\x1b[0m' +if _IS_APPLE_TERMINAL: + _DIM = '\x1b[38;5;244m' # mid-gray — no \x1b[2m, no "shadow" + _ACCENT = '\x1b[38;5;105m' # 256-slot light purple, closest to iTerm rendered look + _BORDER = '\x1b[38;5;146m' # light lavender +else: + _DIM = '\x1b[2m' + _ACCENT = '\x1b[38;2;94;106;210m' # Linear lavender #5e6ad2 + _BORDER = '\x1b[38;5;146m' +_INK_U = '\x1b[38;5;234m' # user ink — near-black, strong (as requested) +# Linear surface ladder: user gets its own panel; AI = plain white surface. +_TILE_U = '\x1b[48;5;251m' + _INK_U # user panel (near-black ink) +_MARK = _ACCENT + '❯' + _RST # prompt mark — the single accent +_BG_TOK = {str(n) for n in list(range(40, 48)) + [49] + list(range(100, 108))} +_SGR_RE = re.compile(r'\x1b\[([0-9;]*)m') +_CSI_ERASE_RE = re.compile(r'\x1b\[[0-9;?]*[JK]') +_SGR_TOKEN_RE = re.compile(r'\x1b\[[0-9;]*m') + + +def _tile(s: str, style: str) -> str: + # re-assert style after every reset so muted-markdown \x1b[0m can't punch + # a hole in the block; \x1b[K fills to the edge (CJK-safe full-width tile). + return style + s.replace(_RST, _RST + style) + '\x1b[K' + _RST + + +def _border(left: str, right: str, width: int, style: str = _BORDER) -> str: + width = max(1, width) + if width == 1: + return style + left + _RST + return style + left + '─' * max(0, width - 2) + right + _RST + + +def _strip_bg(s: str) -> str: + """Drop only BACKGROUND SGR — keep foreground colour (curated syntax/diff + stays, Linear-style functional colour) but no ugly box behind code.""" + def repl(m: re.Match) -> str: + toks = m.group(1).split(';') if m.group(1) else ['0'] + out, i = [], 0 + while i < len(toks): + t = toks[i] + if t == '48': + i += 3 if (i + 1 < len(toks) and toks[i + 1] == '5') else \ + 5 if (i + 1 < len(toks) and toks[i + 1] == '2') else 1 + continue + if t in _BG_TOK: + i += 1; continue + out.append(t); i += 1 + return '\x1b[' + ';'.join(out) + 'm' if out else '\x1b[0m' + return _SGR_RE.sub(repl, s) +_ESC_RE = re.compile(rb'\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b.') +_FILE_REF_RE = re.compile(r'@([\w./\-~]+)') +_PASTE_PH_RE = re.compile(r'\[Pasted text #(\d+) \+\d+ lines\]') +_FILE_PH_RE = re.compile(r'\[File #(\d+)\]') +_IMG_PH_RE = re.compile(r'\[Image #(\d+)\]') +# All paste placeholders — used for whole-block delete (v2 parity): backspace +# flush against any of these wipes the entire placeholder, not one char. +_PLACEHOLDER_RES = (_PASTE_PH_RE, _IMG_PH_RE, _FILE_PH_RE) +_IMAGE_EXTS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff', '.tif', '.ico'} +_TURN_MK_RE = re.compile( + r'\*\*LLM Running \(Turn \d+\)[^\n]*\*\*' # native live-format marker + r'|^[ \t]*Turn \d+\s*\.{3,}[ \t]*$', # plain subagent form on its own line + re.M) +_TOOL_RE = re.compile( + r'🛠️ Tool: `([^`]+)`[^\n]*\n' # 1 = name + r'(`{3,})[^\n]*\n(.*?)\n\2[ \t]*\n*' # 2 = fence delim, 3 = args body + r'(?:' + r'(`{5,})[^\n]*\n(.*?)\n\4[ \t]*\n*' # 4 = result fence (5-bt), 5 = body + r'|' # ─ OR ─ + r'(.*?)(?=^🛠️ Tool: `|^\*\*LLM Running|^|\Z)' # 6 = live exec trace + r')', + re.DOTALL | re.MULTILINE) +# Prompted-style tool wrappers GA models emit AS TEXT in saved logs (no +# structured tool_use block). Fold them into chips too so /continue replays +# match live mode. Whitelist = every name in assets/tools_schema.json + the +# native metadata wrappers; user HTML (
///

/