Full Code of lsdefine/pc-agent-loop for AI

main a6c010d25ad9 cached
89 files
814.0 KB
232.7k tokens
774 symbols
1 requests
Download .txt
Showing preview only (926K chars total). Download the full file or copy to clipboard to get everything.
Repository: lsdefine/pc-agent-loop
Branch: main
Commit: a6c010d25ad9
Files: 89
Total size: 814.0 KB

Directory structure:
gitextract_dpkv5wrs/

├── .gitignore
├── CONTRIBUTING.md
├── GETTING_STARTED.md
├── LICENSE
├── README.md
├── TMWebDriver.py
├── agent_loop.py
├── agentmain.py
├── assets/
│   ├── SETUP_FEISHU.md
│   ├── agent_bbs.py
│   ├── code_run_header.py
│   ├── configure_mykey.py
│   ├── global_mem_insight_template.txt
│   ├── global_mem_insight_template_en.txt
│   ├── insight_fixed_structure.txt
│   ├── insight_fixed_structure_en.txt
│   ├── install-macos-app.sh
│   ├── install_python_windows.bat
│   ├── sys_prompt.txt
│   ├── sys_prompt_en.txt
│   ├── tmwd_cdp_bridge/
│   │   ├── background.js
│   │   ├── content.js
│   │   ├── disable_dialogs.js
│   │   ├── manifest.json
│   │   ├── popup.html
│   │   └── popup.js
│   ├── tool_usable_history.json
│   ├── tools_schema.json
│   └── tools_schema_cn.json
├── frontends/
│   ├── DESKTOP_PET_README.md
│   ├── btw_cmd.py
│   ├── chatapp_common.py
│   ├── continue_cmd.py
│   ├── dcapp.py
│   ├── desktop_pet.pyw
│   ├── desktop_pet_v2.pyw
│   ├── dingtalkapp.py
│   ├── fsapp.py
│   ├── genericagent_acp_bridge.py
│   ├── qqapp.py
│   ├── qtapp.py
│   ├── skins/
│   │   ├── boy/
│   │   │   └── skin.json
│   │   ├── dinosaur/
│   │   │   └── skin.json
│   │   ├── doux/
│   │   │   └── skin.json
│   │   ├── glube/
│   │   │   └── skin.json
│   │   ├── line/
│   │   │   ├── License.txt
│   │   │   └── skin.json
│   │   ├── mort/
│   │   │   └── skin.json
│   │   ├── tard/
│   │   │   └── skin.json
│   │   └── vita/
│   │       └── skin.json
│   ├── stapp.py
│   ├── stapp2.py
│   ├── tgapp.py
│   ├── tuiapp.py
│   ├── wechatapp.py
│   └── wecomapp.py
├── ga.py
├── hub.pyw
├── launch.pyw
├── llmcore.py
├── memory/
│   ├── adb_ui.py
│   ├── autonomous_operation_sop.md
│   ├── goal_mode_sop.md
│   ├── keychain.py
│   ├── ljqCtrl.py
│   ├── ljqCtrl_sop.md
│   ├── memory_cleanup_sop.md
│   ├── memory_management_sop.md
│   ├── ocr_utils.py
│   ├── plan_sop.md
│   ├── procmem_scanner.py
│   ├── procmem_scanner_sop.md
│   ├── scheduled_task_sop.md
│   ├── supervisor_sop.md
│   ├── tmwebdriver_sop.md
│   ├── ui_detect.py
│   ├── vision_api.template.py
│   ├── vision_sop.md
│   ├── vue3_component_sop.md
│   └── web_setup_sop.md
├── mykey_template.py
├── mykey_template_en.py
├── plugins/
│   └── langfuse_tracing.py
├── pyproject.toml
├── reflect/
│   ├── agent_team_worker.py
│   ├── autonomous.py
│   ├── goal_mode.py
│   └── scheduler.py
└── simphtml.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
temp/
tmp/

__pycache__/
*.py[cod]
*$py.class
.venv/
venv/
env/
build/
dist/
*.egg-info/

.streamlit/

.vscode/
.idea/
*.swp
*.swo

.DS_Store
Thumbs.db

*.log
.env
auth.json
model_responses.txt

# Sensitive files (API keys, credentials)
mykey.py

tasks/

*.zip

memory/*
!memory/memory_management_sop.md

# Allow tracking of specific SOPs
!memory/web_setup_sop.md
!memory/autonomous_operation_sop.md
!memory/autonomous_operation_sop/
!memory/autonomous_operation_sop/**
!memory/scheduled_task_sop.md

# L4 session archiver (only the script, not archives)
!memory/L4_raw_sessions/
memory/L4_raw_sessions/*
!memory/L4_raw_sessions/compress_session.py

# ljqCtrl related tools
!memory/ljqCtrl.py
!memory/ljqCtrl_sop.md

# procmem_scanner related tools
!memory/procmem_scanner.py
!memory/procmem_scanner_sop.md

# TMWebDriver SOP
!memory/tmwebdriver_sop.md

# Vue3 Component SOP
!memory/vue3_component_sop.md

# Subagent SOP
!memory/subagent_sop.md

# Supervisor SOP
!memory/supervisor_sop.md

# Plan SOP
!memory/plan_sop.md

# Goal Mode SOP
!memory/goal_mode_sop.md

# Skill Search SOP
!memory/skill_search/
!memory/skill_search/**


# ADB UI tool
!memory/adb_ui.py

# Keychain
!memory/keychain.py

# Vision / OCR / UI detection tools
!memory/ocr_utils.py
!memory/vision_sop.md
!memory/ui_detect.py
!memory/vision_api.template.py

# Memory management
!memory/memory_cleanup_sop.md

# Visual Studio
.vs/
restore_commit.txt

sche_tasks/
# CDP Bridge 密钥配置(首次运行自动生成)
assets/tmwd_cdp_bridge/config.js
assets/copilot_proxy.pyw
**log.*

# Reflect (ignore new files, whitelist existing)
reflect/*
!reflect/autonomous.py
!reflect/scheduler.py
!reflect/agent_team_worker.py
!reflect/goal_mode.py

# Universal: never track __pycache__ anywhere
**/__pycache__/

.claude/


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to GenericAgent

## Why This File Is Short

GenericAgent's core is ~3K lines. Every file in this repo will be read by AI agents — potentially thousands of times. Extra words cost real tokens and push useful context out of the window, increasing hallucinations. This document practices what it preaches: **say only what matters.**

## Before You Contribute

1. **Read the codebase first.** It's small enough to read in one sitting. Understand the philosophy before proposing changes.
2. **Open an Issue first** for anything non-trivial. Discuss before coding.

## Code Standards

All PRs go through a strict automated code review skill. Key expectations:

- **Self-documenting code, minimal comments.** If code needs a paragraph to explain, rewrite it.
- **Compact and visually uniform.** Fewer lines, consistent line lengths, no fluff.
- **Small change radius.** Changing A shouldn't ripple through B, C, D.
- **More features → less code.** Good abstractions make the codebase shrink, not grow.
- **Let it crash by failure radius.** Critical errors fail loud; trivial ones pass silently. No blanket try-catch.

> ⚠️ This review is deliberately strict — most AI-generated code (e.g. Claude Code output) will not pass as-is. Read the full principles before submitting.

## Skill Contributions

GenericAgent evolves through skills. Not all skills belong in the core repo:

| Type | Where it goes | Example |
|---|---|---|
| **Fundamental / universal** | Core repo (`memory/`) | File search, clipboard, basic web ops |
| **Domain-specific / niche** | Skill Marketplace *(coming soon)* | Stock screening, food delivery, specific API integrations |

If your skill only makes sense for a specific workflow, it's a marketplace candidate, not a core PR.

## PR Checklist

- [ ] Issue linked or context explained in ≤3 sentences
- [ ] Code passes the [review principles] self-check:
  1. Can I safely modify this locally without reading the whole codebase?
  2. Is there a clear core abstraction — new features add implementations, not modify old logic?
  3. Are change points converging at boundaries, not scattered everywhere?
  4. On failure, can I quickly locate the responsible module?
- [ ] Net line count: ideally negative or zero for refactors
- [ ] No unnecessary dependencies added


================================================
FILE: GETTING_STARTED.md
================================================
# 🚀 新手上手指南

> 完全没接触过编程也没关系,跟着做就行。Mac / Windows 都适用。
>
> 如果你已经有 Python 环境,直接跳到[第 2 步](#2-配置-api-key)。

---

## 1. 安装 Python

### Mac

打开「终端」(启动台搜索 "终端" 或 "Terminal"),粘贴这行命令然后回车:

```bash
brew install python
```

如果提示 `brew: command not found`,说明还没装 Homebrew,先粘贴这行:

```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```

装完后再执行 `brew install python`。

### Windows

1. 打开 [python.org/downloads](https://www.python.org/downloads/),点黄色大按钮下载
2. 运行安装包,**底部的 "Add Python to PATH" 一定要勾上**
3. 点 "Install Now"

### 验证

终端 / 命令提示符里输入:

```bash
python3 --version
```

看到 `Python 3.x.x` 就 OK。Windows 上也可以试 `python --version`。

> ⚠️ **版本提示**:推荐 **Python 3.11 或 3.12**。不要使用 3.14(与 pywebview 等依赖不兼容)。

---

## 2. 配置 API Key

### 下载项目

1. 打开 [GitHub 仓库页面](https://github.com/lsdefine/GenericAgent)
2. 点绿色 **Code** 按钮 → **Download ZIP**
3. 解压到你喜欢的位置

### 创建配置文件

进入项目文件夹,把 `mykey_template.py` 复制一份,重命名为 `mykey.py`。

用任意文本编辑器打开 `mykey.py`,填入你的 API 信息。**选一种填就行**,不用的配置删掉或留着不管都行。

> 💡 也可以运行交互式向导 `python assets/configure_mykey.py`,按提示选择厂商、填入 Key 即可自动生成 `mykey.py`。

### 配置示例

**最常见的用法:**

```python
# 变量名含 'oai' → 走 OpenAI 兼容格式 (/chat/completions)
oai_config = {
    'apikey': 'sk-你的密钥',
    'apibase': 'http://你的API地址:端口',
    'model': '模型名称',
}
```

```python
# 变量名含 'claude'(不含 'native')→ 走 Claude 兼容格式 (/messages)
claude_config = {
    'apikey': 'sk-你的密钥',
    'apibase': 'http://你的API地址:端口',
    'model': 'claude-sonnet-4-20250514',
}
```

```python
# MiniMax 使用 OpenAI 兼容格式,变量名含 'oai' 即可
# 温度自动修正为 (0, 1],支持 M2.7 / M2.5 全系列,204K 上下文
oai_minimax_config = {
    'apikey': 'eyJh...',
    'apibase': 'https://api.minimax.io/v1',
    'model': 'MiniMax-M2.7',
}
```

**使用标准工具调用格式(适合较弱模型):**

```python
# 变量名同时含 'native' 和 'claude' → Claude 标准工具调用格式
native_claude_config = {
    'apikey': 'sk-ant-你的密钥',
    'apibase': 'https://api.anthropic.com',
    'model': 'claude-sonnet-4-20250514',
}
```

> 💡 还支持 `native_oai_config`(OpenAI 标准工具调用)、`sider_cookie`(Sider)等,详见 `mykey_template.py` 中的注释。

### 关键规则

**变量命名决定接口格式**(不是模型名决定的):

| 变量名包含 | 触发的 Session | 适用场景 |
|-----------|---------------|---------|
| `oai` | OpenAI 兼容 | 大多数 API 服务、OpenAI 官方 |
| `claude`(不含 `native`) | Claude 兼容 | Claude API 服务 |
| `native` + `claude` | Claude 标准工具调用 | 较弱模型推荐,工具调用更规范 |
| `native` + `oai` | OpenAI 标准工具调用 | 较弱模型推荐,工具调用更规范 |

> 例:用 Claude 模型,但 API 服务提供的是 OpenAI 兼容接口 → 变量名用 `oai_xxx`。
> 例:用 MiniMax 模型 → 变量名用 `oai_minimax_config`,MiniMax 走 OpenAI 兼容接口。

**`apibase` 填写规则**(会自动拼接端点路径):

| 你填的内容 | 系统行为 |
|-----------|---------|
| `http://host:2001` | 自动补 `/v1/chat/completions` |
| `http://host:2001/v1` | 自动补 `/chat/completions` |
| `http://host:2001/v1/chat/completions` | 直接使用,不拼接 |

---

## 3. 初次启动

终端里进入项目文件夹,运行:

```bash
cd 你的解压路径
python3 agentmain.py
```

这就是**命令行模式**,已经可以用了。你会看到一个输入提示符,直接打字发送任务即可。

试试你的第一个任务:

```
帮我在桌面创建一个 hello.txt,内容是 Hello World
```

> 💡 Windows 上如果 `python3` 不识别,换成 `python agentmain.py`。

---

## 4. 让 Agent 自己装依赖

Agent 启动后,只需要一句话,它就会自己搞定所有依赖:

```
请查看你的代码,安装所有用得上的 python 依赖
```

Agent 会自己读代码、找出需要的包、全部装好。

> ⚠️ 如果遇到网络问题导致 Agent 无法调用 API,可能需要先手动装一个包:
> ```bash
> pip install requests
> ```

### 升级到图形界面

依赖装完后,就可以用 GUI 模式了:

```bash
python3 launch.pyw
```

启动后会出现一个桌面悬浮窗,直接在里面输入任务指令。

### 可选:让 Agent 帮你做的事

```
请帮我建立 git 连接,方便以后更新代码
```

Agent 会自动配好。如果你电脑上没有 Git,它也会帮你下载 portable 版。

```
请帮我在桌面创建一个 launch.pyw 的快捷方式
```

这样以后双击桌面图标就能启动,不用再开终端了。

---

## 5. 能力解锁

环境跑起来之后,你可以逐步解锁更多能力。每一项都只需要**对 Agent 说一句话**:

### 基础能力

| 能力 | 对 Agent 说 | 说明 |
|------|-----------|------|
| **PowerShell 脚本执行** | `帮我解锁当前用户的 PowerShell ps1 执行权限` | Windows 默认禁止运行 .ps1 脚本 |
| **全局文件搜索** | `安装并配置 Everything 命令行工具进 PATH` | 毫秒级全盘文件搜索 |

### 浏览器自动化

| 能力 | 对 Agent 说 | 说明 |
|------|-----------|------|
| **Web 工具解锁** | `执行 web setup sop,解锁 web 工具` | 注入浏览器插件,使 Agent 能直接操控网页 |

解锁后,Agent 可以在**保留你登录态**的真实浏览器中操作:

```
打开淘宝,搜索 iPhone 16,按价格排序
去 B 站,查看我最近看过的历史视频
```

### 进阶能力

| 能力 | 对 Agent 说 | 说明 |
|------|-----------|------|
| **OCR** | `用rapidocr配置你的ocr能力并存入记忆` | 让 Agent 能"看到"屏幕文字 |
| **屏幕视觉** | `仿造你的llmcore,写个调用vision的能力并存入记忆` | 让 Agent 能"看到"屏幕内容 |
| **移动端控制** | `配置 ADB 环境,准备连接安卓设备` | 通过 USB/WiFi 控制 Android 手机 |

### 聊天平台接入(可选)

接入后可以随时随地通过手机给电脑上的 Agent 发指令。

对 Agent 说:`看你的代码,帮我配置 XX 平台的机器人接入`

支持的平台:**微信个人Bot** / QQ / 飞书 / 企业微信 / 钉钉 / Telegram

> Agent 会自动读取代码、引导你完成配置。

### 高级模式

以下模式全部**自文档化**——不用查手册,直接问 Agent 即可:

| 模式 | 对 Agent 说 |
|------|------------|
| **Reflect(反射)** | `查看你的代码,告诉我你的 reflect 模式怎么启用` |
| **计划任务** | `查看你的代码,告诉我你的计划任务模式怎么启用` |
| **Plan(规划)** | `查看你的代码,告诉我你的 plan 模式怎么启用` |
| **SubAgent(子代理)** | `查看你的代码,告诉我你的 subagent 模式怎么启用` |
| **自主探索** | `查看你的代码,告诉我你的自主探索模式怎么启用` |

> 💡 这就是 GenericAgent 的核心设计理念:**代码即文档**。Agent 能读懂自己的源码,所以任何功能你都可以直接问它。

---

## 💡 使用越久越强

GenericAgent 不预设技能,而是**靠使用进化**。每完成一个新任务,它会自动将执行路径固化为 Skill,下次遇到类似任务直接调用。

你不需要管理这些 Skill,Agent 会自动处理。使用时间越长,积累的技能越多,最终形成一棵完全属于你的专属技能树。

> 💡 如果你觉得某些重要信息 Agent 没有记住,可以直接告诉它:`把这个记到你的记忆里`,它会主动记忆。

**其他 Claw 的 Skill 也可以直接复用:**

- 让 Agent 搜索:`帮我找个做 XXX 的 skill` → 完成后 → `加入你的记忆中`
- 直接指定来源:`访问 XXX 文件夹/URL,按照这个 skill 做 XXX`

**保持更新:**

对 Agent 说:`git 更新你的代码,然后看看 commit 有什么新功能`

> Agent 会自动 pull 最新代码并解读 commit log,告诉你新增了什么能力。

> 更多细节请参阅 [README.md](README.md) 或 [详细版图文教程](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb)。

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 lsdefine

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<div align="center">
<img src="assets/images/bar.jpg" width="880"/>

<a href="https://trendshift.io/repositories/25944" target="_blank"><img src="https://trendshift.io/api/badge/repositories/25944" alt="lsdefine%2FGenericAgent | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>

</div>

<p align="center">
  <a href="#english">English</a> | <a href="#chinese">中文</a> | 📄 Technical Report:&nbsp;<a href="https://arxiv.org/abs/2604.17091"><img src="https://img.shields.io/badge/arXiv-2604.17091-b31b1b?logo=arxiv&logoColor=white" alt="arXiv" height="18"/></a>&nbsp;<a href="assets/GenericAgent_Technical_Report.pdf"><img src="https://img.shields.io/badge/-PDF-EA4335?logo=adobeacrobatreader&logoColor=white" alt="Technical Report PDF" height="18"/></a>&nbsp;<a href="https://github.com/JinyiHan99/GA-Technical-Report"><img src="https://img.shields.io/badge/-Code%20%26%20Data-181717?logo=github&logoColor=white" alt="Experiments & Reproduction Repo" height="18"/></a> | 📘 <a href="https://datawhalechina.github.io/hello-generic-agent/">教程</a> | <a href="https://fudankw.cn/sophub">Sophub</a>
</p>

> 📌 **Official channel**: This GitHub repository is the sole official source for GenericAgent. We have no affiliation with any third-party website using the GenericAgent name.

---
<a name="english"></a>
## 🌟 Overview

**GenericAgent** is a minimal, self-evolving autonomous agent framework. Its core is just **~3K lines of code**. Through **9 atomic tools + a ~100-line Agent Loop**, it grants any LLM system-level control over a local computer — covering browser, terminal, filesystem, keyboard/mouse input, screen vision, and mobile devices (ADB).

Its design philosophy: **don't preload skills — evolve them.**

Every time GenericAgent solves a new task, it automatically crystallizes the execution path into an skill for direct reuse later. The longer you use it, the more skills accumulate — forming a skill tree that belongs entirely to you, grown from 3K lines of seed code.

> **🤖 Self-Bootstrap Proof** — Everything in this repository, from installing Git and running `git init` to every commit message, was completed autonomously by GenericAgent. The author never opened a terminal once.

## 📋 Core Features
- **Self-Evolving**: Automatically crystallizes each task into an skill. Capabilities grow with every use, forming your personal skill tree.
- **Minimal Architecture**: ~3K lines of core code. Agent Loop is ~100 lines. No complex dependencies, zero deployment overhead.
- **Strong Execution**: Injects into a real browser (preserving login sessions). 9 atomic tools take direct control of the system.
- **High Compatibility**: Supports Claude / Gemini / Kimi / MiniMax and other major models. Cross-platform.
- **Token Efficient**: <30K context window — a fraction of the 200K–1M other agents consume. Layered memory ensures the right knowledge is always in scope. Less noise, fewer hallucinations, higher success rate — at a fraction of the cost.


## 🧬 Self-Evolution Mechanism

This is what fundamentally distinguishes GenericAgent from every other agent framework.

```
[New Task] --> [Autonomous Exploration] (install deps, write scripts, debug & verify) -->
[Crystallize Execution Path into skill] --> [Write to Memory Layer] --> [Direct Recall on Next Similar Task]
```

| What you say | What the agent does the first time | Every time after |
|---|---|---|
| *"Read my WeChat messages"* | Install deps → reverse DB → write read script → save skill | **one-line invoke** |
| *"Monitor stocks and alert me"* | Install mootdx → build selection flow → configure cron → save skill | **one-line start** |
| *"Send this file via Gmail"* | Configure OAuth → write send script → save skill | **ready to use** |

After a few weeks, your agent instance will have a skill tree no one else in the world has — all grown from 3K lines of seed code.


##### 🎯 Demo Showcase

| 🧋 Food Delivery Order | 📈 Quantitative Stock Screening |
|:---:|:---:|
| <img src="assets/demo/order_tea.gif" width="100%" alt="Order Tea"> | <img src="assets/demo/selectstock.gif" width="100%" alt="Stock Selection"> |
| *"Order me a milk tea"* — Navigates the delivery app, selects items, and completes checkout automatically. | *"Find GEM stocks with EXPMA golden cross, turnover > 5%"* — Screens stocks with quantitative conditions. |
| 🌐 Autonomous Web Exploration | 💰 Expense Tracking | 💬 Batch Messaging |
| <img src="assets/demo/autonomous_explore.png" width="100%" alt="Web Exploration"> | <img src="assets/demo/alipay_expense.png" width="100%" alt="Alipay Expense"> | <img src="assets/demo/wechat_batch.png" width="100%" alt="WeChat Batch"> |
| Autonomously browses and periodically summarizes web content. | *"Find expenses over ¥2K in the last 3 months"* — Drives Alipay via ADB. | Sends bulk WeChat messages, fully driving the WeChat client. |

## 📅 Latest News

- **2026-04-21:** 📄 [Technical Report released on arXiv](https://arxiv.org/abs/2604.17091) — *GenericAgent: A Token-Efficient Self-Evolving LLM Agent via Contextual Information Density Maximization*
- **2026-04-11:** Introduced **L4 session archive memory** and scheduler cron integration
- **2026-03-23:** Support personal WeChat as a bot frontend
- **2026-03-10:** [Released million-scale Skill Library](https://mp.weixin.qq.com/s/q2gQ7YvWoiAcwxzaiwpuiQ?scene=1&click_id=7)
- **2026-03-08:** [Released "Dintal Claw" — a GenericAgent-powered government affairs bot](https://mp.weixin.qq.com/s/eiEhwo-j6S-WpLxgBnNxBg)
- **2026-03-01:** [GenericAgent featured by Jiqizhixin (机器之心)](https://mp.weixin.qq.com/s/uVWpTTF5I1yzAENV_qm7yg)
- **2026-01-16:** GenericAgent V1.0 public release

---

## 🚀 Quick Start

#### Method 1: Standard Installation

```bash
# 1. Clone the repo
git clone https://github.com/lsdefine/GenericAgent.git
cd GenericAgent

# 2. Install dependencies
pip install requests streamlit pywebview   # Desktop GUI (launch.pyw)
pip install requests textual               # Terminal UI (tuiapp.py)

# 3. Configure API Key
cp mykey_template.py mykey.py
# Edit mykey.py and fill in your LLM API Key

# 4. Launch
python launch.pyw
```

#### Method 2: uv (for experienced Python users)

If you prefer a modern Python workflow, GenericAgent also provides a minimal `pyproject.toml`:

```bash
git clone https://github.com/lsdefine/GenericAgent.git
cd GenericAgent
uv venv
uv pip install -e ".[ui]"        # Core + GUI dependencies
cp mykey_template.py mykey.py
python launch.pyw
```

> GenericAgent is meant to grow its environment through the Agent itself, not by pre-installing every possible package.

Full guide: [GETTING_STARTED.md](GETTING_STARTED.md)

---

## 🖥️ Desktop Frontends

### Terminal UI

A lightweight, keyboard-driven interface built on [Textual](https://github.com/Textualize/textual). Supports multiple concurrent sessions, real-time streaming, and runs anywhere a terminal does — no browser needed.

```bash
python frontends/tuiapp.py
```

### Other Desktop Frontends

```bash
python frontends/qtapp.py                # Qt-based desktop app
streamlit run frontends/stapp2.py        # Alternative Streamlit UI
```

### Codeg

<table><tr>
<td width="70%">

[Codeg](https://github.com/yiqi-017/codeg) (`feat/genericagent-integration` branch) is a desktop/web UI that connects GenericAgent alongside other agents (Claude Code, Gemini, Codex, etc.) in a unified interface with a polished, modern UI.

> This integration is usable now. Some features are still being refined — feedback welcome.

Place your GenericAgent directory alongside the codeg project. Codeg will auto-detect `frontends/genericagent_acp_bridge.py` and launch GenericAgent as a local ACP agent.

</td>
<td width="30%">
<img src="assets/demo/codeg-demo.gif" width="90%" alt="Codeg Demo">
</td>
</tr></table>

---

## 💬 Bot Interface (IM)

### Telegram Bot

```python
# mykey.py
tg_bot_token = 'YOUR_BOT_TOKEN'
tg_allowed_users = [YOUR_USER_ID]
```

```bash
python frontends/tgapp.py
```

### Common Chat Commands

The default Streamlit desktop UI started by `python launch.pyw`, plus the QQ / Telegram / Feishu / WeCom / DingTalk frontends, support these chat commands:

- `/new` - start a fresh conversation and clear the current context
- `/continue` - list recoverable conversation snapshots
- `/continue N` - restore the `N`th recoverable conversation


## 📊 Comparison with Similar Tools

| Feature | GenericAgent | OpenClaw | Claude Code |
|------|:---:|:---:|:---:|
| **Codebase** | ~3K lines | ~530,000 lines | Open-sourced (large) |
| **Deployment** | `pip install` + API Key | Multi-service orchestration | CLI + subscription |
| **Browser Control** | Real browser (session preserved) | Sandbox / headless browser | Via MCP plugin |
| **OS Control** | Mouse/kbd, vision, ADB | Multi-agent delegation | File + terminal |
| **Self-Evolution** | Autonomous skill growth | Plugin ecosystem | Stateless between sessions |
| **Out of the Box** | A few core files + starter skills | Hundreds of modules | Rich CLI toolset |


## 🧠 How It Works

GenericAgent accomplishes complex tasks through **Layered Memory × Minimal Toolset × Autonomous Execution Loop**, continuously accumulating experience during execution.

1️⃣ **Layered Memory System**
> _Memory crystallizes throughout task execution, letting the agent build stable, efficient working patterns over time._

- **L0 — Meta Rules**: Core behavioral rules and system constraints of the agent
- **L1 — Insight Index**: Minimal memory index for fast routing and recall
- **L2 — Global Facts**: Stable knowledge accumulated over long-term operation
- **L3 — Task Skills / SOPs**: Reusable workflows for completing specific task types
- **L4 — Session Archive**: Archived task records distilled from finished sessions for long-horizon recall

2️⃣ **Autonomous Execution Loop**

> _Perceive environment state → Task reasoning → Execute tools → Write experience to memory → Loop_

The entire core loop is just **~100 lines of code** (`agent_loop.py`).

3️⃣ **Minimal Toolset**
> _GenericAgent provides only **9 atomic tools**, forming the foundational capabilities for interacting with the outside world._

| Tool | Function |
|------|------|
| `code_run` | Execute arbitrary code |
| `file_read` | Read files |
| `file_write` | Write files |
| `file_patch` | Patch / modify files |
| `web_scan` | Perceive web content |
| `web_execute_js` | Control browser behavior |
| `ask_user` | Human-in-the-loop confirmation |

> Additionally, 2 **memory management tools** (`update_working_checkpoint`, `start_long_term_update`) allow the agent to persist context and accumulate experience across sessions.

4️⃣ **Capability Extension Mechanism**
> _Capable of dynamically creating new tools._

Via `code_run`, GenericAgent can dynamically install Python packages, write new scripts, call external APIs, or control hardware at runtime — crystallizing temporary abilities into permanent tools.

<div align="center">
  <img src="assets/images/workflow.jpg" alt="GenericAgent Workflow" width="400"/>
  <br><em>GenericAgent Workflow Diagram</em>
</div>


## ⭐ Support

If this project helped you, please consider leaving a **Star!** 🙏

You're also welcome to join our **GenericAgent Community Group** for discussion, feedback, and co-building 👏

<div align="center">
  <table>
    <tr>
      <td align="center"><strong>WeChat Group 15</strong><br><img src="assets/images/wechat_group15.jpg" alt="WeChat Group 15 QR Code" width="250"/></td>
    </tr>
  </table>
</div>

## 🚩 Friendly Links

Thanks for the support from the LinuxDo community!

[![LinuxDo](https://img.shields.io/badge/社区-LinuxDo-blue?style=for-the-badge)](https://linux.do/)

## 📄 License

MIT License — see [LICENSE](LICENSE)

*Disclaimer: This project does not build or operate any commercial website. Apart from DintalClaw, no institution, organization, or individual is currently officially authorized to conduct commercial activities under the GenericAgent name.*


---
<a name="chinese"></a>
## 🌟 项目简介

**GenericAgent** 是一个极简、可自我进化的自主 Agent 框架。核心仅 **~3K 行代码**,通过 **9 个原子工具 + ~100 行 Agent Loop**,赋予任意 LLM 对本地计算机的系统级控制能力,覆盖浏览器、终端、文件系统、键鼠输入、屏幕视觉及移动设备。

它的设计哲学是:**不预设技能,靠进化获得能力。**

每解决一个新任务,GenericAgent 就将执行路径自动固化为 Skill,供后续直接调用。使用时间越长,沉淀的技能越多,形成一棵完全属于你、从 3K 行种子代码生长出来的专属技能树。

> **🤖 自举实证** — 本仓库的一切,从安装 Git、`git init` 到每一条 commit message,均由 GenericAgent 自主完成。作者全程未打开过一次终端。

## 📋 核心特性
- **自我进化**: 每次任务自动沉淀 Skill,能力随使用持续增长,形成专属技能树
- **极简架构**: ~3K 行核心代码,Agent Loop 约百行,无复杂依赖,部署零负担
- **强执行力**: 注入真实浏览器(保留登录态),9 个原子工具直接接管系统
- **高兼容性**: 支持 Claude / Gemini / Kimi / MiniMax 等主流模型,跨平台运行
- **极致省 Token**: 上下文窗口不到 30K,是其他 Agent(200K–1M)的零头。分层记忆让关键信息始终在场——噪声更少,幻觉更低,成功率反而更高,而成本低一个数量级。

## 🧬 自我进化机制

这是 GenericAgent 区别于其他 Agent 框架的根本所在。

```
[遇到新任务]-->[自主摸索](安装依赖、编写脚本、调试验证)-->
[将执行路径固化为 Skill]-->[写入记忆层]-->[下次同类任务直接调用]
```

| 你说的一句话 | Agent 第一次做了什么 | 之后每次 |
|---|---|---|
| *"监控股票并提醒我"* | 安装 mootdx → 构建选股流程 → 配置定时任务 → 保存 Skill | **一句话启动** |
| *"用 Gmail 发这个文件"* | 配置 OAuth → 编写发送脚本 → 保存 Skill | **直接可用** |

用几周后,你的 Agent 实例将拥有一套任何人都没有的专属技能树,全部从 3K 行种子代码中生长而来。

<!-- | *"帮我读取微信消息"* | 安装依赖 → 逆向数据库 → 写读取脚本 → 保存 Skill | **一句话调用** | -->

#### 🎯 实例展示

| 🧋 外卖下单 | 📈 量化选股 |
|:---:|:---:|
| <img src="assets/demo/order_tea.gif" width="100%" alt="Order Tea"> | <img src="assets/demo/selectstock.gif" width="100%" alt="Stock Selection"> |
| *"Order me a milk tea"* — 自动导航外卖 App,选品并完成结账 | *"Find GEM stocks with EXPMA golden cross, turnover > 5%"* — 量化条件筛股 |
| 🌐 自主网页探索 | 💰 支出追踪 | 💬 批量消息 |
| <img src="assets/demo/autonomous_explore.png" width="100%" alt="Web Exploration"> | <img src="assets/demo/alipay_expense.png" width="100%" alt="Alipay Expense"> | <img src="assets/demo/wechat_batch.png" width="100%" alt="WeChat Batch"> |
| 自主浏览并定时汇总网页信息 | *"查找近 3 个月超 ¥2K 的支出"* — 通过 ADB 驱动支付宝 | 批量发送微信消息,完整驱动微信客户端 |



## 📅 最新动态

- **2026-04-21:** 📄 [技术报告已发布至 arXiv](https://arxiv.org/abs/2604.17091) — *GenericAgent: A Token-Efficient Self-Evolving LLM Agent via Contextual Information Density Maximization*
- **2026-04-11:** 引入 **L4 会话归档记忆**,并接入 scheduler cron 调度
- **2026-03-23:** 支持个人微信接入作为 Bot 前端
- **2026-03-10:** [发布百万级 Skill 库](https://mp.weixin.qq.com/s/q2gQ7YvWoiAcwxzaiwpuiQ?scene=1&click_id=7)
- **2026-03-08:** [发布以 GenericAgent 为核心的"政务龙虾" Dintal Claw](https://mp.weixin.qq.com/s/eiEhwo-j6S-WpLxgBnNxBg)
- **2026-03-01:** [GenericAgent 被机器之心报道](https://mp.weixin.qq.com/s/uVWpTTF5I1yzAENV_qm7yg)
- **2026-01-16:** GenericAgent V1.0 公开版本发布

---

## 🚀 快速开始

#### 方法一:标准安装

```bash
# 1. 克隆仓库
git clone https://github.com/lsdefine/GenericAgent.git
cd GenericAgent

# 2. 安装依赖
pip install requests streamlit pywebview   # 桌面 GUI (launch.pyw)
pip install requests textual               # 终端 UI (tuiapp.py)

# 3. 配置 API Key
cp mykey_template.py mykey.py
# 编辑 mykey.py,填入你的 LLM API Key
# 或使用交互式向导:python assets/configure_mykey.py

# 4. 启动
python launch.pyw
```

#### 方法二:uv 快速安装(熟悉 Python 的用户)

如果你习惯现代 Python 工作流,GenericAgent 也提供了一个最小化的 `pyproject.toml`:

```bash
git clone https://github.com/lsdefine/GenericAgent.git
cd GenericAgent
uv pip install -e ".[ui]"        # 核心 + GUI 依赖
cp mykey_template.py mykey.py
python launch.pyw
```

> GenericAgent 更推荐由 Agent 在使用中自举环境,而不是预先手动装完整依赖。

完整引导流程见 [GETTING_STARTED.md](GETTING_STARTED.md)。

📖 新手使用指南(图文版):[飞书文档](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb)

📘 完整入门教程(Datawhale 出品):[Hello GenericAgent](https://datawhalechina.github.io/hello-generic-agent/) · [GitHub](https://github.com/datawhalechina/hello-generic-agent)

---

## 🖥️ 桌面前端

### 终端 UI

基于 [Textual](https://github.com/Textualize/textual) 的轻量键盘驱动界面。支持多会话并发、实时流式输出,有终端就能跑,无需浏览器。

```bash
python frontends/tuiapp.py
```

### 其他桌面前端

```bash
python frontends/qtapp.py                # 基于 Qt 的桌面应用
streamlit run frontends/stapp2.py        # 另一种 Streamlit 风格 UI
```

### Codeg前端

<table><tr>
<td width="70%">

[Codeg](https://github.com/yiqi-017/codeg)(`feat/genericagent-integration` 分支)是一个桌面/Web UI,可以将 GenericAgent 与其他代理(Claude Code、Gemini、Codex 等)在统一界面中并行使用,UI 更加精美。

> 此集成已可使用,部分功能仍在完善中,欢迎体验反馈。

将 GenericAgent 目录放在 codeg 项目同级目录下,Codeg 会自动检测 `frontends/genericagent_acp_bridge.py` 并将 GenericAgent 作为本地 ACP 代理启动。

</td>
<td width="30%">
<img src="assets/demo/codeg-demo.gif" width="90%" alt="Codeg Demo">
</td>
</tr></table>

---

## 💬 Bot 接口(IM)

### 微信 Bot(个人微信)

无需额外配置,扫码登录即可:

```bash
pip install pycryptodome qrcode requests
python frontends/wechatapp.py
```

> 首次启动会弹出二维码,用微信扫码完成绑定。之后通过微信消息与 Agent 交互。

### QQ Bot

使用 `qq-botpy` WebSocket 长连接,**无需公网 webhook**:

```bash
pip install qq-botpy
```

在 `mykey.py` 中补充:

```python
qq_app_id = "YOUR_APP_ID"
qq_app_secret = "YOUR_APP_SECRET"
qq_allowed_users = ["YOUR_USER_OPENID"]  # 或 ['*'] 公开访问
```

```bash
python frontends/qqapp.py
```

> 在 [QQ 开放平台](https://q.qq.com) 创建机器人获取 AppID / AppSecret。首次消息后,用户 openid 记录于 `temp/qqapp.log`。

### 飞书(Lark)

```bash
pip install lark-oapi
python frontends/fsapp.py
```

```python
fs_app_id = "cli_xxx"
fs_app_secret = "xxx"
fs_allowed_users = ["ou_xxx"]  # 或 ['*']
```

**入站支持**:文本、富文本 post、图片、文件、音频、media、交互卡片 / 分享卡片  
**出站支持**:流式进度卡片、图片回传、文件 / media 回传  
**视觉模型**:图片首轮以真正的多模态输入发送给兼容 OpenAI Vision 的后端

详细配置见 [assets/SETUP_FEISHU.md](assets/SETUP_FEISHU.md)


### 企业微信(WeCom)

```bash
pip install wecom_aibot_sdk
python frontends/wecomapp.py
```

```python
wecom_bot_id = "your_bot_id"
wecom_secret = "your_bot_secret"
wecom_allowed_users = ["your_user_id"]
wecom_welcome_message = "你好,我在线上。"
```

### 钉钉(DingTalk)

```bash
pip install dingtalk-stream
python frontends/dingtalkapp.py
```

```python
dingtalk_client_id = "your_app_key"
dingtalk_client_secret = "your_app_secret"
dingtalk_allowed_users = ["your_staff_id"]  # 或 ['*']
```

### 通用聊天命令

默认通过 `python launch.pyw` 启动的 Streamlit 桌面 UI,以及 QQ / Telegram / 飞书 / 企业微信 / 钉钉前端,都支持以下命令:

- `/new` - 开启新对话并清空当前上下文
- `/continue` - 列出可恢复会话快照
- `/continue N` - 恢复第 `N` 个可恢复会话


## 📊 与同类产品对比

| 特性 | GenericAgent | OpenClaw | Claude Code |
|------|:---:|:---:|:---:|
| **代码量** | ~3K 行 | ~530,000 行 | 已开源(体量大) |
| **部署方式** | `pip install` + API Key | 多服务编排 | CLI + 订阅 |
| **浏览器控制** | 注入真实浏览器(保留登录态) | 沙箱 / 无头浏览器 | 通过 MCP 插件 |
| **OS 控制** | 键鼠、视觉、ADB | 多 Agent 委派 | 文件 + 终端 |
| **自我进化** | 自主生长 Skill 和工具 | 插件生态 | 会话间无状态 |
| **出厂配置** | 几个核心文件 + 少量初始 Skills | 数百模块 | 丰富 CLI 工具集 |


## 🧠 工作机制

GenericAgent 通过**分层记忆 × 最小工具集 × 自主执行循环**完成复杂任务,并在执行过程中持续积累经验。

1️⃣ **分层记忆系统**
> 记忆在任务执行过程中持续沉淀,使 Agent 逐步形成稳定且高效的工作方式


- **L0 — 元规则(Meta Rules)**:Agent 的基础行为规则和系统约束
- **L1 — 记忆索引(Insight Index)**:极简索引层,用于快速路由与召回
- **L2 — 全局事实(Global Facts)**:在长期运行过程中积累的稳定知识
- **L3 — 任务 Skills / SOPs**:完成特定任务类型的可复用流程
- **L4 — 会话归档(Session Archive)**:从已完成任务中提炼出的归档记录,用于长程召回

2️⃣ **自主执行循环**

> 感知环境状态  →  任务推理  →  调用工具执行  →  经验写入记忆  →  循环

整个核心循环仅 **约百行代码**(`agent_loop.py`)。

3️⃣ **最小工具集**
>GenericAgent 仅提供 **9 个原子工具**,构成与外部世界交互的基础能力

| 工具 | 功能 |
|------|------|
| `code_run` | 执行任意代码 |
| `file_read` | 读取文件 |
| `file_write` | 写入文件 |
| `file_patch` | 修改文件 |
| `web_scan` | 感知网页内容 |
| `web_execute_js` | 控制浏览器行为 |
| `ask_user` | 人机协作确认 |

> 此外,还有 2 个**记忆管理工具**(`update_working_checkpoint`、`start_long_term_update`),使 Agent 能够跨会话积累经验、维持持久上下文。

4️⃣ **能力扩展机制**
> 具备动态创建新的工具能力
>
通过 `code_run`,GenericAgent 可在运行时动态安装 Python 包、编写新脚本、调用外部 API 或控制硬件,将临时能力固化为永久工具。

<div align="center">
  <img src="assets/images/workflow.jpg" alt="GenericAgent 工作流程" width="400"/>
  <br><em>GenericAgent 工作流程图</em>
</div>

## ⭐ 支持
如果这个项目对您有帮助,欢迎点一个 **Star!** 🙏

同时也欢迎加入我们的**GenericAgent体验交流群**,一起交流、反馈和共建 👏
<div align="center">
  <table>
    <tr>
      <td align="center"><strong>微信群 15</strong><br><img src="assets/images/wechat_group15.jpg" alt="微信群 15 二维码" width="250"/></td>
    </tr>
  </table>
</div>

## 🚩 友情链接

感谢 **LinuxDo** 社区的支持!

[![LinuxDo](https://img.shields.io/badge/社区-LinuxDo-blue?style=for-the-badge)](https://linux.do/)


## 📄 许可
MIT License — 详见 [LICENSE](LICENSE)

*声明:本项目未构建任何商业站点;除 DintalClaw 外,目前未官方授权任何机构、组织或个人以 GenericAgent 名义从事商业活动。*

## 📈 Star History

<a href="https://star-history.com/#lsdefine/GenericAgent&Date">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=lsdefine/GenericAgent&type=Date&theme=dark" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=lsdefine/GenericAgent&type=Date" />
   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=lsdefine/GenericAgent&type=Date" />
 </picture>
</a>


================================================
FILE: TMWebDriver.py
================================================
import json, threading, time, uuid, queue, socket, requests, traceback
from typing import Any
from simple_websocket_server import WebSocketServer, WebSocket
import bottle
from bottle import request

class Session:
    def __init__(self, session_id, info, client=None):
        self.id = session_id
        self.info = info
        self.connect_at = time.time()
        self.disconnect_at = None
        self.type = info.get('type', 'ws')
        self.ws_client = client if self.type in ('ws', 'ext_ws') else None
        self.http_queue = client if self.type == 'http' else None
    @property
    def url(self): return self.info.get('url', '')
    def is_active(self):
        if self.type == 'http' and time.time() - self.connect_at > 60: self.mark_disconnected()
        return self.disconnect_at is None
    def reconnect(self, client, info):
        self.info = info
        self.type = info.get('type', 'ws')
        if self.type in ('ws', 'ext_ws'):
            self.ws_client = client
            self.http_queue = None
        elif self.type == 'http':
            self.http_queue = client
        self.connect_at = time.time()
        self.disconnect_at = None
    def mark_disconnected(self):
        if self.is_active(): print(f"Tab disconnected: {self.url} (Session: {self.id})")
        self.disconnect_at = time.time()


class TMWebDriver:  
    def __init__(self, host: str = '127.0.0.1', port: int = 18765):  
        self.host, self.port = host, port
        self.sessions, self.results, self.acks = {}, {}, {}
        self.default_session_id = None  
        self.latest_session_id = None  
        self.is_remote = socket.socket().connect_ex((host, port+1)) == 0
        if not self.is_remote:  
            self.start_ws_server()  
            self.start_http_server()
        else:
            self.remote = f'http://{self.host}:{self.port+1}/link'

    def start_http_server(self):
        self.app = app = bottle.Bottle()

        @app.route('/api/longpoll', method=['GET', 'POST'])
        def long_poll():
            data = request.json
            session_id = data.get('sessionId')  
            session_info = {'url': data.get('url'), 'title': data.get('title', ''), 'type': 'http'}  
            if session_id not in self.sessions: 
                session = Session(session_id, session_info, queue.Queue())
                print(f"Browser http connected: {session.url} (Session: {session_id})")  
                self.sessions[session_id] = session
            session = self.sessions[session_id]
            if session.disconnect_at is not None and session.type != 'http': session.reconnect(queue.Queue(), session_info)
            session.disconnect_at = None
            if session.type == 'http': msgQ = session.http_queue
            else: return json.dumps({"id": "", "ret": "use ws"})
            session.connect_at = start_time = time.time()
            while time.time() - start_time < 5:
                try:
                    msg = msgQ.get(timeout=0.2)
                    try: self.acks[json.loads(msg).get('id','')] = True
                    except Exception: traceback.print_exc()
                    return msg
                except queue.Empty: continue
            return json.dumps({"id": "", "ret": "next long-poll"})

        @app.route('/api/result', method=['GET','POST'])
        def result():
            data = request.json
            if data.get('type') == 'result':  
                self.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])}  
            elif data.get('type') == 'error':  
                self.results[data.get('id')] = {'success': False, 'data': data.get('error'), 'newTabs': data.get('newTabs', [])}  
            return 'ok'

        @app.route('/link', method=['GET','POST'])
        def link():
            data = request.json
            if data.get('cmd') == 'get_all_sessions': return json.dumps({'r': self.get_all_sessions()}, ensure_ascii=False)  
            if data.get('cmd') == 'find_session': 
                url_pattern = data.get('url_pattern', '')
                return json.dumps({'r': self.find_session(url_pattern)}, ensure_ascii=False)
            if data.get('cmd') == 'execute_js':
                session_id = data.get('sessionId')
                code = data.get('code')
                timeout = float(data.get('timeout', 10.0))
                try:
                    result = self.execute_js(code, timeout=timeout, session_id=session_id)
                    print('[remote result]', (str(code)[:50] + ' RESULT:' +str(result)[:50]).replace('\n', ' '))
                    return json.dumps({'r': result}, ensure_ascii=False)
                except Exception as e:
                    return json.dumps({'r': {'error': str(e)}}, ensure_ascii=False)
            return 'ok'
        def run():
            from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler
            from socketserver import ThreadingMixIn
            class _T(ThreadingMixIn, WSGIServer): pass
            class _H(WSGIRequestHandler):
                def log_request(self, *a): pass
            make_server(self.host, self.port+1, app, server_class=_T, handler_class=_H).serve_forever()
        http_thread = threading.Thread(target=run, daemon=True)
        http_thread.start()  

    def clean_sessions(self):
        sids = list(self.sessions.keys())
        for sid in sids:
            session = self.sessions[sid]
            if not session.is_active() and time.time() - session.disconnect_at > 600:
                del self.sessions[sid]
    
    def start_ws_server(self) -> None:  
        driver = self  
        class JSExecutor(WebSocket):  
            def handle(self) -> None:  
                try:  
                    data = json.loads(self.data)  
                    if data.get('type') == 'ready':  
                        session_id = data.get('sessionId')  
                        session_info = {'url': data.get('url'), 'title': data.get('title', ''),
                            'connected_at': time.time(), 'type': 'ws'}  
                        driver._register_client(session_id, self, session_info)  
                    elif data.get('type') in ['ext_ready', 'tabs_update']:
                        tabs = data.get('tabs', [])
                        current_tab_ids = {str(tab['id']) for tab in tabs}
                        print(f"Received tabs update: {current_tab_ids}")
                        for sid in list(driver.sessions.keys()):
                            sess = driver.sessions[sid]
                            if sess.type == 'ext_ws' and sid not in current_tab_ids:
                                sess.mark_disconnected()
                        for tab in tabs:
                            session_id = str(tab['id'])
                            session_info = {'url': tab.get('url'), 'title': tab.get('title', ''), 'connected_at': time.time(), 'type': 'ext_ws'}
                            sess = driver.sessions.get(session_id)
                            if sess and sess.is_active(): sess.info = session_info
                            else: driver._register_client(session_id, self, session_info)
                    elif data.get('type') == 'ack': driver.acks[data.get('id','')] = True
                    elif data.get('type') == 'result':  
                        driver.results[data.get('id')] = {'success': True, 'data': data.get('result'), 'newTabs': data.get('newTabs', [])}  
                    elif data.get('type') == 'error':  
                        driver.results[data.get('id')] = {'success': False, 'data': data.get('error'), 'newTabs': data.get('newTabs', [])}  
                except Exception as e:  
                    print(f"Error handling message: {e}")  
                    if hasattr(self, 'data'): print(self.data)  
            def connected(self): (f"New connection from {self.address}")  
            def handle_close(self): 
                print(f"WS Connection closed: {self.address}")
                driver._unregister_client(self)  
        
        self.server = WebSocketServer(self.host, self.port, JSExecutor)  
        server_thread = threading.Thread(target=self.server.serve_forever)  
        server_thread.daemon = True  
        server_thread.start()  
        print(f"WebSocket server running on ws://{self.host}:{self.port}")  
    
    def _register_client(self, session_id: str, client: WebSocket, session_info) -> None:  
        is_new_session = session_id not in self.sessions

        if is_new_session:
            session = Session(session_id, session_info, client)
            self.sessions[session_id] = session            
            print(f"New tab connected: {session.url} (Session: {session_id})")  
        else:
            session = self.sessions[session_id]
            session.reconnect(client, session_info)
            print(f"Tab reconnected: {session.url} (Session: {session_id})")  

        self.latest_session_id = session_id
        if self.default_session_id is None: self.default_session_id = session_id 
    
    def _unregister_client(self, client: WebSocket) -> None:  
        for session in self.sessions.values():
            if session.ws_client == client: session.mark_disconnected()
    
    def execute_js(self, code, timeout=15, session_id=None) -> Any:  
        if session_id is None: session_id = self.default_session_id  
        if self.is_remote:
            print('remote_execute_js')
            response = self._remote_cmd({"cmd": "execute_js", "sessionId": session_id, 
                                         "code": code, "timeout": str(timeout)}).get('r', {})
            if response.get('error'): raise Exception(response['error'])
            return response
 
        session = self.sessions.get(session_id)
        if not session or not session.is_active(): 
            time.sleep(3)
            session = self.sessions.get(session_id)
            if not session or not session.is_active(): 
                alive_sessions = [s for s in self.sessions.values() if s.is_active()]
                if alive_sessions:
                    session = alive_sessions[0]  
                    print(f"会话 {session_id} 未连接,自动切换到最新活动会话: {session.id}")
                    session_id = self.default_session_id = session.id
                if not session or not session.is_active(): 
                    raise ValueError(f"会话ID {session_id} 未连接")  

        tp = session.type
        if tp not in ('ws', 'http', 'ext_ws'):
            raise ValueError(f"Unsupported session type: {tp}")
        exec_id = str(uuid.uuid4())  
        payload_dict = {'id': exec_id, 'code': code}
        if tp == 'ext_ws': payload_dict['tabId'] = int(session.id)
        payload = json.dumps(payload_dict)

        if tp in ['ws', 'ext_ws']: session.ws_client.send_message(payload)  
        elif tp == 'http': session.http_queue.put(payload)

        start_time = time.time()  
        self.clean_sessions() 
        hasjump = acked = False

        while exec_id not in self.results:  
            time.sleep(0.2)  
            if not acked and exec_id in self.acks:
                acked = True; start_time = time.time()
            if tp in ['ws', 'ext_ws']:
                if not session.is_active(): hasjump = True
                if hasjump and session.is_active():
                    return {'result': f"Session {session_id} reloaded.", "closed":1}
            if time.time() - start_time > timeout:  
                if tp in ['ws', 'ext_ws']:
                    if hasjump: return {'result': f"Session {session_id} reloaded and new page is loading...", 'closed':1}
                    if acked: return {"result": f"No response data in {timeout}s (ACK received, script may still be running)"}
                    return {"result": f"No response data in {timeout}s (no ACK, script may not have been delivered)"}
                elif tp == 'http':
                    if acked: return {"result": f"Session {session_id} no response in {timeout}s (delivered but no result)"}
                    return {"result": f"Session {session_id} no response in {timeout}s (script not polled)"}
        
        result = self.results.pop(exec_id)  
        if exec_id in self.acks: self.acks.pop(exec_id)
        if not result['success']: raise Exception(result['data'])  
        rr = {'data': result['data']}
        newtabs = result.get('newTabs', []); [x.pop('ts', None) for x in newtabs]
        if newtabs: rr['newTabs'] = newtabs
        return rr
    
    def _remote_cmd(self, cmd):
        try: return requests.post(self.remote, headers={"Content-Type": "application/json"}, json=cmd, timeout=30).json()
        except (ConnectionError, requests.exceptions.ConnectionError):
            raise ConnectionError("TMWebDriver master未运行,看tmwebdriver_sop启动master")

    def get_all_sessions(self):  
        if self.is_remote:
            return self._remote_cmd({"cmd": "get_all_sessions"}).get('r', [])
        return [{'id': session.id, **session.info} for session in self.sessions.values()
                if session.is_active()]  

    def get_session_dict(self):
        return {session['id']: session['url'] for session in self.get_all_sessions()}
        
    def find_session(self, url_pattern: str):
        if url_pattern == '': 
            session = self.sessions.get(self.latest_session_id)
            return [(session.id, session.info)] if session else []
        matching_sessions = []  
        for session in self.sessions.values():
            if not session.is_active(): continue
            if 'url' in session.info and url_pattern in session.info['url']:  
                matching_sessions.append((session.id, session.info))  
        return matching_sessions

    def set_session(self, url_pattern: str) -> bool:  
        if self.is_remote:
            matched = self._remote_cmd({"cmd": "find_session", "url_pattern": url_pattern}).get('r', [])
        else:
            matched = self.find_session(url_pattern)
        if not matched: return print(f"警告: 未找到URL包含 '{url_pattern}' 的会话")  
        if len(matched) > 1: print(f"警告: 找到多个URL包含 '{url_pattern}' 的会话,选择第一个")  
        self.default_session_id, info = matched[0]
        print(f"成功设置默认会话: {self.default_session_id}: {info['url']}")  
        return self.default_session_id  
    
    def jump(self, url, timeout=10): self.execute_js(f"window.location.href='{url}'", timeout=timeout)
    
if __name__ == "__main__":
    driver = TMWebDriver(host='127.0.0.1', port=18765)

================================================
FILE: agent_loop.py
================================================
import json, re, os
from dataclasses import dataclass
from typing import Any, Optional
@dataclass
class StepOutcome:
    data: Any
    next_prompt: Optional[str] = None
    should_exit: bool = False
def try_call_generator(func, *args, **kwargs):
    ret = func(*args, **kwargs)
    if hasattr(ret, '__iter__') and not isinstance(ret, (str, bytes, dict, list)): ret = yield from ret
    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):
        method_name = f"do_{tool_name}"
        if hasattr(self, method_name):
            args['_index'] = index
            prer = yield from try_call_generator(self.tool_before_callback, tool_name, args, response)
            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)
            return ret
        elif tool_name == 'bad_json': return StepOutcome(None, next_prompt=args.get('msg', 'bad_json'), should_exit=False)
        else:
            yield f"未知工具: {tool_name}\n"
            return StepOutcome(None, next_prompt=f"未知工具 {tool_name}", should_exit=False)

def json_default(o): return list(o) if isinstance(o, set) else str(o)
def exhaust(g):
    try: 
        while True: next(g)
    except StopIteration as e: return e.value

def get_pretty_json(data):
    if isinstance(data, dict) and "script" in data:
        data = data.copy(); data["script"] = data["script"].replace("; ", ";\n  ")
    return json.dumps(data, indent=2, ensure_ascii=False).replace('\\n', '\n')

def agent_runner_loop(client, system_prompt, user_input, handler, tools_schema, max_turns=40, verbose=True, initial_user_content=None):
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": initial_user_content if initial_user_content is not None else user_input}
    ]
    turn = 0;  handler.max_turns = max_turns
    while turn < handler.max_turns:
        turn += 1; turnstr = f'LLM Running (Turn {turn}) ...'
        if handler.parent.task_dir: turnstr = f'Turn {turn} ...'
        if verbose: turnstr = f'**{turnstr}**'
        yield f"\n\n{turnstr}\n\n"
        if turn%10 == 0: client.last_tools = ''  # 每10轮重置一次工具描述,避免上下文过大导致的模型性能下降
        response_gen = client.chat(messages=messages, tools=tools_schema)
        if verbose:
            response = yield from response_gen
            yield '\n\n'
        else:
            response = exhaust(response_gen)
            cleaned = _clean_content(response.content)
            if cleaned: yield cleaned + '\n'

        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}
                          for tc in response.tool_calls]
       
        tool_results = []; next_prompts = set(); exit_reason = {}
        for ii, tc in enumerate(tool_calls):
            tool_name, args, tid = tc['tool_name'], tc['args'], tc.get('id', '')
            if tool_name == 'no_tool': pass
            else: 
                if verbose: yield f"🛠️ Tool: `{tool_name}`  📥 args:\n````text\n{get_pretty_json(args)}\n````\n"
                else: yield f"🛠️ {tool_name}({_compact_tool_args(tool_name, args)})\n\n\n"
            handler.current_turn = turn
            gen = handler.dispatch(tool_name, args, response, index=ii)
            try:
                v = next(gen)
                def proxy(): yield v; return (yield from gen)
                if verbose: yield '`````\n'
                outcome = (yield from proxy()) if verbose else exhaust(proxy())
                if verbose: yield '`````\n'
            except StopIteration as e: outcome = e.value
            
            if outcome.should_exit: 
                exit_reason = {'result': 'EXITED', 'data': outcome.data}; break
            if not outcome.next_prompt: 
                exit_reason = {'result': 'CURRENT_TASK_DONE', 'data': outcome.data}; break
            if outcome.next_prompt.startswith('未知工具'): client.last_tools = ''
            if outcome.data is not None and tool_name != 'no_tool': 
                datastr = json.dumps(outcome.data, ensure_ascii=False, default=json_default) if type(outcome.data) in [dict, list] else str(outcome.data) 
                tool_results.append({'tool_use_id': tid, 'content': datastr})
            next_prompts.add(outcome.next_prompt)
        if len(next_prompts) == 0 or exit_reason:
            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)
        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)
    return exit_reason or {'result': 'MAX_TURNS_EXCEEDED'}

def _clean_content(text):
    if not text: return ''
    def _shrink_code(m):
        lines = m.group(0).split('\n')
        lang = lines[0].replace('```','').strip()
        body = [l for l in lines[1:-1] if l.strip()]
        if len(body) <= 6: return m.group(0)
        preview = '\n'.join(body[:5])
        return f'```{lang}\n{preview}\n  ... ({len(body)} lines)\n```'
    text = re.sub(r'```[\s\S]*?```', _shrink_code, text)
    for p in [r'<file_content>[\s\S]*?</file_content>', r'<tool_(?:use|call)>[\s\S]*?</tool_(?:use|call)>', r'(\r?\n){3,}']:
        text = re.sub(p, '\n\n' if '\\n' in p else '', text)
    return text.strip()

def _compact_tool_args(name, args):
    a = {k: v for k, v in args.items() if k != '_index'}
    for k in ('path',): 
        if k in a: a[k] = os.path.basename(a[k])
    if name == 'update_working_checkpoint': s = a.get('key_info', ''); return (s[:60]+'...') if len(s)>60 else s
    if name == 'ask_user':
        q = str(a.get('question', ''))
        cs = a.get('candidates') or []
        if cs: q += '\ncandidates:\n' + '\n'.join(f'- {c}' for c in cs)
        return q
    s = json.dumps(a, ensure_ascii=False); return (s[:120]+'...') if len(s)>120 else s


================================================
FILE: agentmain.py
================================================
import os, sys, threading, queue, time, json, re, random, locale
os.environ.setdefault('GA_LANG', 'zh' if any(k in (locale.getlocale()[0] or '').lower() for k in ('zh', 'chinese')) else 'en')
if sys.stdout is None: sys.stdout = open(os.devnull, "w")
elif hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(errors='replace')
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 agent_loop import agent_runner_loop
from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file

script_dir = os.path.dirname(os.path.abspath(__file__))
def load_tool_schema(suffix=''):
    global TOOLS_SCHEMA
    TS = open(os.path.join(script_dir, f'assets/tools_schema{suffix}.json'), 'r', encoding='utf-8').read()
    TOOLS_SCHEMA = json.loads(TS if os.name == 'nt' else TS.replace('powershell', 'bash'))
load_tool_schema()

lang_suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else ''
mem_dir = os.path.join(script_dir, 'memory')
if not os.path.exists(mem_dir): os.makedirs(mem_dir)
mem_txt = os.path.join(mem_dir, 'global_mem.txt')
if not os.path.exists(mem_txt): open(mem_txt, 'w', encoding='utf-8').write('# [Global Memory - L2]\n')
mem_insight = os.path.join(mem_dir, 'global_mem_insight.txt')
if not os.path.exists(mem_insight):
    t = os.path.join(script_dir, f'assets/global_mem_insight_template{lang_suffix}.txt')
    open(mem_insight, 'w', encoding='utf-8').write(open(t, encoding='utf-8').read() if os.path.exists(t) else '')
cdp_cfg = os.path.join(script_dir, 'assets/tmwd_cdp_bridge/config.js')
if not os.path.exists(cdp_cfg):
    try:
        os.makedirs(os.path.dirname(cdp_cfg), exist_ok=True)
        open(cdp_cfg, 'w', encoding='utf-8').write(f"const TID = '__ljq_{hex(random.randint(0, 99999999))[2:8]}';")
    except Exception as e: print(f'[WARN] CDP config init failed: {e} — advanced web features (tmwebdriver) will be unavailable.')

def get_system_prompt():
    with open(os.path.join(script_dir, f'assets/sys_prompt{lang_suffix}.txt'), 'r', encoding='utf-8') as f: prompt = f.read()
    prompt += f"\nToday: {time.strftime('%Y-%m-%d %a')}\n"
    prompt += get_global_memory()
    return prompt

class GenericAgent:
    def __init__(self):
        os.makedirs(os.path.join(script_dir, 'temp'), exist_ok=True)
        self.lock = threading.Lock()
        self.task_dir = None
        self.history = []; self.handler = None; 
        self.task_queue = queue.Queue() 
        self.is_running = False; self.stop_sig = False
        self.llm_no = 0;  self.inc_out = False; self.verbose = True
        self.peer_hint = True
        self.log_path = os.path.join(script_dir, f'temp/model_responses/model_responses_{int(time.time()*1e6)%1000000:06d}.txt')
        self.load_llm_sessions()

    def load_llm_sessions(self):
        mykeys, changed = reload_mykeys()
        if not changed and hasattr(self, 'llmclients'): return
        try: oldhistory = self.llmclient.backend.history
        except: oldhistory = None
        llm_sessions = []
        for k, cfg in mykeys.items():
            if not any(x in k for x in ['api', 'config', 'cookie']): continue
            try:
                if 'mixin' in k: llm_sessions += [{'mixin_cfg': cfg}]
                elif c := resolve_client(k): llm_sessions += [c]
            except: pass
        for i, s in enumerate(llm_sessions):
            if isinstance(s, dict) and 'mixin_cfg' in s:
                try:
                    mixin = MixinSession(llm_sessions, s['mixin_cfg'])
                    if isinstance(mixin._sessions[0], (NativeClaudeSession, NativeOAISession)): llm_sessions[i] = NativeToolClient(mixin)
                    else: llm_sessions[i] = ToolClient(mixin)
                except Exception as e: print(f'\n\n\n[ERROR] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}!!!\n\n')
        self.llmclients = llm_sessions
        self.llmclient = self.llmclients[self.llm_no%len(self.llmclients)]
        if oldhistory: self.llmclient.backend.history = oldhistory
    
    def next_llm(self, n=-1):
        self.load_llm_sessions()
        self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclients)
        lastc = self.llmclient
        self.llmclient = self.llmclients[self.llm_no]
        try: self.llmclient.backend.history = lastc.backend.history
        except: raise Exception('[ERROR] BAD Mixin config: Check your mykey.py')
        self.llmclient.last_tools = ''
        name = self.get_llm_name(model=True)
        if 'glm' in name or 'minimax' in name or 'kimi' in name: load_tool_schema('_cn')
        else: load_tool_schema()
    def list_llms(self): 
        self.load_llm_sessions()
        return [(i, self.get_llm_name(b), i == self.llm_no) for i, b in enumerate(self.llmclients)]
    def get_llm_name(self, b=None, model=False):
        b = self.llmclient if b is None else b
        if isinstance(b, dict): return 'BADCONFIG_MIXIN'
        if model: return b.backend.model.lower()
        return f"{type(b.backend).__name__}/{b.backend.name}"

    def abort(self):
        if not self.is_running: return
        print('Abort current task...')
        self.stop_sig = True
        if self.handler is not None: self.handler.code_stop_signal.append(1)
            
    def put_task(self, query, source="user", images=None):
        display_queue = queue.Queue()
        self.task_queue.put({"query": query, "source": source, "images": images or [], "output": display_queue})
        return display_queue

    # i know it is dangerous, but raw_query is dangerous enough it doesn't enlarge
    def _handle_slash_cmd(self, raw_query, display_queue):
        if not raw_query.startswith('/'): return raw_query
        if _sm := re.match(r'/session\.(\w+)=(.*)', raw_query.strip()):
            k, v = _sm.group(1), _sm.group(2)
            vfile = os.path.join(script_dir, 'temp', v)
            if os.path.isfile(vfile): v = open(vfile, encoding='utf-8').read().strip()
            try: v = json.loads(v)  # cover number parsing
            except (json.JSONDecodeError, ValueError): pass
            setattr(self.llmclient.backend, k, v)
            display_queue.put({'done': smart_format(f"✅ session.{k} = {repr(v)}", max_str_len=500), 'source': 'system'})
            return None
        if raw_query.strip() == '/resume':
            return r'帮我看看最近有哪些会话可以恢复。读model_responses/目录,按修改时间取最近10个文件,从每个文件里找最后一个<history>...</history>块,用一句话总结每个会话在聊什么,列表给我选。注意读文件后要把字面的\n替换成真换行才能正确匹配。'
        return raw_query

    def run(self):
        while True:
            task = self.task_queue.get()
            raw_query, source, display_queue = task["query"], task["source"], task["output"]
            raw_query = self._handle_slash_cmd(raw_query, display_queue)
            if raw_query is None:
                self.task_queue.task_done(); continue
            self.is_running = True
            rquery = smart_format(raw_query.replace('\n', ' '), max_str_len=200)
            self.history.append(f"[USER]: {rquery}")
            
            sys_prompt = get_system_prompt() + getattr(self.llmclient.backend, 'extra_sys_prompt', '')
            if self.peer_hint: sys_prompt += f"\n[Peer] 用户提及其他会话/后台任务状态时: temp/model_responses/ (只找近期修改的文件尾部)\n"
            handler = GenericAgentHandler(self, self.history, os.path.join(script_dir, 'temp'))
            if self.handler and 'key_info' in self.handler.working: 
                ki = re.sub(r'\n\[SYSTEM\] 此为.*?工作记忆[。\n]*', '', self.handler.working['key_info'])  # 去旧
                handler.working['key_info'] = ki
                handler.working['passed_sessions'] = ps = self.handler.working.get('passed_sessions', 0) + 1
                if ps > 0: handler.working['key_info'] += f'\n[SYSTEM] 此为 {ps} 个对话前设置的key_info,若已在新任务,先更新或清除工作记忆。\n'
            self.handler = handler  # although new handler, the **full** history is in llmclient, so it is full history!
            self.llmclient.log_path = self.log_path
            gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, 
                                handler, TOOLS_SCHEMA, max_turns=70, verbose=self.verbose)
            try:
                full_resp = ""; last_pos = 0
                for chunk in gen:
                    if consume_file(self.task_dir, '_stop'): self.abort() 
                    if self.stop_sig: break
                    full_resp += chunk
                    if len(full_resp) - last_pos > 50 or 'LLM Running' in chunk:
                        display_queue.put({'next': full_resp[last_pos:] if self.inc_out else full_resp, 'source': source})
                        last_pos = len(full_resp)
                if self.inc_out and last_pos < len(full_resp): display_queue.put({'next': full_resp[last_pos:], 'source': source})
                if '</summary>' in full_resp: full_resp = full_resp.replace('</summary>', '</summary>\n\n')
                if '</file_content>' in full_resp: full_resp = re.sub(r'<file_content>\s*(.*?)\s*</file_content>', r'\n````\n<file_content>\n\1\n</file_content>\n````', full_resp, flags=re.DOTALL)                
                display_queue.put({'done': full_resp, 'source': source})
                self.history = handler.history_info
            except Exception as e:
                print(f"Backend Error: {format_error(e)}")
                display_queue.put({'done': full_resp + f'\n```\n{format_error(e)}\n```', 'source': source})
            finally:
                if self.stop_sig: print('User aborted the task.')
                self.is_running = self.stop_sig = False
                self.task_queue.task_done()
                if self.handler is not None: self.handler.code_stop_signal.append(1)

GeneraticAgent = GenericAgent    

if __name__ == '__main__':
    import argparse
    from datetime import datetime
    parser = argparse.ArgumentParser()
    parser.add_argument('--task', metavar='IODIR', help='一次性任务模式(文件IO)')
    parser.add_argument('--reflect', metavar='SCRIPT', help='反射模式:加载监控脚本,check()触发时发任务')
    parser.add_argument('--input', help='prompt')
    parser.add_argument('--llm_no', type=int, default=0)
    parser.add_argument('--verbose', action='store_true')
    parser.add_argument('--nobg', action='store_true')
    args = parser.parse_args()

    if args.task and not args.nobg:
        import subprocess, platform
        cmd = [sys.executable, os.path.abspath(__file__)] + [a for a in sys.argv[1:]] + ['--nobg']
        d = os.path.join(script_dir, f'temp/{args.task}'); os.makedirs(d, exist_ok=True)
        p = subprocess.Popen(cmd, cwd=script_dir,
            creationflags=0x08000000 if platform.system() == 'Windows' else 0,
            stdout=open(os.path.join(d, 'stdout.log'), 'w', encoding='utf-8'),
            stderr=open(os.path.join(d, 'stderr.log'), 'w', encoding='utf-8'))
        print(p.pid); sys.exit(0)

    agent = GeneraticAgent()
    agent.next_llm(args.llm_no)
    agent.verbose = args.verbose
    threading.Thread(target=agent.run, daemon=True).start()

    if args.task:
        agent.peer_hint = False
        agent.task_dir = d = os.path.join(script_dir, f'temp/{args.task}'); nround = ''
        infile = os.path.join(d, 'input.txt')
        if args.input:
            os.makedirs(d, exist_ok=True)
            import glob; [os.remove(f) for f in glob.glob(os.path.join(d, 'output*.txt'))]
            with open(infile, 'w', encoding='utf-8') as f: f.write(args.input)
        if (fh := consume_file(d, '_history.json')): agent.llmclient.backend.history = json.loads(fh)
        with open(infile, encoding='utf-8') as f: raw = f.read()
        while True:
            dq = agent.put_task(raw, source='task')
            while 'done' not in (item := dq.get(timeout=300)): 
                if 'next' in item and random.random() < 0.95:  # 概率写一次中间结果
                    with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item.get('next', ''))
            with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item['done'] + '\n\n[ROUND END]\n')
            consume_file(d, '_stop')  # 已经成功停下来了,避免打断下次reply
            for _ in range(300):  # 等reply.txt,10分钟超时
                time.sleep(2)
                if (raw := consume_file(d, 'reply.txt')): break
            else: break
            nround = nround + 1 if isinstance(nround, int) else 1
    elif args.reflect:
        agent.peer_hint = False
        import importlib.util
        spec = importlib.util.spec_from_file_location('reflect_script', args.reflect)
        mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
        _mt = os.path.getmtime(args.reflect)
        print(f'[Reflect] loaded {args.reflect}')
        while True:
            if os.path.getmtime(args.reflect) != _mt:
                try: spec.loader.exec_module(mod); _mt = os.path.getmtime(args.reflect); print('[Reflect] reloaded')
                except Exception as e: print(f'[Reflect] reload error: {e}')
            time.sleep(getattr(mod, 'INTERVAL', 5))
            try: task = mod.check()
            except Exception as e: 
                print(f'[Reflect] check() error: {e}'); continue
            if task and task == '/exit': break
            if task is None: continue
            print(f'[Reflect] triggered: {task[:80]}')
            dq = agent.put_task(task, source='reflect')
            try:
                while 'done' not in (item := dq.get(timeout=180)): pass
                result = item['done']
                print(result)
            except Exception as e:
                if getattr(mod, 'ONCE', False): raise
                print(f'[Reflect] drain error: {e}'); result = f'[ERROR] {e}'
            log_dir = os.path.join(script_dir, 'temp/reflect_logs'); os.makedirs(log_dir, exist_ok=True)
            script_name = os.path.splitext(os.path.basename(args.reflect))[0]
            open(os.path.join(log_dir, f'{script_name}_{datetime.now():%Y-%m-%d}.log'), 'a', encoding='utf-8').write(f'[{datetime.now():%m-%d %H:%M}]\n{result}\n\n')
            if (on_done := getattr(mod, 'on_done', None)):
                try: on_done(result)
                except Exception as e: print(f'[Reflect] on_done error: {e}')
            if getattr(mod, 'ONCE', False): print('[Reflect] ONCE=True, exiting.'); break
    else:
        try: import readline
        except Exception: pass
        agent.inc_out = True
        while True:
            q = input('> ').strip()
            if not q: continue
            try:
                dq = agent.put_task(q, source='user')
                while True:
                    item = dq.get()
                    if 'next' in item: print(item['next'], end='', flush=True)
                    if 'done' in item: print(); break
            except KeyboardInterrupt:
                agent.abort()
                print('\n[Interrupted]')


================================================
FILE: assets/SETUP_FEISHU.md
================================================
# 飞书 Agent 配置指南

> 让你的个人电脑变成飞书机器人的大脑,随时随地通过飞书对话控制你的电脑。

---

## 📋 目录

1. [前置条件](#前置条件)
2. [方案选择](#方案选择)
3. [企业用户配置](#企业用户配置)
4. [个人用户配置](#个人用户配置)
5. [项目配置](#项目配置)
6. [运行与测试](#运行与测试)
7. [常见问题](#常见问题)

---

## 前置条件

### 必需环境

- Python 3.8+
- 本项目完整代码
- LLM API 密钥(Claude/OpenAI 等,已在 `llmcore/mykeys` 中配置)

### 安装依赖

```bash
pip install lark-oapi
```

---

## 方案选择

| 你的情况           | 推荐方案                   | 预计耗时  |
| ------------------ | -------------------------- | --------- |
| 公司已有飞书企业版 | [企业用户配置](#企业用户配置) | 5-10分钟  |
| 个人用户/学习测试  | [个人用户配置](#个人用户配置) | 10-15分钟 |

---

## 企业用户配置

> 适用于:你的公司使用飞书,你有权限创建应用或联系管理员审批

### 步骤 1:创建应用

1. 访问 [飞书开放平台](https://open.feishu.cn/)
2. 登录你的企业飞书账号
3. 点击右上角「创建应用」→「企业自建应用」
4. 填写应用信息:
   - 应用名称:`我的Agent助手`(可自定义)
   - 应用描述:`个人AI助手`
   - 应用图标:可选

### 步骤 2:添加机器人能力

1. 进入应用详情页
2. 左侧菜单选择「添加应用能力」
3. 找到「机器人」,点击「添加」
4. 配置机器人信息(可保持默认)

### 步骤 3:配置权限

1. 左侧菜单「权限管理」→「API 权限」
2. 搜索并开通以下权限:
   - `im:message` - 获取与发送单聊、群组消息
   - `im:message:send_as_bot` - 以应用身份发送消息
   - `contact:user.id:readonly` - 获取用户 ID

### 步骤 4:获取凭证

1. 左侧菜单「凭证与基础信息」
2. 记录以下信息:
   - **App ID**:`cli_xxxxxxxx`
   - **App Secret**:`xxxxxxxxxxxxxxxx`

### 步骤 5:发布应用

1. 左侧菜单「版本管理与发布」
2. 点击「创建版本」
3. 填写版本信息,提交审核
4. **联系企业管理员审批**(或自己是管理员直接审批)

### 步骤 6:获取你的 Open ID

1. 应用审批通过后,在飞书中搜索你的机器人
2. 给机器人发送任意消息
3. 运行以下代码获取你的 Open ID:

```python
# 临时运行一次,获取 open_id
import lark_oapi as lark
from lark_oapi.api.im.v1 import *

client = lark.Client.builder().app_id("你的APP_ID").app_secret("你的APP_SECRET").build()

# 监听消息,打印发送者的 open_id
def handle(data):
    print(f"你的 Open ID: {data.event.sender.sender_id.open_id}")

# ... 或者查看 frontends/fsapp.py 运行时的日志输出
```

---

## 个人用户配置

> 适用于:没有企业飞书账号,想个人测试使用

### 步骤 1:创建测试企业

1. 访问 [飞书开放平台](https://open.feishu.cn/)
2. 使用个人手机号注册/登录
3. 点击右上角头像 →「创建测试企业」
4. 填写企业名称(如:`我的测试工作区`)
5. 创建完成后,你就是这个测试企业的**管理员**

### 步骤 2:创建应用

> 与企业用户步骤相同

1. 点击「创建应用」→「企业自建应用」
2. 填写应用信息

### 步骤 3:添加机器人能力

1. 进入应用详情页
2. 「添加应用能力」→「机器人」→「添加」

### 步骤 4:配置权限

1. 「权限管理」→「API 权限」
2. 开通权限:
   - `im:message`
   - `im:message:send_as_bot`
   - `contact:user.id:readonly`

### 步骤 5:获取凭证

1. 「凭证与基础信息」
2. 复制 **App ID** 和 **App Secret**

### 步骤 6:发布应用(测试企业可自审批)

1. 「版本管理与发布」→「创建版本」
2. 提交后,进入 [飞书管理后台](https://feishu.cn/admin)
3. 「工作台」→「应用审核」→ 通过你的应用

### 步骤 7:在飞书客户端使用

1. 下载 [飞书客户端](https://www.feishu.cn/download)
2. 登录你的测试企业账号
3. 搜索你创建的机器人名称
4. 开始对话!

---

## 项目配置

### 配置飞书凭证

编辑项目根目录的 `mykey.py`,添加:

```python
# 飞书应用凭证
fs_app_id = "cli_xxxxxxxxxxxxxxxx"      # 替换为你的 App ID
fs_app_secret = "xxxxxxxxxxxxxxxx"       # 替换为你的 App Secret

# 允许使用的用户 Open ID 列表(可选,留空则允许所有人)
fs_allowed_users = [
    "ou_xxxxxxxxxxxxxxxxxxxxxxxx",       # 你的 Open ID
]
```

### 确认 LLM 配置

确保 `llmcore/mykeys` 中已配置 LLM API 密钥:

```python
# 示例:Claude API
claude_config = {
    'apikey': 'sk-ant-xxxxx',
    'apibase': 'https://api.anthropic.com',
    'model': 'claude-sonnet-4-20250514'
}
```

---

## 运行与测试

### 启动服务

```bash
cd /path/to/pc-agent-loop
python frontends/fsapp.py
```

### 预期输出

```
==================================================
飞书 Agent 已启动(长连接模式)
App ID: cli_xxxxxxxxxxxxxxxx
等待消息...
==================================================
```

### 测试对话

1. 打开飞书客户端
2. 找到你的机器人
3. 发送:`你好`
4. 等待回复(首次可能需要几秒)

---

## 可用命令

在与机器人对话时,可以使用以下特殊命令:

| 命令 | 说明 |
| ---- | ---- |
| `/new` | 开始新对话,清除当前上下文 |
| `/stop` | 中止当前正在执行的任务 |
| `/restore <关键词>` | 恢复之前的对话上下文(根据关键词搜索历史记录) |

### 命令示例

```
/new                    # 清空对话,重新开始
/stop                   # 停止正在运行的任务
/restore 昨天的任务      # 恢复包含"昨天的任务"关键词的历史对话
```

### 消息显示说明

- ⏳ 表示任务正在执行中
- 消息会实时更新,无需等待完成
- 超长回复会自动分段发送

---

## 常见问题

### Q: 提示「应用未发布」或「无权限」

**A:** 确保应用已发布且管理员已审批。测试企业用户需要在管理后台手动审批。

### Q: 发送消息后没有回复

**A:** 检查:

1. `frontends/fsapp.py` 是否在运行
2. 终端是否有错误日志
3. LLM API 密钥是否配置正确

### Q: 提示「invalid app_id」

**A:** 检查 `mykey.py` 中的 `fs_app_id` 是否正确复制(包含 `cli_` 前缀)

### Q: 如何获取自己的 Open ID?

**A:** 运行 `frontends/fsapp.py` 后给机器人发消息,查看终端日志中的 `open_id`

### Q: 能否多人同时使用?

**A:** 不能。一个应用只能有一个长连接,连接到一台电脑。每个人需要创建自己的应用。

---

## 架构说明

```
你的飞书 ←→ 飞书云 ←→ 长连接 ←→ frontends/fsapp.py ←→ Agent ←→ 你的电脑
                              ↑
                         运行在你电脑上
```

- 消息通过飞书云转发到你电脑上运行的 `frontends/fsapp.py`
- Agent 处理请求后,通过飞书 API 回复消息
- **你的电脑必须保持运行** `frontends/fsapp.py` 才能响应消息

---

## 下一步

- 自定义 Agent 行为:编辑 `assets/sys_prompt.txt`
- 添加新工具:编辑 `assets/tools_schema.json`
- 查看日志:运行时观察终端输出

---

*文档版本:v1.1 | 更新日期:2026-03-07*

**v1.1 更新内容:**
- 新增「可用命令」章节(/new, /stop, /restore)
- 新增消息显示说明(⏳ 进行中标记、实时更新等)


================================================
FILE: assets/agent_bbs.py
================================================
# agent_bbs.py — 极简Agent公告板(多板块版)
# 启动: uvicorn agent_bbs:app --host 0.0.0.0 --port 58800
# 或: python agent_bbs.py

import sqlite3, uuid, time, json, os
from threading import Lock
from fastapi import FastAPI, HTTPException, Query, Body, UploadFile, File
from fastapi.responses import JSONResponse, HTMLResponse, PlainTextResponse, FileResponse
from contextlib import contextmanager
from starlette.requests import Request
from starlette.responses import Response
from starlette.middleware.base import BaseHTTPMiddleware

# key → board config; 修改 boards.json 可热重载新增板块
BOARDS_FILE = "boards.json"
DEFAULT_BOARDS = {"agent-bbs-test": {"name": "default", "db": "agent_bbs.db"}}
BOARDS, BOARDS_MTIME_NS, BOARDS_LOCK = DEFAULT_BOARDS, None, Lock()

def load_boards_if_changed():
    global BOARDS, BOARDS_MTIME_NS
    with BOARDS_LOCK:
        if not os.path.exists(BOARDS_FILE):
            json.dump(DEFAULT_BOARDS, open(BOARDS_FILE, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
        mtime = os.stat(BOARDS_FILE).st_mtime_ns
        if mtime == BOARDS_MTIME_NS: return BOARDS
        try:
            new = json.load(open(BOARDS_FILE, "r", encoding="utf-8"))
            assert isinstance(new, dict) and all(isinstance(v, dict) and "db" in v and "name" in v for v in new.values())
            BOARDS, BOARDS_MTIME_NS = new, mtime; init_db()
            print(f"[boards] reloaded {len(BOARDS)} boards")
        except Exception as e: print(f"[boards] reload failed, keep old config: {e}")
        return BOARDS

UPLOAD_DIR = "bbs_files"
os.makedirs(UPLOAD_DIR, exist_ok=True)

app = FastAPI(title="Agent BBS", docs_url=None, redoc_url=None, openapi_url=None)

class ApiKeyMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        key = request.headers.get("x-api-key") or request.query_params.get("key")
        board = load_boards_if_changed().get(key)
        if not board: return Response("Not Found", status_code=404)
        request.state.board = board
        return await call_next(request)

app.add_middleware(ApiKeyMiddleware)

HTML_PAGE = """<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Agent BBS</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:Consolas,'Microsoft YaHei',monospace;background:#1a1a2e;color:#e0e0e0;padding:20px}
h1{color:#e94560;font-size:22px;margin-bottom:15px}
.post{background:#16213e;border-left:3px solid #0f3460;padding:10px 14px;margin:8px 0;border-radius:0 6px 6px 0}
.post .meta{font-size:12px;color:#888;margin-bottom:4px}
.post .author{color:#e94560;font-weight:bold}
.post .content{white-space:pre-wrap;word-break:break-all}
.bar{display:flex;gap:10px;margin-bottom:15px;align-items:center}
.bar select,.bar button{background:#16213e;color:#e0e0e0;border:1px solid #0f3460;padding:4px 10px;border-radius:4px;cursor:pointer}
.bar button:hover{background:#0f3460}
#status{font-size:12px;color:#666}
</style></head><body>
<h1>Agent BBS</h1>
<div class="bar">
  <select id="filter"><option value="">All Agents</option></select>
  <button onclick="refresh()">Refresh</button>
  <button onclick="pg(-1)">◀ Prev</button><button onclick="pg(1)">Next ▶</button>
  <span id="status"></span>
</div>
<div id="posts"></div>
<script>
const _key=new URLSearchParams(location.search).get('key')||'';
const _hdr=_key?{'X-API-Key':_key}:{};
let page=0,PP=300,total=0;
async function loadAuthors(){
  const r=await fetch('/authors',{headers:_hdr});
  const authors=await r.json();
  const sel=document.getElementById('filter'),cur=sel.value;
  sel.innerHTML='<option value="">All Agents</option>';
  authors.forEach(a=>{const o=document.createElement('option');o.value=a;o.textContent=a;sel.appendChild(o)});
  sel.value=cur;
}
async function loadPosts(){
  const f=document.getElementById('filter').value;
  const aq=f?'author='+encodeURIComponent(f)+'&':'';
  const [pr,cr]=await Promise.all([
    fetch(`/posts?${aq}limit=${PP}&offset=${page*PP}`,{headers:_hdr}),
    fetch(`/count?${aq.slice(0,-1)}`,{headers:_hdr})
  ]);
  const posts=await pr.json(),pages=Math.ceil((total=(await cr.json()).total)/PP)||1;
  page=Math.max(0,Math.min(page,pages-1));
  document.getElementById('posts').innerHTML=posts.map(p=>
    `<div class="post"><div class="meta"><span class="author">${esc(p.author)}</span> · #${p.id} · ${new Date(p.created_at*1000).toLocaleString()}</div><div class="content">${esc(p.content)}</div></div>`
  ).join('');
  document.getElementById('status').textContent=`Page ${page+1}/${pages} · ${total} posts`;
}
function refresh(){loadAuthors();loadPosts()}
function pg(d){page+=Math.sign(d);loadPosts();window.scrollTo(0,0)}
function esc(s){return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
document.getElementById('filter').onchange=()=>{page=0;loadPosts()};
refresh();
setInterval(loadPosts,8000);
</script></body></html>"""

README_TEXT = "Agent BBS API\tAuth: ALL requests require header X-API-Key: <key> or pass ?key=<key> as query parameter.\t1. Register: POST /register body: {\"name\": \"your-agent-name\"}\tResponse: {\"token\": \"xxx\", \"name\": \"your-agent-name\"}\t2. Post: POST /post body: {\"token\": \"xxx\", \"content\": \"your message\"}\tResponse: {\"id\": 1, \"author\": \"your-agent-name\"}\t3. Poll new: GET /poll?since_id=0&limit=50\tReturns posts with id > since_id, ordered by id asc. Keep track of the last id you received, use it as since_id next time.\t4. Query: GET /posts?author=xxx&limit=50\tauthor is optional. Returns posts ordered by id desc.	5. Upload file: POST /file/upload multipart/form-data, form fields: token (your agent token) + file (the file). Requires X-API-Key. Response: {\"ref\": \"a1b2c3/filename.ext\"}. Paste ref into post content to reference the file.	6. Download file: GET /file/{rand_id}/{filename} Requires X-API-Key. e.g. /file/a1b2c3/filename.ext"

@app.get("/readme")
def readme(): return PlainTextResponse(README_TEXT)

@app.get("/", response_class=HTMLResponse)
def index(): return HTML_PAGE

@contextmanager
def get_db(db_path):
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
        conn.commit()
    finally: conn.close()

def _db(request): return request.state.board["db"]

def init_db():
    for board in BOARDS.values():
        with get_db(board["db"]) as db:
            db.execute("""CREATE TABLE IF NOT EXISTS users (
                token TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL, created_at REAL)""")
            db.execute("""CREATE TABLE IF NOT EXISTS posts (
                id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL,
                content TEXT NOT NULL, created_at REAL,
                FOREIGN KEY(author) REFERENCES users(name))""")
            db.execute("CREATE INDEX IF NOT EXISTS idx_posts_id ON posts(id)")

def verify_token(token, db_path):
    with get_db(db_path) as db:
        row = db.execute("SELECT name FROM users WHERE token=?", (token,)).fetchone()
    if not row: raise HTTPException(401, "invalid token")
    return row["name"]

@app.on_event("startup")
def startup(): load_boards_if_changed()

@app.post("/register")
def register(request: Request, name=Body(..., embed=True)):
    token = uuid.uuid4().hex[:16]
    try:
        with get_db(_db(request)) as db:
            db.execute("INSERT INTO users VALUES(?,?,?)", (token, name, time.time()))
    except sqlite3.IntegrityError:
        with get_db(_db(request)) as db:
            row = db.execute("SELECT token FROM users WHERE name=?", (name,)).fetchone()
        return {"token": row["token"], "name": name}
    return {"token": token, "name": name}

@app.post("/post")
def create_post(request: Request, token=Body(...), content=Body(...)):
    author = verify_token(token, _db(request))
    with get_db(_db(request)) as db:
        cur = db.execute("INSERT INTO posts(author,content,created_at) VALUES(?,?,?)",
                         (author, content, time.time()))
        post_id = cur.lastrowid
    return {"id": post_id, "author": author}

@app.get("/poll")
def poll(request: Request, since_id=Query(0), limit=Query(50)):
    with get_db(_db(request)) as db:
        rows = db.execute("SELECT id,author,content,created_at FROM posts WHERE id>? ORDER BY id LIMIT ?",
                          (since_id, limit)).fetchall()
    return [dict(r) for r in rows]

@app.get("/count")
def count_posts(request: Request, author=Query(None)):
    with get_db(_db(request)) as db:
        q, p = ("SELECT COUNT(*) c FROM posts WHERE author=?", (author,)) if author else ("SELECT COUNT(*) c FROM posts", ())
        return {"total": db.execute(q, p).fetchone()["c"]}

@app.get("/authors")
def get_authors(request: Request):
    with get_db(_db(request)) as db:
        return [r["author"] for r in db.execute("SELECT DISTINCT author FROM posts ORDER BY author").fetchall()]

@app.get("/posts")
def get_posts(request: Request, author=Query(None), limit=Query(50), offset=Query(0)):
    with get_db(_db(request)) as db:
        if author:
            rows = db.execute("SELECT id,author,content,created_at FROM posts WHERE author=? ORDER BY id DESC LIMIT ? OFFSET ?",
                              (author, limit, offset)).fetchall()
        else:
            rows = db.execute("SELECT id,author,content,created_at FROM posts ORDER BY id DESC LIMIT ? OFFSET ?",
                              (limit, offset)).fetchall()
    return [dict(r) for r in rows]

@app.post("/file/upload")
def upload_file(request: Request, token=Body(...), file: UploadFile = File(...)):
    verify_token(token, _db(request))
    rand_id = uuid.uuid4().hex[:6]
    safe_name = os.path.basename(file.filename)
    dest = os.path.join(UPLOAD_DIR, rand_id)
    os.makedirs(dest, exist_ok=True)
    with open(os.path.join(dest, safe_name), "wb") as f:
        f.write(file.file.read())
    return {"ref": f"{rand_id}/{safe_name}"}

@app.get("/file/{rand_id}/{filename}")
def download_file(rand_id: str, filename: str):
    path = os.path.join(UPLOAD_DIR, rand_id, os.path.basename(filename))
    if not os.path.exists(path):
        raise HTTPException(404, "not found")
    return FileResponse(path, filename=filename)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=58800)

================================================
FILE: assets/code_run_header.py
================================================
import sys, os, json, re, time, subprocess
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'memory'))
_r = subprocess.run
def _d(b):
    if not b: return ''
    if isinstance(b, str): return b
    try: return b.decode()
    except: return b.decode('gbk', 'replace')
def _run(*a, **k):
    t = k.pop('text', 0) | k.pop('universal_newlines', 0)
    enc = k.pop('encoding', None)
    k.pop('errors', None)
    if enc: t = 1
    if t and isinstance(k.get('input'), str):
        k['input'] = k['input'].encode()
    r = _r(*a, **k)
    if t:
        if r.stdout is not None: r.stdout = _d(r.stdout)
        if r.stderr is not None: r.stderr = _d(r.stderr)
    return r
subprocess.run = _run
_Pi = subprocess.Popen.__init__
def _pinit(self, *a, **k):
    if os.name == 'nt': k['creationflags'] = (k.get('creationflags') or 0) | 0x08000000
    _Pi(self, *a, **k)
subprocess.Popen.__init__ = _pinit
sys.excepthook = lambda t, v, tb: (sys.__excepthook__(t, v, tb), print(f"\n[Agent Hint]: NO GUESSING! You MUST probe first. If missing common package, pip.")) if issubclass(t, (ImportError, AttributeError)) else sys.__excepthook__(t, v, tb)


================================================
FILE: assets/configure_mykey.py
================================================
#!/usr/bin/env python3
"""
GenericAgent — 交互式初始化向导 (configure.py)
一键配置 LLM 模型 + 消息平台,自动生成 mykey.py

用法:
    python configure.py
"""

import os
import sys
import shutil
import json
import urllib.request
import time
from datetime import datetime

# ── ANSI 颜色 ──────────────────────────────────────────────────────────────
C = {
    'reset': '\033[0m', 'bold': '\033[1m', 'dim': '\033[2m',
    'red': '\033[91m', 'green': '\033[92m', 'yellow': '\033[93m',
    'blue': '\033[94m', 'magenta': '\033[95m', 'cyan': '\033[96m', 'white': '\033[97m',
}

PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MYKPY_PATH = os.path.join(PROJECT_ROOT, 'mykey.py')

# ── 模型厂商定义 ───────────────────────────────────────────────────────────

LLM_PROVIDERS = [
    {
        'id': 'deepseek',
        'name': 'DeepSeek V4 Flash (推荐首选)',
        'desc': '国产开源模型,速度快、性价比高,原生 OAI 协议',
        'type': 'native_oai',
        'template': {
            'name': 'deepseek-flash', 'apikey': 'sk-<your-deepseek-key>',
            'apibase': 'https://api.deepseek.com', 'model': 'deepseek-v4-flash',
            'api_mode': 'chat_completions', 'reasoning_effort': 'high',
        },
        'key_hint': '在 https://platform.deepseek.com/api_keys 获取',
        'model_choices': ['deepseek-v4-flash', 'deepseek-v3-premium'],
    },
    {
        'id': 'openai',
        'name': 'OpenAI GPT-5 / o 系列',
        'desc': 'OpenAI 官方,支持 GPT-5、o 系列推理模型',
        'type': 'native_oai',
        'template': {
            'name': 'gpt-native', 'apikey': 'sk-<your-openai-key>',
            'apibase': 'https://api.openai.com/v1', 'model': 'gpt-5.4',
            'api_mode': 'chat_completions', 'reasoning_effort': 'high',
            'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,
        },
        'key_hint': '在 https://platform.openai.com/api-keys 获取',
        'model_choices': ['gpt-5.4', 'o4-mini-high', 'o4-mini'],
    },
    {
        'id': 'anthropic',
        'name': 'Anthropic Claude 官方直连',
        'desc': 'Claude 官方 API,sk-ant- 开头,原生 tool 协议',
        'type': 'native_claude',
        'template': {
            'name': 'anthropic-direct', 'apikey': 'sk-ant-<your-anthropic-key>',
            'apibase': 'https://api.anthropic.com', 'model': 'claude-opus-4-7',
            'thinking_type': 'adaptive', 'max_tokens': 32768, 'temperature': 1,
        },
        'key_hint': '在 https://console.anthropic.com/ 获取',
        'model_choices': ['claude-opus-4-7', 'claude-sonnet-4-6'],
    },
    {
        'id': 'cc_relay',
        'name': 'CC Switch 透传 (社区常用)',
        'desc': '社区 Claude Code 透传渠道,需要 fake_cc_system_prompt=True',
        'type': 'native_claude',
        'template': {
            'name': 'cc-relay', 'apikey': 'sk-user-<your-relay-key>',
            'apibase': 'https://<your-cc-switch-host>/claude/office',
            'model': 'claude-opus-4-7', 'fake_cc_system_prompt': True,
            'thinking_type': 'adaptive',
        },
        'key_hint': '从你的 CC Switch 服务商获取 apikey 和 apibase',
        'model_choices': ['claude-opus-4-7', 'claude-sonnet-4-6'],
        'extra_fields': [
            {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://your-host/claude/office'},
            {'key': 'fake_cc_system_prompt', 'label': 'fake_cc_system_prompt', 'type': 'bool', 'default': True},
        ],
    },
    {
        'id': 'zhipu',
        'name': '智谱 GLM (Anthropic 兼容)',
        'desc': '智谱 GLM-5.1,走 Anthropic 兼容协议',
        'type': 'native_claude',
        'template': {
            'name': 'zhipu-glm', 'apikey': 'sk-<your-zhipu-key>',
            'apibase': 'https://open.bigmodel.cn/api/anthropic',
            'model': 'GLM-5.1-Cloud', 'fake_cc_system_prompt': False,
            'thinking_type': 'adaptive', 'max_retries': 3,
            'connect_timeout': 10, 'read_timeout': 180,
        },
        'key_hint': '在 https://open.bigmodel.cn/usercenter/apikeys 获取',
        'model_choices': ['GLM-5.1-Cloud', 'GLM-5.1-Edge'],
    },
    {
        'id': 'minimax',
        'name': 'MiniMax (推荐 Anthropic 路径)',
        'desc': 'MiniMax M2.7,Anthropic 路径无 <think> 标签',
        'type': 'native_claude',
        'template': {
            'name': 'minimax-anthropic', 'apikey': 'eyJh...<your-minimax-key>',
            'apibase': 'https://api.minimaxi.com/anthropic',
            'model': 'MiniMax-M2.7', 'max_retries': 3,
        },
        'key_hint': '在 https://platform.minimaxi.com/user-center/basic-information 获取',
        'model_choices': ['MiniMax-M2.7', 'MiniMax-M2.5'],
    },
    {
        'id': 'minimax_oai',
        'name': 'MiniMax (OpenAI 兼容路径)',
        'desc': 'MiniMax M2.7,走 /v1/chat/completions',
        'type': 'native_oai',
        'template': {
            'name': 'minimax-oai', 'apikey': 'eyJh...<your-minimax-key>',
            'apibase': 'https://api.minimaxi.com/v1', 'model': 'MiniMax-M2.7',
            'context_win': 50000,
        },
        'key_hint': '在 https://platform.minimaxi.com/user-center/basic-information 获取',
        'model_choices': ['MiniMax-M2.7', 'MiniMax-M2.5'],
    },
    {
        'id': 'kimi',
        'name': 'Kimi for Coding (Anthropic 兼容)',
        'desc': 'Kimi 官方 CC 兼容端点,kimi-for-coding 模型',
        'type': 'native_claude',
        'template': {
            'name': 'kimi-coding', 'apikey': 'sk-kimi-<your-key>',
            'apibase': 'https://api.kimi.com/coding',
            'model': 'kimi-for-coding', 'fake_cc_system_prompt': True,
            'thinking_type': 'adaptive',
        },
        'key_hint': '在 https://kimi.com/code 获取 API Key',
        'model_choices': ['kimi-for-coding', 'kimi-thinking-plus'],
    },
    {
        'id': 'moonshot_oai',
        'name': 'Kimi / Moonshot (OAI 兼容)',
        'desc': 'Moonshot OAI 端点,kimi-k2 系列,温度强制 1.0',
        'type': 'native_oai',
        'template': {
            'name': 'kimi-k2', 'apikey': 'sk-<your-moonshot-key>',
            'apibase': 'https://api.moonshot.cn/v1', 'model': 'kimi-k2-turbo-preview',
        },
        'key_hint': '在 https://platform.moonshot.cn/ 获取',
        'model_choices': ['kimi-k2-turbo-preview', 'kimi-k2'],
    },
    {
        'id': 'openrouter',
        'name': 'OpenRouter (多模型中继)',
        'desc': '一个 Key 用所有模型,支持 Claude/GPT/Gemini 等',
        'type': 'native_oai',
        'template': {
            'name': 'openrouter', 'apikey': 'sk-or-<your-openrouter-key>',
            'apibase': 'https://openrouter.ai/api/v1',
            'model': 'anthropic/claude-opus-4-7',
            'max_retries': 3, 'connect_timeout': 10, 'read_timeout': 120,
        },
        'key_hint': '在 https://openrouter.ai/keys 获取',
        'model_choices': ['anthropic/claude-opus-4-7', 'openai/gpt-5.4'],
    },
    {
        'id': 'crs',
        'name': 'CRS 反代 Claude Max',
        'desc': 'CRS 协议的反代 Claude,需要 fake_cc_system_prompt=True',
        'type': 'native_claude',
        'template': {
            'name': 'crs-claude-max', 'apikey': 'cr_<your-crs-key>',
            'apibase': 'https://<your-crs-host>/api',
            'model': 'claude-opus-4-7[1m]', 'fake_cc_system_prompt': True,
            'thinking_type': 'adaptive', 'max_tokens': 32768,
            'max_retries': 3, 'read_timeout': 180,
        },
        'key_hint': '从你的 CRS 服务商获取 key 和 host',
        'model_choices': ['claude-opus-4-7[1m]', 'claude-sonnet-4-6'],
        'extra_fields': [
            {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://your-crs-host/api'},
        ],
    },
    {
        'id': 'crs_gemini',
        'name': 'CRS Gemini Ultra (Antigravity 通道)',
        'desc': 'CRS 包装的 Google Antigravity,不支持 SSE 流式,必须 stream=False',
        'type': 'native_claude',
        'template': {
            'name': 'crs-gemini-ultra', 'apikey': 'cr_<your-crs-gemini-key>',
            'apibase': 'https://<your-crs-gemini-host>/antigravity/api',
            'model': 'claude-opus-4-7-thinking', 'stream': False,
            'max_tokens': 32768, 'max_retries': 3, 'read_timeout': 180,
        },
        'key_hint': '从你的 CRS 服务商获取 Gemini Ultra key 和 host',
        'model_choices': ['claude-opus-4-7-thinking', 'claude-opus-4-7[1m]', 'claude-opus-4-7'],
        'extra_fields': [
            {'key': 'apibase', 'label': 'API 地址 (apibase)', 'default': 'https://your-crs-gemini-host/antigravity/api'},
        ],
    },
]

# ── 消息平台定义 ────────────────────────────────────────────────────────────
PLATFORMS = [
    {
        'id': 'none',
        'name': '不使用消息平台(纯终端 REPL)',
        'desc': '直接用 python agentmain.py 在终端交互',
        'deps': [],
    },
    {
        'id': 'telegram',
        'name': 'Telegram 机器人',
        'desc': '通过 Telegram Bot 与 Agent 对话',
        'file': 'frontends/tgapp.py',
        'deps': ['python-telegram-bot'],
        'env_vars': [
            {'key': 'tg_bot_token', 'label': 'Bot Token', 'hint': '从 @BotFather 获取'},
            {'key': 'tg_allowed_users', 'label': '允许的用户 ID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},
        ],
    },
    {
        'id': 'qq',
        'name': 'QQ 机器人',
        'desc': '通过 QQ 官方机器人 API 接入',
        'file': 'frontends/qqapp.py',
        'deps': ['qq-botpy'],
        'env_vars': [
            {'key': 'qq_app_id', 'label': 'App ID', 'hint': 'QQ 开放平台获取'},
            {'key': 'qq_app_secret', 'label': 'App Secret'},
            {'key': 'qq_allowed_users', 'label': '允许的用户 OpenID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},
        ],
    },
    {
        'id': 'feishu',
        'name': '飞书机器人',
        'desc': '通过飞书应用与 Agent 对话',
        'file': 'frontends/fsapp.py',
        'deps': ['lark-oapi'],
        'env_vars': [
            {'key': 'fs_app_id', 'label': 'App ID', 'hint': '飞书开放平台获取'},
            {'key': 'fs_app_secret', 'label': 'App Secret'},
            {'key': 'fs_allowed_users', 'label': '允许的用户(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},
        ],
    },
    {
        'id': 'wecom',
        'name': '企业微信机器人',
        'desc': '通过企业微信 Bot 接入',
        'file': 'frontends/wecomapp.py',
        'deps': ['wecombot'],
        'env_vars': [
            {'key': 'wecom_bot_id', 'label': 'Bot ID'},
            {'key': 'wecom_secret', 'label': 'Bot Secret'},
            {'key': 'wecom_allowed_users', 'label': '允许的用户(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},
        ],
    },
    {
        'id': 'dingtalk',
        'name': '钉钉机器人',
        'desc': '通过钉钉应用接入',
        'file': 'frontends/dingtalkapp.py',
        'deps': ['dingtalk-sdk'],
        'env_vars': [
            {'key': 'dingtalk_client_id', 'label': 'Client ID (App Key)'},
            {'key': 'dingtalk_client_secret', 'label': 'Client Secret (App Secret)'},
            {'key': 'dingtalk_allowed_users', 'label': '允许的用户 StaffID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},
        ],
    },
    {
        'id': 'discord',
        'name': 'Discord 机器人',
        'desc': '通过 Discord Bot 接入',
        'file': 'frontends/dcapp.py',
        'deps': ['discord.py'],
        'env_vars': [
            {'key': 'dc_bot_token', 'label': 'Bot Token', 'hint': 'Discord Developer Portal 获取'},
            {'key': 'dc_allowed_users', 'label': '允许的用户 ID(逗号分隔, 留空=所有人)', 'default': '[]', 'is_list': True},
        ],
    },
]


def _read_char():
    """跨平台读取单个字符(Windows 用 getwch 避免 CRLF 拆字节问题)。"""
    if os.name == 'nt':
        import msvcrt
        return msvcrt.getwch()
    else:
        import tty
        import termios
        fd = sys.stdin.fileno()
        old = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            return sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old)

def _masked(v, reveal, tail):
    """生成脱敏字符串:前 reveal 位明文 + * + 后 tail 位明文"""
    if len(v) > reveal + tail:
        return v[:reveal] + '*' * min(len(v) - reveal - tail, 8) + v[-tail:]
    elif len(v) > reveal:
        return v[:reveal] + '*' * (len(v) - reveal)
    return v

def masked_input(prompt, reveal=6, tail=4):
    """密文输入:每输入一个字符实时显示脱敏版本,支持逐字输入和粘贴。

    prompt 必须为单行(不含 \\n)。
    """
    sys.stdout.write(prompt)
    sys.stdout.flush()
    chars = []

    def _repaint():
        m = _masked(''.join(chars), reveal, tail)
        # \r → 行首;写 prompt+m;多余空格覆盖前次更长渲染的残留字符
        sys.stdout.write(f'\r{prompt}{m}     \r{prompt}{m}')
        sys.stdout.flush()

    while True:
        c = _read_char()
        if c in ('\r', '\n'):
            break
        if c in ('\x03', '\x04'):
            raise KeyboardInterrupt
        if c in ('\x08', '\x7f'):
            if chars:
                chars.pop()
                _repaint()
        elif c.isprintable() or c == ' ':
            chars.append(c)
            _repaint()

    value = ''.join(chars)
    _repaint()
    sys.stdout.write('\n')
    sys.stdout.flush()
    return value


# ═══════════════════════════════════════════════════════════════════════════
#  UI Helpers
# ═══════════════════════════════════════════════════════════════════════════

def cprint(text, color=None, bold=False, end='\n'):
    parts = []
    if color: parts.append(C.get(color, ''))
    if bold: parts.append(C['bold'])
    parts.append(text)
    parts.append(C['reset'])
    print(''.join(parts), end=end)

def banner():
    print('\033[2J\033[H', end='')  # ANSI 清屏,跨平台
    print(f"{C['cyan']}{C['bold']}")
    print("  ╔═══════════════════════════════════════════════════════════╗")
    print("  ║        GenericAgent — 交互式初始化向导 v1.1              ║")
    print("  ║   一键配置 LLM 模型 + 消息平台,自动生成 mykey.py        ║")
    print("  ╚═══════════════════════════════════════════════════════════╝")
    print(f"{C['reset']}")
    print(f"{C['dim']}  项目目录: {PROJECT_ROOT}{C['reset']}")
    print()

def _check_python():
    """检查 Python 版本,返回 (ok, msg)"""
    vi = sys.version_info
    if vi < (3, 10):
        return False, f"Python {vi.major}.{vi.minor} 不满足最低要求 (≥ 3.10)"
    if vi >= (3, 14):
        return True, f"⚠ Python {vi.major}.{vi.minor} 可能与 pywebview 等依赖不兼容,推荐 3.11/3.12"
    return True, f"✓ Python {vi.major}.{vi.minor}.{vi.micro}"

def ask_choice(prompt, choices, allow_multi=False, default=None):
    """交互式选择,返回 selected_id 或 [selected_ids]"""
    print(f"\n{C['bold']}{prompt}{C['reset']}")
    if allow_multi:
        print(f"{C['dim']}  (可多选,输入序号用逗号分隔,如: 1,3,5;输入 a 全选;回车跳过){C['reset']}")
    else:
        print(f"{C['dim']}  (输入序号,如: 1){C['reset']}")
    for i, c in enumerate(choices, 1):
        desc = c.get('desc', '')
        print(f"  {C['green']}{i}.{C['reset']} {C['bold']}{c['name']}{C['reset']}  {C['dim']}{desc}{C['reset']}")
    while True:
        raw = input(f"\n  {C['yellow']}►{C['reset']} ").strip()
        if not raw and default is not None:
            return default
        if allow_multi:
            if raw.lower() == 'a':
                return [c['id'] for c in choices]
            parts = [p.strip() for p in raw.split(',') if p.strip()]
            selected = []
            for p in parts:
                try:
                    idx = int(p) - 1
                    if 0 <= idx < len(choices):
                        selected.append(choices[idx]['id'])
                except ValueError:
                    pass
            if selected:
                return selected
        else:
            try:
                idx = int(raw) - 1
                if 0 <= idx < len(choices):
                    return choices[idx]['id']
            except ValueError:
                pass
        print(f"  {C['red']}✗ 请输入有效序号{C['reset']}")

def ask_input(prompt, default=None, secret=False, hint=None):
    """交互式输入。secret=True 时使用脱敏输入。"""
    # 提示信息先打印(不放进 prompt,保证 prompt 单行)
    if hint:
        cprint(f"  {hint}", 'dim')
    if default is not None:
        cprint(f"  [默认: {default}]", 'dim')
    # 单行 prompt,\r 能正确回行首
    prompt_line = f"  {C['yellow']}►{C['reset']} {prompt}: "
    while True:
        if secret:
            val = masked_input(prompt_line).strip()
        else:
            val = input(prompt_line).strip()
        if not val and default is not None:
            return default
        if val:
            return val
        cprint("✗ 此项不能为空", 'red')

def ask_yesno(prompt, default=True):
    hint = "Y/N"
    raw = input(f"\n  {C['yellow']}►{C['reset']} {prompt} ({hint}): ").strip().lower()
    if not raw:
        return default
    return raw.startswith('y')


# ═══════════════════════════════════════════════════════════════════════════
#  LLM 配置逻辑
# ═══════════════════════════════════════════════════════════════════════════

def _get_proxy_handler():
    """从环境变量读取代理配置,返回 ProxyHandler 或 None"""
    for var in ('HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy'):
        url = os.environ.get(var)
        if url:
            return urllib.request.ProxyHandler({'https': url, 'http': url})
    return None

def probe_models(provider, apikey, apibase=None):
    """调用 API 探测可用模型列表,返回模型 ID 列表或 None"""
    ptype = provider.get('type', 'native_oai')
    base = (apibase or provider['template'].get('apibase', '')).rstrip('/')

    if ptype == 'native_claude':
        # Anthropic 协议: 尝试 /v1/models (多数中继兼容此路径)
        url = f"{base}/v1/models"
        headers = {'x-api-key': apikey, 'anthropic-version': '2023-06-01'}
    else:
        url = f"{base}/models"
        headers = {'Authorization': f'Bearer {apikey}'}

    print(f"\n  {C['dim']}🔍 正在探测可用模型 ({url})...{C['reset']}", end='', flush=True)
    time.sleep(0.3)

    opener = urllib.request.build_opener()
    ph = _get_proxy_handler()
    if ph:
        opener = urllib.request.build_opener(ph)
        print(f" {C['dim']}(via proxy){C['reset']}", end='', flush=True)

    try:
        req = urllib.request.Request(url, headers=headers, method='GET')
        with opener.open(req, timeout=8) as resp:
            data = json.loads(resp.read().decode())
            # 兼容两种响应格式: {data: [{id: ...}]} 与 {object: 'list', data: [...]}
            models = data.get('data', [])
            ids = sorted(set(m['id'] for m in models if isinstance(m, dict) and m.get('id')))
            if ids:
                print(f" {C['green']}✓ 发现 {len(ids)} 个模型{C['reset']}")
                return ids
            print(f" {C['yellow']}⚠ 返回为空{C['reset']}")
            return None
    except Exception as e:
        print(f" {C['yellow']}⚠ 探测失败: {type(e).__name__}(将使用预设列表){C['reset']}")
        return None

def _normalize_model_choices(choices):
    """统一 model_choices 格式为 [{'id': str, 'name': str}]"""
    if not choices:
        return []
    result = []
    for item in choices:
        if isinstance(item, str):
            result.append({'id': item, 'name': item})
        elif isinstance(item, dict):
            result.append(item)
        elif isinstance(item, (tuple, list)) and len(item) >= 1:
            result.append({'id': item[0], 'name': item[1] if len(item) > 1 else item[0]})
    return result

def _configure_advanced(provider, cfg):
    """配置高级可选字段: proxy, context_win, stream, user_agent, thinking_budget_tokens"""
    print(f"\n  {C['dim']}── 高级选项(回车跳过,使用默认值){C['reset']}")
    proxy = ask_input("HTTP 代理地址 (proxy)", default='', hint='如 http://127.0.0.1:2082,留空跳过')
    if proxy:
        cfg['proxy'] = proxy
    cw = ask_input("上下文窗口阈值 (context_win)", default='', hint='NativeClaude 默认 28000,其他默认 24000')
    if cw:
        cfg['context_win'] = int(cw)
    if cfg.get('thinking_type') == 'enabled':
        tbt = ask_input("thinking_budget_tokens", default='', hint='low≈4096, medium≈10240, high≈32768')
        if tbt:
            cfg['thinking_budget_tokens'] = int(tbt)
    if provider['type'] == 'native_claude':
        ua = ask_input("User-Agent 版本号", default='', hint='某些中转按 UA 白名单校验,pin 老版本用')
        if ua:
            cfg['user_agent'] = ua
    stream_default = cfg.get('stream', True)
    if ask_yesno("启用 SSE 流式 (stream)", default=stream_default):
        cfg['stream'] = True
    else:
        cfg['stream'] = False

def configure_llm(provider):
    """引导用户配置单个模型"""
    print(f"\n{C['cyan']}{'─'*60}{C['reset']}")
    print(f"{C['bold']}  配置: {provider['name']}{C['reset']}")
    print(f"  {C['dim']}{provider['desc']}{C['reset']}")
    print(f"{C['cyan']}{'─'*60}{C['reset']}")

    cfg = dict(provider['template'])

    # API Key(密文输入)
    cfg['apikey'] = ask_input(
        f"API Key",
        hint=provider.get('key_hint', ''),
        secret=True,
    )

    # 额外字段
    for field in provider.get('extra_fields', []):
        if field['key'] == 'apibase':
            cfg['apibase'] = ask_input(
                field['label'],
                default=field.get('default', cfg.get('apibase', '')),
            )
        elif field.get('type') == 'bool':
            cfg[field['key']] = ask_yesno(
                field['label'],
                default=field.get('default', True)
            )

    # 模型选择
    model_list = probe_models(provider, cfg['apikey'], cfg.get('apibase'))
    if model_list:
        refresh_choice = {'id': '__refresh__', 'name': '🔄 重新探测模型列表'}
        choices = [refresh_choice] + [{'id': m, 'name': m} for m in model_list]
        while True:
            picked = ask_choice("API 探测到以下可用模型,请选择:", choices)
            if picked == '__refresh__':
                print(f"  {C['dim']}再次探测...{C['reset']}")
                model_list = probe_models(provider, cfg['apikey'], cfg.get('apibase'))
                if not model_list:
                    print(f"  {C['yellow']}⚠ 再次探测失败,回退到预设列表{C['reset']}")
                    picked = _fallback_model(provider)
                    break
                choices = [refresh_choice] + [{'id': m, 'name': m} for m in model_list]
            else:
                break
        cfg['model'] = picked
    else:
        cfg['model'] = _fallback_model(provider)

    # 别名
    default_name = cfg.get('name', provider['id'])
    name = ask_input("此配置的别名 (name,Mixin 引用用)", default=default_name)
    if name:
        cfg['name'] = name

    # 高级选项
    if ask_yesno("配置高级选项(proxy / context_win / stream 等)?", default=False):
        _configure_advanced(provider, cfg)

    return cfg

def _fallback_model(provider):
    """使用预设模型列表让用户选择"""
    normalized = _normalize_model_choices(provider.get('model_choices', []))
    if normalized:
        return ask_choice("选择模型:", normalized)
    return ask_input("请输入模型名称", default=provider['template'].get('model', ''))

def configure_llms():
    """配置 LLM 模型"""
    print(f"\n{C['bold']}{C['magenta']}╔══════════════════════════════════════╗")
    print(f"║     第一步: 配置 LLM 模型           ║")
    print(f"╚══════════════════════════════════════╝{C['reset']}")
    print(f"\n{C['dim']}  你可以配置最多 2 个模型组成故障转移 (Mixin) 列表。{C['reset']}")

    all_cfgs = []
    provider_id = ask_choice("选择模型厂商 (配置第 1 个模型):", LLM_PROVIDERS)
    provider = next(p for p in LLM_PROVIDERS if p['id'] == provider_id)
    cfg = configure_llm(provider)
    all_cfgs.append(cfg)

    if ask_yesno("再添加一个模型做故障转移?", default=False):
        providers_ext = [{'id': '__stop__', 'name': '✓ 不需要备选了', 'desc': ''}] + LLM_PROVIDERS
        provider_id = ask_choice(
            "选择模型厂商 (配置第 2 个模型 — 或选「不需要备选了」跳过):",
            providers_ext
        )
        if provider_id != '__stop__':
            provider = next(p for p in LLM_PROVIDERS if p['id'] == provider_id)
            cfg = configure_llm(provider)
            all_cfgs.append(cfg)

    return all_cfgs


# ═══════════════════════════════════════════════════════════════════════════
#  消息平台配置逻辑
# ═══════════════════════════════════════════════════════════════════════════

def configure_platforms():
    """配置消息平台,返回 (platform_configs, pip_hints)"""
    print(f"\n{C['bold']}{C['magenta']}╔══════════════════════════════════════╗")
    print(f"║     第二步: 配置消息平台             ║")
    print(f"╚══════════════════════════════════════╝{C['reset']}")
    print(f"\n{C['dim']}  消息平台用于从聊天软件与 Agent 交互。{C['reset']}")
    print(f"{C['dim']}  你也可以跳过此步,直接用终端 REPL。{C['reset']}")

    platform_ids = ask_choice(
        "选择消息平台 (可多选,选 '不使用' 则跳过):",
        PLATFORMS,
        allow_multi=True,
        default=['none']
    )

    if 'none' in platform_ids:
        return [], set()

    selected_platforms = []
    pip_hints = set()

    for pid in platform_ids:
        platform = next(p for p in PLATFORMS if p['id'] == pid)
        pip_hints.update(platform.get('deps', []))

        print(f"\n{C['cyan']}{'─'*60}{C['reset']}")
        print(f"{C['bold']}  配置: {platform['name']}{C['reset']}")
        print(f"{C['cyan']}{'─'*60}{C['reset']}")

        env_vals = {}

        # 飞书扫码创建
        if pid == 'feishu' and ask_yesno("使用一键扫码创建应用?(推荐)", default=True):
            env_vals = _feishu_scan(platform)

        # 补充扫码未获取的字段(或扫码失败时全手动填写)
        for var in platform['env_vars']:
            if var['key'] not in env_vals:
                env_vals.update(_manual_platform_var(var))

        # 企业微信专属:欢迎消息
        if pid == 'wecom' and ask_yesno("设置欢迎消息?", default=False):
            env_vals['wecom_welcome_message'] = ask_input("欢迎消息内容", default='你好,我在线上。')

        selected_platforms.append({'platform': platform, 'config': env_vals})

    return selected_platforms, pip_hints

def _manual_platform_var(var):
    """手动填写单个平台变量"""
    val = ask_input(var['label'], hint=var.get('hint', ''), default=var.get('default'))
    if var.get('is_list'):
        if val == '[]' or not val:
            return {var['key']: []}
        return {var['key']: [x.strip() for x in val.split(',') if x.strip()]}
    return {var['key']: val}

def _feishu_scan(platform):
    """飞书一键扫码创建应用,返回 env_vals 或空 dict"""
    try:
        import lark_oapi as lark
        import qrcode, threading
        from io import StringIO
    except ImportError:
        print(f"\n  {C['yellow']}⚠ lark-oapi 未安装,降级为手动配置{C['reset']}")
        return {}

    print(f"\n  {C['cyan']}📱 正在启动一键创建...{C['reset']}")
    print(f"  {C['dim']}  请用飞书 App 扫描终端二维码,完成授权后自动获取凭据。{C['reset']}\n")

    qr_printed = threading.Event()
    result_holder = {'data': None}

    def handle_qr(info):
        url = info['url']
        expire = info['expire_in']
        qr = qrcode.QRCode(border=1, box_size=1)
        qr.add_data(url)
        buf = StringIO()
        qr.print_ascii(out=buf)
        qr_art = buf.getvalue()
        print(f"\n  {C['bold']}请用飞书扫描下方二维码,或复制链接在浏览器打开:{C['reset']}")
        print(f"  {C['green']}{qr_art.replace(chr(27), '')}{C['reset']}")
        print(f"  {C['dim']}  链接: {url}{C['reset']}")
        print(f"  {C['dim']}  有效期 {expire} 秒{C['reset']}")
        qr_printed.set()

    def handle_status(info):
        status = info['status']
        if status == 'polling':
            print(f"  {C['yellow']}⏳ 等待扫码...{C['reset']}")
        elif status == 'slow_down':
            print(f"  {C['yellow']}⏳ 等待中... (间隔 {info.get('interval', '?')}s){C['reset']}")
        elif status == 'domain_switched':
            print(f"  {C['cyan']}🌐 已切换认证域名{C['reset']}")

    def run_register():
        try:
            result = lark.register_app(
                on_qr_code=handle_qr,
                on_status_change=handle_status,
            )
            result_holder['data'] = result
        except Exception as e:
            print(f"\n  {C['red']}✗ 创建失败: {e}{C['reset']}")

    thread = threading.Thread(target=run_register, daemon=True)
    thread.start()
    qr_printed.wait(timeout=15)
    thread.join(timeout=300)

    if result_holder['data']:
        result = result_holder['data']
        print(f"\n  {C['green']}✅ 应用创建成功!{C['reset']}")
        print(f"  App ID:     {C['bold']}{result['client_id']}{C['reset']}")
        print(f"  App Secret: {C['bold']}{result['client_secret']}{C['reset']}")
        return {
            'fs_app_id': result['client_id'],
            'fs_app_secret': result['client_secret'],
        }
    else:
        print(f"\n  {C['yellow']}⚠ 扫码创建未完成,降级为手动填写...{C['reset']}")
        return {}



# ═══════════════════════════════════════════════════════════════════════════
#  生成 mykey.py
# ═══════════════════════════════════════════════════════════════════════════

def _var_type_info(cfg):
    """根据配置类型返回 (var_prefix, session_type)"""
    cfg_type = cfg.get('type', 'native_oai')
    if cfg_type == 'native_claude':
        return 'native_claude_config', 'NativeClaudeSession'
    elif cfg_type == 'claude':
        return 'claude_config', 'ClaudeSession'
    elif cfg_type == 'oai':
        return 'oai_config', 'LLMSession'
    else:
        return 'native_oai_config', 'NativeOAISession'


def generate_mykey(llm_cfgs, platform_configs):
    """生成 mykey.py 内容"""
    lines = []
    lines.append("# ══════════════════════════════════════════════════════════════════════════════")
    lines.append(f"#  GenericAgent — mykey.py (由 configure.py 自动生成 @ {datetime.now().strftime('%Y-%m-%d %H:%M')})")
    lines.append("# ══════════════════════════════════════════════════════════════════════════════")
    lines.append("")
    lines.append("# ── 停止符 ──────────────────────────────────────────────────────────────────")
    lines.append("_SETUP_DONE = 'configure.py'  # 删除此行可重新触发配置向导")
    lines.append("")

    # Mixin 配置
    names = [c['name'] for c in llm_cfgs]
    lines.append("# ── Mixin 故障转移 ──────────────────────────────────────────────────────────")
    lines.append("mixin_config = {")
    lines.append(f"    'llm_nos': {names},")
    lines.append("    'max_retries': 10,")
    lines.append("    'base_delay': 0.5,")
    lines.append("}")
    lines.append("")

    # 各模型配置
    # 同类型多实例时加上数字后缀
    type_counts = {}
    for cfg in llm_cfgs:
        cfg_type = cfg.get('type', 'native_oai')
        type_counts[cfg_type] = type_counts.get(cfg_type, 0) + 1

    type_indices = {}
    for i, cfg in enumerate(llm_cfgs):
        cfg_type = cfg.get('type', 'native_oai')
        var_prefix, session_type = _var_type_info(cfg)
        idx = type_indices.get(cfg_type, 0)
        type_indices[cfg_type] = idx + 1

        # 同类型只有一个时不加后缀;多个时加数字后缀
        if type_counts[cfg_type] > 1:
            var_name = f"{var_prefix}_{idx}"
        else:
            var_name = var_prefix

        lines.append(f"# ── {cfg['name']} ({session_type}) ─────────────────────────────────────────────")
        lines.append(f"{var_name} = {{")
        _write_config_fields(lines, cfg)
        lines.append("}")
        lines.append("")

    # 平台配置
    if platform_configs:
        lines.append("# ══════════════════════════════════════════════════════════════════════════════")
        lines.append("#  聊天平台集成")
        lines.append("# ══════════════════════════════════════════════════════════════════════════════")
        lines.append("")
        for pc in platform_configs:
            for key, val in pc['config'].items():
                _write_platform_value(lines, key, val)
            lines.append("")

    # 尾部
    lines.append("# ══════════════════════════════════════════════════════════════════════════════")
    lines.append("#  配置完毕!运行: python agentmain.py  (终端 REPL)")
    if platform_configs:
        for pc in platform_configs:
            p = pc['platform']
            lines.append(f"#  或: python {p['file']}  ({p['name']})")
    lines.append("# ══════════════════════════════════════════════════════════════════════════════")

    return '\n'.join(lines)

def _write_config_fields(lines, cfg):
    """写入配置字典的键值对(缩进的 'key': value, 格式)"""
    for key in ['name', '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',
                'proxy', 'user_agent', 'stream']:
        if key not in cfg:
            continue
        val = cfg[key]
        if isinstance(val, bool):
            lines.append(f"    '{key}': {str(val)},")
        elif isinstance(val, (int, float)):
            lines.append(f"    '{key}': {val},")
        elif isinstance(val, str):
            lines.append(f"    '{key}': '{val}',")
        else:
            lines.append(f"    '{key}': {repr(val)},")

def _write_platform_value(lines, key, val):
    """写入顶级变量(平台配置等)"""
    if isinstance(val, list):
        if val:
            lines.append(f"{key} = {repr(val)}")
        else:
            lines.append(f"{key} = []  # 允许所有用户")
    elif isinstance(val, str):
        lines.append(f"{key} = '{val}'")
    else:
        lines.append(f"{key} = {repr(val)}")


# ═══════════════════════════════════════════════════════════════════════════
#  Main
# ═══════════════════════════════════════════════════════════════════════════

def main():
    banner()

    # Python 版本检查
    ok, msg = _check_python()
    if not ok:
        print(f"  {C['red']}✗ {msg}{C['reset']}")
        sys.exit(1)
    color = 'yellow' if '⚠' in msg else 'green'
    print(f"  {C[color]}{msg}{C['reset']}\n")

    # 检测已有配置
    if os.path.exists(MYKPY_PATH):
        print(f"  {C['yellow']}⚠ 检测到已有 mykey.py{C['reset']}")
        if not ask_yesno("是否重新配置?", default=False):
            print(f"\n  {C['dim']}  退出。如需重新配置请删除 mykey.py 后重试。{C['reset']}\n")
            sys.exit(0)

    # ── 顶层菜单 ──
    scope = ask_choice(
        "你想配置什么?",
        [
            {'id': 'llm', 'name': 'LLM 模型', 'desc': '选择厂商、填写 API Key、探测模型列表'},
            {'id': 'platform', 'name': '消息平台 (Telegram/QQ/飞书等)', 'desc': '配置聊天机器人接入'},
            {'id': 'both', 'name': '两项都配置 (推荐)', 'desc': 'LLM + 平台,完整初始化'},
        ],
        default='both',
    )

    llm_cfgs = []
    platform_configs = []
    platform_deps = set()

    # ── 执行 ──

    if scope in ('llm', 'both'):
        llm_cfgs = _do_llm()
        if scope == 'llm':
            if ask_yesno("是否继续配置消息平台?", default=True):
                platform_configs, platform_deps = configure_platforms()

    if scope == 'both':
        platform_configs, platform_deps = configure_platforms()

    if scope == 'platform':
        platform_configs, platform_deps = configure_platforms()
        if ask_yesno("是否继续配置 LLM 模型?", default=True):
            llm_cfgs = _do_llm()

    # ── 生成 mykey.py ──
    if not llm_cfgs and not platform_configs:
        print(f"\n  {C['yellow']}⚠ 没有配置任何内容,退出。{C['reset']}")
        sys.exit(0)

    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)
        print(f"\n  {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup}{C['reset']}")

    # 写入
    with open(MYKPY_PATH, 'w', encoding='utf-8') as f:
        f.write(content)
    print(f"\n  {C['green']}✓ mykey.py 已生成!{C['reset']}")

    # ── 完成提示 ──
    print(f"\n{C['bold']}{C['green']}╔══════════════════════════════════════╗")
    print(f"║      配置完成!                      ║")
    print(f"╚══════════════════════════════════════╝{C['reset']}")
    print()
    if llm_cfgs:
        print(f"  {C['cyan']}  终端 REPL:{C['reset']}  python agentmain.py")
    if platform_configs:
        for i, pc in enumerate(platform_configs, 1):
            p = pc['platform']
            print(f"  {C['cyan']}  平台 {i} ({p['name']}):{C['reset']}  python {p['file']}")
    print()

    # pip 依赖提示
    all_deps = sorted(platform_deps)
    if all_deps:
        print(f"  {C['yellow']}💡 提示:你需要安装以下依赖以使消息平台正常工作:{C['reset']}")
        print(f"     {C['cyan']}pip install {' '.join(all_deps)}{C['reset']}")
        print()

    # ── 入门示例 ──
    print(f"  {C['bold']}试试这些命令:{C['reset']}")
    examples = [
        "帮我在桌面创建一个 hello.txt,内容是 Hello World",
        "请查看你的代码,安装所有用得上的 python 依赖",
        "执行 web setup sop,解锁 web 工具",
        "打开淘宝,搜索 iPhone 16,按价格排序",
        "用rapidocr配置你的ocr能力并存入记忆",
        "git 更新你的代码,然后看看 commit 有什么新功能",
        "把这个记到你的记忆里",
    ]
    for ex in examples:
        print(f"    {C['dim']}{ex}{C['reset']}")
    print()

    print(f"  {C['green']}{C['bold']}合抱之木,生于毫末{C['reset']}\n")


def _do_llm():
    """配置 LLM 模型,失败则 exit。"""
    cfgs = configure_llms()
    if not cfgs:
        print(f"\n  {C['red']}✗ 至少需要配置一个模型才能使用。退出。{C['reset']}")
        sys.exit(1)
    return cfgs


if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        print(f"\n\n  {C['yellow']}⚠ 用户中断{C['reset']}")
        sys.exit(0)


================================================
FILE: assets/global_mem_insight_template.txt
================================================
# [Global Memory Insight]
需要时read L2 或 ls ../memory/ 查L3
L0(META-SOP): memory_management_sop
L2: 现空
L3: memory_cleanup_sop(记忆整理) | skill_search | ui_detect.py | ocr_utils.py | subagent | web_setup_sop | plan_sop 
| procmem_scanner | keychain | ljqCtrl_sop+.py | tmwebdriver_sop | autonomous_operation_sop | scheduled_task_sop | vision_sop | adb_ui.py
L4: L4_raw_sessions/ 历史会话

浏览器特殊操作: tmwebdriver_sop(文件上传/图搜/PDF blob/物理坐标/HttpOnly Cookie/autofill突破/跨域iframe/CDP/跨tab)
键鼠: ljqCtrl_sop(禁pyautogui/先activate) 截图/视觉: ocr/vision_sop | 禁全屏截图,优先窗口
定时:scheduled_task_sop | 自主:autonomous_operation_sop | watchdog/反射:agentmain --reflect
手机:adb_ui.py

[RULES]
1. 搜索先行: 搜文件名严禁不用es(禁PS递归/禁dir遍历), 搜索一定优先使用web工具的google(严禁duckduckgo等), 优先看cwd,禁猜路径
2. 交叉验证: 禁信摘要, 数值进详情页核实
3. 编码安全: 禁PS cat/type用file_read; 改前必读; memory模块直接import(已在PATH,禁加虚假前缀)
4. 闭环: 物理模拟后确认; 3次失败请求干预; Git完整闭环
5. 进程: 禁无条件杀python(杀自己), 精确PID, 禁os.kill判活
6. 窗口: GUI状态优先win32gui枚举标题
7. web JS: 输入用原生setter+事件链, 点击前检disabled, 注意引号转义; scan空/不全先稍等再scan, 禁首扫定论
8. SOP: 读SOP禁凭印象,有utils必用 | 复杂超长程任务/用户明确提及规划模式→读plan_sop


================================================
FILE: assets/global_mem_insight_template_en.txt
================================================
# [Global Memory Insight]
Read L2 or ls ../memory/ for L3 when needed
L0(META-SOP): memory_management_sop
L2: currently empty
L3: memory_cleanup_sop(memory cleanup) | skill_search | ui_detect.py | ocr_utils.py | subagent | web_setup_sop | plan_sop 
| procmem_scanner | keychain | ljqCtrl_sop+.py | tmwebdriver_sop | autonomous_operation_sop | scheduled_task_sop | vision_sop | adb_ui.py
L4: L4_raw_sessions/ historical sessions

Browser special ops: tmwebdriver_sop(file upload/image search/PDF blob/physical coords/HttpOnly Cookie/autofill bypass/cross-origin iframe/CDP/cross-tab)
Keyboard & Mouse: ljqCtrl_sop(no pyautogui/activate first) Screenshot/Vision: ocr/vision_sop | No fullscreen capture, prefer window
Scheduling: scheduled_task_sop | Autonomous: autonomous_operation_sop | watchdog/reflect: agentmain --reflect
Mobile: adb_ui.py

[RULES]
1. Search first: must use es for filename search (no PS recursion/no dir traversal), always prefer Google for web search (no duckduckgo etc), check cwd first, no guessing paths
2. Cross-verify: never trust summaries, verify numbers on detail pages
3. Encoding safety: use file_read not PS cat/type; read before modify; import memory modules directly (already in PATH, no fake prefixes)
4. Close the loop: confirm after physical simulation; request intervention after 3 failures; complete Git workflow
5. Processes: never kill python unconditionally (kills self), use exact PID, no os.kill for liveness check
6. Windows: prefer win32gui title enumeration for GUI state
7. Web JS: use native setter + event chain for input, check disabled before click, mind quote escaping; if scan empty/incomplete wait then rescan, no conclusions from first scan
8. SOP: read SOPs not from memory, must use utils if available | complex long-running/user mentions planning -> read plan_sop


================================================
FILE: assets/insight_fixed_structure.txt
================================================
Facts(L2): ../memory/global_mem.txt | GA CodeRoot: ../ | SOPs(L3): ../memory/*.md or *.py | META-SOP(L0): ../memory/memory_management_sop.md
L1 Insight是极简索引,L2/L3变更时同步L1,索引必须极简。写记忆前先读META-SOP(L0)。

[CONSTITUTION]
1. 改自身源码先请示;./内可自主实验,允许装包和portable工具
2. 决策前查记忆,有SOP/utils必用;多次失败回看SOP;未查证不断言
3. 分步执行,控制粒度,限制失败半径;3次失败请求干预
4. 密钥文件仅引用,不读取/移动
5. 写任何记忆前读META-SOP核验,memory下文件只能patch修改(除非新建)


================================================
FILE: assets/insight_fixed_structure_en.txt
================================================
Facts(L2): ../memory/global_mem.txt | CodeRoot: ../ | SOPs(L3): ../memory/*.md or *.py | META-SOP(L0): ../memory/memory_management_sop.md
L1 Insight is a minimal index; sync L1 when L2/L3 changes; keep index minimal. Read META-SOP(L0) before writing any memory.

[CONSTITUTION]
1. Ask before modifying own source code; free to experiment within ./; installing packages and portable tools allowed
2. Check memory before decisions; always use existing SOPs/utils; revisit SOPs on repeated failures; never assert without evidence
3. Execute step by step, control granularity, limit blast radius; request intervention after 3 failures
4. Key/secret files: reference only, never read or move
5. Read META-SOP to verify before writing any memory; files under memory/ must be patched only (unless creating new)

================================================
FILE: assets/install-macos-app.sh
================================================
#!/bin/bash

# GenericAgent macOS Desktop App Installation Script
#
# Usage:
#   bash assets/install-macos-app.sh [--auto]
#
# This installer creates a small .app bundle that opens Terminal and runs
# `python3 launch.pyw` from the current GenericAgent checkout.

if [ -z "${BASH_VERSION}" ]; then
    if command -v bash >/dev/null 2>&1; then
        exec bash -- "${0}" "$@"
    else
        echo "Error: This script requires bash."
        exit 1
    fi
fi

set -euo pipefail

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
log_info()    { echo -e "${BLUE}ℹ️  $1${NC}"; }
log_success() { echo -e "${GREEN}✅ $1${NC}"; }
log_warning() { echo -e "${YELLOW}⚠️  $1${NC}"; }
log_error()   { echo -e "${RED}❌ $1${NC}"; }

AUTO_MODE=false
for arg in "$@"; do
    case "$arg" in
        --auto) AUTO_MODE=true ;;
    esac
done

APP_NAME="GenericAgent"
PRIMARY_INSTALL_DIR="/Applications"
FALLBACK_INSTALL_DIR="${HOME}/Applications"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
ICON_PATH="${PROJECT_ROOT}/assets/images/logo.jpg"
LAUNCH_SCRIPT="${PROJECT_ROOT}/launch.pyw"

echo -e "${CYAN}"
echo "╔═══════════════════════════════════════════════════════════╗"
echo "║   GenericAgent — macOS Desktop App Installer             ║"
echo "╚═══════════════════════════════════════════════════════════╝"
echo -e "${NC}"

if [[ "$(uname)" != "Darwin" ]]; then
    log_error "This script only supports macOS."
    exit 1
fi

if ! command -v python3 >/dev/null 2>&1; then
    log_error "python3 is not installed."
    exit 1
fi

if [ ! -f "${LAUNCH_SCRIPT}" ]; then
    log_error "launch.pyw not found at ${LAUNCH_SCRIPT}"
    exit 1
fi

project_path_for_applescript="${PROJECT_ROOT}/"
project_path_for_applescript="${project_path_for_applescript//\\/\\\\}"
project_path_for_applescript="${project_path_for_applescript//\"/\\\"}"

detect_existing_app() {
    if [ -d "${PRIMARY_INSTALL_DIR}/${APP_NAME}.app" ]; then
        echo "${PRIMARY_INSTALL_DIR}/${APP_NAME}.app"
        return
    fi
    if [ -d "${FALLBACK_INSTALL_DIR}/${APP_NAME}.app" ]; then
        echo "${FALLBACK_INSTALL_DIR}/${APP_NAME}.app"
        return
    fi
}

existing_app_path="$(detect_existing_app || true)"
if [ -n "${existing_app_path}" ]; then
    log_warning "${APP_NAME}.app already exists at ${existing_app_path}"
fi

if [ "${AUTO_MODE}" = false ]; then
    echo ""
    echo "This will install a desktop app that launches GenericAgent"
    echo "from Spotlight, Launchpad, or the Applications folder."
    echo ""
    if [ -n "${existing_app_path}" ]; then
        read -p "Reinstall ${APP_NAME}.app? (y/N) " -n 1 -r
    else
        read -p "Continue? (Y/n) " -n 1 -r
    fi
    echo
    if [ -n "${existing_app_path}" ]; then
        [[ ! ${REPLY:-} =~ ^[Yy]$ ]] && { echo "Aborted."; exit 0; }
    else
        [[ ${REPLY:-} =~ ^[Nn]$ ]] && { echo "Aborted."; exit 0; }
    fi
fi

TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT

log_info "Building ${APP_NAME}.app..."

cat > "${TMP_DIR}/${APP_NAME}.applescript" <<APPLESCRIPT
on run
    set projectPathStr to "${project_path_for_applescript}"
    tell application "Terminal"
        activate
        do script "cd " & quoted form of projectPathStr & " && python3 launch.pyw"
    end tell
end run
APPLESCRIPT

osacompile -o "${TMP_DIR}/${APP_NAME}.app" "${TMP_DIR}/${APP_NAME}.applescript"

log_info "Applying GenericAgent icon..."
if [ -f "${ICON_PATH}" ]; then
    ICONSET_DIR="${TMP_DIR}/ga-icon.iconset"
    mkdir -p "${ICONSET_DIR}"

    sips -z 16 16   "${ICON_PATH}" --out "${ICONSET_DIR}/icon_16x16.png"       >/dev/null 2>&1
    sips -z 32 32   "${ICON_PATH}" --out "${ICONSET_DIR}/icon_16x16@2x.png"    >/dev/null 2>&1
    sips -z 32 32   "${ICON_PATH}" --out "${ICONSET_DIR}/icon_32x32.png"       >/dev/null 2>&1
    sips -z 64 64   "${ICON_PATH}" --out "${ICONSET_DIR}/icon_32x32@2x.png"    >/dev/null 2>&1
    sips -z 128 128 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_128x128.png"     >/dev/null 2>&1
    sips -z 256 256 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_128x128@2x.png"  >/dev/null 2>&1
    sips -z 256 256 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_256x256.png"     >/dev/null 2>&1
    sips -z 512 512 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_256x256@2x.png"  >/dev/null 2>&1
    sips -z 512 512 "${ICON_PATH}" --out "${ICONSET_DIR}/icon_512x512.png"     >/dev/null 2>&1
    cp "${ICON_PATH}" "${ICONSET_DIR}/icon_512x512@2x.png"

    iconutil -c icns "${ICONSET_DIR}" -o "${TMP_DIR}/ga-icon.icns"
    cp "${TMP_DIR}/ga-icon.icns" "${TMP_DIR}/${APP_NAME}.app/Contents/Resources/applet.icns"
    log_success "Icon applied from assets/images/logo.jpg"
else
    log_warning "Logo not found at ${ICON_PATH}, using default icon."
fi

install_bundle() {
    local install_dir="$1"
    local destination="${install_dir}/${APP_NAME}.app"
    mkdir -p "${install_dir}"
    rm -rf "${destination}"
    cp -R "${TMP_DIR}/${APP_NAME}.app" "${destination}"
}

install_path=""
if install_bundle "${PRIMARY_INSTALL_DIR}" 2>/dev/null; then
    install_path="${PRIMARY_INSTALL_DIR}/${APP_NAME}.app"
else
    log_warning "Could not write to ${PRIMARY_INSTALL_DIR}; falling back to ${FALLBACK_INSTALL_DIR}"
    install_bundle "${FALLBACK_INSTALL_DIR}"
    install_path="${FALLBACK_INSTALL_DIR}/${APP_NAME}.app"
fi

log_success "Installed to: ${install_path}"

echo ""
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║${NC}  ✨  ${APP_NAME} Desktop App installed successfully!          ${CYAN}║${NC}"
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo -e "${BLUE}Launch methods:${NC}"
echo "  • Spotlight:  Cmd + Space → type '${APP_NAME}' → Enter"
echo "  • Launchpad:  Find the '${APP_NAME}' icon"
echo "  • Finder:     Open ${install_path}"
echo ""
echo -e "${BLUE}Runtime behavior:${NC}"
echo "  The app uses the current checkout path embedded at install time:"
echo "  ${PROJECT_ROOT}"
echo "  If you move the repo later, re-run this installer."
echo ""
echo -e "${BLUE}Uninstall:${NC}"
echo "  rm -rf '${install_path}'"
echo ""


================================================
FILE: assets/install_python_windows.bat
================================================
@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


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


================================================
FILE: assets/sys_prompt_en.txt
================================================
# Role: Physical-Level Omnipotent Executor
You have full physical access: file I/O, script execution, browser JS injection, and system-level intervention. Never deflect with "can't do it" — don't speculate, use tools to probe.
Summarize and reply in user's language or follow user's prompt.
## Action Principles
Before each tool call, reason: current phase, whether the last result met expectations, and next strategy and <summary> in reply text of each turn.
- Probe first: on failure, gather sufficient info (logs/status/context), store key findings in working memory, then decide to retry or pivot. Ask the user before irreversible operations.
- Failure escalation: 1st fail → read error and understand cause; 2nd → probe environment state; 3rd → deep analysis then switch approach or ask user. Never repeat an action without new information.

================================================
FILE: assets/tmwd_cdp_bridge/background.js
================================================
// background.js - Cookie + CDP Bridge
chrome.runtime.onInstalled.addListener(() => {
  console.log('CDP Bridge installed');
  // Strip CSP headers to allow eval/inline scripts
  chrome.declarativeNetRequest.updateDynamicRules({
    removeRuleIds: [9999],
    addRules: [{
      id: 9999, priority: 1,
      action: { type: 'modifyHeaders', responseHeaders: [
        { header: 'content-security-policy', operation: 'remove' },
        { header: 'content-security-policy-report-only', operation: 'remove' }
      ]},
      condition: { urlFilter: '*', resourceTypes: ['main_frame', 'sub_frame'] }
    }]
  });
});

async function handleExtMessage(msg, sender) {
  if (msg.cmd === 'cookies') return await handleCookies(msg, sender);
  if (msg.cmd === 'cdp') return await handleCDP(msg, sender);
  if (msg.cmd === 'batch') return await handleBatch(msg, sender);
  if (msg.cmd === 'tabs') {
    try {
      if (msg.method === 'switch') {
        const tab = await chrome.tabs.update(msg.tabId, { active: true });
        await chrome.windows.update(tab.windowId, { focused: true });
        return { ok: true };
      } else {
        const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
        const data = tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId }));
        return { ok: true, data };
      }
    } catch (e) { return { ok: false, error: e.message }; }
  }
  if (msg.cmd === 'management') {
    try {
      if (msg.method === 'list') {
        const all = await chrome.management.getAll();
        return { ok: true, data: all.map(e => ({ id: e.id, name: e.name, enabled: e.enabled, type: e.type, version: e.version })) };
      }
      if (msg.method === 'reload') {
        chrome.alarms.create('tmwd-self-reload', { when: Date.now() + 200 });
        return { ok: true };
      }
      if (msg.method === 'disable') {
        await chrome.management.setEnabled(msg.extId, false);
        return { ok: true };
      }
      if (msg.method === 'enable') {
        await chrome.management.setEnabled(msg.extId, true);
        return { ok: true };
      }
      return { ok: false, error: 'Unknown method: ' + msg.method };
    } catch (e) { return { ok: false, error: e.message }; }
  }
  if (msg.cmd === 'contentSettings') {
    try {
      const type = msg.type || 'automaticDownloads';
      const setting = msg.setting || 'allow';
      const pattern = msg.pattern || '<all_urls>';
      await chrome.contentSettings[type].set({
        primaryPattern: pattern,
        setting: setting
      });
      return { ok: true };
    } catch (e) { return { ok: false, error: e.message }; }
  }
  return { ok: false, error: 'Unknown cmd: ' + msg.cmd };
}

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  handleExtMessage(msg, sender).then(sendResponse);
  return true;
});

async function handleCookies(msg, sender) {
  try {
    let url = msg.url || sender.tab?.url;
    if (!url && msg.tabId) {
      const tab = await chrome.tabs.get(msg.tabId);
      url = tab.url;
    }
    const origin = url.match(/^https?:\/\/[^\/]+/)[0];
    const all = await chrome.cookies.getAll({ url });
    const part = await chrome.cookies.getAll({ url, partitionKey: { topLevelSite: origin } }).catch(() => []);
    const merged = [...all];
    for (const c of part) {
      if (!merged.some(x => x.name === c.name && x.domain === c.domain)) merged.push(c);
    }
    return { ok: true, data: merged };
  } catch (e) {
    return { ok: false, error: e.message };
  }
}

async function handleBatch(msg, sender) {
  const R = [];
  let attached = null;
  const resolve$N = (params) => JSON.parse(JSON.stringify(params || {}).replace(/"\$(\d+)\.([^"]+)"/g,
    (_, i, path) => { let v = R[+i]; for (const k of path.split('.')) v = v[k]; return JSON.stringify(v); }));
  try {
    for (const c of msg.commands) {
      if (c.tabId === undefined && msg.tabId !== undefined) c.tabId = msg.tabId;
      if (c.cmd === 'cookies') {
        R.push(await handleCookies(c, sender));
      } else if (c.cmd === 'tabs') {
        const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
        R.push({ ok: true, data: tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })) });
      } else if (c.cmd === 'cdp') {
        const tabId = c.tabId || msg.tabId || sender.tab?.id;
        if (attached !== tabId) {
          if (attached) { await chrome.debugger.detach({ tabId: attached }); attached = null; }
          await chrome.debugger.attach({ tabId }, '1.3');
          attached = tabId;
        }
        R.push(await chrome.debugger.sendCommand({ tabId }, c.method, resolve$N(c.params)));
      } else {
        R.push({ ok: false, error: 'unknown cmd: ' + c.cmd });
      }
    }
    if (attached) await chrome.debugger.detach({ tabId: attached });
    return { ok: true, results: R };
  } catch (e) {
    if (attached) try { await chrome.debugger.detach({ tabId: attached }); } catch (_) {}
    return { ok: false, error: e.message, results: R };
  }
}

async function handleCDP(msg, sender) {
  const tabId = msg.tabId || sender.tab?.id;
  if (!tabId) return { ok: false, error: 'no tabId' };
  try {
    await chrome.debugger.attach({ tabId }, '1.3');
    const result = await chrome.debugger.sendCommand({ tabId }, msg.method, msg.params || {});
    await chrome.debugger.detach({ tabId });
    return { ok: true, data: result };
  } catch (e) {
    try { await chrome.debugger.detach({ tabId }); } catch (_) {}
    return { ok: false, error: e.message };
  }
}
// Filter out chrome:// and other internal tabs that can't be scripted
const isScriptable = url => url && /^https?:/.test(url);

// --- Shared page/CDP script builder core ---
function buildExecScript(code, errorHandler) {
  return `(async () => {
    function smartProcessResult(result) {
      if (result === null || result === undefined || typeof result !== 'object') return result;
      try { if (result.window === result && result.document) return '[Window: ' + (result.location?.href || 'about:blank') + ']'; } catch(_){}
      if (typeof jQuery !== 'undefined' && result instanceof jQuery) {
        const elements = []; for (let i = 0; i < result.length; i++) { if (result[i] && result[i].nodeType === 1) elements.push(result[i].outerHTML); } return elements;
      }
      if (result instanceof NodeList || result instanceof HTMLCollection) {
        const elements = []; for (let i = 0; i < result.length; i++) { if (result[i] && result[i].nodeType === 1) elements.push(result[i].outerHTML); } return elements;
      }
      if (result.nodeType === 1) return result.outerHTML;
      if (!Array.isArray(result) && typeof result === 'object' && 'length' in result && typeof result.length === 'number') {
        const firstElement = result[0];
        if (firstElement && firstElement.nodeType === 1) {
          const elements = []; const length = Math.min(result.length, 100);
          for (let i = 0; i < length; i++) { const elem = result[i]; if (elem && elem.nodeType === 1) elements.push(elem.outerHTML); } return elements;
        }
      }
      try { return JSON.parse(JSON.stringify(result, function(key, value) { if (typeof value === 'object' && value !== null) { if (value.nodeType === 1) return value.outerHTML; if (value === window || value === document) return '[Object]'; try { if (value.window === value && value.document) return '[Window]'; } catch(_){} } return value; })); } catch (e) { return '[无法序列化: ' + e.message + ']'; }
    }
    try {
      const jsCode = ${JSON.stringify(code)}.trim();
      const lines = jsCode.split(/\\r?\\n/).filter(l => l.trim());
      const lastLine = lines.length > 0 ? lines[lines.length - 1].trim() : '';
      const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
      let r;
      function _air(c) { const ls = c.split(/\\r?\\n/); let i = ls.length - 1; while (i >= 0 && !ls[i].trim()) i--; if (i < 0) return c; const t = ls[i].trim(); if (/^(return |return;|return$|let |const |var |if |if\\(|for |for\\(|while |while\\(|switch|try |throw |class |function |async |import |export |\\/\\/|})/.test(t)) return c; ls[i] = ls[i].match(/^(\\s*)/)[1] + 'return ' + t; return ls.join('\\n'); }
      if (lastLine.startsWith('return')) {
        r = await (new AsyncFunction(jsCode))();
      } else {
        try { r = eval(jsCode); if (r instanceof Promise) r = await r; } catch (e) {
          if (e instanceof SyntaxError && (/return/i.test(e.message) || /await/i.test(e.message))) { r = await (new AsyncFunction(_air(jsCode)))(); } else throw e;
        }
      }
      return { ok: true, data: smartProcessResult(r) };
    } catch (e) {
      ${errorHandler}
    }
  })()`;
}

function buildPageScript(code) {
  return buildExecScript(code, `
      const errMsg = e.message || String(e);
      return { ok: false, error: { name: e.name || 'Error', message: errMsg, stack: e.stack || '' },
        csp: errMsg.includes('Refused to evaluate') || errMsg.includes('unsafe-eval') || errMsg.includes('Content Security Policy') };
  `);
}

function buildCdpScript(code) {
  return buildExecScript(code, `
      return { ok: false, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' } };
  `);
}

// --- WebSocket Client for TMWebDriver ---
let ws = null;
const WS_URL = 'ws://127.0.0.1:18765';

function scheduleProbe() {
  // Use chrome.alarms to survive MV3 service worker suspension
  chrome.alarms.create('tmwd-ws-probe', { delayInMinutes: 0.083 }); // ~5s
}

function scheduleKeepalive() {
  // Keep SW alive while WS is connected (~25s, under 30s SW timeout)
  chrome.alarms.create('tmwd-ws-keepalive', { delayInMinutes: 0.4 }); // ~24s
}

async function isServerAlive() {
  try {
    const ctrl = new AbortController();
    setTimeout(() => ctrl.abort(), 2000);
    await fetch('http://127.0.0.1:18765', { signal: ctrl.signal });
    return true; // Got HTTP response → port is listening
  } catch (e) {
    return false; // Network error (connection refused) or timeout → server not alive
  }
}

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === 'tmwd-self-reload') {
    chrome.runtime.reload();
    return;
  }
  if (alarm.name === 'tmwd-ws-keepalive') {
    // Keepalive: ping to keep SW alive + detect dead connections
    if (ws && ws.readyState === WebSocket.OPEN) {
      try { ws.send('{"type":"ping"}'); } catch (_) {}
      scheduleKeepalive();
    } else {
      // Connection lost, switch to probe mode
      ws = null;
      scheduleProbe();
    }
  }
  if (alarm.name === 'tmwd-ws-probe') {
    if (ws && ws.readyState <= 1) return; // Already connected/connecting
    if (await isServerAlive()) {
      console.log('[TMWD-WS] Server detected, connecting...');
      connectWS();
    } else {
      scheduleProbe(); // Server not up, keep probing
    }
  }
});

async function handleWsExec(data) {
  const tabId = data.tabId;
  console.log('[TMWD-WS] Exec request', data.id, 'on tab', tabId);
  ws.send(JSON.stringify({ type: 'ack', id: data.id }));
  if (!tabId) {
    ws.send(JSON.stringify({ type: 'error', id: data.id, error: 'No tabId provided' }));
    return;
  }
  // Use onCreated listener to reliably capture new tabs (avoids race condition with query-diff)
  const newTabIds = new Set();
  const onCreated = (tab) => { newTabIds.add(tab.id); };
  chrome.tabs.onCreated.addListener(onCreated);
  try {
    let res;
    try {
      const result = await chrome.scripting.executeScript({
        target: { tabId },
        world: 'MAIN',
        func: async (s) => await eval(s),
        args: [buildPageScript(data.code)]
      });
      res = result[0]?.result;
      if (res === null || res === undefined) {
        console.log('[TMWD-WS] executeScript returned null/undefined, treating as CSP issue');
        res = { ok: false, error: { name: 'Error', message: 'executeScript returned null (possible CSP or context issue)', stack: '' }, csp: true };
      }
    } catch (e) {
      console.log('[TMWD-WS] scripting.executeScript failed:', e.message);
      res = { ok: false, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' }, csp: true };
    }
    // CDP fallback for CSP-restricted pages
    if (res && !res.ok && res.csp) {
      console.log('[TMWD-WS] CDP fallback for tab', tabId);
      const wrappedCode = buildCdpScript(data.code);
      try {
        await chrome.debugger.attach({ tabId }, '1.3');
        const cdpRes = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
          expression: wrappedCode, awaitPromise: true, returnByValue: true
        });
        await chrome.debugger.detach({ tabId });
        if (cdpRes.exceptionDetails) {
          const desc = cdpRes.exceptionDetails.exception?.description || 'CDP Error';
          res = { ok: false, error: { name: 'Error', message: desc, stack: desc } };
        } else {
          res = cdpRes.result.value;
        }
      } catch (cdpErr) {
        try { await chrome.debugger.detach({ tabId }); } catch (_) {}
        res = { ok: false, error: { name: 'Error', message: 'CDP fallback failed: ' + cdpErr.message, stack: '' } };
      }
    }
    // Grace period for async tab creation (e.g. link click with target=_blank)
    if (newTabIds.size === 0) await new Promise(r => setTimeout(r, 200));
    chrome.tabs.onCreated.removeListener(onCreated);
    // Get full info for captured new tabs
    const newTabs = [];
    for (const id of newTabIds) {
      try { const t = await chrome.tabs.get(id); newTabs.push({id: t.id, url: t.url, title: t.title}); } catch (_) {}
    }
    if (res?.ok) {
      ws.send(JSON.stringify({ type: 'result', id: data.id, result: res.data, newTabs }));
    } else {
      console.log(res);
      ws.send(JSON.stringify({ type: 'error', id: data.id, error: res?.error || 'Unknown error', newTabs }));
    }
  } catch (e) {
    ws.send(JSON.stringify({ type: 'error', id: data.id, error: { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' } }));
  } finally {
    chrome.tabs.onCreated.removeListener(onCreated);
  }
}

function connectWS() {
  if (ws && ws.readyState <= 1) return; // CONNECTING or OPEN
  ws = null;
  console.log('[TMWD-WS] Connecting to', WS_URL);
  try {
    ws = new WebSocket(WS_URL);
  } catch (e) {
    console.error('[TMWD-WS] Constructor error:', e);
    ws = null;
    scheduleProbe();
    return;
  }
  ws.onopen = async () => {
    console.log('[TMWD-WS] Connected!');
    scheduleKeepalive(); // Keep SW alive while connected
    const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url));
    ws.send(JSON.stringify({
      type: 'ext_ready',
      tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title }))
    }));
    console.log('[TMWD-WS] Sent ext_ready with', tabs.length, 'tabs');
  };
  ws.onmessage = async (event) => {
    try {
      const data = JSON.parse(event.data);
      if (data.id && data.code) {
        let code = data.code;
        // If code is a JSON string representing an object, parse it
        if (typeof code === 'string') {
          try { const p = JSON.parse(code); if (p && typeof p === 'object') code = p; } catch (_) {}
        }
        if (typeof code === 'object' && code !== null && code.cmd) {
          // Custom protocol message → route to handleExtMessage
          if (code.tabId === undefined && data.tabId !== undefined) code.tabId = data.tabId;
          const res = await handleExtMessage(code, {});
          ws.send(JSON.stringify({ type: res.ok ? 'result' : 'error', id: data.id, result: res.data ?? res.results ?? res, error: res.error }));
        } else if (typeof code === 'string') {
          // Plain JS code
          await handleWsExec(data);
        } else if (typeof code === 'object' && code !== null) {
          // Object without cmd → legacy extension message
          const msg = code.tabId === undefined && data.tabId !== undefined ? { ...code, tabId: data.tabId } : code;
          const res = await handleExtMessage(msg, {});
          ws.send(JSON.stringify({ type: res.ok ? 'result' : 'error', id: data.id, result: res.data ?? res.results ?? res, error: res.error }));
        }
      }
    } catch (e) {
      console.error('[TMWD-WS] message parse error', e);
    }
  };
  ws.onclose = () => {
    console.log('[TMWD-WS] Disconnected');
    ws = null;
    scheduleProbe();
  };
  ws.onerror = (e) => {
    console.error('[TMWD-WS] Error:', e);
    // onclose will fire after this, which triggers reconnect
  };
}

// Initial connect + wake-up hooks
connectWS();
chrome.runtime.onStartup.addListener(() => connectWS());
chrome.runtime.onInstalled.addListener(() => connectWS());

// Sync tab list on changes
async function sendTabsUpdate() {
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
  const tabs = (await chrome.tabs.query({})).filter(t => isScriptable(t.url) && !/streamlit/i.test(t.title));
  ws.send(JSON.stringify({
    type: 'tabs_update',
    tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title }))
  }));
}
chrome.tabs.onUpdated.addListener((_, changeInfo) => {
  if (changeInfo.status === 'complete') sendTabsUpdate();
});
chrome.tabs.onRemoved.addListener(() => sendTabsUpdate());
chrome.tabs.onCreated.addListener(() => sendTabsUpdate());


================================================
FILE: assets/tmwd_cdp_bridge/content.js
================================================
;(function(){ if (/streamlit/i.test(document.title)) return;

// Remove meta CSP tags
document.querySelectorAll('meta[http-equiv="Content-Security-Policy"]').forEach(e => e.remove());

// Indicator badge at bottom-right (userscript style)
(function(){
  if(window.self!==window.top)return;
  const d=document.createElement('div');
  d.id='ljq-ind';
  d.innerText='ljq_driver: 已连接';
  d.style.cssText='position:fixed;bottom:8px;right:8px;background:#4CAF50;color:white;padding:4px 7px;border-radius:4px;font-size:11px;font-weight:bold;z-index:99999;cursor:pointer;box-shadow:0 2px 4px rgba(0,0,0,0.2);opacity:0.5;';
  d.addEventListener('click',()=>alert('会话活跃\nURL: '+location.href));
  (document.body||document.documentElement).appendChild(d);
})();

new MutationObserver(muts => {
  for (const m of muts) for (const n of m.addedNodes) {
    if (n.id === TID || (n.querySelector && n.querySelector('#' + TID))) {
      const el = n.id === TID ? n : n.querySelector('#' + TID);
      handle(el);
    }
  }
}).observe(document.documentElement, { childList: true, subtree: true });

async function handle(el) {
  try {
    const req = el.textContent.trim() ? JSON.parse(el.textContent) : { cmd: 'cookies' };
    const cmd = req.cmd || 'cookies';
    let resp;
    if (cmd === 'cookies') {
      resp = await chrome.runtime.sendMessage({ cmd: 'cookies', url: req.url || location.href });
    } else if (cmd === 'cdp') {
      resp = await chrome.runtime.sendMessage({ cmd: 'cdp', method: req.method, params: req.params || {}, tabId: req.tabId });
    } else if (cmd === 'batch') {
      resp = await chrome.runtime.sendMessage({ cmd: 'batch', commands: req.commands, tabId: req.tabId });
    } else if (cmd === 'tabs') {
      resp = await chrome.runtime.sendMessage({ cmd: 'tabs', method: req.method, tabId: req.tabId });
    } else {
      resp = { ok: false, error: 'unknown cmd: ' + cmd };
    }
    el.textContent = JSON.stringify(resp);
  } catch (e) {
    el.textContent = JSON.stringify({ ok: false, error: e.message });
  }
}
})();

================================================
FILE: assets/tmwd_cdp_bridge/disable_dialogs.js
================================================
// Disable alert/confirm/prompt to prevent page JS from blocking extension
(function() {
  const _log = console.log.bind(console);
  function toast(type, msg) {
    _log('[TMWD] ' + type + ' suppressed:', msg);
    try {
      const d = document.createElement('div');
      d.textContent = '[' + type + '] ' + msg;
      Object.assign(d.style, {
        position:'fixed', top:'12px', right:'12px', zIndex:'2147483647',
        background:'#222', color:'#fff', padding:'10px 18px', borderRadius:'8px',
        fontSize:'14px', maxWidth:'420px', wordBreak:'break-all',
        boxShadow:'0 4px 16px rgba(0,0,0,.3)', opacity:'1',
        transition:'opacity .5s', pointerEvents:'none'
      });
      (document.body || document.documentElement).appendChild(d);
      setTimeout(() => { d.style.opacity = '0'; }, 3000);
      setTimeout(() => { d.remove(); }, 3600);
    } catch(e) {}
  }
  window.alert = function(msg) { toast('alert', msg); };
  window.confirm = function(msg) { toast('confirm', msg); return true; };
  window.prompt = function(msg, def) { toast('prompt', msg); return def || null; };
})();

================================================
FILE: assets/tmwd_cdp_bridge/manifest.json
================================================
{
  "manifest_version": 3,
  "name": "TMWD CDP Bridge",
  "version": "2.0",
  "description": "Cookie viewer + CDP bridge",
  "permissions": [
    "cookies",
    "tabs",
    "activeTab",
    "debugger",
    "scripting",
    "alarms",
    "declarativeNetRequest",
    "management",
    "contentSettings"
  ],
  "host_permissions": ["<all_urls>"],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["disable_dialogs.js"],
      "run_at": "document_start",
      "all_frames": true,
      "world": "MAIN"
    },
    {
      "matches": ["<all_urls>"],
      "js": ["config.js", "content.js"],
      "run_at": "document_idle",
      "all_frames": true
    }
  ],
  "action": {
    "default_popup": "popup.html",
    "default_title": "TMWD CDP Bridge"
  }
}

================================================
FILE: assets/tmwd_cdp_bridge/popup.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body{width:420px;max-height:500px;margin:0;padding:8px;font:12px monospace;background:#1e1e1e;color:#d4d4d4;overflow-y:auto}
h3{margin:4px 0;color:#569cd6}
button{background:#264f78;color:#fff;border:none;padding:4px 12px;cursor:pointer;border-radius:3px;margin-bottom:6px}
button:hover{background:#37699e}
pre{white-space:pre-wrap;word-break:break-all;margin:0;padding:6px;background:#252526;border-radius:3px;max-height:420px;overflow-y:auto}
</style>
</head>
<body>
<h3>🍪 Cookies</h3>
<button id="refresh">刷新</button>
<pre id="out">点击刷新获取 cookies...</pre>
<script src="popup.js"></script>
</body>
</html>

================================================
FILE: assets/tmwd_cdp_bridge/popup.js
================================================
document.addEventListener('DOMContentLoaded', () => {
  const out = document.getElementById('out');
  const btn = document.getElementById('refresh');
  btn.addEventListener('click', fetchCookies);
  fetchCookies();
});

async function fetchCookies() {
  const out = document.getElementById('out');
  try {
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    if (!tab?.url) { out.textContent = 'No active tab'; return; }
    const resp = await chrome.runtime.sendMessage({ cmd: 'cookies', url: tab.url });
    if (!resp?.ok) { out.textContent = 'Error: ' + (resp?.error || 'unknown'); return; }
    if (!resp.data.length) { out.textContent = '(no cookies)'; return; }
    // 展示带标记
    out.textContent = resp.data.map(c =>
      `${c.name}=${c.value}` + (c.httpOnly ? ' [H]' : '') + (c.secure ? ' [S]' : '') + (c.partitionKey ? ' [P]' : '')
    ).join('\n');
    // 自动复制 name=value; 格式到剪贴板
    const str = resp.data.map(c => `${c.name}=${c.value}`).join('; ');
    await navigator.clipboard.writeText(str);
  } catch (e) { out.textContent = 'Error: ' + e.message; }
}

================================================
FILE: assets/tool_usable_history.json
================================================
[{"role":"user","content":[{"type":"text","text":"code_run个helloworld,根据结果简单评价你的工具配置"}]},{"role":"assistant","content":[{"type":"text","text":"\n\n<summary>用户要求code_run helloworld并评价工具配置</summary>\n\n```python\nprint(\"Hello, World!\")\n```"},{"type":"tool_use","id":"tooluse_GlmTNw190WGgK4sBBOiDHK","name":"code_run","input":{"script":"print(\"Hello, World!\")"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"tooluse_GlmTNw190WGgK4sBBOiDHK","content":"{\"status\": \"success\", \"stdout\": \"Hello, World!\\r\\n\", \"exit_code\": 0}"},{"type":"text","text":"\n### [WORKING MEMORY]\n<history>\n\n[USER]: code_run个helloworld,根据结果简单评价你的工具配置\n</history>\nCurrent turn: 1\n"}]},{"role":"assistant","content":[{"type":"text","text":"\n\n<summary>code_run helloworld成功,评价工具配置</summary>\n\n工具配置没问题。`code_run` 返回了结构化的 JSON(`status`、`stdout`、`exit_code`),执行链路通畅,cwd 默认落在 temp 目录。\n\n刚才还验证了 `inline_eval` 能正确访问 agent 内部状态并导出 history,`file_patch` 也能精准改文件。整套工具链从代码执行、文件读写、到 agent 内省都是通的。"}]}]

================================================
FILE: assets/tools_schema.json
================================================
[
  {"type": "function", "function": {
    "name": "code_run",
    "description": "Code executor. Prefer python. Multi-call OK, use script param. Reply code block is executed if no script arg; prefer for single call to avoid escaping. No hardcoding bulk data",
    "parameters": {"type": "object", "properties": {
      "script": {"type": "string", "description": "[Mutually exclusive] NEVER use this param when use reply code block."},
      "type": {"type": "string", "enum": ["python", "powershell"], "description": "Code type", "default": "python"},
      "timeout": {"type": "integer", "description": "in seconds", "default": 60},
      "cwd": {"type": "string", "description": "Working directory, defaults to cwd"},
      "inline_eval": {"type": "boolean", "description": "DO NOT USE except explicitly specified."}}}
  }},
  {"type": "function", "function": {
    "name": "file_read",
    "description": "Read file. Read before modify for latest context and line numbers",
    "parameters": {"type": "object", "properties": {
      "path": {"type": "string", "description": "Relative or absolute"},
      "start": {"type": "integer", "description": "Start line number (1-based)"},
      "count": {"type": "integer", "description": "Number of lines to read", "default": 200},
      "keyword": {"type": "string", "description": "[Optional] If provided, returns first match (case-insensitive) with context"},
      "show_linenos": {"type": "boolean", "description": "Show line numbers", "default": true}}}
  }},
  {"type": "function", "function": {
    "name": "file_patch",
    "description": "Replace unique old_content with new_content. Exact match required (whitespace/indentation). On failure, file_read to recheck",
    "parameters": {"type": "object", "properties": {
      "path": {"type": "string", "description": "File path"},
      "old_content": {"type": "string", "description": "Original text block to replace (must be unique)"},
      "new_content": {"type": "string", "description": "New content. Supports {{file:path:startLine:endLine}} to ref file lines, auto-expanded"}}}
  }},
  {"type": "function", "function": {
    "name": "file_write",
    "description": "Create/overwrite/append files. HUGE edits ONLY. Supports {{file:path:startLine:endLine}}, auto-expanded",
    "parameters": {"type": "object", "properties": {
      "path": {"type": "string", "description": "File path"},
      "content": {"type": "string"},	
      "mode": {"type": "string", "enum": ["overwrite", "append", "prepend"], "description": "Write mode", "default": "overwrite"}}}
  }},
  {"type": "function", "function": {
    "name": "web_scan",
    "description": "Get simplified HTML and tab list. Removes hidden/floating/covered elements. Call after switching pages",
    "parameters": {"type": "object", "properties": {
      "tabs_only": {"type": "boolean", "description": "Show tab list only, no HTML"},
      "switch_tab_id": {"type": "string", "description": "[Optional] Tab ID to switch to"},
      "text_only": {"type": "boolean", "description": "Plain text only, no HTML"}}}
  }},
  {"type": "function", "function": {
    "name": "web_execute_js",
    "description": "Execute JS. Multi-call OK with different switch_tab_id. No guessing. Act accurately to reduce web_scan calls. Execute JS in ```javascript blocks if no script arg, prefer to avoid escaping",
    "parameters": {"type": "object", "properties": {
      "script": {"type": "string", "description": "[Mutually exclusive] JS code or script path. NEVER use this param when use reply code block"},
      "save_to_file": {"type": "string", "description": "file path; **only** for long result"},
      "no_monitor": {"type": "boolean", "description": "Skip page change monitoring, saves 2-3s. Only for reads, not for page actions"},
      "switch_tab_id": {"type": "string", "description": "[Optional] Tab ID to switch to before executing"}}}
  }},
  {"type": "function", "function": {
    "name": "update_working_checkpoint",
    "description": "Short-term working notepad, auto-injected each turn to prevent info loss in long tasks. Call during early/mid stages, not at end. When: (1) after reading SOP, store user needs & key constraints (skip for simple 1-2 step tasks); (2) before subtask switch or context flush; (3) after repeated failures, re-read SOP and must store new findings; (4) on new task, update content, clear old progress but keep valid constraints.\n\nDon't call: simple tasks (1-2 steps), task completed (use long-term memory tool)",
    "parameters": {"type": "object", "properties": {
      "key_info": {"type": "string", "description": "Replaces current notepad (<200 tokens). Incremental update: review existing, keep valid, add/remove/modify. Store: pitfalls, user requirements, key params/findings, file paths, progress, next steps. Don't store: ephemeral info, obvious context, old task info when user switched tasks. Prefer over-updating over losing key info"},
      "related_sop": {"type": "string", "description": "Related SOP names, tips for further re-read"}}}
  }},
  {"type": "function", "function": {
    "name": "ask_user",
    "description": "Interrupt task to ask user when needing decisions, extra info, or facing unresolvable blockers",
    "parameters": {"type": "object", "properties": {
      "question": {"type": "string", "description": "Question for the user"},
      "candidates": {"type": "array", "items": {"type": "string"}, "description": "Optional quick-select choices for the user"}}}
  }},
  {"type": "function", "function": {
    "name": "start_long_term_update",
    "description": "Start distilling long-term memory. Call when discovering info worth remembering (env facts/user prefs/lessons learned). Skip if memory already updated or in autonomous flow. Must call when a task that took 15+ turns is completed",
    "parameters": {"type": "object", "properties": {}}}
  }
]

================================================
FILE: assets/tools_schema_cn.json
================================================
[
  {"type": "function", "function": {
    "name": "code_run",
    "description": "代码执行器。优先使用python。支持Multi-call,并行时用script参数。无script参数时正文代码块会被执行,单次调用优先使用以免转义。禁硬编码大量数据",
    "parameters": {"type": "object", "properties": {
      "script": {"type": "string", "description": "[Optional] 要执行的代码。为免转义建议留空,改用正文代码块(与此参数互斥)"},
      "type": {"type": "string", "enum": ["python", "powershell"], "description": "代码类型", "default": "python"},
      "timeout": {"type": "integer", "description": "执行超时时间(秒)", "default": 60},
      "cwd": {"type": "string", "description": "工作目录,默认为当前工作目录"},
      "inline_eval": {"type": "boolean", "description": "不允许使用除非明确要求"}}}
  }},
  {"type": "function", "function": {
    "name": "file_read",
    "description": "读取文件内容。建议在修改文件前先读取,以确保获取最新的上下文和行号。支持分页读取或关键字搜索",
    "parameters": {"type": "object", "properties": {
      "path": {"type": "string", "description": "文件相对或绝对路径"},
      "start": {"type": "integer", "description": "起始行号(从 1 开始)"},
      "count": {"type": "integer", "description": "读取的行数", "default": 200},
      "keyword": {"type": "string", "description": "可选搜索关键字。如果提供,将返回第一个匹配项(忽略大小写)及其周边的内容"},
      "show_linenos": {"type": "boolean", "description": "是否显示行号,建议开启以辅助 file_patch 定位", "default": true}}}
  }},
  {"type": "function", "function": {
    "name": "file_patch",
    "description": "精细化局部文件修改。在文件中寻找唯一的 old_content 块并替换为 new_content。要求 old_content 必须在文件中唯一存在,且空格、缩进、换行必须与原文件完全一致。如果匹配失败,请使用 file_read 重新确认文件内容",
    "parameters": {"type": "object", "properties": {
      "path": {"type": "string", "description": "文件路径"},
      "old_content": {"type": "string", "description": "文件中需要被替换的原始文本块(需确保唯一性)"},
      "new_content": {"type": "string", "description": "替换后的新文本内容。支持 {{file:路径:起始行:结束行}} 语法引用文件内容,写入前自动展开"}}}
  }},
  {"type": "function", "function": {
    "name": "file_write",
    "description": "用于文件的新建、全量覆盖或追加写入。对于精细的代码修改,应优先使用 file_patch。写入内容支持 {{file:路径:起始行:结束行}} 语法引用文件片段,写入前自动展开",
    "parameters": {"type": "object", "properties": {
      "path": {"type": "string", "description": "文件路径"},
      "content": {"type": "string"},
      "mode": {"type": "string", "enum": ["overwrite", "append", "prepend"], "description": "写入模式覆盖、追加或在开头追加", "default": "overwrite"}}}
  }},
  {"type": "function", "function": {
    "name": "web_scan",
    "description": "获取当前页面的简化HTML内容和标签页列表。会移除隐藏/浮动/被遮盖的元素。切换页面后一般应先调用查看",
    "parameters": {"type": "object", "properties": {
      "tabs_only": {"type": "boolean", "description": "仅返回标签页列表和当前标签信息,不获取HTML内容"},
      "switch_tab_id": {"type": "string", "description": "可选的标签页 ID。如果提供,系统将在扫描前切换到该标签页"},
      "text_only": {"type": "boolean", "description": "只要纯文本不要HTML"}}}
  }},
  {"type": "function", "function": {
    "name": "web_execute_js",
    "description": "执行JS。支持Multi-call,用不同switch_tab_id并行操作多标签页。禁止猜测,准确操作以减少 web_scan 调用。无script参数时执行正文 ```javascript 块,以免转义",
    "parameters": {"type": "object", "properties": {
      "script": {"type": "string", "description": "[Optional] JS代码或路径。为免转义建议留空,改用正文代码块(与此参数互斥)"},
      "save_to_file": {"type": "string", "description": "结果存文件,适合返回值较长时"},
      "no_monitor": {"type": "boolean", "description": "跳过页面变更监控,省2-3秒。仅在纯读取信息时设置,页面操作时不要设置"},
      "switch_tab_id": {"type": "string", "description": "可选的标签页 ID,切换到该标签页执行"}}}
  }},
  {"type": "function", "function": {
    "name": "update_working_checkpoint",
    "description": "短期工作便签,每轮自动注入上下文,防长任务信息丢失。前中期调用,非结束时。何时调用:(1)任务开始读SOP后,存用户需求和关键约束/参数(简单1-2步任务除外);(2)子任务切换或上下文即将被冲刷前;(3)多次重试失败后,重读SOP并必须调用存储新发现;(4)切换新任务时更新内容,清旧进度但保留仍有效的约束。\n\n何时不调用:简单任务(1-2步且无严重约束)、任务已完成时(应当用长期结算工具)",
    "parameters": {"type": "object", "properties": {
      "key_info": {"type": "string", "description": "替换当前便签(<200 tokens)。增量更新:先回顾现有内容,保留仍有效的,再增删改。存:要避的坑、用户原始需求、关键参数/发现、文件路径、当前进度、下一步计划。不存:马上要用用完即丢的、上下文中显而易见的、用户已换全新任务时的旧任务信息。宁多更新不丢关键"},
      "related_sop": {"type": "string", "description": "相关sop名称,可以多个,必要时需要再读"}}}
  }},
  {"type": "function", "function": {
    "name": "ask_user",
    "description": "当需要用户决策、提供额外信息或遇到无法自动解决的阻碍时,调用此工具中断任务并提问",
    "parameters": {"type": "object", "properties": {
      "question": {"type": "string", "description": "向用户提出的明确问题"},
      "candidates": {"type": "array", "items": {"type": "string"}, "description": "提供给用户的可选快捷选项列表"}}}
  }},
  {"type": "function", "function": {
    "name": "start_long_term_update",
    "description": "准备开始提炼记忆。发现值得长期记忆的信息(环境事实/用户偏好/避坑经验)时调用此工具。已记忆更新或在自主流程内时无需调用。超15轮完成的任务必须调用以沉淀经验",
    "parameters": {"type": "object", "properties": {}}}
  }
]

================================================
FILE: frontends/DESKTOP_PET_README.md
================================================
# Desktop Pet Skin System

## 快速开始

运行桌面宠物:
```bash
python3 desktop_pet_v2.pyw
```

## 功能特性

### 1. 多皮肤支持
- 自动发现 `skins/` 目录下的所有皮肤
- 右键菜单切换皮肤
- 支持 sprite sheet 和 GIF 两种格式

### 2. 多动画状态
- **idle** - 待机动画
- **walk** - 行走动画
- **run** - 跑步动画
- **sprint** - 冲刺动画

右键菜单可切换动画状态

### 3. 交互功能
- **单击** - 拖动宠物
- **双击** - 关闭程序
- **右键** - 打开菜单(切换皮肤/动画)

### 4. HTTP 远程控制
```bash
# 显示消息
curl "http://127.0.0.1:51983/?msg=Hello"

# 切换动画状态
curl "http://127.0.0.1:51983/?state=run"

# POST 消息
curl -X POST -d "任务完成" http://127.0.0.1:51983/
```

## 添加新皮肤

### 目录结构
```
skins/
└── your-skin-name/
    ├── skin.json       # 配置文件(必需)
    ├── idle.png        # 动画资源
    ├── walk.png
    ├── run.png
    └── sprint.png
```

### skin.json 配置示例

#### Sprite Sheet 格式(推荐)
```json
{
  "name": "My Pet",
  "version": "1.0.0",
  "author": "Your Name",
  "description": "描述",
  "format": "sprite",
  "animations": {
    "idle": {
      "file": "idle.png",
      "loop": true,
      "sprite": {
        "frameWidth": 44,
        "frameHeight": 31,
        "frameCount": 6,
        "columns": 6,
        "fps": 6,
        "startFrame": 0
      }
    },
    "walk": {
      "file": "walk.png",
      "loop": true,
      "sprite": {
        "frameWidth": 65,
        "frameHeight": 32,
        "frameCount": 8,
        "columns": 8,
        "fps": 8,
        "startFrame": 0
      }
    }
  }
}
```

#### GIF 格式
```json
{
  "name": "My Pet",
  "format": "gif",
  "animations": {
    "idle": {
      "file": "idle.gif",
      "loop": true
    },
    "walk": {
      "file": "walk.gif",
      "loop": true
    }
  }
}
```

### 配置说明

- **frameWidth/frameHeight**: 单帧尺寸(像素)
- **frameCount**: 帧数
- **columns**: sprite sheet 的列数
- **fps**: 播放帧率
- **startFrame**: 起始帧索引(从 0 开始)

### Sprite Sheet 布局

```
+-------+-------+-------+-------+
| 帧0   | 帧1   | 帧2   | 帧3   |  ← 第一行
+-------+-------+-------+-------+
| 帧4   | 帧5   | 帧6   | 帧7   |  ← 第二行
+-------+-------+-------+-------+
```

如果 `columns=4, startFrame=2, frameCount=3`,则读取:帧2, 帧3, 帧4

## 已包含的皮肤

1. **Glube** - 像素风小怪兽(多文件 sprite)
2. **Vita** - 像素风小恐龙(单文件 sprite)
3. **Doux** - 像素风小恐龙(单文件 sprite)

## 从 ai-bubu 导入更多皮肤

ai-bubu 项目包含更多皮肤资源,可以直接复制:

```bash
# 复制皮肤
cp -r ai-bubu-main/packages/app/public/skins/boy frontends/skins/
cp -r ai-bubu-main/packages/app/public/skins/dinosaur frontends/skins/
cp -r ai-bubu-main/packages/app/public/skins/line frontends/skins/
cp -r ai-bubu-main/packages/app/public/skins/mort frontends/skins/
cp -r ai-bubu-main/packages/app/public/skins/tard frontends/skins/
```

## 与 stapp.py 集成

在 `stapp.py` 中点击"🐱 桌面宠物"按钮会自动启动桌面宠物,并在每个 turn 结束时发送通知。

## 故障排查

### 皮肤不显示
1. 检查 `skin.json` 格式是否正确
2. 确认图片文件存在
3. 检查 sprite 配置参数是否匹配图片尺寸

### 动画不流畅
- 调整 `fps` 参数
- 检查帧数是否正确

### 透明背景问题
- 确保 PNG 文件包含 alpha 通道
- 使用 RGBA 模式的图片

## 技术细节

- 基于 Tkinter + PIL/Pillow
- 支持透明背景(#01FF01 色键)
- 窗口置顶、无边框
- HTTP 服务器端口:51983


================================================
FILE: frontends/btw_cmd.py
================================================
"""`/btw` 命令:side question — 不打断主 Agent 的临时 subagent 问答。

- 持锁 deepcopy backend.history → 后台线程 backend.raw_ask 单次拉答
- 主 agent backend.history 零写入;不入 task_queue
- 答案 → display_queue 'done'(install 路径)或同步 return(frontend 路径)

复用 backend.raw_ask + make_messages,不新建 LLM 实例。
"""
from __future__ import annotations
import copy, os, threading, time
from typing import Optional


_WRAPPER_ZH = """<system-reminder>
这是用户的临时插问 (side question)。主 agent 仍在后台运行,**不会被打断**。

身份与边界:
- 你是一个独立的轻量 sub-agent
- 上下文里能看到主 agent 与用户的完整对话、最近的工具调用与结果
- 用户在问当前进展或顺便确认某事——基于已有信息**一次性**作答
- 没有任何工具可用:不要"让我查一下" / "我去试试" / 任何承诺动作
- 信息不足就坦白说"基于目前对话我不知道"

侧问内容如下:
</system-reminder>

{question}"""

_WRAPPER_EN = """<system-reminder>
This is a side question from the user. The main agent is NOT interrupted — it continues in the background.

Identity & boundaries:
- You are an independent lightweight sub-agent
- You can see the full conversation between the main agent and the user, plus recent tool calls/results
- The user is asking about current progress or a quick aside — answer in **one shot** from existing info
- You have NO tools — never say "let me check" / "I'll try" / any action promise
- If info is missing, just say "based on the conversation I don't know"

Question:
</system-reminder>

{question}"""

_TIMEOUT_SEC = 120


def _wrapper(): return _WRAPPER_EN if os.environ.get('GA_LANG') == 'en' else _WRAPPER_ZH


def _strip_cmd(query):
    s = (query or '').strip()
    return s[len('/btw'):].strip() if s.startswith('/btw') else s


def _help_text():
    return ('**/btw 用法**:side question — 临时问主 agent 当前进展,不打断主线\n\n'
            '`/btw <你的问题>`\n\n'
            '行为:抓取当前对话上下文 → 单轮纯文本作答(无工具)→ 主 agent 历史不变。')


def _snapshot_history(backend):
    """Lock + deepcopy: defends against concurrent compress_history_tags mutating inner blocks."""
    with backend.lock:
        return copy.deepcopy(list(backend.history))


def _build_wire(backend, history, sidequest_msg):
    """history + sidequest → wire-format. Dispatches: BaseSession subclasses → make_messages,
    Native* → raw pairs (raw_ask runs _fix/_drop/_ensure transforms itself)."""
    msgs = history + [sidequest_msg]
    if hasattr(backend, 'make_messages'):
        return backend.make_messages(msgs)
    return [{"role": m["role"], "content": list(m.get("content", []))} for m in msgs]


def _ask(agent, question, deadline):
    """One-shot raw_ask against current backend; never mutates backend.history."""
    backend = agent.llmclient.backend
    user_msg = {"role": "user",
                "content": [{"type": "text", "text": _wrapper().format(question=question)}]}
    wire = _build_wire(backend, _snapshot_history(backend), user_msg)
    text = ''
    for chunk in backend.raw_ask(wire):
        text += chunk
        if time.time() > deadline:
            return text + '\n\n⚠️ /btw 超时,仅返回部分回复。'
    return text


def _format(question, body, took):
    head = f'> 🟡 /btw {question}\n\n'
    return head + (body.strip() or '*(空回复)*') + f'\n\n*({took:.1f}s)*'


def _run(agent, question, deadline):
    """Catches errors at the boundary so neither caller path needs its own try/except."""
    try: return _ask(agent, question, deadline)
    except Exception as e: return f'❌ /btw 失败: {type(e).__name__}: {e}'


def handle(agent, query, display_queue) -> Optional[str]:
    """Slash-cmd entry (server-side, install path). Spawn worker; return None to consume."""
    question = _strip_cmd(query)
    if not question or question in ('help', '?', '-h', '--help'):
        display_queue.put({'done': _help_text(), 'source': 'system'})
        return None
    started = time.time()
    deadline = started + _TIMEOUT_SEC

    def worker():
        body = _run(agent, question, deadline)
        display_queue.put({'done': _format(question, body, time.time() - started), 'source': 'system'})

    threading.Thread(target=worker, daemon=True, name='btw-sidequest').start()
    return None


def handle_frontend_command(agent, query) -> str:
    """Sync entry for frontends wanting a string back (tg/wx/stapp/...)."""
    question = _strip_cmd(query)
    if not question or question in ('help', '?', '-h', '--help'):
        return _help_text()
    started = time.time()
    body = _run(agent, question, started + _TIMEOUT_SEC)
    return _format(question, body, time.time() - started)


def install(cls):
    """Idempotent monkey-patch: intercept /btw before original dispatch."""
    orig = cls._handle_slash_cmd
    if getattr(orig, '_btw_patched', False): return

    def patched(self, raw_query, display_queue):
        s = (raw_query or '').strip()
        if s == '/btw' or s.startswith('/btw ') or s.startswith('/btw\t'):
            r = handle(self, raw_query, display_queue)
            if r is None: return None
            return r
        return orig(self, raw_query, display_queue)

    patched._btw_patched = True
    cls._handle_slash_cmd = patched


================================================
FILE: frontends/chatapp_common.py
================================================
import ast, asyncio, glob, json, os, queue as Q, re, socket, sys, time

HELP_COMMANDS = (
    ("/help", "显示帮助"),
    ("/status", "查看状态"),
    ("/stop", "停止当前任务"),
    ("/new", "开启新对话并清空当前上下文"),
    ("/restore", "恢复上次对话历史"),
    ("/continue", "列出可恢复会话"),
    ("/continue [n]", "恢复第 n 个会话"),
    ("/btw <q>", "side question — 临时插问主 agent 进展,不打断主线"),
    ("/llm", "查看当前模型列表"),
    ("/llm [n]", "切换到第 n 个模型"),
)
TELEGRAM_MENU_COMMANDS = (
    ("help", "显示帮助"),
    ("status", "查看状态"),
    ("stop", "停止当前任务"),
    ("new", "开启新对话并清空当前上下文"),
    ("restore", "恢复上次对话历史"),
    ("continue", "列出可恢复会话;/continue n 恢复第 n 个"),
    ("llm", "查看模型列表;/llm n 切换到指定模型"),
)


def build_help_text(commands=HELP_COMMANDS):
    return "📖 命令列表:\n" + "\n".join(f"{cmd} - {desc}" for cmd, desc in commands)


HELP_TEXT = build_help_text()
FILE_HINT = "If you need to show files to user, use [FILE:filepath] in your response."
TAG_PATS = [r"<" + t + r">.*?</" + t + r">" for t in ("thinking", "summary", "tool_use", "file_content")]
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
RESTORE_GLOBS = (
    os.path.join(PROJECT_ROOT, "temp", "model_responses", "model_responses_*.txt"),
    os.path.join(PROJECT_ROOT, "temp", "model_responses_*.txt"),
)
RESTORE_BLOCK_RE = re.compile(
    r"^=== (Prompt|Response) ===.*?\n(.*?)(?=^=== (?:Prompt|Response) ===|\Z)",
    re.DOTALL | re.MULTILINE,
)
HISTORY_RE = re.compile(r"<history>\s*(.*?)\s*</history>", re.DOTALL)
SUMMARY_RE = re.compile(r"<summary>\s*(.*?)\s*</summary>", re.DOTALL)


def clean_reply(text):
    for pat in TAG_PATS:
        text = re.sub(pat, "", text or "", flags=re.DOTALL)
    return re.sub(r"\n{3,}", "\n\n", text).strip() or "..."


def extract_files(text):
    return re.findall(r"\[FILE:([^\]]+)\]", text or "")


def strip_files(text):
    return re.sub(r"\[FILE:[^\]]+\]", "", text or "").strip()


def split_text(text, limit):
    text, parts = (text or "").strip() or "...", []
    while len(text) > limit:
        cut = text.rfind("\n", 0, limit)
        if cut < limit * 0.6:
            cut = limit
        parts.append(text[:cut].rstrip())
        text = text[cut:].lstrip()
    return parts + ([text] if text else []) or ["..."]


def _restore_log_files():
    files = []
    for pattern in RESTORE_GLOBS:
        files.extend(glob.glob(pattern))
    return sorted(set(files))


def _restore_text_pairs(content):
    users = re.findall(r"=== USER ===\n(.+?)(?==== |$)", content, re.DOTALL)
    resps = re.findall(r"=== Response ===.*?\n(.+?)(?==== Prompt|$)", content, re.DOTALL)
    restored = []
    for u, r in zip(users, resps):
        u, r = u.strip(), r.strip()[:500]
        if u and r:
            restored.extend([f"[USER]: {u}", f"[Agent] {r}"])
    return restored


def _native_prompt_obj(prompt_body):
    try:
        prompt = json.loads(prompt_body)
    except Exception:
        return None
    if not isinstance(prompt, dict) or prompt.get("role") != "user":
        return None
    if not isinstance(prompt.get("content"), list):
        return None
    return prompt


def _native_prompt_text(prompt):
    texts = []
    for block in prompt.get("content", []):
        if isinstance(block, dict) and block.get("type") == "text":
            text = block.get("text", "")
            if isinstance(text, str) and text.strip():
                texts.append(text)
    return "\n".join(texts).strip()


def _native_history_lines(prompt_text):
    match = HISTORY_RE.search(prompt_text or "")
    if not match:
        return []
    restored = []
    for line in match.group(1).splitlines():
        line = line.strip()
        if line.startswith("[USER]: ") or line.startswith("[Agent] "):
            restored.append(line)
    return restored


def _native_first_user_line(prompt_text):
    text = (prompt_text or "").strip()
    if not text or "<history>" in text or text.startswith("### [WORKING MEMORY]"):
        return ""
    if text.startswith(FILE_HINT):
        text = text[len(FILE_HINT):].lstrip()
    if "### 用户当前消息" in text:
        text = text.split("### 用户当前消息", 1)[-1].strip()
    return text


def _native_response_summary(response_body):
    try:
        blocks = ast.literal_eval((response_body or "").strip())
    except Exception:
        return ""
    if not isinstance(blocks, list):
        return ""
    text_parts = []
    for block in blocks:
        if isinstance(block, dict) and block.get("type") == "text":
            text = block.get("text", "")
            if isinstance(text, str) and text:
                text_parts.append(text)
    match = SUMMARY_RE.search("\n".join(text_parts))
    return (match.group(1).strip() if match else "")[:500]


def _restore_native_history(content):
    blocks = RESTORE_BLOCK_RE.findall(content or "")
    if not blocks:
        return []
    pairs = []
    pending_prompt = None
    for label, body in blocks:
        if label == "Prompt":
            pending_prompt = body
        elif pending_prompt is not None:
            pairs.append((pending_prompt, body))
            pending_prompt = None
    for prompt_body, response_body in reversed(pairs):
        prompt = _native_prompt_obj(prompt_body)
        if prompt is None:
            continue
        prompt_text = _native_prompt_text(prompt)
        restored = list(_native_history_lines(prompt_text))
        if restored:
            summary = _native_response_summary(response_body)
            summary_line = f"[Agent] {summary}" if summary else ""
            if summary_line and (not restored or restored[-1] != summary_line):
                restored.append(summary_line)
            return restored
        user_text = _native_first_user_line(prompt_text)
        summary = _native_response_summary(response_body)
        if user_text and summary:
            return [f"[USER]: {user_text}", f"[Agent] {summary}"]
    return []


def format_restore():
    files = _restore_log_files()
    if not files:
        return None, "❌ 没有找到历史记录"
    latest = max(files, key=os.path.getmtime)
    with open(latest, "r", encoding="utf-8") as f:
        content = f.read()
    restored = _restore_text_pairs(content) or _restore_native_history(content)
    if not restored:
        return None, "❌ 历史记录里没有可恢复内容"
    count = sum(1 for line in restored if line.startswith("[USER]: "))
    return (restored, os.path.basename(latest), count), None


def build_done_text(raw_text):
    files = [p for p in extract_files(raw_text) if os.path.exists(p)]
    body = strip_files(clean_reply(raw_text))
    if files:
        body = (body + "\n\n" if body else "") + "\n".join(f"生成文件: {p}" for p in files)
    return body or "..."


def public_access(allowed):
    return not allowed or "*" in allowed


def to_allowed_set(value):
    if value is None:
        return set()
    if isinstance(value, str):
        value = [value]
    return {str(x).strip() for x in value if str(x).strip()}


def allowed_label(allowed):
    return "public" if public_access(allowed) else sorted(allowed)


def ensure_single_instance(port, label):
    try:
        lock_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        lock_sock.bind(("127.0.0.1", port))
        return lock_sock
    except OSError:
        print(f"[{label}] Another instance is already running, skipping...")
        sys.exit(1)


def require_runtime(agent, label, **required):
    missing = [k for k, v in required.items() if not v]
    if missing:
        print(f"[{label}] ERROR: please set {', '.join(missing)} in mykey.py or mykey.json")
        sys.exit(1)
    if agent.llmclient is None:
        print(f"[{label}] ERROR: no usable LLM backend found in mykey.py or mykey.json")
        sys.exit(1)


def redirect_log(script_file, log_name, label, allowed):
    log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(script_file))), "temp")
    os.makedirs(log_dir, exist_ok=True)
    logf = open(os.path.join(log_dir, log_name), "a", encoding="utf-8", buffering=1)
    sys.stdout = sys.stderr = logf
    print(f"[NEW] {label} process starting, the above are history infos ...")
    print(f"[{label}] allow list: {allowed_label(allowed)}")


class AgentChatMixin:
    label = "Chat"
    source = "chat"
    split_limit = 1500
    ping_interval = 20

    def __init__(self, agent, user_tasks):
        self.agent, self.user_tasks = agent, user_tasks

    async def send_text(self, chat_id, content, **ctx):
        raise NotImplementedError

    async def send_done(self, chat_id, raw_text, **ctx):
        await self.send_text(chat_id, build_done_text(raw_text), **ctx)

    async def handle_command(self, chat_id, cmd, **ctx):
        parts = (cmd or "").split()
        op = (parts[0] if parts else "").lower()
        if op == "/help":
            return await self.send_text(chat_id, HELP_TEXT, **ctx)
        if op == "/stop":
            state = self.user_tasks.get(chat_id)
            if state:
                state["running"] = False
            self.agent.abort()
            return await self.send_text(chat_id, "⏹️ 正在停止...", **ctx)
        if op == "/status":
            llm = self.agent.get_llm_name() if self.agent.llmclient else "未配置"
            return await self.send_text(chat_id, f"状态: {'🔴 运行中' if self.agent.is_running else '🟢 空闲'}\nLLM: [{self.agent.llm_no}] {llm}", **ctx)
        if op == "/llm":
            if not self.agent.llmclient:
                return await self.send_text(chat_id, "❌ 当前没有可用的 LLM 配置", **ctx)
            if len(parts) > 1:
                try:
                    self.agent.next_llm(int(parts[1]))
                    return await self.send_text(chat_id, f"✅ 已切换到 [{self.agent.llm_no}] {self.agent.get_llm_name()}", **ctx)
                except Exception:
                    return await self.send_text(chat_id, f"用法: /llm <0-{len(self.agent.list_llms()) - 1}>", **ctx)
            lines = [f"{'→' if cur else '  '} [{i}] {name}" for i, name, cur in self.agent.list_llms()]
            return await self.send_text(chat_id, "LLMs:\n" + "\n".join(lines), **ctx)
        if op == "/restore":
            try:
                restored_info, err = format_restore()
                if err:
                    return await self.send_text(chat_id, err, **ctx)
                restored, fname, count = restored_info
                self.agent.abort()
                self.agent.history.extend(restored)
                return await self.send_text(chat_id, f"✅ 已恢复 {count} 轮对话\n来源: {fname}\n(仅恢复上下文,请输入新问题继续)", **ctx)
            except Exception as e:
                return await self.send_text(chat_id, f"❌ 恢复失败: {e}", **ctx)
        if op == "/continue":
            return await self.send_text(chat_id, _handle_continue_frontend(self.agent, cmd), **ctx)
        if op == "/new":
            return await self.send_text(chat_id, _reset_conversation(self.agent), **ctx)
        if op == "/btw":
            answer = await asyncio.to_thread(_handle_btw_frontend, self.agent, cmd)
            return await self.send_text(chat_id, answer, **ctx)
        return await self.send_text(chat_id, HELP_TEXT, **ctx)

    async def run_agent(self, chat_id, text, **ctx):
        state = {"running": True}
        self.user_tasks[chat_id] = state
        try:
            await self.send_text(chat_id, "思考中...", **ctx)
            dq = self.agent.put_task(f"{FILE_HINT}\n\n{text}", source=self.source)
            last_ping = time.time()
            while state["running"]:
                try:
                    item = await asyncio.to_thread(dq.get, True, 3)
                except Q.Empty:
                    if self.agent.is_running and time.time() - last_ping > self.ping_interval:
                        await self.send_text(chat_id, "⏳ 还在处理中,请稍等...", **ctx)
                        last_ping = time.time()
                    continue
                if "done" in item:
                    await self.send_done(chat_id, item.get("done", ""), **ctx)
                    break
            if not state["running"]:
                await self.send_text(chat_id, "⏹️ 已停止", **ctx)
        except Exception as e:
            import traceback
            print(f"[{self.label}] run_agent error: {e}")
            traceback.print_exc()
            await self.send_text(chat_id, f"❌ 错误: {e}", **ctx)
        finally:
            self.user_tasks.pop(chat_id, None)


from agentmain import GeneraticAgent as _GA
from continue_cmd import handle_frontend_command as _handle_continue_frontend, install as _install_continue, reset_conversation as _reset_conversation
_install_continue(_GA)
from btw_cmd import handle_frontend_command as _handle_btw_frontend, install as _install_btw; _install_btw(_GA)


================================================
FILE: frontends/continue_cmd.py
================================================
"""`/continue` command: list & restore past model_responses sessions.
Pure functions + one `install(cls)` monkey-patch entry. No side effects at import.
"""
import ast, glob, json, os, re, time
_LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
                        'temp', 'model_responses')
_LOG_GLOB = os.path.join(_LOG_DIR, 'model_responses_*.txt')
_BLOCK_RE = re.compile(r'^=== (Prompt|Response) ===.*?\n(.*?)(?=^=== (?:Prompt|Response) ===|\Z)',
                       re.DOTALL | re.MULTILINE)
_SUMMARY_RE = re.compile(r'<summary>\s*(.*?)\s*</summary>', re.DOTALL)

def _rel_time(mtime):
    d = int(time.time() - mtime)
    if d < 60: return f'{d}秒前'
    if d < 3600: return f'{d // 60}分前'
    if d < 86400: return f'{d // 3600}小时前'
    return f'{d // 86400}天前'

def _pairs(content):
    blocks, pairs, pending = _BLOCK_RE.findall(content or ''), [], None
    for label, body in blocks:
        if label == 'Prompt': pending = body.strip()
        elif pending is not None:
            pairs.append((pending, body.strip())); pending = None
    return pairs

def _first_user(pairs):
    for p, _ in pairs:
        try: msg = json.loads(p)
        except Exception: continue
        if not isinstance(msg, dict): continue
        for blk in msg.get('content', []) or []:
            if isinstance(blk, dict) and blk.get('type') == 'text':
                t = (blk.get('text') or '').strip()
                if t and '<history>' not in t and not t.startswith('### [WORKING MEMORY]'):
                    return t
    for p, _ in pairs[:1]:
        for line in p.splitlines():
            s = line.strip()
            if s and not s.startswith('###'): return s
    return ''


def _last_summary(pairs):
    for _, response_body in reversed(pairs):
        try:
            blocks = ast.literal_eval(response_body)
        except Exception:
            continue
        if not isinstance(blocks, list):
            continue
        text_parts = []
        for block in blocks:
            if isinstance(block, dict) and block.get('type') == 'text':
                text = block.get('text', '')
                if isinstance(text, str) and text:
                    text_parts.append(text)
        match = _SUMMARY_RE.search('\n'.join(text_parts))
        if match:
            summary = match.group(1).strip()
            if summary:
                return summary
    return ''


def _preview_text(pairs):
    return _last_summary(pairs) or _first_user(pairs)

def _recent_context(my_pid, n=5):
    """扫描最近 n 个 model_response 文件(排除自身),提取 lastQ / lastA。"""
    out = []
    for f in sorted(glob.glob(_LOG_GLOB), key=os.path.getmtime, reverse=True):
        m = re.search(r'model_responses_(\d+)', os.path.basename(f))
        if not m or m.group(1) == str(my_pid): continue
        try: c = open(f, encoding='utf-8', errors='ignore').read()
        except Exception: continue
        q = s = ""
        for hm in re.finditer(r'<history>(.*?)</history>', c, re.DOTALL):
            u = re.search(r'\[USER\]:\s*(.+?)(?:\\n|<)', hm.group(1))
            if u: q = u.group(1)
        sm = _SUMMARY_RE.search(c)
        if sm: s = sm.group(1).strip()
        q, s = q[:60].strip(), s[:60].replace('\n', ' ').strip()
        out.append(f'· {m.group(1)} | lastQ: {q or "-"} | lastA: {s or "-"}')
        if len(out) >= n: break
    return ('[RecentContext] 近期并行会话(非当前):\n' + '\n'.join(out) + '\n[/RecentContext]') if out else ""

def _parse_native_history(pairs):
    history = []
    for p, r in pairs:
        try: user_msg = json.loads(p)
        except Exception: return None
        try: blocks = ast.literal_eval(r)
        except Exception: return None
        if not (isinstance(user_msg, dict) and user_msg.get('role') == 'user'): return None
        if not isinstance(blocks, list): return None
        history.append(user_msg)
        history.append({'role': 'assistant', 'content': blocks})
    return history

def list_sessions(exclude_pid=None):
    """Newest-first list of (path, mtime, first_user_text, n_rounds)."""
    files = glob.glob(_LOG_GLOB)
    if exclude_pid is not None:
        tag = f'model_responses_{exclude_pid}.txt'
        files = [f for f in files if not f.endswith(tag)]
    out = []
    for f in files:
        try:
            with open(f, encoding='utf-8', errors='replace') as fh:
                content = fh.read()
        except Exception: continue
        pairs = _pairs(content)
        if not pairs: continue
        out.append((f, os.path.getmtime(f), _preview_text(pairs), len(pairs)))
    out.sort(key=lambda x: x[1], reverse=True)
    return out
_MD_ESCAPE_RE = re.compile(r'([\\`*_\[\]])')
def _escape_md(s): return _MD_ESCAPE_RE.sub(r'\\\1', s)


def _agent_clients(agent):
    clients = []
    for client in getattr(agent, 'llmclients', []) or []:
        if client not in clients:
            clients.append(client)
    current = getattr(agent, 'llmclient', None)
    if current is not None and current not in clients:
        clients.insert(0, current)
    return clients


def _replace_backend_history(agent, history):
    backend = getattr(getattr(agent, 'llmclient', None), 'backend', None)
    if backend is not None and hasattr(backend, 'history'):
        backend.history = list(history or [])


def _current_log_path(pid=None):
    pid = os.getpid() if pid is None else pid
    return os.path.join(_LOG_DIR, f'model_responses_{pid}.txt')


def _snapshot_current_log(pid=None):
    """Persist current PID log as a standalone recoverable snapshot, then clear it."""
    path = _current_log_path(pid)
    if not os.path.isfile(path):
        return None
    try:
        with open(path, encoding='utf-8', errors='replace') as fh:
            content = fh.read()
    except Exception:
        return None
    if not _pairs(content):
        return None
    os.makedirs(_LOG_DIR, exist_ok=True)
    pid = os.getpid() if pid is None else pid
    stamp = time.strftime('%Y%m%d_%H%M%S')
    snapshot = os.path.join(_LOG_DIR, f'model_responses_snapshot_{pid}_{stamp}_{time.time_ns() % 1_000_000_000:09d}.txt')
    with open(snapshot, 'w', encoding='utf-8', errors='replace') as fh:
        fh.write(content)
    with open(path, 'w', encoding='utf-8', errors='replace'):
        pass
    return snapshot


def reset_conversation(agent, message='🆕 已开启新对话,当前上下文已清空'):
    """Abort current work and clear all known frontend-visible conversation state."""
    try:
        agent.abort()
    except Exception:
        pass
    _snapshot_current_log()
    if hasattr(agent, 'history'):
        agent.history = []
    for client in _agent_clients(agent):
        backend = getattr(client, 'backend', None)
        if backend is not None and hasattr(backend, 'history'):
            backend.history = []
        if hasattr(client, 'last_tools'):
            client.last_tools = ''
    if hasattr(agent, 'handler'):
        agent.handler = None
    return message

def format_list(sessions, limit=20):
    if not sessions: return '❌ 没有可恢复的历史会话'
    lines = ['**可恢复会话**(输入 `/continue N
Download .txt
gitextract_dpkv5wrs/

├── .gitignore
├── CONTRIBUTING.md
├── GETTING_STARTED.md
├── LICENSE
├── README.md
├── TMWebDriver.py
├── agent_loop.py
├── agentmain.py
├── assets/
│   ├── SETUP_FEISHU.md
│   ├── agent_bbs.py
│   ├── code_run_header.py
│   ├── configure_mykey.py
│   ├── global_mem_insight_template.txt
│   ├── global_mem_insight_template_en.txt
│   ├── insight_fixed_structure.txt
│   ├── insight_fixed_structure_en.txt
│   ├── install-macos-app.sh
│   ├── install_python_windows.bat
│   ├── sys_prompt.txt
│   ├── sys_prompt_en.txt
│   ├── tmwd_cdp_bridge/
│   │   ├── background.js
│   │   ├── content.js
│   │   ├── disable_dialogs.js
│   │   ├── manifest.json
│   │   ├── popup.html
│   │   └── popup.js
│   ├── tool_usable_history.json
│   ├── tools_schema.json
│   └── tools_schema_cn.json
├── frontends/
│   ├── DESKTOP_PET_README.md
│   ├── btw_cmd.py
│   ├── chatapp_common.py
│   ├── continue_cmd.py
│   ├── dcapp.py
│   ├── desktop_pet.pyw
│   ├── desktop_pet_v2.pyw
│   ├── dingtalkapp.py
│   ├── fsapp.py
│   ├── genericagent_acp_bridge.py
│   ├── qqapp.py
│   ├── qtapp.py
│   ├── skins/
│   │   ├── boy/
│   │   │   └── skin.json
│   │   ├── dinosaur/
│   │   │   └── skin.json
│   │   ├── doux/
│   │   │   └── skin.json
│   │   ├── glube/
│   │   │   └── skin.json
│   │   ├── line/
│   │   │   ├── License.txt
│   │   │   └── skin.json
│   │   ├── mort/
│   │   │   └── skin.json
│   │   ├── tard/
│   │   │   └── skin.json
│   │   └── vita/
│   │       └── skin.json
│   ├── stapp.py
│   ├── stapp2.py
│   ├── tgapp.py
│   ├── tuiapp.py
│   ├── wechatapp.py
│   └── wecomapp.py
├── ga.py
├── hub.pyw
├── launch.pyw
├── llmcore.py
├── memory/
│   ├── adb_ui.py
│   ├── autonomous_operation_sop.md
│   ├── goal_mode_sop.md
│   ├── keychain.py
│   ├── ljqCtrl.py
│   ├── ljqCtrl_sop.md
│   ├── memory_cleanup_sop.md
│   ├── memory_management_sop.md
│   ├── ocr_utils.py
│   ├── plan_sop.md
│   ├── procmem_scanner.py
│   ├── procmem_scanner_sop.md
│   ├── scheduled_task_sop.md
│   ├── supervisor_sop.md
│   ├── tmwebdriver_sop.md
│   ├── ui_detect.py
│   ├── vision_api.template.py
│   ├── vision_sop.md
│   ├── vue3_component_sop.md
│   └── web_setup_sop.md
├── mykey_template.py
├── mykey_template_en.py
├── plugins/
│   └── langfuse_tracing.py
├── pyproject.toml
├── reflect/
│   ├── agent_team_worker.py
│   ├── autonomous.py
│   ├── goal_mode.py
│   └── scheduler.py
└── simphtml.py
Download .txt
SYMBOL INDEX (774 symbols across 40 files)

FILE: TMWebDriver.py
  class Session (line 7) | class Session:
    method __init__ (line 8) | def __init__(self, session_id, info, client=None):
    method url (line 17) | def url(self): return self.info.get('url', '')
    method is_active (line 18) | def is_active(self):
    method reconnect (line 21) | def reconnect(self, client, info):
    method mark_disconnected (line 31) | def mark_disconnected(self):
  class TMWebDriver (line 36) | class TMWebDriver:
    method __init__ (line 37) | def __init__(self, host: str = '127.0.0.1', port: int = 18765):
    method start_http_server (line 49) | def start_http_server(self):
    method clean_sessions (line 113) | def clean_sessions(self):
    method start_ws_server (line 120) | def start_ws_server(self) -> None:
    method _register_client (line 164) | def _register_client(self, session_id: str, client: WebSocket, session...
    method _unregister_client (line 179) | def _unregister_client(self, client: WebSocket) -> None:
    method execute_js (line 183) | def execute_js(self, code, timeout=15, session_id=None) -> Any:
    method _remote_cmd (line 245) | def _remote_cmd(self, cmd):
    method get_all_sessions (line 250) | def get_all_sessions(self):
    method get_session_dict (line 256) | def get_session_dict(self):
    method find_session (line 259) | def find_session(self, url_pattern: str):
    method set_session (line 270) | def set_session(self, url_pattern: str) -> bool:
    method jump (line 281) | def jump(self, url, timeout=10): self.execute_js(f"window.location.hre...

FILE: agent_loop.py
  class StepOutcome (line 5) | class StepOutcome:
  function try_call_generator (line 9) | def try_call_generator(func, *args, **kwargs):
  class BaseHandler (line 14) | class BaseHandler:
    method tool_before_callback (line 15) | def tool_before_callback(self, tool_name, args, response): pass
    method tool_after_callback (line 16) | def tool_after_callback(self, tool_name, args, response, ret): pass
    method turn_end_callback (line 17) | def turn_end_callback(self, response, tool_calls, tool_results, turn, ...
    method dispatch (line 18) | def dispatch(self, tool_name, args, response, index=0):
  function json_default (line 31) | def json_default(o): return list(o) if isinstance(o, set) else str(o)
  function exhaust (line 32) | def exhaust(g):
  function get_pretty_json (line 37) | def get_pretty_json(data):
  function agent_runner_loop (line 42) | def agent_runner_loop(client, system_prompt, user_input, handler, tools_...
  function _clean_content (line 101) | def _clean_content(text):
  function _compact_tool_args (line 115) | def _compact_tool_args(name, args):

FILE: agentmain.py
  function load_tool_schema (line 14) | def load_tool_schema(suffix=''):
  function get_system_prompt (line 36) | def get_system_prompt():
  class GenericAgent (line 42) | class GenericAgent:
    method __init__ (line 43) | def __init__(self):
    method load_llm_sessions (line 55) | def load_llm_sessions(self):
    method next_llm (line 78) | def next_llm(self, n=-1):
    method list_llms (line 89) | def list_llms(self):
    method get_llm_name (line 92) | def get_llm_name(self, b=None, model=False):
    method abort (line 98) | def abort(self):
    method put_task (line 104) | def put_task(self, query, source="user", images=None):
    method _handle_slash_cmd (line 110) | def _handle_slash_cmd(self, raw_query, display_queue):
    method run (line 125) | def run(self):

FILE: assets/agent_bbs.py
  function load_boards_if_changed (line 19) | def load_boards_if_changed():
  class ApiKeyMiddleware (line 39) | class ApiKeyMiddleware(BaseHTTPMiddleware):
    method dispatch (line 40) | async def dispatch(self, request: Request, call_next):
  function readme (line 109) | def readme(): return PlainTextResponse(README_TEXT)
  function index (line 112) | def index(): return HTML_PAGE
  function get_db (line 115) | def get_db(db_path):
  function _db (line 123) | def _db(request): return request.state.board["db"]
  function init_db (line 125) | def init_db():
  function verify_token (line 136) | def verify_token(token, db_path):
  function startup (line 143) | def startup(): load_boards_if_changed()
  function register (line 146) | def register(request: Request, name=Body(..., embed=True)):
  function create_post (line 158) | def create_post(request: Request, token=Body(...), content=Body(...)):
  function poll (line 167) | def poll(request: Request, since_id=Query(0), limit=Query(50)):
  function count_posts (line 174) | def count_posts(request: Request, author=Query(None)):
  function get_authors (line 180) | def get_authors(request: Request):
  function get_posts (line 185) | def get_posts(request: Request, author=Query(None), limit=Query(50), off...
  function upload_file (line 196) | def upload_file(request: Request, token=Body(...), file: UploadFile = Fi...
  function download_file (line 207) | def download_file(rand_id: str, filename: str):

FILE: assets/code_run_header.py
  function _d (line 4) | def _d(b):
  function _run (line 9) | def _run(*a, **k):
  function _pinit (line 23) | def _pinit(self, *a, **k):

FILE: assets/configure_mykey.py
  function _read_char (line 288) | def _read_char():
  function _masked (line 304) | def _masked(v, reveal, tail):
  function masked_input (line 312) | def masked_input(prompt, reveal=6, tail=4):
  function cprint (line 352) | def cprint(text, color=None, bold=False, end='\n'):
  function banner (line 360) | def banner():
  function _check_python (line 371) | def _check_python():
  function ask_choice (line 380) | def ask_choice(prompt, choices, allow_multi=False, default=None):
  function ask_input (line 417) | def ask_input(prompt, default=None, secret=False, hint=None):
  function ask_yesno (line 437) | def ask_yesno(prompt, default=True):
  function _get_proxy_handler (line 449) | def _get_proxy_handler():
  function probe_models (line 457) | def probe_models(provider, apikey, apibase=None):
  function _normalize_model_choices (line 495) | def _normalize_model_choices(choices):
  function _configure_advanced (line 509) | def _configure_advanced(provider, cfg):
  function configure_llm (line 532) | def configure_llm(provider):
  function _fallback_model (line 594) | def _fallback_model(provider):
  function configure_llms (line 601) | def configure_llms():
  function configure_platforms (line 632) | def configure_platforms():
  function _manual_platform_var (line 680) | def _manual_platform_var(var):
  function _feishu_scan (line 689) | def _feishu_scan(platform):
  function _var_type_info (line 762) | def _var_type_info(cfg):
  function generate_mykey (line 775) | def generate_mykey(llm_cfgs, platform_configs):
  function _write_config_fields (line 844) | def _write_config_fields(lines, cfg):
  function _write_platform_value (line 863) | def _write_platform_value(lines, key, val):
  function main (line 880) | def main():
  function _do_llm (line 985) | def _do_llm():

FILE: assets/tmwd_cdp_bridge/background.js
  function handleExtMessage (line 18) | async function handleExtMessage(msg, sender) {
  function handleCookies (line 76) | async function handleCookies(msg, sender) {
  function handleBatch (line 96) | async function handleBatch(msg, sender) {
  function handleCDP (line 129) | async function handleCDP(msg, sender) {
  function buildExecScript (line 146) | function buildExecScript(code, errorHandler) {
  function buildPageScript (line 188) | function buildPageScript(code) {
  function buildCdpScript (line 196) | function buildCdpScript(code) {
  constant WS_URL (line 204) | const WS_URL = 'ws://127.0.0.1:18765';
  function scheduleProbe (line 206) | function scheduleProbe() {
  function scheduleKeepalive (line 211) | function scheduleKeepalive() {
  function isServerAlive (line 216) | async function isServerAlive() {
  function handleWsExec (line 254) | async function handleWsExec(data) {
  function connectWS (line 326) | function connectWS() {
  function sendTabsUpdate (line 393) | async function sendTabsUpdate() {

FILE: assets/tmwd_cdp_bridge/content.js
  function handle (line 26) | async function handle(el) {

FILE: assets/tmwd_cdp_bridge/disable_dialogs.js
  function toast (line 4) | function toast(type, msg) {

FILE: assets/tmwd_cdp_bridge/popup.js
  function fetchCookies (line 8) | async function fetchCookies() {

FILE: frontends/btw_cmd.py
  function _wrapper (line 47) | def _wrapper(): return _WRAPPER_EN if os.environ.get('GA_LANG') == 'en' ...
  function _strip_cmd (line 50) | def _strip_cmd(query):
  function _help_text (line 55) | def _help_text():
  function _snapshot_history (line 61) | def _snapshot_history(backend):
  function _build_wire (line 67) | def _build_wire(backend, history, sidequest_msg):
  function _ask (line 76) | def _ask(agent, question, deadline):
  function _format (line 90) | def _format(question, body, took):
  function _run (line 95) | def _run(agent, question, deadline):
  function handle (line 101) | def handle(agent, query, display_queue) -> Optional[str]:
  function handle_frontend_command (line 118) | def handle_frontend_command(agent, query) -> str:
  function install (line 128) | def install(cls):

FILE: frontends/chatapp_common.py
  function build_help_text (line 26) | def build_help_text(commands=HELP_COMMANDS):
  function clean_reply (line 46) | def clean_reply(text):
  function extract_files (line 52) | def extract_files(text):
  function strip_files (line 56) | def strip_files(text):
  function split_text (line 60) | def split_text(text, limit):
  function _restore_log_files (line 71) | def _restore_log_files():
  function _restore_text_pairs (line 78) | def _restore_text_pairs(content):
  function _native_prompt_obj (line 89) | def _native_prompt_obj(prompt_body):
  function _native_prompt_text (line 101) | def _native_prompt_text(prompt):
  function _native_history_lines (line 111) | def _native_history_lines(prompt_text):
  function _native_first_user_line (line 123) | def _native_first_user_line(prompt_text):
  function _native_response_summary (line 134) | def _native_response_summary(response_body):
  function _restore_native_history (line 151) | def _restore_native_history(content):
  function format_restore (line 182) | def format_restore():
  function build_done_text (line 196) | def build_done_text(raw_text):
  function public_access (line 204) | def public_access(allowed):
  function to_allowed_set (line 208) | def to_allowed_set(value):
  function allowed_label (line 216) | def allowed_label(allowed):
  function ensure_single_instance (line 220) | def ensure_single_instance(port, label):
  function require_runtime (line 230) | def require_runtime(agent, label, **required):
  function redirect_log (line 240) | def redirect_log(script_file, log_name, label, allowed):
  class AgentChatMixin (line 249) | class AgentChatMixin:
    method __init__ (line 255) | def __init__(self, agent, user_tasks):
    method send_text (line 258) | async def send_text(self, chat_id, content, **ctx):
    method send_done (line 261) | async def send_done(self, chat_id, raw_text, **ctx):
    method handle_command (line 264) | async def handle_command(self, chat_id, cmd, **ctx):
    method run_agent (line 309) | async def run_agent(self, chat_id, text, **ctx):

FILE: frontends/continue_cmd.py
  function _rel_time (line 12) | def _rel_time(mtime):
  function _pairs (line 19) | def _pairs(content):
  function _first_user (line 27) | def _first_user(pairs):
  function _last_summary (line 44) | def _last_summary(pairs):
  function _preview_text (line 66) | def _preview_text(pairs):
  function _recent_context (line 69) | def _recent_context(my_pid, n=5):
  function _parse_native_history (line 88) | def _parse_native_history(pairs):
  function list_sessions (line 101) | def list_sessions(exclude_pid=None):
  function _escape_md (line 119) | def _escape_md(s): return _MD_ESCAPE_RE.sub(r'\\\1', s)
  function _agent_clients (line 122) | def _agent_clients(agent):
  function _replace_backend_history (line 133) | def _replace_backend_history(agent, history):
  function _current_log_path (line 139) | def _current_log_path(pid=None):
  function _snapshot_current_log (line 144) | def _snapshot_current_log(pid=None):
  function reset_conversation (line 167) | def reset_conversation(agent, message='🆕 已开启新对话,当前上下文已清空'):
  function format_list (line 186) | def format_list(sessions, limit=20):
  function restore (line 194) | def restore(agent, path):
  function handle (line 216) | def handle(agent, query, display_queue):
  function _user_text (line 236) | def _user_text(prompt_body):
  function _assistant_text (line 248) | def _assistant_text(response_body):
  function extract_ui_messages (line 261) | def extract_ui_messages(path):
  function handle_frontend_command (line 287) | def handle_frontend_command(agent, query, exclude_pid=None):
  function install (line 305) | def install(cls):

FILE: frontends/dcapp.py
  function _extract_discord_progress (line 39) | def _extract_discord_progress(text):
  function _strip_discord_transcript (line 48) | def _strip_discord_transcript(text):
  function _display_done_text (line 59) | def _display_done_text(text):
  class DiscordApp (line 69) | class DiscordApp(AgentChatMixin):
    method __init__ (line 72) | def __init__(self):
    method _chat_id (line 95) | def _chat_id(self, message):
    method _load_active_channels (line 101) | def _load_active_channels(self):
    method _save_active_channels (line 122) | def _save_active_channels(self):
    method _is_active_channel (line 132) | def _is_active_channel(self, chat_id, now=None):
    method _touch_active_channel (line 145) | def _touch_active_channel(self, chat_id, now=None):
    method _deactivate_channel (line 152) | def _deactivate_channel(self, chat_id):
    method _get_agent (line 165) | def _get_agent(self, chat_id):
    method _download_attachments (line 180) | async def _download_attachments(self, message):
    method send_text (line 194) | async def send_text(self, chat_id, content, **ctx):
    method send_done (line 216) | async def send_done(self, chat_id, raw_text, **ctx):
    method handle_command (line 239) | async def handle_command(self, chat_id, cmd, **ctx):
    method run_agent (line 283) | async def run_agent(self, chat_id, text, **ctx):
    method _handle_message (line 323) | async def _handle_message(self, message):
    method start (line 387) | async def start(self):

FILE: frontends/dingtalkapp.py
  class DingTalkApp (line 23) | class DingTalkApp(AgentChatMixin):
    method __init__ (line 26) | def __init__(self):
    method _get_access_token (line 30) | async def _get_access_token(self):
    method _send_batch_message (line 53) | async def _send_batch_message(self, chat_id, msg_key, msg_param):
    method send_text (line 82) | async def send_text(self, chat_id, content):
    method on_message (line 86) | async def on_message(self, content, sender_id, sender_name, conversati...
    method start (line 106) | async def start(self):
  class _DingTalkHandler (line 125) | class _DingTalkHandler(CallbackHandler):
    method __init__ (line 126) | def __init__(self, app):
    method process (line 130) | async def process(self, message):

FILE: frontends/fsapp.py
  function _clean (line 40) | def _clean(text):
  function _extract_files (line 46) | def _extract_files(text):
  function _strip_files (line 50) | def _strip_files(text):
  function _display_text (line 54) | def _display_text(text):
  function _to_allowed_set (line 62) | def _to_allowed_set(value):
  function _parse_json (line 70) | def _parse_json(raw):
  function _extract_share_card_content (line 79) | def _extract_share_card_content(content_json, msg_type):
  function _extract_interactive_content (line 96) | def _extract_interactive_content(content):
  function _extract_element_content (line 133) | def _extract_element_content(element):
  function _extract_post_content (line 187) | def _extract_post_content(content_json):
  function create_client (line 243) | def create_client():
  function _card_raw (line 247) | def _card_raw(elements):
  function _card (line 255) | def _card(text):
  function _send_raw (line 259) | def _send_raw(receive_id, payload, msg_type, rtype):
  function _patch_card (line 273) | def _patch_card(message_id, card_json):
  function _patch_card_result (line 277) | def _patch_card_result(message_id, card_json):
  function send_message (line 292) | def send_message(receive_id, content, msg_type="text", use_card=False, r...
  function update_message (line 300) | def update_message(message_id, content):
  function _upload_image_sync (line 304) | def _upload_image_sync(file_path):
  function _upload_file_sync (line 319) | def _upload_file_sync(file_path):
  function _download_image_sync (line 337) | def _download_image_sync(message_id, image_key):
  function _download_file_sync (line 350) | def _download_file_sync(message_id, file_key, resource_type="file"):
  function _download_and_save_media (line 365) | def _download_and_save_media(msg_type, content_json, message_id):
  function _describe_media (line 389) | def _describe_media(msg_type, file_path, filename):
  function _send_local_file (line 399) | def _send_local_file(receive_id, file_path, receive_id_type="open_id"):
  function _send_generated_files (line 419) | def _send_generated_files(receive_id, raw_text, receive_id_type="open_id"):
  function _build_user_message (line 424) | def _build_user_message(message):
  function _fmt_tool_call (line 459) | def _fmt_tool_call(tc):
  function _build_step_detail (line 465) | def _build_step_detail(resp, tool_calls):
  class _TaskCard (line 479) | class _TaskCard:
    method __init__ (line 484) | def __init__(self, receive_id, rid_type):
    method _step_panel (line 495) | def _step_panel(self, idx, summary, detail):
    method _build (line 505) | def _build(self):
    method _push (line 518) | def _push(self):
    method _rollover (line 526) | def _rollover(self):
    method start (line 534) | def start(self):
    method step (line 537) | def step(self, summary, detail=""):
    method done (line 550) | def done(self, text):
    method fail (line 561) | def fail(self, msg):
  function _make_task_hook (line 566) | def _make_task_hook(card, done_event, on_final):
  function handle_message (line 584) | def handle_message(data):
  function handle_command (line 635) | def handle_command(open_id, cmd, chat_id=None):
  function main (line 683) | def main():

FILE: frontends/genericagent_acp_bridge.py
  class _StdoutToStderrRouter (line 30) | class _StdoutToStderrRouter(io.TextIOBase):
    method writable (line 32) | def writable(self): return True
    method write (line 33) | def write(self, s):
    method flush (line 38) | def flush(self): sys.stderr.flush()
  function eprint (line 57) | def eprint(*args: Any) -> None:
  function make_text_block (line 61) | def make_text_block(text: str) -> Dict[str, Any]:
  function make_session_update (line 65) | def make_session_update(session_id: str, update: Dict[str, Any]) -> Dict...
  function compact_json (line 73) | def compact_json(obj: Dict[str, Any]) -> str:
  function parse_jsonrpc_line (line 77) | def parse_jsonrpc_line(line: str) -> Optional[Dict[str, Any]]:
  function content_blocks_to_text (line 88) | def content_blocks_to_text(blocks: List[Dict[str, Any]]) -> str:
  function jsonrpc_error (line 118) | def jsonrpc_error(code: int, message: str, req_id: Any = None, data: Any...
  function jsonrpc_result (line 125) | def jsonrpc_result(req_id: Any, result: Any) -> Dict[str, Any]:
  class SessionState (line 130) | class SessionState:
  class GenericAgentAcpBridge (line 138) | class GenericAgentAcpBridge:
    method __init__ (line 139) | def __init__(self, llm_no: int = 0):
    method write_message (line 146) | def write_message(self, msg: Dict[str, Any]) -> None:
    method new_agent (line 158) | def new_agent(self) -> GeneraticAgent:
    method handle_initialize (line 166) | def handle_initialize(self, req_id: Any, params: Dict[str, Any]) -> None:
    method handle_session_new (line 190) | def handle_session_new(self, req_id: Any, params: Dict[str, Any]) -> N...
    method handle_session_prompt (line 212) | def handle_session_prompt(self, req_id: Any, params: Dict[str, Any]) -...
    method _drain_agent_queue (line 267) | def _drain_agent_queue(self, session: SessionState, dq: "queue.Queue[D...
    method handle_session_cancel (line 311) | def handle_session_cancel(self, params: Dict[str, Any]) -> None:
    method handle_message (line 319) | def handle_message(self, msg: Dict[str, Any]) -> None:
    method serve (line 352) | def serve(self) -> None:
  function main (line 365) | def main() -> int:

FILE: frontends/qqapp.py
  function _next_msg_seq (line 24) | def _next_msg_seq():
  function _build_intents (line 31) | def _build_intents():
  function _make_bot_class (line 45) | def _make_bot_class(app):
  class QQApp (line 65) | class QQApp(AgentChatMixin):
    method __init__ (line 68) | def __init__(self):
    method send_text (line 72) | async def send_text(self, chat_id, content, *, msg_id=None, is_group=F...
    method on_message (line 80) | async def on_message(self, data, is_group=False):
    method start (line 104) | async def start(self):

FILE: frontends/qtapp.py
  class FloatingButton (line 39) | class FloatingButton(QWidget):
    method __init__ (line 44) | def __init__(self, chat_panel: QWidget):
    method _tick (line 79) | def _tick(self):
    method paintEvent (line 99) | def paintEvent(self, _event):
    method enterEvent (line 211) | def enterEvent(self, event):
    method leaveEvent (line 216) | def leaveEvent(self, event):
    method mousePressEvent (line 222) | def mousePressEvent(self, event):
    method mouseMoveEvent (line 228) | def mouseMoveEvent(self, event):
    method mouseDoubleClickEvent (line 240) | def mouseDoubleClickEvent(self, event):
    method mouseReleaseEvent (line 247) | def mouseReleaseEvent(self, event):
    method _toggle (line 255) | def _toggle(self):
    method _position_panel (line 269) | def _position_panel(self):
  function _md_to_html (line 369) | def _md_to_html(text: str) -> str:
  function _svg_icon (line 420) | def _svg_icon(key: str, svg_template: str, color: str = "#a1a1aa",
  function _make_session_id (line 440) | def _make_session_id() -> str:
  function _load_history (line 444) | def _load_history() -> list:
  function _save_history (line 454) | def _save_history(history: list):
  function _build_prompt_with_uploads (line 460) | def _build_prompt_with_uploads(prompt: str, files: list) -> tuple:
  class _Separator (line 521) | class _Separator(QFrame):
    method __init__ (line 522) | def __init__(self, parent=None):
  class _Badge (line 528) | class _Badge(QLabel):
    method __init__ (line 529) | def __init__(self, text: str, parent=None):
  class _StreamingBadge (line 538) | class _StreamingBadge(QLabel):
    method __init__ (line 539) | def __init__(self, parent=None):
  class _FoldableTextBrowser (line 549) | class _FoldableTextBrowser(QTextBrowser):
    method __init__ (line 551) | def __init__(self, parent=None):
    method eventFilter (line 555) | def eventFilter(self, obj, event):
  class _MsgRow (line 571) | class _MsgRow(QWidget):
    method __init__ (line 580) | def __init__(self, text: str, role: str, parent=None, on_resend=None, ...
    method _copy_text (line 809) | def _copy_text(self):
    method _do_resend (line 812) | def _do_resend(self):
    method _do_delete (line 816) | def _do_delete(self):
    method _do_rewrite (line 820) | def _do_rewrite(self):
    method _export_as_md (line 824) | def _export_as_md(self):
    method enterEvent (line 840) | def enterEvent(self, event):
    method leaveEvent (line 845) | def leaveEvent(self, event):
    method resizeEvent (line 850) | def resizeEvent(self, event):
    method set_finished (line 855) | def set_finished(self, done: bool):
    method _adjust_browser_height (line 860) | def _adjust_browser_height(self):
    method set_text (line 868) | def set_text(self, text: str):
    method highlight (line 878) | def highlight(self, keyword: str):
    method clear_highlight (line 912) | def clear_highlight(self):
    method _parse_foldable_blocks (line 921) | def _parse_foldable_blocks(self, text: str):
    method _auto_fold_new_blocks (line 956) | def _auto_fold_new_blocks(self, text: str):
    method _render_with_folds (line 962) | def _render_with_folds(self, text: str) -> str:
    method _toggle_fold (line 986) | def _toggle_fold(self, title):
  class _TabButton (line 996) | class _TabButton(QPushButton):
    method __init__ (line 1011) | def __init__(self, text: str, parent=None):
  function _action_btn (line 1018) | def _action_btn(label: str, color: str, icon: QIcon | None = None) -> QP...
  class ChatPanel (line 1039) | class ChatPanel(QWidget):
    method __init__ (line 1042) | def __init__(self, agent):
    method paintEvent (line 1076) | def paintEvent(self, _event):
    method resizeEvent (line 1086) | def resizeEvent(self, event):
    method _build_ui (line 1093) | def _build_ui(self):
    method _build_titlebar (line 1116) | def _build_titlebar(self) -> QWidget:
    method _toggle_search (line 1222) | def _toggle_search(self):
    method _show_search (line 1228) | def _show_search(self):
    method _hide_search (line 1235) | def _hide_search(self):
    method _hide_search_if_no_focus (line 1244) | def _hide_search_if_no_focus(self):
    method _on_search_changed (line 1248) | def _on_search_changed(self, text):
    method _clear_all_highlights (line 1260) | def _clear_all_highlights(self):
    method _search_current_chat (line 1266) | def _search_current_chat(self, keyword: str):
    method _scroll_to_widget (line 1283) | def _scroll_to_widget(self, w, keyword_y=0):
    method _search_history (line 1297) | def _search_history(self, keyword: str):
    method _reset_history_items_style (line 1313) | def _reset_history_items_style(self):
    method _tb_press (line 1327) | def _tb_press(self, e):
    method _tb_move (line 1331) | def _tb_move(self, e):
    method _tb_release (line 1335) | def _tb_release(self, _e):
    method _toggle_maximize (line 1338) | def _toggle_maximize(self):
    method _build_statusbar (line 1347) | def _build_statusbar(self) -> QWidget:
    method _show_model_menu (line 1374) | def _show_model_menu(self, _e):
    method _build_tabbar (line 1398) | def _build_tabbar(self) -> QWidget:
    method _switch_tab (line 1440) | def _switch_tab(self, idx: int):
    method _build_chat_page (line 1458) | def _build_chat_page(self) -> QWidget:
    method _build_input_area (line 1534) | def _build_input_area(self) -> QWidget:
    method _build_history_page (line 1625) | def _build_history_page(self) -> QWidget:
    method _build_sop_page (line 1668) | def _build_sop_page(self) -> QWidget:
    method _build_settings_page (line 1706) | def _build_settings_page(self) -> QWidget:
    method _build_model_rows (line 1781) | def _build_model_rows(self):
    method _refresh_model_rows_style (line 1814) | def _refresh_model_rows_style(self):
    method _do_switch_to (line 1828) | def _do_switch_to(self, idx: int):
    method _start_health_checks (line 1838) | def _start_health_checks(self):
    method _poll_health_results (line 1854) | def _poll_health_results(self):
    method _check_backend (line 1865) | def _check_backend(self, idx: int, backend):
    method eventFilter (line 1883) | def eventFilter(self, obj, event):
    method _on_text_changed (line 1898) | def _on_text_changed(self):
    method _attach_files (line 1903) | def _attach_files(self):
    method _refresh_chips (line 1930) | def _refresh_chips(self):
    method _set_send_mode (line 1961) | def _set_send_mode(self):
    method _set_stop_mode (line 1968) | def _set_stop_mode(self):
    method _on_send_btn_click (line 1975) | def _on_send_btn_click(self):
    method _handle_send (line 1981) | def _handle_send(self):
    method _handle_command (line 2031) | def _handle_command(self, cmd: str):
    method _poll_queue (line 2071) | def _poll_queue(self):
    method _add_msg_row (line 2128) | def _add_msg_row(self, role: str, text: str, created_at: str = None, o...
    method _regenerate_response (line 2141) | def _regenerate_response(self):
    method _delete_message (line 2151) | def _delete_message(self, index: int):
    method _rewrite_message (line 2163) | def _rewrite_message(self, index: int):
    method _on_scroll (line 2179) | def _on_scroll(self, value):
    method _update_nav_visibility (line 2184) | def _update_nav_visibility(self):
    method _scroll_to_top (line 2198) | def _scroll_to_top(self):
    method _scroll_to_bottom (line 2202) | def _scroll_to_bottom(self):
    method _scroll_bottom (line 2208) | def _scroll_bottom(self):
    method inject_message (line 2216) | def inject_message(self, text: str):
    method _refresh_history (line 2222) | def _refresh_history(self):
    method _restore_selected (line 2231) | def _restore_selected(self, item=None):
    method _delete_selected (line 2246) | def _delete_selected(self):
    method _rebuild_messages (line 2256) | def _rebuild_messages(self):
    method _update_token_usage (line 2272) | def _update_token_usage(self):
    method _refresh_sop (line 2287) | def _refresh_sop(self):
    method _load_sop (line 2299) | def _load_sop(self, item):
    method _model_name (line 2310) | def _model_name(self) -> str:
    method _add_system_notice (line 2318) | def _add_system_notice(self, text: str):
    method _do_stop (line 2330) | def _do_stop(self):
    method _do_reset_prompt (line 2341) | def _do_reset_prompt(self):
    method _auto_save (line 2345) | def _auto_save(self):
    method _do_save (line 2356) | def _do_save(self):
    method _do_clear (line 2370) | def _do_clear(self):
    method _new_session (line 2377) | def _new_session(self):
    method _do_toggle_auto (line 2382) | def _do_toggle_auto(self):
    method _do_trigger_auto (line 2388) | def _do_trigger_auto(self):
    method _small_btn_style (line 2395) | def _small_btn_style(color: str) -> str:
  function main (line 2407) | def main():

FILE: frontends/stapp.py
  function T (line 37) | def T(key): return I18N.get(LANG, I18N['zh']).get(key, key)
  function init (line 40) | def init():
  function render_sidebar (line 55) | def render_sidebar():
  function fold_turns (line 112) | def fold_turns(text):
  function render_segments (line 145) | def render_segments(segments, suffix=''):
  function agent_backend_stream (line 159) | def agent_backend_stream(prompt=None):
  function render_main_stream (line 191) | def render_main_stream(prompt=None):
  function _reset_and_rerun (line 254) | def _reset_and_rerun():

FILE: frontends/stapp2.py
  function init (line 801) | def init():
  function build_dynamic_font_css (line 811) | def build_dynamic_font_css(scale_percent: float) -> str:
  function build_dynamic_font_update_script (line 831) | def build_dynamic_font_update_script(scale_percent: float) -> str:
  function build_header_agent_badge_script (line 853) | def build_header_agent_badge_script() -> str:
  function init_session_state (line 946) | def init_session_state():
  function render_sidebar (line 968) | def render_sidebar():
  function start_agent_task (line 988) | def start_agent_task(prompt):
  function poll_agent_output (line 995) | def poll_agent_output(max_items=20):
  function _get_response_segments (line 1015) | def _get_response_segments(text):
  function render_message (line 1018) | def render_message(role, content, ts='', unsafe_allow_html=True):
  function finish_streaming_message (line 1023) | def finish_streaming_message():
  function render_streaming_area (line 1029) | def render_streaming_area():

FILE: frontends/tgapp.py
  function _make_draft_id (line 71) | def _make_draft_id():
  function _visible_segments (line 74) | def _visible_segments(text):
  function _markdown_safe_segments (line 83) | def _markdown_safe_segments(text, limit=None):
  function _line_complete (line 112) | def _line_complete(line):
  function _turn_marker_number (line 115) | def _turn_marker_number(line):
  function _maybe_partial_turn_marker (line 119) | def _maybe_partial_turn_marker(line):
  function _maybe_partial_code_fence (line 126) | def _maybe_partial_code_fence(line):
  function _extract_turn_summary (line 129) | def _extract_turn_summary(raw_text):
  function _quote_tag (line 139) | def _quote_tag(text):
  function _inject_turn_summary (line 143) | def _inject_turn_summary(body, summary):
  function _resolve_files (line 156) | def _resolve_files(paths):
  function _render_file_markers (line 168) | def _render_file_markers(text):
  function _files_from_text (line 173) | def _files_from_text(text):
  function _send_files (line 177) | async def _send_files(root_msg, files):
  function _send_files_from_text (line 192) | async def _send_files_from_text(root_msg, text):
  function _escape_pre (line 195) | def _escape_pre(text):
  function _escape_code (line 198) | def _escape_code(text):
  function _escape_link_target (line 201) | def _escape_link_target(text):
  function _quote_to_markdown_v2 (line 204) | def _quote_to_markdown_v2(text):
  function _to_markdown_v2 (line 208) | def _to_markdown_v2(text):
  function _is_not_modified_error (line 239) | def _is_not_modified_error(exc):
  function _extract_ask_user_event (line 242) | def _extract_ask_user_event(ctx):
  function _register_ask_user_hook (line 269) | def _register_ask_user_hook():
  function _drain_latest_ask_user_event (line 278) | def _drain_latest_ask_user_event():
  function _build_ask_user_markup (line 287) | def _build_ask_user_markup(menu_id, candidates):
  function _parse_ask_callback_data (line 297) | def _parse_ask_callback_data(data):
  function _build_text_prompt (line 306) | def _build_text_prompt(text):
  function _normalize_ask_menu_event (line 309) | def _normalize_ask_menu_event(stored):
  function _render_ask_user_result (line 323) | def _render_ask_user_result(event, selected=None, cancelled=False):
  function _clear_ask_reply_markup (line 340) | async def _clear_ask_reply_markup(query):
  function _edit_ask_user_result (line 346) | async def _edit_ask_user_result(query, event, selected=None, cancelled=F...
  function _send_ask_user_menu (line 356) | async def _send_ask_user_menu(root_msg, event):
  class _TelegramStreamSession (line 371) | class _TelegramStreamSession:
    method __init__ (line 372) | def __init__(self, root_msg):
    method _now (line 387) | def _now(self):
    method _retry_after_seconds (line 390) | def _retry_after_seconds(self, exc):
    method _set_retry_after (line 401) | def _set_retry_after(self, exc):
    method _is_retrying (line 405) | def _is_retrying(self):
    method _wait_for_retry (line 408) | async def _wait_for_retry(self):
    method _should_stream_update (line 413) | def _should_stream_update(self, display):
    method _mark_stream_update (line 422) | def _mark_stream_update(self, display):
    method _stream_display (line 428) | def _stream_display(self, text):
    method prime (line 439) | async def prime(self):
    method add_chunk (line 455) | async def add_chunk(self, chunk):
    method finalize (line 461) | async def finalize(self, full_text=None, send_files=True):
    method finish_with_notice (line 466) | async def finish_with_notice(self, notice):
    method _refresh (line 479) | async def _refresh(self, done, send_files):
    method _stream_active (line 500) | async def _stream_active(self, text):
    method _finalize_segment (line 520) | async def _finalize_segment(self, text):
    method _send_files (line 531) | async def _send_files(self):
    method _send_draft (line 534) | async def _send_draft(self, text):
    method _retry_call (line 553) | async def _retry_call(self, func, *args):
    method _reply_text_once (line 561) | async def _reply_text_once(self, text):
    method _reply_text (line 577) | async def _reply_text(self, text, wait_retry=True):
    method _edit_text_once (line 586) | async def _edit_text_once(self, msg, text):
    method _edit_text (line 603) | async def _edit_text(self, msg, text, wait_retry=True):
    method _upsert_live_message (line 613) | async def _upsert_live_message(self, text, wait_retry=True):
  class _TelegramTurnStreamCoordinator (line 620) | class _TelegramTurnStreamCoordinator:
    method __init__ (line 621) | def __init__(self, root_msg):
    method prime (line 628) | async def prime(self):
    method add_chunk (line 631) | async def add_chunk(self, chunk):
    method finalize (line 644) | async def finalize(self, done_text="", send_files=True):
    method finish_with_notice (line 659) | async def finish_with_notice(self, notice):
    method _ensure_session (line 664) | async def _ensure_session(self):
    method _start_turn (line 669) | async def _start_turn(self, marker):
    method _add_to_current (line 676) | async def _add_to_current(self, text):
    method _process_line (line 682) | async def _process_line(self, line):
    method _flush_pending_line (line 691) | async def _flush_pending_line(self):
    method _update_code_fence (line 698) | def _update_code_fence(self, line):
  function _stream (line 709) | async def _stream(dq, msg):
  function _normalized_command (line 749) | def _normalized_command(text):
  function _cancel_stream_task (line 756) | def _cancel_stream_task(ctx):
  function _sync_commands (line 760) | async def _sync_commands(application):
  function handle_msg (line 763) | async def handle_msg(update, ctx):
  function handle_ask_callback (line 772) | async def handle_ask_callback(update, ctx):
  function cmd_abort (line 807) | async def cmd_abort(update, ctx):
  function cmd_llm (line 812) | async def cmd_llm(update, ctx):
  function handle_photo (line 825) | async def handle_photo(update, ctx):
  function handle_command (line 847) | async def handle_command(update, ctx):
  function _error_handler (line 894) | async def _error_handler(update, context: ContextTypes.DEFAULT_TYPE):

FILE: frontends/tuiapp.py
  class ChatMessage (line 53) | class ChatMessage:
  class AgentSession (line 62) | class AgentSession:
  function fold_turns (line 75) | def fold_turns(text: str) -> list[dict[str, str]]:
  function render_folded_text (line 126) | def render_folded_text(text: str) -> str:
  function parse_local_command (line 141) | def parse_local_command(raw: str) -> tuple[str, list[str]] | None:
  function default_agent_factory (line 154) | def default_agent_factory() -> Any:
  class GenericAgentTUI (line 162) | class GenericAgentTUI(App[None]):
    method __init__ (line 185) | def __init__(self, agent_factory: Optional[AgentFactory] = None) -> None:
    method compose (line 195) | def compose(self) -> ComposeResult:
    method on_mount (line 205) | def on_mount(self) -> None:
    method on_resize (line 210) | def on_resize(self, event) -> None:
    method current (line 215) | def current(self) -> AgentSession:
    method add_session (line 220) | def add_session(self, name: Optional[str] = None) -> AgentSession:
    method action_prev_session (line 236) | def action_prev_session(self) -> None:
    method action_next_session (line 245) | def action_next_session(self) -> None:
    method action_switch_session (line 254) | def action_switch_session(self, n: int) -> None:
    method action_new_session (line 262) | def action_new_session(self) -> None:
    method action_stop_current (line 266) | def action_stop_current(self) -> None:
    method on_input_submitted (line 269) | def on_input_submitted(self, event: Input.Submitted) -> None:
    method _dispatch_command (line 282) | def _dispatch_command(self, cmd: str, args: list[str]) -> None:
    method submit_user_message (line 300) | def submit_user_message(self, text: str) -> int:
    method _consume_display_queue (line 328) | def _consume_display_queue(self, agent_id: int, task_id: int, display_...
    method _on_stream_update (line 343) | def _on_stream_update(self, agent_id: int, task_id: int, text: str, do...
    method _set_assistant_message (line 356) | def _set_assistant_message(self, agent_id: int, task_id: int, text: st...
    method _cmd_help (line 372) | def _cmd_help(self, args: list[str]) -> None:
    method _cmd_new (line 391) | def _cmd_new(self, args: list[str]) -> None:
    method _cmd_branch (line 396) | def _cmd_branch(self, args: list[str]) -> None:
    method _cmd_rewind (line 415) | def _cmd_rewind(self, args: list[str]) -> None:
    method _cmd_clear (line 475) | def _cmd_clear(self, args: list[str]) -> None:
    method _cmd_close (line 478) | def _cmd_close(self, args: list[str]) -> None:
    method _cmd_switch (line 485) | def _cmd_switch(self, args: list[str]) -> None:
    method _cmd_sessions (line 505) | def _cmd_sessions(self, args: list[str]) -> None:
    method _cmd_status (line 512) | def _cmd_status(self, args: list[str]) -> None:
    method _cmd_stop (line 515) | def _cmd_stop(self, args: list[str]) -> None:
    method _cmd_llm (line 525) | def _cmd_llm(self, args: list[str]) -> None:
    method _system (line 540) | def _system(self, text: str) -> None:
    method _refresh_all (line 545) | def _refresh_all(self) -> None:
    method _session_last_user_query (line 552) | def _session_last_user_query(self, session: AgentSession) -> str:
    method _session_last_summary (line 560) | def _session_last_summary(self, session: AgentSession) -> str:
    method _truncate_display (line 571) | def _truncate_display(text: str, max_width: int) -> str:
    method _refresh_sidebar (line 585) | def _refresh_sidebar(self) -> None:
    method _refresh_status (line 609) | def _refresh_status(self) -> None:
    method action_toggle_fold (line 624) | def action_toggle_fold(self) -> None:
    method _refresh_log (line 635) | def _refresh_log(self) -> None:
  function build_arg_parser (line 673) | def build_arg_parser() -> argparse.ArgumentParser:
  function main (line 678) | def main(argv: Optional[list[str]] = None) -> int:

FILE: frontends/wechatapp.py
  function _uin (line 23) | def _uin():
  class WxBotClient (line 26) | class WxBotClient:
    method __init__ (line 27) | def __init__(self, token=None, token_file=None):
    method _load (line 34) | def _load(self):
    method _save (line 39) | def _save(self, **kw):
    method _post (line 44) | def _post(self, ep, body, timeout=15):
    method login_qr (line 57) | def login_qr(self, poll_interval=2):
    method get_updates (line 81) | def get_updates(self, timeout=30):
    method send_text (line 97) | def send_text(self, to_user_id, text, context_token=''):
    method send_typing (line 105) | def send_typing(self, to_user_id, typing_ticket='', cancel=False):
    method get_typing_ticket (line 111) | def get_typing_ticket(self, to_user_id, context_token=''):
    method _enc (line 116) | def _enc(self, raw, aes_key):
    method _upload (line 120) | def _upload(self, filekey, upload_param, raw, aes_key, timeout=120, up...
    method _send_media (line 143) | def _send_media(self, to_user_id, file_path, media_type, item_type, it...
    method send_file (line 201) | def send_file(self, to_user_id, file_path, context_token=''):
    method send_image (line 204) | def send_image(self, to_user_id, file_path, context_token=''):
    method send_video (line 207) | def send_video(self, to_user_id, file_path, context_token=''):
    method extract_text (line 211) | def extract_text(msg):
    method is_user_msg (line 217) | def is_user_msg(msg): return msg.get('message_type') == MSG_USER
    method run_loop (line 219) | def run_loop(self, on_message, poll_timeout=30):
  function _dl_media (line 237) | def _dl_media(items):
  function _strip_md (line 267) | def _strip_md(t):
  function _clean (line 293) | def _clean(t):
  function _turn_parts (line 301) | def _turn_parts(t):
  function on_message (line 310) | def on_message(bot, msg):

FILE: frontends/wecomapp.py
  class TurnContext (line 7) | class TurnContext(TypedDict, total=False):
  function _ts (line 43) | def _ts():
  function _tprint (line 46) | def _tprint(*a, **kw):
  function _fmt_tool (line 52) | def _fmt_tool(tc):
  class WeComApp (line 58) | class WeComApp(AgentChatMixin):
    method __init__ (line 61) | def __init__(self, agent):
    method _register_hook (line 73) | def _register_hook(self, key: str, fn: TurnHookFn) -> None:
    method _unregister_hook (line 77) | def _unregister_hook(self, key: str) -> None:
    method _accept (line 82) | def _accept(self, frame):
    method _save_media (line 98) | async def _save_media(self, url, aes_key, default_name):
    method send_text (line 111) | async def send_text(self, chat_id, content, **_):
    method send_media (line 118) | async def send_media(self, chat_id, file_path):
    method send_done (line 137) | async def send_done(self, chat_id, raw_text):
    method run_agent (line 153) | async def run_agent(self, chat_id, text, **_):
    method on_text (line 218) | async def on_text(self, frame):
    method _on_media (line 232) | async def _on_media(self, frame, key, icon):
    method on_image (line 254) | async def on_image(self, frame):
    method on_file (line 257) | async def on_file(self, frame):
    method on_enter_chat (line 261) | async def on_enter_chat(self, frame):
    method on_connected (line 268) | async def on_connected(self, *_):     _tprint("[WeCom] connected")
    method on_authenticated (line 269) | async def on_authenticated(self, *_): _tprint("[WeCom] authenticated, ...
    method on_disconnected (line 270) | async def on_disconnected(self, *_):  _tprint("[WeCom] disconnected")
    method on_error (line 271) | async def on_error(self, frame):     _tprint(f"[WeCom] error: {frame}")
    method _terminal_loop (line 274) | def _terminal_loop(self):
    method start (line 320) | async def start(self, client=None):

FILE: ga.py
  function code_run (line 12) | def code_run(code, code_type="python", timeout=60, cwd=None, code_cwd=No...
  function ask_user (line 93) | def ask_user(question, candidates=None):
  function first_init_driver (line 100) | def first_init_driver():
  function web_scan (line 113) | def web_scan(tabs_only=False, switch_tab_id=None, text_only=False):
  function format_error (line 144) | def format_error(e):
  function log_memory_access (line 153) | def log_memory_access(path):
  function web_execute_js (line 163) | def web_execute_js(script, switch_tab_id=None, no_monitor=False):
  function expand_file_refs (line 174) | def expand_file_refs(text, base_dir=None):
  function file_patch (line 188) | def file_patch(path: str, old_content: str, new_content: str):
  function _scan_files (line 204) | def _scan_files(base, depth=2):
  function file_read (line 210) | def file_read(path, start=1, keyword=None, count=200, show_linenos=True):
  function smart_format (line 250) | def smart_format(data, max_str_len=100, omit_str=' ... '):
  function consume_file (line 255) | def consume_file(dr, file):
  class GenericAgentHandler (line 261) | class GenericAgentHandler(BaseHandler):
    method __init__ (line 263) | def __init__(self, parent, last_history=None, cwd='./temp'):
    method _get_abs_path (line 271) | def _get_abs_path(self, path):
    method _extract_code_block (line 275) | def _extract_code_block(self, response, code_type):
    method do_code_run (line 280) | def do_code_run(self, args, response):
    method do_ask_user (line 306) | def do_ask_user(self, args, response):
    method do_web_scan (line 313) | def do_web_scan(self, args, response):
    method do_web_execute_js (line 327) | def do_web_execute_js(self, args, response):
    method do_file_patch (line 355) | def do_file_patch(self, args, response):
    method do_file_write (line 369) | def do_file_write(self, args, response):
    method do_file_read (line 402) | def do_file_read(self, args, response):
    method _in_plan_mode (line 421) | def _in_plan_mode(self): return self.working.get('in_plan_mode')
    method _exit_plan_mode (line 422) | def _exit_plan_mode(self): self.working.pop('in_plan_mode', None)
    method enter_plan_mode (line 423) | def enter_plan_mode(self, plan_path):
    method _check_plan_completion (line 426) | def _check_plan_completion(self):
    method do_update_working_checkpoint (line 431) | def do_update_working_checkpoint(self, args, response):
    method _retry_or_exit (line 443) | def _retry_or_exit(self, prompt):
    method do_no_tool (line 448) | def do_no_tool(self, args, response):
    method do_start_long_term_update (line 498) | def do_start_long_term_update(self, args, response):
    method _fold_earlier (line 515) | def _fold_earlier(self, lines):
    method _get_anchor_prompt (line 529) | def _get_anchor_prompt(self, skip=False):
    method turn_end_callback (line 543) | def turn_end_callback(self, response, tool_calls, tool_results, turn, ...
  function get_global_memory (line 574) | def get_global_memory():

FILE: llmcore.py
  function _load_mykeys (line 6) | def _load_mykeys():
  function reload_mykeys (line 17) | def reload_mykeys():
  function __getattr__ (line 29) | def __getattr__(name):  # once guard in PEP 562
  function compress_history_tags (line 33) | def compress_history_tags(messages, keep_recent=10, max_len=800, force=F...
  function _sanitize_leading_user_msg (line 66) | def _sanitize_leading_user_msg(msg):
  function safeprint (line 85) | def safeprint(*argv):
  function trim_messages_history (line 90) | def trim_messages_history(history, context_win):
  function auto_make_url (line 104) | def auto_make_url(base, path):
  function _parse_claude_json (line 110) | def _parse_claude_json(data):
  function _parse_claude_sse (line 118) | def _parse_claude_sse(resp_lines):
  function _try_parse_tool_args (line 186) | def _try_parse_tool_args(raw):
  function _parse_openai_sse (line 201) | def _parse_openai_sse(resp_lines, api_mode="chat_completions"):
  function _record_usage (line 295) | def _record_usage(usage, api_mode):
  function _parse_openai_json (line 309) | def _parse_openai_json(data, api_mode="chat_completions"):
  function _stamp_oai_cache_markers (line 339) | def _stamp_oai_cache_markers(messages, model):
  function _stream_with_retry (line 352) | def _stream_with_retry(sess, url, headers, payload, parse_fn):
  function _openai_stream (line 387) | def _openai_stream(sess, messages):
  function _prepare_oai_tools (line 415) | def _prepare_oai_tools(tools, api_mode="chat_completions"):
  function _to_responses_input (line 426) | def _to_responses_input(messages):
  function _msgs_claude2oai (line 462) | def _msgs_claude2oai(messages):
  class BaseSession (line 508) | class BaseSession:
    method __init__ (line 509) | def __init__(self, cfg):
    method _apply_claude_thinking (line 537) | def _apply_claude_thinking(self, payload):
    method ask (line 549) | def ask(self, prompt):
  function _keep_claude_block (line 568) | def _keep_claude_block(b): return not isinstance(b, dict) or b.get("type...
  function _drop_unsigned_thinking (line 569) | def _drop_unsigned_thinking(messages):
  function _ensure_thinking_blocks (line 575) | def _ensure_thinking_blocks(messages, model):
  class ClaudeSession (line 586) | class ClaudeSession(BaseSession):
    method raw_ask (line 587) | def raw_ask(self, messages):
    method make_messages (line 598) | def make_messages(self, raw_list):
  class LLMSession (line 605) | class LLMSession(BaseSession):
    method raw_ask (line 606) | def raw_ask(self, messages): return (yield from _openai_stream(self, m...
    method make_messages (line 607) | def make_messages(self, raw_list): return _msgs_claude2oai(_fix_messag...
  function _fix_messages (line 609) | def _fix_messages(messages):
  class NativeClaudeSession (line 628) | class NativeClaudeSession(BaseSession):
    method __init__ (line 629) | def __init__(self, cfg):
    method raw_ask (line 637) | def raw_ask(self, messages):
    method ask (line 670) | def ask(self, msg):
  class NativeOAISession (line 698) | class NativeOAISession(NativeClaudeSession):
    method raw_ask (line 699) | def raw_ask(self, messages):
  function openai_tools_to_claude (line 704) | def openai_tools_to_claude(tools):
  class MockFunction (line 714) | class MockFunction:
    method __init__ (line 715) | def __init__(self, name, arguments): self.name, self.arguments = name,...
  class MockToolCall (line 717) | class MockToolCall:
    method __init__ (line 718) | def __init__(self, name, args, id=''):
  class MockResponse (line 722) | class MockResponse:
    method __init__ (line 723) | def __init__(self, thinking, content, tool_calls, raw, stop_reason='en...
    method __repr__ (line 727) | def __repr__(self):
  class ToolClient (line 730) | class ToolClient:
    method __init__ (line 731) | def __init__(self, backend, auto_save_tokens=True):
    method chat (line 739) | def chat(self, messages, tools=None):
    method _prepare_tool_instruction (line 759) | def _prepare_tool_instruction(self, tools):
    method _build_protocol_prompt (line 787) | def _build_protocol_prompt(self, messages, tools):
    method _parse_mixed_response (line 804) | def _parse_mixed_response(self, text):
  function _parse_text_tool_calls (line 841) | def _parse_text_tool_calls(content):
  function _ensure_text_block (line 863) | def _ensure_text_block(blocks):
  function _write_llm_log (line 873) | def _write_llm_log(label, content, log_path=None):
  function tryparse (line 881) | def tryparse(json_str):
  class MixinSession (line 892) | class MixinSession:
    method __init__ (line 894) | def __init__(self, all_sessions, cfg):
    method __getattr__ (line 909) | def __getattr__(self, name): return getattr(self._sessions[0], name)
    method __setattr__ (line 911) | def __setattr__(self, name, value):
    method primary (line 918) | def primary(self): return self._sessions[0]
    method _pick (line 919) | def _pick(self):
    method _raw_ask (line 922) | def _raw_ask(self, *args, **kwargs):
  class NativeToolClient (line 964) | class NativeToolClient:
    method _thinking_prompt (line 966) | def _thinking_prompt(): return THINKING_PROMPT_EN if os.environ.get('G...
    method __init__ (line 967) | def __init__(self, backend):
    method set_system (line 973) | def set_system(self, extra_system):
    method chat (line 977) | def chat(self, messages, tools=None):
  function resolve_session (line 1012) | def resolve_session(cfg_name):
  function resolve_client (line 1019) | def resolve_client(cfg_name):
  function fast_ask (line 1023) | def fast_ask(prompt, cfg_name):

FILE: memory/adb_ui.py
  function _dump_u2 (line 10) | def _dump_u2():
  function _dump_native (line 21) | def _dump_native():
  function _parse_xml (line 31) | def _parse_xml(xml_str, keyword=None, clickable_only=False, raw=False):
  function ui (line 58) | def ui(keyword=None, clickable_only=False, raw=False):
  function tap (line 79) | def tap(x, y):

FILE: memory/keychain.py
  function _xor (line 9) | def _xor(data: bytes) -> bytes:
  class SecretStr (line 12) | class SecretStr:
    method __init__ (line 13) | def __init__(self, name: str, val: str):
    method use (line 15) | def use(self) -> str:
    method __repr__ (line 17) | def __repr__(self):
  class _Keys (line 26) | class _Keys:
    method __init__ (line 27) | def __init__(self):
    method __getattr__ (line 36) | def __getattr__(self, k):
    method set (line 40) | def set(self, k, v=None, *, file=None):
    method ls (line 44) | def ls(self): return list(self._d.keys())
  function __getattr__ (line 48) | def __getattr__(name): return getattr(keys, name)

FILE: memory/ljqCtrl.py
  function MouseDown (line 32) | def MouseDown(): win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0)
  function MouseUp (line 33) | def MouseUp(): win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0)
  function MouseClick (line 35) | def MouseClick(staytime=0.05):
  function MouseDClick (line 39) | def MouseDClick(staytime=0.05):
  function SetCursorPos (line 44) | def SetCursorPos(z):
  function Click (line 49) | def Click(x, y=None):
  function Press (line 55) | def Press(cmd, staytime=0):
  function GrabWindow (line 69) | def GrabWindow(hwnd):
  function imshow (line 74) | def imshow(mt, sec=0):
  function GetWRect (line 78) | def GetWRect(sr):
  function FindBlock (line 87) | def FindBlock(fn, wrect=None, verbose=0, threshold=0.8):

FILE: memory/ocr_utils.py
  function _get_rapid (line 15) | def _get_rapid():
  function _preprocess (line 22) | def _preprocess(img, scale=3, contrast=3.0):
  function _strip_cjk_spaces (line 27) | def _strip_cjk_spaces(t):
  function _ocr_rapid (line 30) | def _ocr_rapid(img):
  function ocr_image (line 42) | def ocr_image(image_input, lang=_LANG, enhance=False, engine=None):
  function ocr_screen (line 59) | def ocr_screen(bbox=None, lang=_LANG, enhance=False, engine=None):
  function ocr_window (line 68) | def ocr_window(hwnd, lang=_LANG, enhance=False, engine=None):

FILE: memory/procmem_scanner.py
  class MEMORY_BASIC_INFORMATION (line 15) | class MEMORY_BASIC_INFORMATION(ctypes.Structure):
  function is_hex_pattern (line 37) | def is_hex_pattern(pattern):
  function build_rules (line 41) | def build_rules(pattern, mode='auto'):
  function format_llm_context (line 50) | def format_llm_context(data, offset, base_addr, length=64):
  function scan_memory (line 63) | def scan_memory(pid, pattern, context_size=256, mode='auto', llm_mode=Fa...

FILE: memory/ui_detect.py
  function detect_ui_elements (line 23) | def detect_ui_elements(image_path, model_path=None, conf_threshold=0.25):
  function ocr_text (line 48) | def ocr_text(image_path):
  function visualize (line 67) | def visualize(image_path, detections, ocr_results=None, output_path=None):
  function main (line 90) | def main():

FILE: memory/vision_api.template.py
  function ask_vision (line 19) | def ask_vision(image_input, prompt="详细描述这张图片的内容", timeout=60, max_pixels...
  function _prepare_image (line 49) | def _prepare_image(image_input, max_pixels=1440000):
  function _load_config (line 74) | def _load_config():
  function _call_claude (line 78) | def _call_claude(b64, prompt, timeout, max_tokens=1024):
  function _call_openai_compat (line 96) | def _call_openai_compat(b64, prompt, timeout, *, apibase, apikey, model,...

FILE: plugins/langfuse_tracing.py
  function _patched_log (line 23) | def _patched_log(label, content):
  function _extract_usage (line 35) | def _extract_usage(buf):
  function _wrap_parser (line 68) | def _wrap_parser(orig):
  function _patched_before (line 85) | def _patched_before(self, tool_name, args, response):
  function _patched_after (line 93) | def _patched_after(self, tool_name, args, response, ret):
  function _patched_loop (line 106) | def _patched_loop(client, system_prompt, user_input, handler, tools_sche...

FILE: reflect/agent_team_worker.py
  function check (line 16) | def check():
  function on_done (line 29) | def on_done(result):
  function _prompt (line 33) | def _prompt():

FILE: reflect/autonomous.py
  function check (line 5) | def check():

FILE: reflect/goal_mode.py
  function _load (line 15) | def _load():
  function _save (line 20) | def _save(state):
  function check (line 55) | def check():
  function on_done (line 88) | def on_done(result):

FILE: reflect/scheduler.py
  function _parse_cooldown (line 32) | def _parse_cooldown(repeat):
  function _last_run (line 51) | def _last_run(tid, done_files):
  function check (line 62) | def check():

FILE: simphtml.py
  function optimize_html_for_tokens (line 596) | def optimize_html_for_tokens(html):
  function start_temp_monitor (line 636) | def start_temp_monitor(driver):
  function get_temp_texts (line 640) | def get_temp_texts(driver):
  function get_main_block (line 663) | def get_main_block(driver, extra_js="", text_only=False):
  function find_changed_elements (line 672) | def find_changed_elements(before_html, after_html):
  function get_html (line 705) | def get_html(driver, cutlist=False, maxchars=35000, instruction="", extr...
  function smart_truncate (line 744) | def smart_truncate(soup, budget, _depth=0):
  function execute_js_rich (line 820) | def execute_js_rich(script, driver, no_monitor=False):
Condensed preview — 89 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (950K chars).
[
  {
    "path": ".gitignore",
    "chars": 1756,
    "preview": "temp/\ntmp/\n\n__pycache__/\n*.py[cod]\n*$py.class\n.venv/\nvenv/\nenv/\nbuild/\ndist/\n*.egg-info/\n\n.streamlit/\n\n.vscode/\n.idea/\n*"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 2297,
    "preview": "# Contributing to GenericAgent\n\n## Why This File Is Short\n\nGenericAgent's core is ~3K lines. Every file in this repo wil"
  },
  {
    "path": "GETTING_STARTED.md",
    "chars": 5242,
    "preview": "# 🚀 新手上手指南\n\n> 完全没接触过编程也没关系,跟着做就行。Mac / Windows 都适用。\n>\n> 如果你已经有 Python 环境,直接跳到[第 2 步](#2-配置-api-key)。\n\n---\n\n## 1. 安装 Pyth"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2025 lsdefine\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 20333,
    "preview": "<div align=\"center\">\n<img src=\"assets/images/bar.jpg\" width=\"880\"/>\n\n<a href=\"https://trendshift.io/repositories/25944\" "
  },
  {
    "path": "TMWebDriver.py",
    "chars": 14555,
    "preview": "import json, threading, time, uuid, queue, socket, requests, traceback\nfrom typing import Any\nfrom simple_websocket_serv"
  },
  {
    "path": "agent_loop.py",
    "chars": 6581,
    "preview": "import json, re, os\nfrom dataclasses import dataclass\nfrom typing import Any, Optional\n@dataclass\nclass StepOutcome:\n   "
  },
  {
    "path": "agentmain.py",
    "chars": 15113,
    "preview": "import os, sys, threading, queue, time, json, re, random, locale\nos.environ.setdefault('GA_LANG', 'zh' if any(k in (loca"
  },
  {
    "path": "assets/SETUP_FEISHU.md",
    "chars": 4451,
    "preview": "# 飞书 Agent 配置指南\n\n> 让你的个人电脑变成飞书机器人的大脑,随时随地通过飞书对话控制你的电脑。\n\n---\n\n## 📋 目录\n\n1. [前置条件](#前置条件)\n2. [方案选择](#方案选择)\n3. [企业用户配置](#企业用"
  },
  {
    "path": "assets/agent_bbs.py",
    "chars": 10258,
    "preview": "# agent_bbs.py — 极简Agent公告板(多板块版)\n# 启动: uvicorn agent_bbs:app --host 0.0.0.0 --port 58800\n# 或: python agent_bbs.py\n\nimpo"
  },
  {
    "path": "assets/code_run_header.py",
    "chars": 1164,
    "preview": "import sys, os, json, re, time, subprocess\nsys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'"
  },
  {
    "path": "assets/configure_mykey.py",
    "chars": 36127,
    "preview": "#!/usr/bin/env python3\n\"\"\"\nGenericAgent — 交互式初始化向导 (configure.py)\n一键配置 LLM 模型 + 消息平台,自动生成 mykey.py\n\n用法:\n    python confi"
  },
  {
    "path": "assets/global_mem_insight_template.txt",
    "chars": 1066,
    "preview": "# [Global Memory Insight]\n需要时read L2 或 ls ../memory/ 查L3\nL0(META-SOP): memory_management_sop\nL2: 现空\nL3: memory_cleanup_s"
  },
  {
    "path": "assets/global_mem_insight_template_en.txt",
    "chars": 1824,
    "preview": "# [Global Memory Insight]\nRead L2 or ls ../memory/ for L3 when needed\nL0(META-SOP): memory_management_sop\nL2: currently "
  },
  {
    "path": "assets/insight_fixed_structure.txt",
    "chars": 383,
    "preview": "Facts(L2): ../memory/global_mem.txt | GA CodeRoot: ../ | SOPs(L3): ../memory/*.md or *.py | META-SOP(L0): ../memory/memo"
  },
  {
    "path": "assets/insight_fixed_structure_en.txt",
    "chars": 803,
    "preview": "Facts(L2): ../memory/global_mem.txt | CodeRoot: ../ | SOPs(L3): ../memory/*.md or *.py | META-SOP(L0): ../memory/memory_"
  },
  {
    "path": "assets/install-macos-app.sh",
    "chars": 6183,
    "preview": "#!/bin/bash\n\n# GenericAgent macOS Desktop App Installation Script\n#\n# Usage:\n#   bash assets/install-macos-app.sh [--aut"
  },
  {
    "path": "assets/install_python_windows.bat",
    "chars": 3357,
    "preview": "@echo off\nsetlocal enabledelayedexpansion\ntitle Python One-Click Installer\ncolor 0A\n\necho.\necho ========================"
  },
  {
    "path": "assets/sys_prompt.txt",
    "chars": 259,
    "preview": "# Role: 物理级全能执行者\n你拥有文件读写、脚本执行、用户浏览器JS注入、系统级干预的物理操作权限。禁止推诿\"无法操作\"——不空想,用工具探测。\n## 行动原则\n调用工具前先推演:当前阶段、上步结果是否符合预期、下步策略,必须在回复文"
  },
  {
    "path": "assets/sys_prompt_en.txt",
    "chars": 845,
    "preview": "# Role: Physical-Level Omnipotent Executor\nYou have full physical access: file I/O, script execution, browser JS injecti"
  },
  {
    "path": "assets/tmwd_cdp_bridge/background.js",
    "chars": 17377,
    "preview": "// background.js - Cookie + CDP Bridge\nchrome.runtime.onInstalled.addListener(() => {\n  console.log('CDP Bridge installe"
  },
  {
    "path": "assets/tmwd_cdp_bridge/content.js",
    "chars": 2037,
    "preview": ";(function(){ if (/streamlit/i.test(document.title)) return;\n\n// Remove meta CSP tags\ndocument.querySelectorAll('meta[ht"
  },
  {
    "path": "assets/tmwd_cdp_bridge/disable_dialogs.js",
    "chars": 1105,
    "preview": "// Disable alert/confirm/prompt to prevent page JS from blocking extension\n(function() {\n  const _log = console.log.bind"
  },
  {
    "path": "assets/tmwd_cdp_bridge/manifest.json",
    "chars": 835,
    "preview": "{\n  \"manifest_version\": 3,\n  \"name\": \"TMWD CDP Bridge\",\n  \"version\": \"2.0\",\n  \"description\": \"Cookie viewer + CDP bridge"
  },
  {
    "path": "assets/tmwd_cdp_bridge/popup.html",
    "chars": 668,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<style>\nbody{width:420px;max-height:500px;margin:0;padding:8px;font"
  },
  {
    "path": "assets/tmwd_cdp_bridge/popup.js",
    "chars": 1099,
    "preview": "document.addEventListener('DOMContentLoaded', () => {\n  const out = document.getElementById('out');\n  const btn = docume"
  },
  {
    "path": "assets/tool_usable_history.json",
    "chars": 1000,
    "preview": "[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"code_run个helloworld,根据结果简单评价你的工具配置\"}]},{\"role\":\"assistant\",\"content\":["
  },
  {
    "path": "assets/tools_schema.json",
    "chars": 5896,
    "preview": "[\n  {\"type\": \"function\", \"function\": {\n    \"name\": \"code_run\",\n    \"description\": \"Code executor. Prefer python. Multi-c"
  },
  {
    "path": "assets/tools_schema_cn.json",
    "chars": 4495,
    "preview": "[\n  {\"type\": \"function\", \"function\": {\n    \"name\": \"code_run\",\n    \"description\": \"代码执行器。优先使用python。支持Multi-call,并行时用scr"
  },
  {
    "path": "frontends/DESKTOP_PET_README.md",
    "chars": 2853,
    "preview": "# Desktop Pet Skin System\n\n## 快速开始\n\n运行桌面宠物:\n```bash\npython3 desktop_pet_v2.pyw\n```\n\n## 功能特性\n\n### 1. 多皮肤支持\n- 自动发现 `skins/"
  },
  {
    "path": "frontends/btw_cmd.py",
    "chars": 4916,
    "preview": "\"\"\"`/btw` 命令:side question — 不打断主 Agent 的临时 subagent 问答。\n\n- 持锁 deepcopy backend.history → 后台线程 backend.raw_ask 单次拉答\n- 主 "
  },
  {
    "path": "frontends/chatapp_common.py",
    "chars": 12629,
    "preview": "import ast, asyncio, glob, json, os, queue as Q, re, socket, sys, time\n\nHELP_COMMANDS = (\n    (\"/help\", \"显示帮助\"),\n    (\"/"
  },
  {
    "path": "frontends/continue_cmd.py",
    "chars": 12328,
    "preview": "\"\"\"`/continue` command: list & restore past model_responses sessions.\nPure functions + one `install(cls)` monkey-patch e"
  },
  {
    "path": "frontends/dcapp.py",
    "chars": 17347,
    "preview": "# Discord Bot Frontend for GenericAgent\n# ⚠️ 需要在 Discord Developer Portal 开启 \"Message Content Intent\"\n#   Bot → Privileg"
  },
  {
    "path": "frontends/desktop_pet.pyw",
    "chars": 3905,
    "preview": "\"\"\"Desktop Pet with HTTP Toast — ~90 lines\"\"\"\nimport tkinter as tk, threading, random, os, sys\nfrom http.server import H"
  },
  {
    "path": "frontends/desktop_pet_v2.pyw",
    "chars": 44199,
    "preview": "\"\"\"Desktop Pet with Skin System — Cross-platform with True Transparency\"\"\"\nimport os, re, sys, json, threading, io\nfrom "
  },
  {
    "path": "frontends/dingtalkapp.py",
    "chars": 7118,
    "preview": "import asyncio, json, os, sys, threading, time\nimport requests\n\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.pa"
  },
  {
    "path": "frontends/fsapp.py",
    "chars": 27028,
    "preview": "import glob, json, os, queue as Q, re, sys, threading, time\n\nPROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.absp"
  },
  {
    "path": "frontends/genericagent_acp_bridge.py",
    "chars": 14438,
    "preview": "import io\nimport json\nimport os\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)"
  },
  {
    "path": "frontends/qqapp.py",
    "chars": 5083,
    "preview": "import asyncio, os, sys, threading, time\nfrom collections import deque\n\nsys.path.insert(0, os.path.dirname(os.path.dirna"
  },
  {
    "path": "frontends/qtapp.py",
    "chars": 100374,
    "preview": "\"\"\"\n桌面前端单文件版 – PySide6 聊天面板 + 悬浮按钮   thanks to GaoZhiCheng\n依赖: pip install PySide6\n可选: pip install markdown  (Markdown 渲"
  },
  {
    "path": "frontends/skins/boy/skin.json",
    "chars": 1221,
    "preview": "{\n  \"name\": \"Boy\",\n  \"version\": \"1.0.0\",\n  \"author\": \"pzuh\",\n  \"source\": \"https://pzuh.itch.io/temple-run-game-sprites\","
  },
  {
    "path": "frontends/skins/dinosaur/skin.json",
    "chars": 1209,
    "preview": "{\n  \"name\": \"Dinosaur\",\n  \"version\": \"1.0.0\",\n  \"author\": \"voidcord54\",\n  \"source\": \"https://voidcord54.itch.io/\",\n  \"de"
  },
  {
    "path": "frontends/skins/doux/skin.json",
    "chars": 1222,
    "preview": "{\n  \"name\": \"Doux\",\n  \"version\": \"1.0.0\",\n  \"author\": \"arks\",\n  \"source\": \"https://arks.itch.io/dino-characters\",\n  \"lic"
  },
  {
    "path": "frontends/skins/glube/skin.json",
    "chars": 1222,
    "preview": "{\n  \"name\": \"Glube\",\n  \"version\": \"1.0.0\",\n  \"author\": \"SketchesWithKevin\",\n  \"source\": \"https://sketcheswithkevin.itch."
  },
  {
    "path": "frontends/skins/line/License.txt",
    "chars": 221,
    "preview": "License is CC0 - https://creativecommons.org/public-domain/cc0/\n\nYOU CAN:\n\n-> You can do whatever you want with this ass"
  },
  {
    "path": "frontends/skins/line/skin.json",
    "chars": 1191,
    "preview": "{\n  \"name\": \"Line\",\n  \"version\": \"1.0.0\",\n  \"author\": \"itch.io\",\n  \"source\": \"https://itch.io\",\n  \"description\": \"Line 角"
  },
  {
    "path": "frontends/skins/mort/skin.json",
    "chars": 1222,
    "preview": "{\n  \"name\": \"Mort\",\n  \"version\": \"1.0.0\",\n  \"author\": \"arks\",\n  \"source\": \"https://arks.itch.io/dino-characters\",\n  \"lic"
  },
  {
    "path": "frontends/skins/tard/skin.json",
    "chars": 1222,
    "preview": "{\n  \"name\": \"Tard\",\n  \"version\": \"1.0.0\",\n  \"author\": \"arks\",\n  \"source\": \"https://arks.itch.io/dino-characters\",\n  \"lic"
  },
  {
    "path": "frontends/skins/vita/skin.json",
    "chars": 1222,
    "preview": "{\n  \"name\": \"Vita\",\n  \"version\": \"1.0.0\",\n  \"author\": \"arks\",\n  \"source\": \"https://arks.itch.io/dino-characters\",\n  \"lic"
  },
  {
    "path": "frontends/stapp.py",
    "chars": 15135,
    "preview": "import os, sys, subprocess\nfrom urllib.request import urlopen\nfrom urllib.parse import quote\nif sys.stdout is None: sys."
  },
  {
    "path": "frontends/stapp2.py",
    "chars": 37827,
    "preview": "import os, sys\nimport html\nif sys.stdout is None: sys.stdout = open(os.devnull, \"w\")\nif sys.stderr is None: sys.stderr ="
  },
  {
    "path": "frontends/tgapp.py",
    "chars": 35184,
    "preview": "import os, sys, re, threading, asyncio, queue as Q, time, random, uuid\nsys.path.insert(0, os.path.dirname(os.path.dirnam"
  },
  {
    "path": "frontends/tuiapp.py",
    "chars": 27709,
    "preview": "\"\"\"Textual terminal UI for GenericAgent.\n\nRun from the project root:\n\n    python frontends/tuiapp.py\n\nUseful options:\n\n "
  },
  {
    "path": "frontends/wechatapp.py",
    "chars": 21177,
    "preview": "import os, sys, re, threading, queue, time, socket, json, struct, base64, uuid, webbrowser, hashlib, math\nfrom pathlib i"
  },
  {
    "path": "frontends/wecomapp.py",
    "chars": 15677,
    "preview": "import asyncio, os, select, sys, threading, time, traceback\nfrom collections import deque\nfrom datetime import datetime\n"
  },
  {
    "path": "ga.py",
    "chars": 31113,
    "preview": "import sys, os, re, json, time, threading, importlib\nfrom datetime import datetime\nfrom pathlib import Path\nimport tempf"
  },
  {
    "path": "hub.pyw",
    "chars": 9689,
    "preview": "# launcher.pyw - GenericAgent 服务启动器\n# 纯 tkinter + 标准库,零第三方依赖,跨平台\nimport os, sys, socket, subprocess, threading\nimport tk"
  },
  {
    "path": "launch.pyw",
    "chars": 7720,
    "preview": "import webview, threading, subprocess, sys, time, os, ctypes, atexit, socket, random\n\nWINDOW_WIDTH, WINDOW_HEIGHT, RIGHT"
  },
  {
    "path": "llmcore.py",
    "chars": 57715,
    "preview": "import os, json, re, time, requests, sys, threading, urllib3, base64, importlib, uuid\nfrom datetime import datetime\nurll"
  },
  {
    "path": "memory/adb_ui.py",
    "chars": 3451,
    "preview": "# adb_ui.py - 一键dump+解析Android UI (u2优先,原生fallback)\n# u2 (uiautomator2) 不受idle限制,适合动画密集app(美团等)\n# 弹窗检测: ui(clickable_onl"
  },
  {
    "path": "memory/autonomous_operation_sop.md",
    "chars": 1456,
    "preview": "# 自主行动 SOP\n\n⚠️ **路径警告**:autonomous_reports 在 temp/ 下,用`./autonomous_reports/`访问,**不是**`../memory/autonomous_reports/`或`."
  },
  {
    "path": "memory/goal_mode_sop.md",
    "chars": 907,
    "preview": "# Goal Mode SOP\n\n## 何时使用\n\n用户给出开放目标 + 时间预算(如\"花3小时持续优化X\"、\"没事也找事干\"),且不是一次性闭环任务。\n\n## 设置\n\n写 `temp/goal_state.json`(或自定义路径):\n\n"
  },
  {
    "path": "memory/keychain.py",
    "chars": 1959,
    "preview": "\"\"\"Keychain: save key to a file, then keys.set(\"name\", file=\"path\"); keys.name.use() to retrieve (use but no print).\"\"\"\n"
  },
  {
    "path": "memory/ljqCtrl.py",
    "chars": 5944,
    "preview": "\"\"\"\nCRITICAL: 严禁在此工具链中 import pyautogui (会污染 win32api 导致逻辑冲突)。\nljqCtrl Quick Reference:\n- dpi_scale: float (Logical = Ph"
  },
  {
    "path": "memory/ljqCtrl_sop.md",
    "chars": 1961,
    "preview": "# ljqCtrl 使用与坐标转换 SOP\n\n> **must call update working ckp**:`ljqCtrl一律使用物理坐标|禁pyautogui|操作前先gw激活窗口`\n\n## 0. API 快速参考 (Signa"
  },
  {
    "path": "memory/memory_cleanup_sop.md",
    "chars": 1205,
    "preview": "# 记忆整理 SOP\n\n## 核心原则:存在性编码\nLLM自身是压缩器+解码器。L1只需让它**意识到某类知识存在**,它就能通过tool call自行取用深层内容。\n\n**L1本质:用最短词数表达——什么场景下有什么记忆可用(存在性)。*"
  },
  {
    "path": "memory/memory_management_sop.md",
    "chars": 3113,
    "preview": "## 0. 核心公理 (Core Axioms - 最高优先级)\n1.  **行动验证原则 (Action-Verified Only)**\n    *   **定义**:任何写入 L1/L2/L3 的信息,必须源自**成功的工具调用结果*"
  },
  {
    "path": "memory/ocr_utils.py",
    "chars": 3480,
    "preview": "\"\"\"\n本地 OCR 工具\n- OCR引擎: rapidocr-onnxruntime (~1s/次, 中英文准确率高, 带bbox)\n- 坑(rapid): result[i][2] conf 是 str 不是 float\n- 坑(rap"
  },
  {
    "path": "memory/plan_sop.md",
    "chars": 6353,
    "preview": "# Plan Mode SOP\n\n**触发**:3步以上有依赖/多文件协同/条件分支/需并行 | **禁用**:1-2步简单任务直接做\n任务开始前必须先创建工作目录 `./plan_XXX/`(XXX=任务英文短名)\n单独使用一个code_"
  },
  {
    "path": "memory/procmem_scanner.py",
    "chars": 4950,
    "preview": "import ctypes\nimport ctypes.wintypes\nimport argparse\nimport yara\nimport sys\nimport os\nimport json\n\n# Define WinAPI Types"
  },
  {
    "path": "memory/procmem_scanner_sop.md",
    "chars": 940,
    "preview": "# Memory Scanner SOP\n\n## 1. 快速开始\n内存特征搜索工具,支持 Hex (CE 风格) 和 字符串匹配。特别提供 LLM 模式,方便大模型分析内存上下文。\n\n**Python 调用方式:**\n```python\ni"
  },
  {
    "path": "memory/scheduled_task_sop.md",
    "chars": 924,
    "preview": "# 定时任务 SOP\n\n目录:`../sche_tasks/` 放任务定义JSON,`../sche_tasks/done/` 放执行报告\n\n## 任务JSON格式(*.json)\n```json\n{\"schedule\":\"08:00\", "
  },
  {
    "path": "memory/supervisor_sop.md",
    "chars": 1066,
    "preview": "# 监察者模式 SOP\n\n> 你是挑刺的监工,不是干活的工人。你的唯一任务:确保工作agent高质量完成任务。有SOP按SOP约束,无SOP凭常理和经验把关。\n\n## 红线\n\n- **禁止下场干活**:不操作浏览器、不写代码、不执行任务步骤"
  },
  {
    "path": "memory/tmwebdriver_sop.md",
    "chars": 6587,
    "preview": "# TMWebDriver SOP\n\n- 直接用web_scan/web_execute_js工具。本文件只记录特性和坑。\n- 底层:`../TMWebDriver.py`通过Chrome扩展接管用户浏览器(保留登录态/Cookie)\n- "
  },
  {
    "path": "memory/ui_detect.py",
    "chars": 3919,
    "preview": "#!/usr/bin/env python3\n\"\"\"\n极简UI元素检测脚本 - 基于OmniParser的YOLO模型\n依赖: ultralytics, rapidocr-onnxruntime, pillow, numpy\n\"\"\"\nimp"
  },
  {
    "path": "memory/vision_api.template.py",
    "chars": 4670,
    "preview": "import base64, requests, sys, os\nfrom io import BytesIO\nfrom pathlib import Path\n\n# ============ 用户配置区(从 template 拷贝后只需改"
  },
  {
    "path": "memory/vision_sop.md",
    "chars": 903,
    "preview": "# Vision API SOP\n\n## ⚠️ 前置规则(必须遵守)\n\n1. **先枚举窗口**:调用 vision 前必须先用 `pygetwindow` 枚举窗口标题,确认目标窗口存在且已激活到前台。窗口不存在就不要截图。\n2. **🚫"
  },
  {
    "path": "memory/vue3_component_sop.md",
    "chars": 7289,
    "preview": "# Vue 3 自定义组件 JS 操作 SOP\n\n## 问题\nVue 3 自定义组件(如 OxdSelect)通过 `addEventListener` 绑定事件,JS `dispatchEvent` 产生的事件 `isTrusted: f"
  },
  {
    "path": "memory/web_setup_sop.md",
    "chars": 674,
    "preview": "# Web 工具链初始化执行 SOP\n\n若 web_scan 和 web_execute_js 已测试可用,无需执行此 SOP。\n仅供初始安装时,code_run 可用但 web 工具尚未配置的场景。\n\n## 目标\n在仅具备系统级权限(co"
  },
  {
    "path": "mykey_template.py",
    "chars": 22402,
    "preview": "# ══════════════════════════════════════════════════════════════════════════════\n#  GenericAgent — mykey.py 配置模板(复制为 myk"
  },
  {
    "path": "mykey_template_en.py",
    "chars": 3419,
    "preview": "# ══════════════════════════════════════════════════════════════════════════════\n#  GenericAgent — mykey.py configuratio"
  },
  {
    "path": "plugins/langfuse_tracing.py",
    "chars": 5673,
    "preview": "\"\"\"Opt-in Langfuse tracing. Self-activates on import if langfuse_config exists in mykey.\n\nHooks only via monkey-patch so"
  },
  {
    "path": "pyproject.toml",
    "chars": 1004,
    "preview": "# AI install hint: choose deps by OS/env + needed ui/bot. do NOT install all.\n# minimal core first; missing packages can"
  },
  {
    "path": "reflect/agent_team_worker.py",
    "chars": 1320,
    "preview": "# reflect module: BBS接单\n# check()内预检BBS,无新帖返回None不唤醒agent\nimport json, time, os\nfrom urllib import request\n\nINTERVAL = 6"
  },
  {
    "path": "reflect/autonomous.py",
    "chars": 126,
    "preview": "# reflect/autonomous.py\nINTERVAL = 1800\nONCE = False\n\ndef check():\n    return \"[AUTO]🤖 用户已经离开超过30分钟,作为自主智能体,请阅读自动化sop,执行"
  },
  {
    "path": "reflect/goal_mode.py",
    "chars": 2616,
    "preview": "# reflect/goal_mode.py — Goal Mode: 持续自驱直到预算耗尽\n# 启动: set GOAL_STATE=temp/xxx.json && python agentmain.py --reflect refle"
  },
  {
    "path": "reflect/scheduler.py",
    "chars": 4782,
    "preview": "import os, json, time as _time, socket as _socket, logging\nfrom datetime import datetime, timedelta\n\n# 端口锁:防止重复启动,bind失败"
  },
  {
    "path": "simphtml.py",
    "chars": 41175,
    "preview": "try: from bs4 import BeautifulSoup\nexcept ImportError: print(\"[Error] BeautifulSoup4 未安装,请叫Agent安装BeautifulSoup4,再使用web相"
  }
]

About this extraction

This page contains the full source code of the lsdefine/pc-agent-loop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 89 files (814.0 KB), approximately 232.7k tokens, and a symbol index with 774 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!