docs: refresh readme and phase2 summary

This commit is contained in:
JOJO 2025-11-23 21:24:09 +08:00
parent b81d6760bd
commit 7348eab83a
7628 changed files with 1932943 additions and 269 deletions

View File

@ -31,7 +31,7 @@ TERMINAL_SANDBOX_IMAGE=python:3.11-slim
TERMINAL_SANDBOX_MOUNT_PATH=/workspace
TERMINAL_SANDBOX_SHELL=/bin/bash
# 资源与网络限制
TERMINAL_SANDBOX_NETWORK=none
TERMINAL_SANDBOX_NETWORK=bridge
TERMINAL_SANDBOX_CPUS=0.5
TERMINAL_SANDBOX_MEMORY=1g
# 附加绑定目录(逗号分隔,可留空)

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ sub_agent/project/
# Misc
*.pid
.env

286
README.md
View File

@ -1,217 +1,139 @@
# AI Agent 系统
# AI Agent 系统v2
一个围绕“真实开发工作流”构建的智能编程助手。系统基于兼容 OpenAI API 的大模型DeepSeek、Qwen、Kimi 等),提供 CLI 与 Web 双入口,支持文件/终端/网络等多种工具调用,并可持续追踪上下文、终端快照和对话历史
多用户、多终端的智能开发助手。每位登录用户都会获得一个独立 Docker 容器,所有终端、一次性命令以及文件操作都在容器内完成;宿主机仅提供挂载和备份,配合实时监控界面可以随时观察容器 CPU/内存/网络/磁盘配额的使用情况
> ⚠️ **项目定位**:学习与实验用的个人项目,代码大量由 AI 协助完成,尚未达到生产级稳定性。欢迎重构、提 Bug、补文档
> ⚠️ **定位**:学习与实验用项目,主要用于探索“智能体 + 真实 Dev Workflow”。代码大量由 AI 生成,尚未达到生产级别,请谨慎部署
---
## 功能亮点
## 近期亮点
1. **多模态阅读工具**
`read_file` 支持 `type=read/search/extract` 三种模式:可按行阅读片段、在文件内搜索关键词并返回上下文窗口、或一次抽取多段内容。所有模式都具备 UTF-8 校验与 `max_chars` 截断Web 前端会直接显示“正在搜索/提取”等状态。
2. **模块化配置**
新的 `config/` 目录按主题拆分(`api.py`、`limits.py`、`paths.py`、`terminal.py` 等),既方便独立维护,又保持 `from config import ...` 的兼容导出。
3. **多终端 & 实时可视化**
支持最多 3 个持久化 Shell 会话 (`terminal_session`),可在 Web 端查看每个终端的实时输出、切换会话、执行命令。CLI/Web 均可在“快速/思考模式”间切换。
4. **可控写入 & 文件安全**
`append_to_file` 采用双阶段协议(写入窗口 + 标记校验),`modify_file` 提供块级替换,`file_manager` 严格校验路径防止越界,聚焦文件机制保证关键信息始终在上下文中。
5. **对话持久化与回放**
所有对话、工具调用、token 统计都会写入 `data/conversations/`Web 端提供会话搜索、加载、删除。`utils/context_manager` 可对长对话进行压缩并保留写入结果。
---
## 目录结构(节选)
```
.
├── main.py # CLI 入口,负责模式选择与终端初始化
├── web_server.py # Flask + Socket.IO Web 服务
├── config/ # 模块化配置目录
│ ├── __init__.py # 聚合导出,兼容 from config import ...
│ ├── api.py # 模型/API/Tavily 配置
│ ├── limits.py # 上下文与工具阈值read/run/terminal 等)
│ ├── terminal.py # 多终端并发、缓冲、超时配置
│ ├── conversation.py # 对话存储目录、索引、备份策略
│ ├── paths.py # 项目/数据/日志路径
│ ├── security.py # 禁止命令、路径、需要确认的工具
│ ├── ui.py # 输出格式、日志等级、版本号
│ ├── memory.py # 记忆文件路径
│ ├── todo.py # 待办工具限制
│ └── auth.py # 管理员账号配置
├── core/
│ ├── main_terminal.py # CLI 主循环,解析命令并调度工具
│ ├── web_terminal.py # Web 适配层:广播工具状态、维护 Web 会话
│ └── tool_config.py # 工具分组与启用状态管理
├── modules/
│ ├── file_manager.py # 路径校验、读写、搜索、抽取
│ ├── terminal_manager.py # 多终端会话池
│ ├── terminal_ops.py # run_command / run_python 实现
│ ├── memory_manager.py # 长/短期记忆文件操作
│ ├── search_engine.py # Tavily 搜索封装
│ ├── todo_manager.py # 待办工具后端
│ ├── webpage_extractor.py # 网页提取与保存
│ └── persistent_terminal.py 等辅助模块
├── utils/
│ ├── api_client.py # 调用模型 API流式输出、工具触发
│ ├── context_manager.py # 对话上下文、聚焦文件、压缩逻辑
│ ├── conversation_manager.py # 对话索引、加载、保存
│ ├── logger.py # 日志初始化
│ └── terminal_factory.py # 终端实例工厂
├── static/
│ ├── index.html # Web 主界面(对话/工具流)
│ ├── terminal.html # 终端独立面板
│ ├── login.html / register.html # 简易登录/注册页
│ ├── claude-colors-simple.html # 主题示例
│ ├── app.js / style.css # 前端逻辑与主样式
│ ├── vendor/ # 第三方库CodeMirror / Socket.IO 客户端等)
│ ├── file_manager/ # 文件预览/编辑辅助资源
│ ├── debug.html # 调试工具页
│ └── backup_*/ # 历史版本前端备份
├── prompts/ # 系统提示词、工具提示模板
├── data/
│ ├── conversations/ # 默认对话存档
│ ├── memory.md / task_memory.md
│ └── conversation_history.json
├── users/ # 多用户独立工作区(会话、项目、上传文件)
├── test/ # 集成测试与脚本(如 api_interceptor_server
├── logs/ # 运行日志、debug_stream.log
├── project/ # 默认项目目录(可在启动时指定)
└── README.md / AGENTS.md / requirements.txt 等文档
```
---
## 配置指南
1. **安装依赖**
```bash
git clone <repo>
cd <repo>
pip install -r requirements.txt
```
2. **配置环境变量(敏感信息)**
```bash
cp .env.example .env
# 编辑 .env填写 API Key / 管理员哈希 / 终端沙箱配置
```
关键项说明:
- `AGENT_API_BASE_URL` / `AGENT_API_KEY` / `AGENT_MODEL_ID`:模型服务入口(必要)。
- `AGENT_THINKING_*`:可选的“思考模式”专用模型,不填则回退到基础模型。
- `AGENT_ADMIN_USERNAME` / `AGENT_ADMIN_PASSWORD_HASH`:管理员账号(使用 `generate_password_hash` 生成 PBKDF2 哈希)。
- `WEB_SECRET_KEY`Flask Session 密钥,建议使用 `secrets.token_hex(32)`
- `TERMINAL_SANDBOX_*`:终端容器镜像、挂载路径、资源额度(默认 0.5 核心 + 1GB 内存),若本地无 Docker可将 `TERMINAL_SANDBOX_REQUIRE` 置为 `0`,系统会自动退回宿主机终端。
- `PROJECT_MAX_STORAGE_MB`:单个项目允许的最大磁盘占用(默认 2GB超限写入会被拒绝
- `MAX_ACTIVE_USER_CONTAINERS`:并发允许的用户容器数量,超过后会提示“资源繁忙”。
3. **(可选)调整 Python 配置**
- `config/limits.py`:读写/搜索等工具上限。
- `config/paths.py`:项目、日志、数据目录。
- `config/terminal.py`:终端并发数、缓冲大小(环境变量会覆盖默认值)。
- `config/security.py`:命令/路径黑名单及需要二次确认的工具。
4. **运行方式**
```bash
# CLI推荐快速试验
python main.py
# Web推荐可视化终端/工具流)
python web_server.py
# 访问 http://localhost:8091
```
启动时可选择“快速模式”或“思考模式”,并指定项目路径(默认为 `./project`)。
5. **子智能体测试服务(可选)**
```bash
python sub_agent/server.py
# 默认监听 8092 端口,供 create_sub_agent/wait_sub_agent 工具调用
```
当前实现为占位服务,会在 `sub_agent/tasks/` 下生成交付文件,并立即返回测试结果,便于主智能体打通基础流程。
---
## 主要工具与能力
| 工具 | 说明 |
| 能力 | 说明 |
| --- | --- |
| `read_file` | `type=read/search/extract`,支持行区间阅读、文件内关键词搜索(带上下文窗口和命中去重)、多段抽取;可设置 `max_chars` 控制返回体量。 |
| `focus_file` / `unfocus_file` | 将 UTF-8 文本持续注入上下文(最多 3 个),适合频繁查看/修改的文件。 |
| `append_to_file` / `modify_file` | 双阶段写入、大块内容追加、结构化补丁替换。 |
| `terminal_session` / `terminal_input` / `run_command` / `run_python` | 管理多终端会话、发送命令或一次性脚本Web 端可实时查看输出。 |
| `web_search` / `extract_webpage` / `save_webpage` | 外部信息检索与网页内容提取/落盘。 |
| `todo_*` / `update_memory` | 记录待办事项、更新全局/任务记忆;适合长任务拆解与结果总结。 |
| **单用户-单容器** | `modules/user_container_manager.py` 会在登录时启动专属容器并在退出或空闲超时后自动销毁。CLI/Web/Toolbox 复用同一容器,资源配额(默认 0.5 vCPU / 1GB RAM / 2GB 磁盘)由 `.env` 控制。 |
| **容器内文件代理** | `modules/container_file_proxy.py` 通过 `docker exec` 调用内置 Python 脚本,对 `create/read/search/write/modify` 等操作进行沙箱化处理,宿主机不再直写用户代码。 |
| **实时监控面板** | Web “用量统计”抽屉实时展示 Token 消耗、容器 CPU/内存、网络上下行速率0.5s 刷新以及项目存储占用5s 刷新。CLI `/status` 命令也会附带容器状态。 |
| **联网能力 + 最小工具集** | 终端镜像改为 `bridge` 网络并预装 `iputils-ping`,方便验证网络连通性;遇到受限环境可以随时在 `.env` 中切换网络模式。 |
---
## Web 前端体验
## 架构概览
- 左侧:对话列表,可搜索/加载历史记录。
- 中间:分层展示思考 (`thinking`)、助手回复、工具执行流;`read_file` 的三种模式会在工具卡片上直接标注“读取/搜索/提取”及执行结果。
- 右侧聚焦文件即时预览终端面板实时刷新Token 统计显示本轮消耗。
- 底部:提供停止/清空/下载日志等快速操作。
```
┌─────────────┐ ┌────────────────┐ ┌────────────────────────┐
│ Browser │◀───▶│ Flask + Socket │◀───▶│ UserContainerManager │
│ (Vue 前端) │ Web │ Web Server │调度 │ (管理 Docker 容器) │
└─────────────┘ │ ├─ WebTerminal │ └────┬───────────────────┘
│ └─ REST APIs │ │
└────────────────┘ ▼
Container
├─ PersistentTerminal / Toolbox
└─ Container File Proxy
```
> 当前版本尚未内置“文件管理器”式的可视化浏览器;若需查看文件树,可在终端使用 `ls` 或配合聚焦机制。
- **core/**`MainTerminal` / `WebTerminal` 负责命令路由、上下文管理与工具调度。
- **modules/**终端管理、FileManager、容器调度、搜索、记忆等独立能力模块。
- **utils/**:模型 API 客户端、上下文压缩、日志、终端工厂等辅助工具。
- **static/**Vue + Socket.IO 单页应用,负责对话流/终端输出/资源监控。
- **docker/**:终端镜像 Dockerfile 及工具容器依赖清单。
更多目录说明请查阅 `tree -L 2` 或仓库内注释。
---
## 常见工作流
## 快速开始
### 1. 阅读 + 修改核心代码
1. `read_file type=search` 定位关键函数。
2. `read_file type=extract` 抽取需要改动的多段内容。
3. `focus_file` 保持核心文件常驻上下文,配合 `modify_file` 输出补丁。
### 1. 环境需求
### 2. 构建脚手架并验证
1. `create_folder` / `create_file` / `append_to_file` 生成工程骨架。
2. `terminal_session` 启动开发服务器;`terminal_input` 查看日志。
3. `todo_*` 记录剩余任务,`update_memory` 写入结论。
- Python 3.11+
- Docker 20+(需启用 cgroup v1/v2 任一,默认通过 `bridge` 网络运行容器)
- Node/Vue 非必需,前端为静态文件
### 3. 信息搜集与整理
1. `web_search` 获取外部资料。
2. `extract_webpage` 抽取正文,或 `save_webpage` 落盘。
3. `read_file type=search/extract` 在本地笔记中快速定位与摘录。
### 2. 安装依赖
```bash
pip install -r requirements.txt
```
### 3. 配置
1. 复制 `.env.example``.env`,填入以下关键变量:
- `AGENT_API_*`:兼容 OpenAI API 的模型服务
- `WEB_SECRET_KEY`Flask session 密钥
- `TERMINAL_SANDBOX_*`:容器镜像/资源/网络(默认 `my-agent-shell:latest` + `bridge`
2. 若需定制终端镜像:
```bash
docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest .
```
### 4. 运行
```bash
# CLI 模式
python main.py
# Web 模式(默认 8091
python web_server.py
```
首次启动会在 `users/<username>/` 下初始化个人工作区与容器。
---
## 开发与测试建议
## 监控与调试
- **本地运行**`python main.py` / `python web_server.py`;调试 Web 端建议同时开启浏览器控制台观察 WebSocket 日志。
- **终端隔离**:默认通过 `TERMINAL_SANDBOX_*` 配置启动 Docker 容器执行命令,每个终端挂载独立工作区;若本地无容器运行时,可暂时将 `TERMINAL_SANDBOX_REQUIRE=0` 退回宿主机模式。
- **快捷命令环境**`run_command` / `run_python` 会在专用的“工具容器”中执行,复用同一虚拟环境与依赖,减少对宿主机的影响;空闲超过 `TOOLBOX_TERMINAL_IDLE_SECONDS` 会自动释放。
- **资源保护**:容器默认限制为 0.5 vCPU / 1GB 内存,项目磁盘超过 2GB 会拒绝写入;当活跃用户容器达到 `MAX_ACTIVE_USER_CONTAINERS` 时系统会返回“资源繁忙”提示,避免服务器过载。
- **日志**CLI 模式下输出使用 `OUTPUT_FORMATS` 定义的 EmojiWeb 模式所有工具事件都会写入 `logs/debug_stream.log`
- **数据隔离**:多用户目录位于 `users/<username>/`;请避免将真实密钥提交到仓库,必要时扩展 `.gitignore`
- **测试**:暂未配置自动化测试,可参考 `test/` 目录(如 `api_interceptor_server.py`)编写自定义脚本。
- **Web 用量面板**:点击右下角「用量统计」可查看 Token/CPU/内存/网络/存储实时状态。
- **CLI `/status`**:显示当前会话、上下文大小、容器资源等,日志位于 `logs/debug_stream.log`
- **容器日志**`logs/container_stats.log` 存储 `docker stats/inspect` 样本,可配合 `tail -f` 或外部监控系统。
---
## 已知限制
## 常见命令
- 缺乏文件管理器式的可视化浏览;需依赖终端与聚焦机制。
- 主要依赖手动测试,尚无完备的自动化/集成测试。
- Windows 路径偶有兼容性问题,建议在类 Unix 环境下运行。
- 仍有部分旧代码需重构(异步策略、异常处理、配置热更新等)。
| 命令 | 说明 |
| --- | --- |
| `/help` | CLI 指令列表 |
| `/status` | 查看系统/容器资源状态 |
| `read_file` / `modify_file` | 读写项目文件(自动通过容器代理) |
| `terminal_session` / `terminal_input` | 管理多终端会话 |
| `run_command` / `run_python` | 工具容器快速执行命令/Python 代码 |
| `todo_*`, `update_memory` | 维护待办与长期记忆 |
---
## 贡献方式
## 配置速览
- 提 Bug附复现场景与日志方便排查。
- 提需求:描述使用场景、期望行为与约束。
- 代码贡献:请遵循现有目录结构与模块职责,提交 PR 前确保基础功能可用。
- 文档贡献:欢迎补充 FAQ、最佳实践或脚本案例。
`config/terminal.py` 支持以下常用变量:
> Commit 建议使用 Conventional Commits首次克隆请运行 `pip install -r requirements.txt` 并根据需要调整 `config/` 下的参数。
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `TERMINAL_SANDBOX_IMAGE` | `my-agent-shell:latest` | 终端容器镜像 |
| `TERMINAL_SANDBOX_NETWORK` | `bridge` | 容器网络模式(`none`/`bridge`/`host` 等) |
| `TERMINAL_SANDBOX_CPUS` | `0.5` | 上限 CPU |
| `TERMINAL_SANDBOX_MEMORY` | `1g` | 上限内存 |
| `TERMINAL_SANDBOX_REQUIRE` | `0` | 若容器启动失败是否降级宿主机模式 |
| `PROJECT_MAX_STORAGE_MB` | `2048` | 每个工作区的磁盘配额 |
| `MAX_ACTIVE_USER_CONTAINERS` | `8` | 并发容器数量 |
更多配置请查阅 `.env.example`
---
## 许可与致谢
## 开发建议
- 协议MIT。
- 致谢:感谢 DeepSeek、Qwen、Kimi 等兼容 OpenAI 的 API及 Tavily 搜索服务提供的数据能力;同时参考了 Claude/ChatGPT 的交互体验设计。
- 若本项目对你有帮助,欢迎 Star 或提交改进建议 🙌
1. **安全**:新增模块前请优先考虑是否可以在容器中实现,避免回退宿主机;如需联网,务必评估外部依赖。
2. **日志**:尽量使用 `utils.logger.setup_logger`,便于统一收集。
3. **测试**`users/<username>/project/test_scripts/` 提供内存压测脚本,可验证容器限制是否生效;可在 `test/` 下添加更多集成测试。
4. **文档**:第二阶段总结见 `doc/phase2_summary.md`,安全基线更新见 `doc/security_review.md`
---
## Roadmap
- [ ] CSRF / 防爆破 / 会话绑定设备指纹
- [ ] 文件上传内容扫描与白名单策略
- [ ] 用户/会话数据迁移到数据库并加密备份
- [ ] 自动化测试与 CI含容器镜像构建
- [ ] Skeleton Web UI组件化、国际化、Dark Mode
欢迎通过 Issue/PR 贡献。

View File

@ -28,7 +28,7 @@ TERMINAL_SANDBOX_MODE = os.environ.get("TERMINAL_SANDBOX_MODE", "docker").lower(
TERMINAL_SANDBOX_IMAGE = os.environ.get("TERMINAL_SANDBOX_IMAGE", "python:3.11-slim")
TERMINAL_SANDBOX_MOUNT_PATH = os.environ.get("TERMINAL_SANDBOX_MOUNT_PATH", "/workspace")
TERMINAL_SANDBOX_SHELL = os.environ.get("TERMINAL_SANDBOX_SHELL", "/bin/bash")
TERMINAL_SANDBOX_NETWORK = os.environ.get("TERMINAL_SANDBOX_NETWORK", "none")
TERMINAL_SANDBOX_NETWORK = os.environ.get("TERMINAL_SANDBOX_NETWORK", "bridge")
TERMINAL_SANDBOX_CPUS = os.environ.get("TERMINAL_SANDBOX_CPUS", "")
TERMINAL_SANDBOX_MEMORY = os.environ.get("TERMINAL_SANDBOX_MEMORY", "")
TERMINAL_SANDBOX_BINDS = _parse_bindings(os.environ.get("TERMINAL_SANDBOX_BINDS", ""))

View File

@ -4,7 +4,7 @@ import asyncio
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Set
from typing import Dict, List, Optional, Set, TYPE_CHECKING
from datetime import datetime
try:
@ -54,11 +54,15 @@ from modules.personalization_manager import (
load_personalization_config,
build_personalization_prompt,
)
from modules.container_monitor import collect_stats, inspect_state
from core.tool_config import TOOL_CATEGORIES
from utils.api_client import DeepSeekClient
from utils.context_manager import ContextManager
from utils.logger import setup_logger
if TYPE_CHECKING:
from modules.user_container_manager import ContainerHandle
logger = setup_logger(__name__)
# 临时禁用长度检查
DISABLE_LENGTH_CHECK = True
@ -68,6 +72,7 @@ class MainTerminal:
project_path: str,
thinking_mode: bool = False,
data_dir: Optional[str] = None,
container_session: Optional["ContainerHandle"] = None,
):
self.project_path = project_path
self.thinking_mode = thinking_mode # False=快速模式, True=思考模式
@ -81,10 +86,14 @@ class MainTerminal:
self.container_cpu_limit = TERMINAL_SANDBOX_CPUS or "未限制"
self.container_memory_limit = TERMINAL_SANDBOX_MEMORY or "未限制"
self.project_storage_limit = f"{PROJECT_MAX_STORAGE_MB}MB" if PROJECT_MAX_STORAGE_MB else "未限制"
self.project_storage_limit_bytes = (
PROJECT_MAX_STORAGE_MB * 1024 * 1024 if PROJECT_MAX_STORAGE_MB else None
)
self.container_session: Optional["ContainerHandle"] = None
self.memory_manager = MemoryManager(data_dir=str(self.data_dir))
self.file_manager = FileManager(project_path)
self.file_manager = FileManager(project_path, container_session=container_session)
self.search_engine = SearchEngine()
self.terminal_ops = TerminalOperator(project_path)
self.terminal_ops = TerminalOperator(project_path, container_session=container_session)
self.ocr_client = OCRClient(project_path, self.file_manager)
# 新增:终端管理器
@ -93,8 +102,10 @@ class MainTerminal:
max_terminals=MAX_TERMINALS,
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
terminal_display_size=TERMINAL_DISPLAY_SIZE,
broadcast_callback=None # CLI模式不需要广播
broadcast_callback=None, # CLI模式不需要广播
container_session=container_session,
)
self._apply_container_session(container_session)
self.todo_manager = TodoManager(self.context_manager)
self.sub_agent_manager = SubAgentManager(
@ -145,6 +156,22 @@ class MainTerminal:
#self.context_manager._web_terminal_callback = message_callback
#self.context_manager._focused_files = self.focused_files # 引用传递
def _apply_container_session(self, session: Optional["ContainerHandle"]):
self.container_session = session
if session and session.mode == "docker":
self.container_mount_path = session.mount_path or (TERMINAL_SANDBOX_MOUNT_PATH or "/workspace")
else:
self.container_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace"
def update_container_session(self, session: Optional["ContainerHandle"]):
self._apply_container_session(session)
if getattr(self, "terminal_manager", None):
self.terminal_manager.update_container_session(session)
if getattr(self, "terminal_ops", None):
self.terminal_ops.set_container_session(session)
if getattr(self, "file_manager", None):
self.file_manager.set_container_session(session)
def _ensure_conversation(self):
"""确保CLI模式下存在可用的对话ID"""
@ -856,7 +883,49 @@ class MainTerminal:
主记忆: {memory_stats['main_memory']['lines']}
任务记忆: {memory_stats['task_memory']['lines']}
"""
container_report = self._container_status_report()
if container_report:
status_text += container_report
print(status_text)
def _container_status_report(self) -> str:
session = getattr(self, "container_session", None)
if not session or session.mode != "docker":
return ""
stats = collect_stats(session.container_name, session.sandbox_bin)
state = inspect_state(session.container_name, session.sandbox_bin)
lines = [f" 容器: {session.container_name or '未知'}"]
if stats:
cpu = stats.get("cpu_percent")
mem = stats.get("memory", {})
net = stats.get("net_io", {})
block = stats.get("block_io", {})
lines.append(f" CPU: {cpu:.2f}%" if cpu is not None else " CPU: 未知")
if mem:
used = mem.get("used_bytes")
limit = mem.get("limit_bytes")
percent = mem.get("percent")
mem_line = " 内存: "
if used is not None:
mem_line += f"{used / (1024 * 1024):.2f}MB"
if limit:
mem_line += f" / {limit / (1024 * 1024):.2f}MB"
if percent is not None:
mem_line += f" ({percent:.2f}%)"
lines.append(mem_line)
if net:
rx = net.get("rx_bytes") or 0
tx = net.get("tx_bytes") or 0
lines.append(f" 网络: ↓{rx/1024:.1f}KB ↑{tx/1024:.1f}KB")
if block:
read = block.get("read_bytes") or 0
write = block.get("write_bytes") or 0
lines.append(f" 磁盘: 读 {read/1024:.1f}KB / 写 {write/1024:.1f}KB")
else:
lines.append(" 指标: 暂无")
if state:
lines.append(f" 状态: {state.get('status')}")
return "\n".join(lines) + "\n"
async def save_state(self):
"""保存状态"""

View File

@ -1,7 +1,7 @@
# core/web_terminal.py - Web终端集成对话持久化
import json
from typing import Dict, List, Optional, Callable
from typing import Dict, List, Optional, Callable, TYPE_CHECKING
from core.main_terminal import MainTerminal
from utils.logger import setup_logger
try:
@ -15,6 +15,9 @@ except ImportError:
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
from modules.terminal_manager import TerminalManager
if TYPE_CHECKING:
from modules.user_container_manager import ContainerHandle
logger = setup_logger(__name__)
class WebTerminal(MainTerminal):
@ -46,10 +49,11 @@ class WebTerminal(MainTerminal):
project_path: str,
thinking_mode: bool = False,
message_callback: Optional[Callable] = None,
data_dir: Optional[str] = None
data_dir: Optional[str] = None,
container_session: Optional["ContainerHandle"] = None,
):
# 调用父类初始化(包含对话持久化功能)
super().__init__(project_path, thinking_mode, data_dir=data_dir)
super().__init__(project_path, thinking_mode, data_dir=data_dir, container_session=container_session)
# Web特有属性
self.message_callback = message_callback
@ -64,7 +68,8 @@ class WebTerminal(MainTerminal):
max_terminals=MAX_TERMINALS,
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
terminal_display_size=TERMINAL_DISPLAY_SIZE,
broadcast_callback=message_callback
broadcast_callback=message_callback,
container_session=self.container_session
)
print(f"[WebTerminal] 初始化完成,项目路径: {project_path}")
@ -257,6 +262,7 @@ class WebTerminal(MainTerminal):
conversation_stats = self.context_manager.get_conversation_statistics()
# 构建状态信息
limit_bytes = getattr(self, "project_storage_limit_bytes", None)
status = {
"project_path": self.project_path,
"thinking_mode": self.thinking_mode,
@ -271,7 +277,10 @@ class WebTerminal(MainTerminal):
"terminals": terminal_status,
"project": {
"total_files": structure['total_files'],
"total_size": structure['total_size']
"total_size": structure['total_size'],
"limit_bytes": limit_bytes,
"limit_label": self.project_storage_limit,
"usage_percent": (structure['total_size'] / limit_bytes * 100) if limit_bytes else None
},
"memory": {
"main": memory_stats['main_memory']['lines'],

39
doc/phase2_summary.md Normal file
View File

@ -0,0 +1,39 @@
# Phase 2 Summary 单用户容器与资源可视化
## 交付成果
1. **单用户-单容器架构落地**
- 新增 `modules/user_container_manager.py`Web/CLI 登录即分配专属容器,退出或空闲超时自动释放。
- 终端会话、一次性命令、Toolbox 都复用该容器的 PID/网络空间,彻底与其它租户隔离。
- `.env`/`config/terminal.py` 默认启用 `bridge` 网络,新 Dockerfile 内置 `iputils-ping`,在受控环境下具备最小联网能力。
2. **文件操作容器化**
- `modules/container_file_proxy.py` 将 FileManager 的读写/搜索/补丁全部转发至容器内脚本执行。
- 宿主机仅保留挂载点与备份逻辑,用户代码不会直接触碰宿主文件系统,横向越权窗口显著收窄。
3. **容器监控与审计**
- `modules/container_monitor.py` 提供 `docker stats`/`inspect` 的解析,`logs/container_stats.log` 保留采样轨迹。
- `/api/status`、CLI `/status` 命令和新的 `/api/container-status`、`/api/project-storage` 轮询接口都会返回 CPU/内存/网络速率与磁盘配额使用情况。
4. **前端用量面板升级**
- “用量统计”抽屉拆成 Token 区 + 容器资源卡片,实时展示 CPU/内存/网络上下行速率与项目配额占用。
- 轮询间隔:容器指标 0.5s、项目存储 5s异常情况宿主机模式、容器停止均有提示。
## 影响评估
- **安全面**:终端 + FileManager 都锁在用户容器内,结合 1GB/0.5 vCPU 限制与 `bridge` 网络,大幅降低对宿主机的侵入风险。
- **可观测性**:管理员能通过 CLI/Web 即时查看资源走势,日志中也有容器采样点,方便审计与容量规划。
- **用户体验**:联网需求不再需要回退宿主机;用量面板让使用者清楚看到自己消耗的配额。
## 仍待处理
- 鉴权/速率限制、CSRF 体系仍未落地,下一阶段需要引入 Flask-Limiter、CSRF token 以及更严格的会话管理。
- 用户与会话数据依旧保存在 JSON 文件,建议迁移到数据库并加入加密/备份策略。
- 文件上传缺乏内容扫描与 MIME 校验,后续需要联动杀毒/白名单策略。
## 验证
- `python3 -m py_compile` 检查核心模块语法。
- Web UI/CLI 手动验证:登录创建容器、执行命令、上传/下载文件,观察实时监控数据。
- `users/jojo/project/test_scripts/memory_pressure_test.py` 用于确认 1GB 内存限制,`docker stats` 监控 CPU/Mem/Net。
- 构建镜像 `docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest .` 并在 `.env` 中启用 `TERMINAL_SANDBOX_NETWORK=bridge`

View File

@ -21,6 +21,11 @@
- ✅ **资源与并发控制**:新增 `PROJECT_MAX_STORAGE_MB`、`MAX_ACTIVE_USER_CONTAINERS` 等限制,写入前会进行配额检查,活跃用户达到上限时提供 `resource_busy` 页面提示。
- ✅ **系统提示**:模型接收的环境信息仅包含容器挂载路径与资源上限,不再暴露宿主机真实路径。
### 1.4 第二阶段增量成果
- ✅ **单用户-单容器 + 文件代理**:每次登录都会启动专属容器(`modules/user_container_manager.py`CLI/Web 终端、`run_command`、FileManager 读写都通过容器代理完成,宿主机只负责挂载与备份。
- ✅ **容器监控与前端可视化**`modules/container_monitor.py` 定期采集 `docker stats`/`inspect`UI 用量面板实时展示 CPU/内存/网络速率与磁盘配额,让管理员能快速审计资源。
- ✅ **联网配置与镜像补强**Dockerfile 新增 `iputils-ping``.env` 默认启用 `bridge` 网络,确保容器在受控环境下具备最小联网能力。
### 1.2 关键数据资产
| 资产 | 存储位置 | 备注 |
| --- | --- | --- |
@ -45,11 +50,11 @@
| # | 严重度 | 问题 & 证据 | 影响 | 建议 |
| --- | --- | --- | --- | --- |
| 1 | Critical | **执行环境无隔离**`modules/persistent_terminal.py:87-178` 直接在宿主机上 `subprocess.Popen` shell所有命令与文件操作共享真实系统。 | 任意 Web 用户可读取/修改宿主机文件、横向移动、破坏系统。 | 必须引入容器/VM 沙箱将项目、依赖与命令执行限制在受控环境并对资源设置限额CPU/Mem/IO。 |
| 1 | Critical | **执行环境隔离就绪**:实时终端、`run_command` 与 FileManager 统统绑定用户专属容器,`modules/container_file_proxy.py` 保证写入只发生在 `/workspace`,宿主机仅承载挂载目录。 | Web 用户仅能访问自己容器内的文件/进程;若容器崩溃可在宿主机安全回收。 | 持续关注容器逃逸与 runtime 补丁,下一步考虑 rootless Docker / gVisor 进一步降低宿主暴露面。 |
| 2 | Critical | **明文 API Key / Secret**`config/api.py:3-25` 存在硬编码模型 key`config/auth.py:1-7` 暴露管理员用户名 + 哈希。 | 仓库一旦共享即泄漏密钥;攻击者可伪造管理员账户或重放 API 请求。 | 将所有 secrets 挪到环境变量 / Secret Manager删除仓库中的明文在启动时校验缺省值并阻止运行。 |
| 3 | High | **Flask SECRET_KEY 固定且公开**`web_server.py:54-58` 将 `SECRET_KEY='your-secret-key-here'` 写死,且默认启用 `CORS(*)`。 | 攻击者可伪造 session cookie、冒充任意用户、解密/篡改会话。 | 将 SECRET_KEY 存储于环境变量,启用 `SESSION_COOKIE_SECURE/HTTPONLY/SAMESITE`,并限制 CORS 源。 |
| 4 | High | **鉴权与速率限制缺失**:登录接口没有 CAPTCHA/速率限制;`api_login_required` 仅检查 sessionSocket.IO 连接未绑定 IP/指纹。 | 暴力破解、会话劫持、重放攻击均无防护;一旦 cookie 泄漏即可接管终端。 | 引入 Flask-Limiter 等中间件记录失败次数必要时强制多因子或设备锁定WebSocket 握手附带一次性 token。 |
| 5 | High | **多租户数据无物理隔离**:虽然 `UserManager.ensure_user_workspace` 为每个用户创建子目录,但同一进程拥有全部路径读写权限,且 FileManager (`modules/file_manager.py`) 仅按“项目根目录”限制,无法阻止管理员会话访问他人目录。 | 横向越权风险高,任何被攻破的账号都可以遍历/窃取其他租户数据。 | 将每个用户放入独立容器/VM并由调度服务负责映射API 层使用数据库/ACL 校验 user_id 与路径的对应关系。 |
| 5 | High | **多租户容器化 + 读写代理**UserManager 登录即创建独立容器FileManager 通过容器内脚本执行 create/read/write/modify宿主机不再直接接触用户代码。 | 横向越权面大幅收窄:除非容器逃逸,否则无法读写他人目录。 | 下一步需在 API 层加上 workspace ACL 与审计日志,防止管理员 session 滥用。 |
| 6 | Medium | **用户与会话数据存储在本地 JSON**`modules/user_manager.py:167-195` 将账号、密码哈希、邀请码写入 `data/users.json`,没有备份策略、并发安全或加密。 | 易被本地用户读取/篡改;当并发写入时有数据损坏风险,也无法满足审计/恢复。 | 引入关系型数据库或托管身份服务;对敏感字段做透明加密,提供备份与迁移策略。 |
| 7 | Medium | **缺乏 CSRF、防重放与安全头部**:所有 POST/PUT 接口(如 `/api/gui/files/*`, `/api/upload`)均未校验 CSRF token。 | 登陆态用户可被恶意网站诱导发起操作(上传/删除/运行命令)。 | 在 Flask 层加入 CSRF 保护WTF-CSRF 或自定义 token并添加 Strict-Transport-Security、Content-Security-Policy 等响应头。 |
| 8 | Medium | **文件上传仅做文件名校验**`web_server.py:841-907, 985-1069` 只调用 `sanitize_filename_preserve_unicode`,但未检测 MIME、内容或执行权限。 | 可上传脚本并经终端执行;针对 Windows/Unix 的路径和符号链接处理也未覆盖。 | 引入内容扫描ClamAV/自建规则)、限制文件类型/数量,并将上传目录与执行目录隔离。 |

View File

@ -17,7 +17,8 @@ RUN apt-get update && \
zip \
unzip \
locales \
tzdata && \
tzdata \
iputils-ping && \
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen && \
locale-gen && \
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,495 @@
"""Utilities to proxy FileManager operations into user containers."""
from __future__ import annotations
import json
import subprocess
import shutil
from pathlib import Path
from typing import Dict, Optional, Any, TYPE_CHECKING
CONTAINER_FILE_SCRIPT = r"""
import json
import sys
import pathlib
import shutil
def _resolve(root: pathlib.Path, rel: str) -> pathlib.Path:
base = root.resolve()
target = (base / rel).resolve()
if not str(target).startswith(str(base)):
raise ValueError("路径越界: %s" % rel)
return target
def _read_text(target: pathlib.Path):
with target.open('r', encoding='utf-8') as fh:
data = fh.read()
lines = data.splitlines(keepends=True)
return data, lines
def _ensure_file(target: pathlib.Path):
if not target.exists():
return {"success": False, "error": "文件不存在"}
if not target.is_file():
return {"success": False, "error": "不是文件"}
return None
def _create_file(root, payload):
rel = payload.get("path")
target = _resolve(root, rel)
target.parent.mkdir(parents=True, exist_ok=True)
content = payload.get("content") or ""
with target.open('w', encoding='utf-8') as fh:
fh.write(content)
return {"success": True, "path": rel, "size": len(content)}
def _delete_file(root, payload):
rel = payload.get("path")
target = _resolve(root, rel)
err = _ensure_file(target)
if err:
return err
target.unlink()
return {"success": True, "path": rel, "action": "deleted"}
def _rename_file(root, payload):
old_rel = payload.get("old_path")
new_rel = payload.get("new_path")
old = _resolve(root, old_rel)
new = _resolve(root, new_rel)
if not old.exists():
return {"success": False, "error": "原文件不存在"}
if new.exists():
return {"success": False, "error": "目标文件已存在"}
new.parent.mkdir(parents=True, exist_ok=True)
old.rename(new)
return {
"success": True,
"old_path": old_rel,
"new_path": new_rel,
"action": "renamed"
}
def _create_folder(root, payload):
rel = payload.get("path")
target = _resolve(root, rel)
if target.exists():
return {"success": False, "error": "文件夹已存在"}
target.mkdir(parents=True, exist_ok=True)
return {"success": True, "path": rel}
def _delete_folder(root, payload):
rel = payload.get("path")
target = _resolve(root, rel)
if not target.exists():
return {"success": False, "error": "文件夹不存在"}
if not target.is_dir():
return {"success": False, "error": "不是文件夹"}
shutil.rmtree(target)
return {"success": True, "path": rel}
def _read_file(root, payload):
rel = payload.get("path")
limit = payload.get("size_limit")
target = _resolve(root, rel)
err = _ensure_file(target)
if err:
return err
size = target.stat().st_size
if limit and size > limit:
return {
"success": False,
"error": f"文件太大 ({size} 字节),超过限制"
}
with target.open('r', encoding='utf-8') as fh:
content = fh.read()
return {"success": True, "path": rel, "content": content, "size": size}
def _read_text_segment(root, payload):
rel = payload.get("path")
start = payload.get("start_line")
end = payload.get("end_line")
limit = payload.get("size_limit")
target = _resolve(root, rel)
err = _ensure_file(target)
if err:
return err
size = target.stat().st_size
if limit and size > limit:
return {
"success": False,
"error": f"文件太大 ({size} 字节),超过限制"
}
data, lines = _read_text(target)
total = len(lines)
line_start = start if start and start > 0 else 1
line_end = end if end and end >= line_start else total
if line_start > total:
return {"success": False, "error": "起始行超出文件长度"}
line_end = min(line_end, total)
snippet = "".join(lines[line_start - 1 : line_end])
return {
"success": True,
"path": rel,
"content": snippet,
"size": size,
"line_start": line_start,
"line_end": line_end,
"total_lines": total
}
def _search_text(root, payload):
rel = payload.get("path")
target = _resolve(root, rel)
err = _ensure_file(target)
if err:
return err
data, lines = _read_text(target)
total = len(lines)
query = payload.get("query") or ""
if not query:
return {"success": False, "error": "缺少搜索关键词"}
max_matches = payload.get("max_matches") or 10
before = payload.get("context_before") or 2
after = payload.get("context_after") or 2
case_sensitive = bool(payload.get("case_sensitive"))
query_cmp = query if case_sensitive else query.lower()
def contains(text):
text_cmp = text if case_sensitive else text.lower()
return query_cmp in text_cmp
matches = []
for idx, line in enumerate(lines, start=1):
if contains(line):
win_start = max(1, idx - before)
win_end = min(total, idx + after)
if matches and win_start <= matches[-1]["line_end"]:
matches[-1]["line_end"] = max(matches[-1]["line_end"], win_end)
matches[-1]["hits"].append(idx)
else:
if len(matches) >= max_matches:
break
matches.append({
"line_start": win_start,
"line_end": win_end,
"hits": [idx]
})
for window in matches:
snippet_lines = lines[window["line_start"] - 1 : window["line_end"]]
window["snippet"] = "".join(snippet_lines)
return {
"success": True,
"path": rel,
"size": target.stat().st_size,
"total_lines": total,
"matches": matches
}
def _extract_segments(root, payload):
rel = payload.get("path")
target = _resolve(root, rel)
err = _ensure_file(target)
if err:
return err
segments = payload.get("segments") or []
if not segments:
return {"success": False, "error": "缺少要提取的行区间"}
_, lines = _read_text(target)
total = len(lines)
extracted = []
for spec in segments:
start = spec.get("start_line")
end = spec.get("end_line")
label = spec.get("label")
if start is None or end is None:
return {"success": False, "error": "segments 中缺少 start_line 或 end_line"}
if start <= 0 or end < start:
return {"success": False, "error": "行区间不合法"}
if start > total:
return {"success": False, "error": f"区间起点 {start} 超出文件行数"}
end = min(end, total)
snippet = "".join(lines[start - 1 : end])
extracted.append({
"label": label,
"line_start": start,
"line_end": end,
"content": snippet
})
return {
"success": True,
"path": rel,
"size": target.stat().st_size,
"total_lines": total,
"segments": extracted
}
def _write_file(root, payload):
rel = payload.get("path")
content = payload.get("content") or ""
mode = payload.get("mode") or "w"
target = _resolve(root, rel)
target.parent.mkdir(parents=True, exist_ok=True)
with target.open(mode, encoding='utf-8') as fh:
fh.write(content)
return {
"success": True,
"path": rel,
"size": len(content),
"mode": mode
}
def _apply_modify_blocks(root, payload):
rel = payload.get("path")
blocks = payload.get("blocks") or []
target = _resolve(root, rel)
err = _ensure_file(target)
if err:
return err
original, _ = _read_text(target)
current = original
results = []
completed = []
failed = []
for block in blocks:
idx = block.get("index")
old_text = (block.get("old") or "").replace('\r\n', '\n')
new_text = (block.get("new") or "").replace('\r\n', '\n')
record = {
"index": idx,
"status": "pending",
"removed_lines": 0,
"added_lines": 0,
"reason": None,
"hint": None
}
if old_text is None or new_text is None:
record.update({
"status": "error",
"reason": "缺少 OLD 或 NEW 内容",
"hint": "请确认补丁是否完整。"
})
failed.append({"index": idx, "reason": "缺少 OLD/NEW"})
results.append(record)
continue
if not old_text:
record.update({
"status": "error",
"reason": "OLD 内容不能为空",
"hint": "请确认要替换的原文是否准确复制。"
})
failed.append({"index": idx, "reason": "OLD 为空"})
results.append(record)
continue
pos = current.find(old_text)
if pos == -1:
record.update({
"status": "not_found",
"reason": "未找到匹配的原文,请确认是否完全复制",
"hint": "可使用终端或搜索确认原文。"
})
failed.append({"index": idx, "reason": "未找到匹配"})
results.append(record)
continue
current = current[:pos] + new_text + current[pos + len(old_text):]
removed_lines = old_text.count('\n')
added_lines = new_text.count('\n')
if old_text and not old_text.endswith('\n'):
removed_lines += 1
if new_text and not new_text.endswith('\n'):
added_lines += 1
record.update({
"status": "success",
"removed_lines": removed_lines,
"added_lines": added_lines
})
completed.append(idx)
results.append(record)
write_performed = False
error = None
if completed:
try:
with target.open('w', encoding='utf-8') as fh:
fh.write(current)
write_performed = True
except Exception as exc:
error = f"写入文件失败: {exc}"
try:
with target.open('w', encoding='utf-8') as fh:
fh.write(original)
except Exception:
pass
return {
"success": bool(completed) and not failed and error is None,
"completed": completed,
"failed": failed,
"results": results,
"write_performed": write_performed,
"error": error
}
def _edit_lines(root, payload):
rel = payload.get("path")
start_line = int(payload.get("start_line") or 1)
end_line = int(payload.get("end_line") or start_line)
content = payload.get("content") or ""
operation = payload.get("operation")
target = _resolve(root, rel)
err = _ensure_file(target)
if err:
return err
if start_line < 1:
return {"success": False, "error": "行号必须从1开始"}
if end_line < start_line:
return {"success": False, "error": "结束行号不能小于起始行号"}
with target.open('r', encoding='utf-8') as fh:
lines = fh.readlines()
total = len(lines)
if start_line > total:
if operation == "insert":
lines.extend([''] * (start_line - total - 1))
lines.append(content if content.endswith('\n') else content + '\n')
affected = len(content.splitlines() or [''])
else:
return {"success": False, "error": f"起始行号 {start_line} 超出文件范围 (共 {total} 行)"}
else:
if end_line > total:
return {"success": False, "error": f"结束行号 {end_line} 超出文件范围 (共 {total} 行)"}
start_idx = start_line - 1
end_idx = end_line
if operation == "replace":
new_lines = content.split('\n') if '\n' in content else [content]
formatted = []
for i, line in enumerate(new_lines):
if i < len(new_lines) - 1 or (end_idx < len(lines) and lines[end_idx - 1].endswith('\n')):
formatted.append(line + '\n' if not line.endswith('\n') else line)
else:
formatted.append(line)
lines[start_idx:end_idx] = formatted
affected = end_line - start_line + 1
elif operation == "insert":
new_lines = content.split('\n') if '\n' in content else [content]
formatted = [line + '\n' if not line.endswith('\n') else line for line in new_lines]
lines[start_idx:start_idx] = formatted
affected = len(formatted)
elif operation == "delete":
del lines[start_idx:end_idx]
affected = end_line - start_line + 1
else:
return {"success": False, "error": f"未知的操作类型: {operation}"}
with target.open('w', encoding='utf-8') as fh:
fh.writelines(lines)
return {
"success": True,
"path": rel,
"operation": operation,
"affected_lines": affected
}
HANDLERS = {
"create_file": _create_file,
"delete_file": _delete_file,
"rename_file": _rename_file,
"create_folder": _create_folder,
"delete_folder": _delete_folder,
"read_file": _read_file,
"read_text_segment": _read_text_segment,
"search_text": _search_text,
"extract_segments": _extract_segments,
"write_file": _write_file,
"apply_modify_blocks": _apply_modify_blocks,
"edit_lines_range": _edit_lines,
}
def main():
raw = sys.stdin.read()
if not raw:
raise RuntimeError("空请求")
request = json.loads(raw)
root = pathlib.Path(request["root"])
action = request["action"]
payload = request.get("payload") or {}
handler = HANDLERS.get(action)
if not handler:
raise RuntimeError(f"未知操作: {action}")
result = handler(root, payload)
sys.stdout.write(json.dumps(result, ensure_ascii=False))
if __name__ == "__main__":
try:
main()
except Exception as exc:
sys.stdout.write(json.dumps({"success": False, "error": str(exc)}, ensure_ascii=False))
"""
if TYPE_CHECKING:
from modules.user_container_manager import ContainerHandle
class ContainerFileProxy:
"""Execute file operations inside a Docker container."""
def __init__(self, container_session: "ContainerHandle"):
self.container_session = container_session
def is_available(self) -> bool:
return bool(
self.container_session
and self.container_session.mode == "docker"
and self.container_session.container_name
)
def update_session(self, session: Optional["ContainerHandle"]):
self.container_session = session
def run(self, action: str, payload: Dict[str, Any]) -> Dict[str, Any]:
if not self.is_available():
return {"success": False, "error": "容器未就绪,无法执行文件操作"}
session = self.container_session
docker_bin = session.sandbox_bin or shutil.which("docker")
if not docker_bin:
return {"success": False, "error": "未找到 Docker 运行时"}
request = {
"action": action,
"root": session.mount_path or "/workspace",
"payload": payload,
}
cmd = [docker_bin, "exec", "-i"]
if session.mount_path:
cmd.extend(["-w", session.mount_path])
cmd.append(session.container_name)
cmd.extend(["python3", "-c", CONTAINER_FILE_SCRIPT])
try:
completed = subprocess.run(
cmd,
input=json.dumps(request, ensure_ascii=False),
text=True,
capture_output=True,
check=False,
timeout=60,
)
except (OSError, subprocess.SubprocessError) as exc:
return {"success": False, "error": f"容器执行失败: {exc}"}
if completed.returncode != 0:
stderr = (completed.stderr or "").strip()
stdout = (completed.stdout or "").strip()
message = stderr or stdout or "未知错误"
return {"success": False, "error": f"容器返回错误: {message}"}
output = completed.stdout or ""
output = output.strip()
if not output:
return {"success": False, "error": "容器未返回任何结果"}
try:
return json.loads(output)
except json.JSONDecodeError:
return {
"success": False,
"error": f"容器响应无法解析: {output[:200]}",
}

View File

@ -0,0 +1,161 @@
"""Collect resource metrics for Docker-based user containers."""
from __future__ import annotations
import json
import re
import shutil
import subprocess
import time
from typing import Dict, Optional, Tuple
SIZE_UNITS = {
"b": 1,
"kb": 1000,
"kib": 1024,
"mb": 1000 ** 2,
"mib": 1024 ** 2,
"gb": 1000 ** 3,
"gib": 1024 ** 3,
"tb": 1000 ** 4,
"tib": 1024 ** 4,
}
def _parse_size(value: str) -> Optional[int]:
if not value:
return None
value = value.strip()
if value in {"", "0"}:
return 0
match = re.match(r"([\d\.]+)\s*([a-zA-Z]+)", value)
if not match:
try:
return int(float(value))
except ValueError:
return None
number = float(match.group(1))
unit = match.group(2).lower()
multiplier = SIZE_UNITS.get(unit, 1)
return int(number * multiplier)
def _parse_pair(value: str) -> Tuple[Optional[int], Optional[int]]:
if not value:
return (None, None)
parts = [part.strip() for part in value.split("/", 1)]
if len(parts) == 1:
return _parse_size(parts[0]), None
return _parse_size(parts[0]), _parse_size(parts[1])
def _parse_percent(value: str) -> Optional[float]:
if not value:
return None
value = value.strip().rstrip("%")
try:
return float(value)
except ValueError:
return None
def collect_stats(container_name: str, docker_bin: Optional[str] = None) -> Optional[Dict]:
"""Return docker stats metrics for a running container."""
docker_bin = docker_bin or shutil.which("docker")
if not docker_bin:
return None
cmd = [
docker_bin,
"stats",
container_name,
"--no-stream",
"--format",
"{{json .}}",
]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=False,
timeout=5,
)
except (OSError, subprocess.SubprocessError):
return None
if result.returncode != 0:
return None
raw_output = (result.stdout or "").strip()
if not raw_output:
return None
try:
data = json.loads(raw_output.splitlines()[-1])
except json.JSONDecodeError:
return None
mem_used, mem_limit = _parse_pair(data.get("MemUsage"))
net_rx, net_tx = _parse_pair(data.get("NetIO"))
block_read, block_write = _parse_pair(data.get("BlockIO"))
return {
"timestamp": time.time(),
"cpu_percent": _parse_percent(data.get("CPUPerc")),
"memory": {
"used_bytes": mem_used,
"limit_bytes": mem_limit,
"percent": _parse_percent(data.get("MemPerc")),
"raw": data.get("MemUsage"),
},
"net_io": {
"rx_bytes": net_rx,
"tx_bytes": net_tx,
"raw": data.get("NetIO"),
},
"block_io": {
"read_bytes": block_read,
"write_bytes": block_write,
"raw": data.get("BlockIO"),
},
"pids": int(data.get("PIDs", 0)) if data.get("PIDs") else None,
"raw": data,
}
def inspect_state(container_name: str, docker_bin: Optional[str] = None) -> Optional[Dict]:
docker_bin = docker_bin or shutil.which("docker")
if not docker_bin:
return None
cmd = [
docker_bin,
"inspect",
"-f",
"{{json .State}}",
container_name,
]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=False,
timeout=5,
)
except (OSError, subprocess.SubprocessError):
return None
if result.returncode != 0:
return None
raw = (result.stdout or "").strip()
if not raw:
return None
try:
state = json.loads(raw)
except json.JSONDecodeError:
return None
return {
"status": state.get("Status"),
"running": state.get("Running"),
"started_at": state.get("StartedAt"),
"finished_at": state.get("FinishedAt"),
"pid": state.get("Pid"),
"error": state.get("Error"),
"exit_code": state.get("ExitCode"),
}

View File

@ -3,7 +3,7 @@
import os
import shutil
from pathlib import Path
from typing import Optional, Dict, List, Tuple
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING
from datetime import datetime
try:
from config import (
@ -28,11 +28,44 @@ except ImportError: # 兼容全局环境中存在同名包的情况
READ_TOOL_MAX_FILE_SIZE,
PROJECT_MAX_STORAGE_BYTES,
)
from modules.container_file_proxy import ContainerFileProxy
if TYPE_CHECKING:
from modules.user_container_manager import ContainerHandle
# 临时禁用长度检查
DISABLE_LENGTH_CHECK = True
class FileManager:
def __init__(self, project_path: str):
def __init__(self, project_path: str, container_session: Optional["ContainerHandle"] = None):
self.project_path = Path(project_path).resolve()
self.container_session: Optional["ContainerHandle"] = None
self._container_proxy: Optional[ContainerFileProxy] = None
self.set_container_session(container_session)
def set_container_session(self, container_session: Optional["ContainerHandle"]):
self.container_session = container_session
if (
container_session
and container_session.mode == "docker"
and container_session.container_name
):
if self._container_proxy is None:
self._container_proxy = ContainerFileProxy(container_session)
else:
self._container_proxy.update_session(container_session)
else:
self._container_proxy = None
def _use_container(self) -> bool:
return self._container_proxy is not None and self._container_proxy.is_available()
def _container_call(self, action: str, payload: Dict) -> Dict:
if not self._use_container():
return {
"success": False,
"error": "容器未就绪,无法执行文件操作"
}
return self._container_proxy.run(action, payload)
def _get_project_size(self) -> int:
"""计算项目目录的总大小(字节)"""
@ -94,6 +127,9 @@ class FileManager:
return False, f"禁止访问系统目录: {forbidden}", None
return True, "", full_path
def _relative_path(self, full_path: Path) -> str:
return str(full_path.relative_to(self.project_path))
def create_file(self, path: str, content: str = "", file_type: str = "txt") -> Dict:
"""创建文件"""
@ -104,6 +140,7 @@ class FileManager:
# 添加文件扩展名
if not full_path.suffix:
full_path = full_path.with_suffix(f".{file_type}")
relative_path = self._relative_path(full_path)
try:
if full_path.parent == self.project_path:
@ -112,14 +149,21 @@ class FileManager:
"error": "禁止在项目根目录直接创建文件,请先创建或选择子目录。",
"suggestion": "创建文件所属文件夹,在其中创建新文件。"
}
if self._use_container():
result = self._container_call("create_file", {
"path": relative_path,
"content": ""
})
if result.get("success"):
print(f"{OUTPUT_FORMATS['file']} 创建文件: {relative_path}")
return result
# 创建父目录
full_path.parent.mkdir(parents=True, exist_ok=True)
# 固定创建空文件,忽略传入内容
with open(full_path, 'w', encoding='utf-8') as f:
f.write("")
relative_path = str(full_path.relative_to(self.project_path))
print(f"{OUTPUT_FORMATS['file']} 创建文件: {relative_path}")
return {
@ -143,7 +187,15 @@ class FileManager:
return {"success": False, "error": "不是文件"}
try:
relative_path = str(full_path.relative_to(self.project_path))
relative_path = self._relative_path(full_path)
if self._use_container():
result = self._container_call("delete_file", {
"path": relative_path
})
if result.get("success"):
print(f"{OUTPUT_FORMATS['file']} 删除文件: {relative_path}")
return result
full_path.unlink()
print(f"{OUTPUT_FORMATS['file']} 删除文件: {relative_path}")
@ -176,10 +228,19 @@ class FileManager:
return {"success": False, "error": "目标文件已存在"}
try:
old_relative = self._relative_path(full_old_path)
new_relative = self._relative_path(full_new_path)
if self._use_container():
result = self._container_call("rename_file", {
"old_path": old_relative,
"new_path": new_relative
})
if result.get("success"):
print(f"{OUTPUT_FORMATS['file']} 重命名: {old_relative} -> {new_relative}")
return result
full_old_path.rename(full_new_path)
old_relative = str(full_old_path.relative_to(self.project_path))
new_relative = str(full_new_path.relative_to(self.project_path))
print(f"{OUTPUT_FORMATS['file']} 重命名: {old_relative} -> {new_relative}")
return {
@ -201,8 +262,14 @@ class FileManager:
return {"success": False, "error": "文件夹已存在"}
try:
relative_path = self._relative_path(full_path)
if self._use_container():
result = self._container_call("create_folder", {"path": relative_path})
if result.get("success"):
print(f"{OUTPUT_FORMATS['file']} 创建文件夹: {relative_path}")
return result
full_path.mkdir(parents=True, exist_ok=True)
relative_path = str(full_path.relative_to(self.project_path))
print(f"{OUTPUT_FORMATS['file']} 创建文件夹: {relative_path}")
return {"success": True, "path": relative_path}
@ -222,8 +289,14 @@ class FileManager:
return {"success": False, "error": "不是文件夹"}
try:
relative_path = self._relative_path(full_path)
if self._use_container():
result = self._container_call("delete_folder", {"path": relative_path})
if result.get("success"):
print(f"{OUTPUT_FORMATS['file']} 删除文件夹: {relative_path}")
return result
shutil.rmtree(full_path)
relative_path = str(full_path.relative_to(self.project_path))
print(f"{OUTPUT_FORMATS['file']} 删除文件夹: {relative_path}")
return {"success": True, "path": relative_path}
@ -280,6 +353,14 @@ class FileManager:
if not full_path.is_file():
return {"success": False, "error": "不是文件"}
if self._use_container():
relative_path = self._relative_path(full_path)
result = self._container_call("read_file", {
"path": relative_path,
"size_limit": MAX_FILE_SIZE
})
return result
result = self._read_text_lines(full_path, size_limit=MAX_FILE_SIZE)
if not result["success"]:
return result
@ -311,6 +392,35 @@ class FileManager:
if not full_path.is_file():
return {"success": False, "error": "不是文件"}
if self._use_container():
relative_path = self._relative_path(full_path)
result = self._container_call("read_text_segment", {
"path": relative_path,
"start_line": start_line,
"end_line": end_line,
"size_limit": size_limit or READ_TOOL_MAX_FILE_SIZE
})
return result
if self._use_container():
relative_path = self._relative_path(full_path)
return self._container_call("search_text", {
"path": relative_path,
"query": query,
"max_matches": max_matches,
"context_before": context_before,
"context_after": context_after,
"case_sensitive": case_sensitive,
})
if self._use_container():
relative_path = self._relative_path(full_path)
return self._container_call("extract_segments", {
"path": relative_path,
"segments": segments,
"size_limit": size_limit or READ_TOOL_MAX_FILE_SIZE
})
result = self._read_text_lines(
full_path,
size_limit=size_limit or READ_TOOL_MAX_FILE_SIZE
@ -505,6 +615,7 @@ class FileManager:
print(f"{OUTPUT_FORMATS['warning']} 检测到大量转义字符,建议检查内容格式")
try:
relative_path = self._relative_path(full_path)
current_size = self._get_project_size()
existing_size = full_path.stat().st_size if full_path.exists() else 0
if mode == "a":
@ -519,13 +630,23 @@ class FileManager:
"project_size_bytes": current_size,
"attempt_size_bytes": len(content)
}
if self._use_container():
result = self._container_call("write_file", {
"path": relative_path,
"content": content,
"mode": mode
})
if result.get("success"):
action = "覆盖" if mode == "w" else "追加"
print(f"{OUTPUT_FORMATS['file']} {action}文件: {relative_path}")
return result
# 创建父目录
full_path.parent.mkdir(parents=True, exist_ok=True)
with open(full_path, mode, encoding='utf-8') as f:
f.write(content)
relative_path = str(full_path.relative_to(self.project_path))
action = "覆盖" if mode == "w" else "追加"
print(f"{OUTPUT_FORMATS['file']} {action}文件: {relative_path}")
@ -561,6 +682,13 @@ class FileManager:
return {"success": False, "error": "不是文件"}
try:
relative_path = self._relative_path(full_path)
if self._use_container():
return self._container_call("apply_modify_blocks", {
"path": relative_path,
"blocks": blocks
})
with open(full_path, 'r', encoding='utf-8') as f:
original_content = f.read()
except Exception as e:
@ -740,6 +868,16 @@ class FileManager:
return {"success": False, "error": "结束行号不能小于起始行号"}
try:
relative_path = self._relative_path(full_path)
if self._use_container():
return self._container_call("edit_lines_range", {
"path": relative_path,
"start_line": start_line,
"end_line": end_line,
"content": content,
"operation": operation
})
# 读取文件内容
with open(full_path, 'r', encoding='utf-8') as f:
lines = f.readlines()

View File

@ -150,6 +150,7 @@ class PersistentTerminal:
self.execution_mode = "host"
self.using_container = False
self._sandbox_bin_path = None
self._owns_container = False
def start(self) -> bool:
"""启动终端进程(支持容器沙箱)"""
@ -227,6 +228,7 @@ class PersistentTerminal:
def _start_host_terminal(self):
"""启动宿主机终端"""
self.using_container = False
self._owns_container = False
self.is_windows = sys.platform == "win32"
shell_cmd = self.host_shell_command
if self.is_windows:
@ -271,7 +273,7 @@ class PersistentTerminal:
return None
def _start_docker_terminal(self):
"""启动容器化终端"""
"""启动或连接容器化终端"""
docker_bin = self.sandbox_options.get("bin") or "docker"
docker_path = shutil.which(docker_bin)
if not docker_path:
@ -281,6 +283,62 @@ class PersistentTerminal:
print(f"{OUTPUT_FORMATS['warning']} {message}")
return None
self._sandbox_bin_path = docker_path
target_container = self.sandbox_options.get("container_name")
if target_container:
return self._start_existing_container_terminal(docker_path, target_container)
return self._start_new_container_terminal(docker_path)
def _start_existing_container_terminal(self, docker_path: str, container_name: str):
"""通过 docker exec 连接到已有容器。"""
if not self._ensure_container_alive(docker_path, container_name):
raise RuntimeError(f"目标容器未运行: {container_name}")
mount_path = self.sandbox_options.get("mount_path") or "/workspace"
container_workdir = self._resolve_container_workdir(mount_path)
shell_path = self.sandbox_options.get("shell") or "/bin/bash"
cmd = [
docker_path,
"exec",
"-i",
]
if container_workdir:
cmd += ["-w", container_workdir]
envs = {
"PYTHONIOENCODING": "utf-8",
"TERM": "xterm-256color",
}
for key, value in (self.sandbox_options.get("env") or {}).items():
if value is not None:
envs[key] = value
for key, value in envs.items():
cmd += ["-e", f"{key}={value}"]
cmd.append(container_name)
cmd.append(shell_path)
if shell_path.endswith("sh"):
cmd.append("-i")
env = os.environ.copy()
process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
env=env
)
self.sandbox_container_name = container_name
self.shell_command = f"{shell_path} (attach:{container_name})"
self.using_container = True
self.is_windows = False
self._owns_container = False
return process
def _start_new_container_terminal(self, docker_path: str):
"""启动全新的容器终端。"""
image = self.sandbox_options.get("image")
if not image:
raise RuntimeError("TERMINAL_SANDBOX_IMAGE 未配置")
@ -354,11 +412,43 @@ class PersistentTerminal:
return None
self.sandbox_container_name = container_name
self._sandbox_bin_path = docker_path
self.shell_command = f"{shell_path} (sandbox:{image})"
self.using_container = True
self.is_windows = False
self._owns_container = True
return process
def _resolve_container_workdir(self, mount_path: str) -> str:
"""推导容器内工作目录路径。"""
mount_path = (mount_path or "/workspace").rstrip("/") or "/workspace"
try:
relative = self.working_dir.relative_to(self.project_path)
if str(relative) == ".":
return mount_path
return f"{mount_path}/{relative.as_posix()}"
except Exception:
return mount_path
def _ensure_container_alive(self, docker_path: str, container_name: str) -> bool:
"""确认目标容器正在运行。"""
try:
result = subprocess.run(
[
docker_path,
"inspect",
"-f",
"{{.State.Running}}",
container_name,
],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
timeout=3,
check=False,
)
except (OSError, subprocess.SubprocessError):
return False
return result.returncode == 0 and result.stdout.strip().lower() == "true"
def _read_output(self):
"""后台线程:持续读取输出(修复版,正确处理编码)"""
@ -866,7 +956,7 @@ class PersistentTerminal:
def _stop_sandbox_container(self, force: bool = False):
"""确保容器终端被停止"""
if not self.sandbox_container_name or not self._sandbox_bin_path:
if not self._owns_container or not self.sandbox_container_name or not self._sandbox_bin_path:
return
try:
subprocess.run(
@ -881,3 +971,4 @@ class PersistentTerminal:
print(f"{OUTPUT_FORMATS['warning']} 强制终止容器 {self.sandbox_container_name} 失败,可能已退出。")
finally:
self.sandbox_container_name = None
self._owns_container = False

View File

@ -1,7 +1,7 @@
# modules/terminal_manager.py - 终端会话管理器
import json
from typing import Dict, List, Optional, Callable
from typing import Dict, List, Optional, Callable, TYPE_CHECKING
from pathlib import Path
from datetime import datetime
try:
@ -56,6 +56,9 @@ except ImportError:
from modules.persistent_terminal import PersistentTerminal
from utils.terminal_factory import TerminalFactory
if TYPE_CHECKING:
from modules.user_container_manager import ContainerHandle
class TerminalManager:
"""管理多个终端会话"""
@ -67,7 +70,8 @@ class TerminalManager:
terminal_display_size: int = None,
broadcast_callback: Callable = None,
sandbox_mode: Optional[str] = None,
sandbox_options: Optional[Dict] = None
sandbox_options: Optional[Dict] = None,
container_session: Optional["ContainerHandle"] = None,
):
"""
初始化终端管理器
@ -87,7 +91,8 @@ class TerminalManager:
self.max_snapshot_lines = TERMINAL_SNAPSHOT_MAX_LINES
self.max_snapshot_chars = TERMINAL_SNAPSHOT_MAX_CHARS
self.broadcast = broadcast_callback
self.sandbox_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
self.default_sandbox_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
self.sandbox_mode = self.default_sandbox_mode
default_sandbox_options = {
"image": TERMINAL_SANDBOX_IMAGE,
"mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
@ -111,6 +116,10 @@ class TerminalManager:
else:
default_sandbox_options[key] = value
self.sandbox_options = default_sandbox_options
self.container_session: Optional["ContainerHandle"] = None
if sandbox_options and sandbox_options.get("container_name"):
self.sandbox_mode = "docker"
self._apply_container_session(container_session)
# 终端会话字典
self.terminals: Dict[str, PersistentTerminal] = {}
@ -120,6 +129,48 @@ class TerminalManager:
# 终端工厂(跨平台支持)
self.factory = TerminalFactory()
def _apply_container_session(self, session: Optional["ContainerHandle"]):
"""根据容器句柄调整执行模式。"""
self.container_session = session
if session and session.mode == "docker":
self.sandbox_mode = "docker"
elif session:
self.sandbox_mode = "host"
else:
self.sandbox_mode = self.default_sandbox_mode
def _build_sandbox_options(self) -> Dict:
"""构造当前终端应使用的沙箱参数。"""
options = dict(self.sandbox_options)
if self.container_session and self.container_session.mode == "docker":
options["container_name"] = self.container_session.container_name
options["mount_path"] = self.container_session.mount_path
else:
options.pop("container_name", None)
return options
@staticmethod
def _same_container(a: Optional["ContainerHandle"], b: Optional["ContainerHandle"]) -> bool:
if a is b:
return True
if not a or not b:
return False
if a.mode != b.mode:
return False
if a.mode == "docker":
return a.container_id == b.container_id and a.container_name == b.container_name
return True
def update_container_session(self, session: Optional["ContainerHandle"]):
"""外部更新容器信息,必要时重置终端。"""
if self._same_container(self.container_session, session):
self._apply_container_session(session)
return
self._apply_container_session(session)
if self.terminals:
print(f"{OUTPUT_FORMATS['warning']} 容器已切换,正在关闭现有终端会话。")
self.close_all()
def open_terminal(
self,
@ -167,6 +218,7 @@ class TerminalManager:
shell_command = self.factory.get_shell_command()
# 创建终端实例
sandbox_options = self._build_sandbox_options()
terminal = PersistentTerminal(
session_name=session_name,
working_dir=str(work_path),
@ -176,7 +228,7 @@ class TerminalManager:
display_size=self.terminal_display_size,
project_path=str(self.project_path),
sandbox_mode=self.sandbox_mode,
sandbox_options=self.sandbox_options
sandbox_options=sandbox_options
)
# 启动终端
@ -293,6 +345,7 @@ class TerminalManager:
terminal.close()
del self.terminals[target_session]
sandbox_options = self._build_sandbox_options()
new_terminal = PersistentTerminal(
session_name=target_session,
working_dir=working_dir,
@ -302,7 +355,7 @@ class TerminalManager:
display_size=self.terminal_display_size,
project_path=str(self.project_path),
sandbox_mode=self.sandbox_mode,
sandbox_options=self.sandbox_options
sandbox_options=sandbox_options
)
if not new_terminal.start():

View File

@ -6,7 +6,7 @@ import asyncio
import subprocess
import shutil
from pathlib import Path
from typing import Dict, Optional, Tuple
from typing import Dict, Optional, Tuple, TYPE_CHECKING
try:
from config import (
CODE_EXECUTION_TIMEOUT,
@ -30,14 +30,18 @@ except ImportError:
)
from modules.toolbox_container import ToolboxContainer
if TYPE_CHECKING:
from modules.user_container_manager import ContainerHandle
class TerminalOperator:
def __init__(self, project_path: str):
def __init__(self, project_path: str, container_session: Optional["ContainerHandle"] = None):
self.project_path = Path(project_path).resolve()
self.process = None
# 自动检测Python命令
self.python_cmd = self._detect_python_command()
print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}")
self._toolbox: Optional[ToolboxContainer] = None
self.container_session: Optional["ContainerHandle"] = container_session
def _detect_python_command(self) -> str:
"""
@ -83,8 +87,16 @@ class TerminalOperator:
self._toolbox = ToolboxContainer(
project_path=str(self.project_path),
idle_timeout=TOOLBOX_TERMINAL_IDLE_SECONDS,
container_session=self.container_session,
)
return self._toolbox
def set_container_session(self, session: Optional["ContainerHandle"]):
if session is self.container_session:
return
self.container_session = session
if self._toolbox:
self._toolbox.set_container_session(session)
def _validate_command(self, command: str) -> Tuple[bool, str]:
"""验证命令安全性"""

View File

@ -5,7 +5,7 @@ import time
import uuid
import shlex
from pathlib import Path
from typing import Optional, Dict
from typing import Optional, Dict, TYPE_CHECKING
from modules.persistent_terminal import PersistentTerminal
from config import (
@ -24,15 +24,18 @@ from config import (
TOOLBOX_TERMINAL_IDLE_SECONDS,
)
if TYPE_CHECKING:
from modules.user_container_manager import ContainerHandle
def _build_sandbox_options() -> Dict:
def _build_sandbox_options(container_session: Optional["ContainerHandle"] = None) -> Dict:
"""构造与终端一致的沙箱配置."""
name_prefix = TERMINAL_SANDBOX_NAME_PREFIX
if name_prefix:
name_prefix = f"{name_prefix}-toolbox"
else:
name_prefix = "toolbox-term"
return {
options = {
"image": TERMINAL_SANDBOX_IMAGE,
"mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
"shell": TERMINAL_SANDBOX_SHELL,
@ -45,6 +48,10 @@ def _build_sandbox_options() -> Dict:
"env": dict(TERMINAL_SANDBOX_ENV),
"require": TERMINAL_SANDBOX_REQUIRE,
}
if container_session and container_session.mode == "docker":
options["container_name"] = container_session.container_name
options["mount_path"] = container_session.mount_path
return options
class ToolboxContainer:
@ -56,10 +63,14 @@ class ToolboxContainer:
sandbox_mode: Optional[str] = None,
sandbox_options: Optional[Dict] = None,
idle_timeout: int = TOOLBOX_TERMINAL_IDLE_SECONDS,
container_session: Optional["ContainerHandle"] = None,
):
self.project_path = Path(project_path).resolve()
self.sandbox_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
options = _build_sandbox_options()
self.default_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
self._default_mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace"
self.container_session: Optional["ContainerHandle"] = None
self.sandbox_mode = self.default_mode
options = _build_sandbox_options(container_session)
if sandbox_options:
for key, value in sandbox_options.items():
if key == "binds" and isinstance(value, list):
@ -69,6 +80,7 @@ class ToolboxContainer:
else:
options[key] = value
self.sandbox_options = options
self._apply_container_session(container_session)
self.idle_timeout = max(0, int(idle_timeout)) if idle_timeout is not None else 0
self._terminal: Optional[PersistentTerminal] = None
@ -76,6 +88,30 @@ class ToolboxContainer:
self._session_name = f"toolbox-{uuid.uuid4().hex[:10]}"
self._last_used = 0.0
def _apply_container_session(self, session: Optional["ContainerHandle"]):
self.container_session = session
if session and session.mode == "docker":
self.sandbox_mode = "docker"
self.sandbox_options["container_name"] = session.container_name
self.sandbox_options["mount_path"] = session.mount_path
elif session:
self.sandbox_mode = "host"
self.sandbox_options.pop("container_name", None)
self.sandbox_options["mount_path"] = self._default_mount_path
else:
self.sandbox_mode = self.default_mode
if session is None:
self.sandbox_options.pop("container_name", None)
self.sandbox_options["mount_path"] = self._default_mount_path
def set_container_session(self, session: Optional["ContainerHandle"]):
if session is self.container_session:
return
self._apply_container_session(session)
if self._terminal:
self._terminal.close()
self._terminal = None
async def _ensure_terminal(self) -> PersistentTerminal:
"""确保容器已启动。"""
async with self._lock:

View File

@ -0,0 +1,324 @@
"""Per-user Docker container manager for main agent."""
from __future__ import annotations
import json
import re
import shutil
import subprocess
import threading
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, Optional
from config import (
MAX_ACTIVE_USER_CONTAINERS,
OUTPUT_FORMATS,
TERMINAL_SANDBOX_BIN,
TERMINAL_SANDBOX_BINDS,
TERMINAL_SANDBOX_CPUS,
TERMINAL_SANDBOX_ENV,
TERMINAL_SANDBOX_IMAGE,
TERMINAL_SANDBOX_MEMORY,
TERMINAL_SANDBOX_MODE,
TERMINAL_SANDBOX_MOUNT_PATH,
TERMINAL_SANDBOX_NAME_PREFIX,
TERMINAL_SANDBOX_NETWORK,
TERMINAL_SANDBOX_REQUIRE,
LOGS_DIR,
)
from modules.container_monitor import collect_stats, inspect_state
@dataclass
class ContainerHandle:
"""Lightweight record describing a user workspace container."""
username: str
mode: str
workspace_path: str
mount_path: str
container_name: Optional[str] = None
container_id: Optional[str] = None
sandbox_bin: Optional[str] = None
created_at: float = field(default_factory=time.time)
last_active: float = field(default_factory=time.time)
def touch(self):
self.last_active = time.time()
def to_dict(self) -> Dict:
return {
"username": self.username,
"mode": self.mode,
"workspace_path": self.workspace_path,
"mount_path": self.mount_path,
"container_name": self.container_name,
"container_id": self.container_id,
"created_at": self.created_at,
"last_active": self.last_active,
}
class UserContainerManager:
"""Create and track long-lived containers for each logged-in user."""
def __init__(
self,
sandbox_mode: Optional[str] = None,
max_containers: int = MAX_ACTIVE_USER_CONTAINERS,
):
self.sandbox_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
self.max_containers = max_containers
self.image = TERMINAL_SANDBOX_IMAGE
self.mount_path = TERMINAL_SANDBOX_MOUNT_PATH or "/workspace"
self.network = TERMINAL_SANDBOX_NETWORK
self.cpus = TERMINAL_SANDBOX_CPUS
self.memory = TERMINAL_SANDBOX_MEMORY
self.binds = list(TERMINAL_SANDBOX_BINDS)
self.sandbox_bin = TERMINAL_SANDBOX_BIN or "docker"
self.name_prefix = TERMINAL_SANDBOX_NAME_PREFIX or "agent-user"
self.require = bool(TERMINAL_SANDBOX_REQUIRE)
self.extra_env = dict(TERMINAL_SANDBOX_ENV)
self._containers: Dict[str, ContainerHandle] = {}
self._lock = threading.Lock()
self._stats_log_path = Path(LOGS_DIR).expanduser().resolve() / "container_stats.log"
self._stats_log_path.parent.mkdir(parents=True, exist_ok=True)
if not self._stats_log_path.exists():
self._stats_log_path.touch()
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def ensure_container(self, username: str, workspace_path: str) -> ContainerHandle:
username = self._normalize_username(username)
workspace = str(Path(workspace_path).expanduser().resolve())
Path(workspace).mkdir(parents=True, exist_ok=True)
with self._lock:
handle = self._containers.get(username)
if handle:
if handle.mode == "docker" and not self._is_container_running(handle):
self._containers.pop(username, None)
self._kill_container(handle.container_name, handle.sandbox_bin)
handle = None
else:
handle.workspace_path = workspace
handle.touch()
return handle
if not self._has_capacity(username):
raise RuntimeError("资源繁忙:容器配额已用尽,请稍候再试。")
handle = self._create_handle(username, workspace)
self._containers[username] = handle
return handle
def release_container(self, username: str, reason: str = "logout"):
username = self._normalize_username(username)
with self._lock:
handle = self._containers.pop(username, None)
if not handle:
return
if handle.mode == "docker" and handle.container_name:
self._kill_container(handle.container_name, handle.sandbox_bin)
print(f"{OUTPUT_FORMATS['info']} 容器已释放: {handle.container_name} ({reason})")
def has_capacity(self, username: Optional[str] = None) -> bool:
username = self._normalize_username(username) if username else None
with self._lock:
if username and username in self._containers:
return True
if self.max_containers <= 0:
return True
return len(self._containers) < self.max_containers
def get_handle(self, username: str) -> Optional[ContainerHandle]:
username = self._normalize_username(username)
with self._lock:
handle = self._containers.get(username)
if handle:
handle.touch()
return handle
def list_containers(self) -> Dict[str, Dict]:
with self._lock:
return {user: handle.to_dict() for user, handle in self._containers.items()}
def get_container_status(self, username: str, include_stats: bool = True) -> Dict:
username = self._normalize_username(username)
with self._lock:
handle = self._containers.get(username)
if not handle:
return {"username": username, "mode": "host"}
info = {
"username": username,
"mode": handle.mode,
"workspace_path": handle.workspace_path,
"mount_path": handle.mount_path,
"container_name": handle.container_name,
"created_at": handle.created_at,
"last_active": handle.last_active,
}
if handle.mode == "docker" and include_stats:
stats = collect_stats(handle.container_name, handle.sandbox_bin)
state = inspect_state(handle.container_name, handle.sandbox_bin)
if stats:
info["stats"] = stats
self._log_stats(username, stats)
if state:
info["state"] = state
return info
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _has_capacity(self, username: str) -> bool:
if self.max_containers <= 0:
return True
existing = 1 if username in self._containers else 0
return (len(self._containers) - existing) < self.max_containers
def _create_handle(self, username: str, workspace: str) -> ContainerHandle:
if self.sandbox_mode != "docker":
return self._host_handle(username, workspace)
docker_path = shutil.which(self.sandbox_bin or "docker")
if not docker_path:
message = f"未找到容器运行时 {self.sandbox_bin}"
if self.require:
raise RuntimeError(message)
print(f"{OUTPUT_FORMATS['warning']} {message},回退到宿主机执行。")
return self._host_handle(username, workspace)
if not self.image:
raise RuntimeError("TERMINAL_SANDBOX_IMAGE 未配置,无法启动容器。")
container_name = self._build_container_name(username)
self._kill_container(container_name, docker_path)
cmd = [
docker_path,
"run",
"-d",
"--name",
container_name,
"-w",
self.mount_path,
"-v",
f"{workspace}:{self.mount_path}",
]
if self.network:
cmd += ["--network", self.network]
if self.cpus:
cmd += ["--cpus", str(self.cpus)]
if self.memory:
cmd += ["--memory", str(self.memory)]
for bind in self.binds:
chunk = bind.strip()
if chunk:
cmd += ["-v", chunk]
envs = {
"PYTHONIOENCODING": "utf-8",
"TERM": "xterm-256color",
}
envs.update({k: v for k, v in self.extra_env.items() if v is not None})
for key, value in envs.items():
cmd += ["-e", f"{key}={value}"]
cmd.append(self.image)
cmd += ["tail", "-f", "/dev/null"]
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
)
if result.returncode != 0:
message = result.stderr.strip() or result.stdout.strip() or "容器启动失败"
if self.require:
raise RuntimeError(message)
print(f"{OUTPUT_FORMATS['warning']} {message},回退到宿主机。")
return self._host_handle(username, workspace)
container_id = result.stdout.strip() or None
print(f"{OUTPUT_FORMATS['success']} 启动用户容器: {container_name} ({username})")
return ContainerHandle(
username=username,
mode="docker",
workspace_path=workspace,
mount_path=self.mount_path,
container_name=container_name,
container_id=container_id,
sandbox_bin=docker_path,
)
def _host_handle(self, username: str, workspace: str) -> ContainerHandle:
return ContainerHandle(
username=username,
mode="host",
workspace_path=workspace,
mount_path=workspace,
)
def _kill_container(self, container_name: Optional[str], docker_bin: Optional[str]):
if not container_name or not docker_bin:
return
subprocess.run(
[docker_bin, "rm", "-f", container_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
def _is_container_running(self, handle: ContainerHandle) -> bool:
if handle.mode != "docker" or not handle.container_name or not handle.sandbox_bin:
return True
try:
result = subprocess.run(
[
handle.sandbox_bin,
"inspect",
"-f",
"{{.State.Running}}",
handle.container_name,
],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
timeout=3,
check=False,
)
except (OSError, subprocess.SubprocessError):
return False
return result.returncode == 0 and result.stdout.strip().lower() == "true"
def _build_container_name(self, username: str) -> str:
slug = re.sub(r"[^a-z0-9\-]", "-", username.lower()).strip("-")
if not slug:
slug = "user"
return f"{self.name_prefix}-{slug}"
def _log_stats(self, username: str, stats: Dict):
try:
record = {
"username": username,
"timestamp": time.time(),
"stats": stats,
}
with self._stats_log_path.open('a', encoding='utf-8') as fh:
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
except Exception:
pass
@staticmethod
def _normalize_username(username: Optional[str]) -> str:
return (username or "").strip().lower()

View File

@ -299,6 +299,20 @@ async function bootstrapApp() {
},
// Token面板折叠状态
tokenPanelCollapsed: true,
projectStorage: {
used_bytes: 0,
limit_bytes: null,
limit_label: '',
usage_percent: null
},
containerStatus: null,
containerStatsTimer: null,
projectStorageTimer: null,
lastContainerSample: null,
containerNetRate: {
down_bps: null,
up_bps: null
},
// 对话压缩状态
compressing: false,
@ -398,6 +412,8 @@ async function bootstrapApp() {
this.$nextTick(() => {
this.autoResizeInput();
});
this.startContainerStatsPolling();
this.startProjectStoragePolling();
},
beforeUnmount() {
@ -420,6 +436,8 @@ async function bootstrapApp() {
clearInterval(this.subAgentPollTimer);
this.subAgentPollTimer = null;
}
this.stopContainerStatsPolling();
this.stopProjectStoragePolling();
const cleanup = this.destroyEasterEggEffect(true);
if (cleanup && typeof cleanup.catch === 'function') {
cleanup.catch(() => {});
@ -816,7 +834,7 @@ async function bootstrapApp() {
// 监听状态更新事件
this.socket.on('status_update', (status) => {
// 更新系统状态信息
this.applyStatusSnapshot(status);
if (status.conversation && status.conversation.current_id) {
this.currentConversationId = status.conversation.current_id;
}
@ -829,13 +847,14 @@ async function bootstrapApp() {
this.socket.on('ai_message_start', () => {
console.log('AI消息开始');
this.cleanupStaleToolActions();
const newMessage = {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null
};
const newMessage = {
role: 'assistant',
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null,
activeThinkingId: null
};
this.messages.push(newMessage);
this.currentMessageIndex = this.messages.length - 1;
this.streamingMessage = true;
@ -863,6 +882,7 @@ async function bootstrapApp() {
const blockId = action.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
action.blockId = blockId;
msg.activeThinkingId = action.id;
this.expandedBlocks.add(blockId);
// 开始思考时自动锁定滚动到底部
this.autoScrollEnabled = true;
@ -879,14 +899,14 @@ async function bootstrapApp() {
const msg = this.messages[this.currentMessageIndex];
msg.streamingThinking += data.content;
const lastAction = msg.actions[msg.actions.length - 1];
if (lastAction && lastAction.type === 'thinking') {
lastAction.content += data.content;
const thinkingAction = this.getActiveThinkingAction(msg);
if (thinkingAction) {
thinkingAction.content += data.content;
}
this.$forceUpdate();
this.$nextTick(() => {
if (lastAction && lastAction.blockId) {
this.scrollThinkingToBottom(lastAction.blockId);
if (thinkingAction && thinkingAction.blockId) {
this.scrollThinkingToBottom(thinkingAction.blockId);
}
this.conditionalScrollToBottom();
});
@ -898,13 +918,13 @@ async function bootstrapApp() {
console.log('思考结束');
if (this.currentMessageIndex >= 0) {
const msg = this.messages[this.currentMessageIndex];
const lastAction = msg.actions[msg.actions.length - 1];
if (lastAction && lastAction.type === 'thinking') {
lastAction.streaming = false;
lastAction.content = data.full_content;
const blockId = lastAction.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
if (!lastAction.blockId) {
lastAction.blockId = blockId;
const thinkingAction = this.getActiveThinkingAction(msg);
if (thinkingAction) {
thinkingAction.streaming = false;
thinkingAction.content = data.full_content;
const blockId = thinkingAction.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
if (!thinkingAction.blockId) {
thinkingAction.blockId = blockId;
}
if (blockId) {
setTimeout(() => {
@ -917,6 +937,7 @@ async function bootstrapApp() {
}
msg.streamingThinking = '';
msg.currentStreamingType = null;
msg.activeThinkingId = null;
this.$forceUpdate();
}
});
@ -1409,7 +1430,8 @@ async function bootstrapApp() {
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null
currentStreamingType: null,
activeThinkingId: null
};
this.messages.push(message);
this.currentMessageIndex = this.messages.length - 1;
@ -1503,6 +1525,7 @@ async function bootstrapApp() {
this.projectPath = statusData.project_path || '';
this.agentVersion = statusData.version || this.agentVersion;
this.thinkingMode = !!statusData.thinking_mode;
this.applyStatusSnapshot(statusData);
// 获取当前对话信息
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
@ -1609,6 +1632,139 @@ async function bootstrapApp() {
toggleTokenPanel() {
this.tokenPanelCollapsed = !this.tokenPanelCollapsed;
},
applyStatusSnapshot(status) {
if (!status || typeof status !== 'object') {
return;
}
if (status.project) {
const project = status.project;
this.projectStorage.used_bytes = project.total_size || 0;
this.projectStorage.limit_bytes = project.limit_bytes ?? null;
this.projectStorage.limit_label = project.limit_label || (project.limit_bytes ? `${(project.limit_bytes / (1024 * 1024)).toFixed(0)} MB` : '未限制');
if (project.limit_bytes) {
const pct = typeof project.usage_percent === 'number'
? project.usage_percent
: (project.total_size / project.limit_bytes * 100);
this.projectStorage.usage_percent = pct;
} else {
this.projectStorage.usage_percent = null;
}
}
if (Object.prototype.hasOwnProperty.call(status, 'container')) {
this.updateContainerStatus(status.container);
}
},
updateContainerStatus(status) {
if (!status || status.mode !== 'docker') {
this.containerStatus = status || null;
this.containerNetRate = { down_bps: null, up_bps: null };
this.lastContainerSample = null;
return;
}
const stats = status.stats;
if (stats && typeof stats.timestamp === 'number') {
const currentSample = {
timestamp: stats.timestamp,
rx_bytes: stats.net_io && typeof stats.net_io.rx_bytes === 'number' ? stats.net_io.rx_bytes : null,
tx_bytes: stats.net_io && typeof stats.net_io.tx_bytes === 'number' ? stats.net_io.tx_bytes : null
};
const last = this.lastContainerSample;
if (
last &&
currentSample.rx_bytes !== null &&
currentSample.tx_bytes !== null &&
last.rx_bytes !== null &&
last.tx_bytes !== null
) {
const deltaT = Math.max(0.001, currentSample.timestamp - last.timestamp);
const downRate = Math.max(0, (currentSample.rx_bytes - last.rx_bytes) / deltaT);
const upRate = Math.max(0, (currentSample.tx_bytes - last.tx_bytes) / deltaT);
this.containerNetRate = {
down_bps: downRate,
up_bps: upRate
};
} else {
this.containerNetRate = { down_bps: null, up_bps: null };
}
this.lastContainerSample = currentSample;
} else {
this.containerNetRate = { down_bps: null, up_bps: null };
this.lastContainerSample = null;
}
this.containerStatus = status;
},
async pollContainerStats() {
try {
const response = await fetch('/api/container-status');
if (!response.ok) {
return;
}
const data = await response.json();
if (data.success) {
this.updateContainerStatus(data.data || null);
}
} catch (error) {
console.warn('获取容器状态异常:', error);
}
},
startContainerStatsPolling() {
if (this.containerStatsTimer) {
return;
}
this.pollContainerStats();
this.containerStatsTimer = setInterval(() => {
this.pollContainerStats();
}, 500);
},
stopContainerStatsPolling() {
if (this.containerStatsTimer) {
clearInterval(this.containerStatsTimer);
this.containerStatsTimer = null;
}
},
pollProjectStorage() {
return fetch('/api/project-storage')
.then(resp => {
if (!resp.ok) {
throw new Error(resp.statusText || '请求失败');
}
return resp.json();
})
.then(data => {
if (data && data.success && data.data) {
this.projectStorage.used_bytes = data.data.used_bytes || 0;
this.projectStorage.limit_bytes = data.data.limit_bytes ?? null;
this.projectStorage.limit_label = data.data.limit_label || '';
this.projectStorage.usage_percent = data.data.usage_percent ?? null;
}
})
.catch(err => {
console.warn('获取存储信息失败:', err);
});
},
startProjectStoragePolling() {
if (this.projectStorageTimer) {
return;
}
this.pollProjectStorage();
this.projectStorageTimer = setInterval(() => {
this.pollProjectStorage();
}, 5000);
},
stopProjectStoragePolling() {
if (this.projectStorageTimer) {
clearInterval(this.projectStorageTimer);
this.projectStorageTimer = null;
}
},
// ==========================================
// 对话管理核心功能
@ -1738,6 +1894,7 @@ async function bootstrapApp() {
const statusResponse = await fetch('/api/status');
const status = await statusResponse.json();
console.log('系统状态:', status);
this.applyStatusSnapshot(status);
// 如果状态中有对话历史字段
if (status.conversation_history && Array.isArray(status.conversation_history)) {
@ -1824,7 +1981,8 @@ async function bootstrapApp() {
actions: [],
streamingThinking: '',
streamingText: '',
currentStreamingType: null
currentStreamingType: null,
activeThinkingId: null
};
}
@ -3019,6 +3177,25 @@ async function bootstrapApp() {
}
this.$forceUpdate();
},
getActiveThinkingAction(msg) {
if (!msg || !Array.isArray(msg.actions)) {
return null;
}
if (msg.activeThinkingId) {
const found = msg.actions.find(action => action && action.id === msg.activeThinkingId && action.type === 'thinking');
if (found) {
return found;
}
}
for (let i = msg.actions.length - 1; i >= 0; i--) {
const action = msg.actions[i];
if (action && action.type === 'thinking' && action.streaming !== false) {
return action;
}
}
return null;
},
// 修复:工具相关方法 - 接收tool对象而不是name
getToolIcon(tool) {
@ -3669,6 +3846,83 @@ async function bootstrapApp() {
} else {
return (num / 1000000).toFixed(1) + 'M';
}
},
formatBytes(bytes) {
if (bytes === null || bytes === undefined) {
return '—';
}
const value = Number(bytes);
if (!Number.isFinite(value)) {
return '—';
}
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let display = value;
let unitIndex = 0;
while (display >= 1024 && unitIndex < units.length - 1) {
display /= 1024;
unitIndex++;
}
const decimals = display >= 10 || unitIndex === 0 ? 0 : 1;
return `${display.toFixed(decimals)} ${units[unitIndex]}`;
},
formatPercentage(value) {
if (typeof value !== 'number' || Number.isNaN(value)) {
return '—';
}
return `${value.toFixed(1)}%`;
},
formatRate(bytesPerSecond) {
if (bytesPerSecond === null || bytesPerSecond === undefined) {
return '—';
}
const value = Number(bytesPerSecond);
if (!Number.isFinite(value)) {
return '—';
}
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
let display = value;
let unitIndex = 0;
while (display >= 1024 && unitIndex < units.length - 1) {
display /= 1024;
unitIndex++;
}
const decimals = display >= 10 || unitIndex === 0 ? 0 : 1;
return `${display.toFixed(decimals)} ${units[unitIndex]}`;
},
hasContainerStats() {
return !!(this.containerStatus && this.containerStatus.mode === 'docker' && this.containerStatus.stats);
},
containerStatusText() {
if (!this.containerStatus) {
return '未知';
}
if (this.containerStatus.mode !== 'docker') {
return '宿主机模式';
}
const state = this.containerStatus.state;
if (state && state.status) {
return state.status;
}
return (state && state.running === false) ? '已停止' : '运行中';
},
containerStatusClass() {
if (!this.containerStatus) {
return {};
}
if (this.containerStatus.mode !== 'docker') {
return { 'status-pill--host': true };
}
const stopped = this.containerStatus.state && this.containerStatus.state.running === false;
return {
'status-pill--running': !stopped,
'status-pill--stopped': stopped
};
}
}
});

View File

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>体育场形裂变动画 Demo</title>
<style>
:root {
font-family: "SF Pro Display", "PingFang SC", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #12100f;
color: #f3efe6;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 42px 16px;
background: transparent;
}
.demo-card {
width: min(880px, 95vw);
padding: 0;
background: transparent;
border-radius: 0;
box-shadow: none;
border: none;
}
.demo-card h1,
.demo-card p,
.hint {
display: none;
}
.shell-wrapper {
position: relative;
}
.input-shell {
position: relative;
width: 100%;
background: linear-gradient(180deg, #fdf7ea 0%, #f2c46e 100%);
padding: 14px 70px;
box-shadow: 0 20px 55px rgba(0, 0, 0, 0.55);
min-height: 64px;
border: 1px solid rgba(255, 255, 255, 0.35);
transition: padding 0.25s ease, box-shadow 0.3s ease, border-color 0.3s ease;
clip-path: url(#stadiumClip);
-webkit-clip-path: url(#stadiumClip);
}
.input-shell.expanded {
padding-top: 22px;
padding-bottom: 22px;
box-shadow: 0 45px 90px rgba(0, 0, 0, 0.65);
border-color: rgba(255, 255, 255, 0.55);
}
textarea {
width: 100%;
min-height: 28px;
max-height: 240px;
border: none;
resize: none;
background: transparent;
font-size: 16px;
line-height: 1.6;
font-family: inherit;
color: #1f160c;
outline: none;
overflow-y: auto;
}
textarea::-webkit-scrollbar {
width: 6px;
height: 6px;
}
textarea::-webkit-scrollbar-thumb {
background: rgba(60, 53, 40, 0.25);
border-radius: 12px;
}
svg.clip-defs {
width: 0;
height: 0;
position: absolute;
}
</style>
</head>
<body>
<div class="demo-card">
<h1>体育场形 → 圆角矩形(裂解动画示例)</h1>
<p>单行时保持典型体育场形,输入多行后,左右半圆沿水平中线裂开成四个四分之一圆,并拉伸出上下两条直边。</p>
<div class="shell-wrapper">
<div class="input-shell" id="shell">
<textarea id="demoInput" rows="1" placeholder="粘贴或输入多行内容,观察动画..."></textarea>
</div>
<svg class="clip-defs">
<clipPath id="stadiumClip" clipPathUnits="userSpaceOnUse">
<path id="stadiumPath" d="" />
</clipPath>
</svg>
</div>
<div class="hint">提示Shift+Enter 换行;窗口尺寸改变也会重新计算形状。</div>
</div>
<script>
(function () {
const textarea = document.getElementById('demoInput');
const shell = document.getElementById('shell');
const path = document.getElementById('stadiumPath');
if (!textarea || !shell || !path) {
return;
}
let animationFrame = null;
let currentProgress = 0;
let targetProgress = 0;
let baseHeight = 0;
let baseRadius = 0;
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
const ensureBase = () => {
if (baseHeight > 0) {
return;
}
baseHeight = shell.offsetHeight;
baseRadius = baseHeight / 2;
shell.dataset.baseHeight = String(baseHeight);
shell.dataset.baseRadius = String(baseRadius);
};
const buildPath = (width, height, progress) => {
if (width <= 0 || height <= 0) {
return '';
}
ensureBase();
const radius = baseRadius;
const midY = height / 2;
const targetTopCenter = radius;
const targetBottomCenter = Math.max(height - radius, radius);
const cyTop = midY - (midY - targetTopCenter) * progress;
const cyBottom = midY + (targetBottomCenter - midY) * progress;
const cxLeft = radius;
const cxRight = width - radius;
const topY = cyTop - radius;
const bottomY = cyBottom + radius;
return [
`M ${cxLeft} ${topY}`,
`H ${cxRight}`,
`A ${radius} ${radius} 0 0 1 ${width} ${cyTop}`,
`V ${cyBottom}`,
`A ${radius} ${radius} 0 0 1 ${cxRight} ${bottomY}`,
`H ${cxLeft}`,
`A ${radius} ${radius} 0 0 1 0 ${cyBottom}`,
`V ${cyTop}`,
`A ${radius} ${radius} 0 0 1 ${cxLeft} ${topY}`,
'Z'
].join(' ');
};
const animateProgress = () => {
if (Math.abs(currentProgress - targetProgress) < 0.002) {
currentProgress = targetProgress;
animationFrame = null;
} else {
currentProgress += (targetProgress - currentProgress) * 0.18;
animationFrame = requestAnimationFrame(animateProgress);
}
updatePath();
};
const updatePath = () => {
ensureBase();
const rect = shell.getBoundingClientRect();
const width = rect.width;
const height = shell.offsetHeight;
const d = buildPath(width, height, currentProgress);
path.setAttribute('d', d);
};
const resizeTextarea = () => {
ensureBase();
textarea.style.height = 'auto';
const computed = window.getComputedStyle(textarea);
const lineHeight = parseFloat(computed.lineHeight || '20') || 20;
const maxHeight = lineHeight * 6;
const nextHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${nextHeight}px`;
const lineCount = Math.max(1, Math.round(nextHeight / lineHeight));
targetProgress = clamp((lineCount - 1) / 4, 0, 1);
shell.classList.toggle('expanded', lineCount > 1);
if (!animationFrame) {
animationFrame = requestAnimationFrame(animateProgress);
}
};
textarea.addEventListener('input', resizeTextarea);
window.addEventListener('resize', () => {
updatePath();
});
// 初始化
textarea.value = '';
resizeTextarea();
updatePath();
})();
</script>
</body>
</html>

View File

@ -328,18 +328,72 @@
<div class="token-drawer" v-if="currentConversationId" :class="{ collapsed: tokenPanelCollapsed }">
<div class="token-display-panel">
<div class="token-panel-content">
<div class="token-stats">
<div class="token-item">
<span class="token-label">当前上下文</span>
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
<div class="token-panel-layout">
<div class="token-card">
<div class="token-stats">
<div class="token-item">
<span class="token-label">当前上下文</span>
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
</div>
<div class="token-item">
<span class="token-label">累计输入</span>
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
</div>
<div class="token-item">
<span class="token-label">累计输出</span>
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
</div>
</div>
</div>
<div class="token-item">
<span class="token-label">累计输入</span>
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
</div>
<div class="token-item">
<span class="token-label">累计输出</span>
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
<div class="container-stats-card" v-if="containerStatus">
<div class="container-stats-header">
<span class="token-label">容器资源</span>
<span class="status-pill" :class="containerStatusClass()">{{ containerStatusText() }}</span>
</div>
<template v-if="hasContainerStats()">
<div class="container-metric-grid">
<div class="container-metric">
<span class="metric-label">CPU</span>
<span class="metric-value">{{ formatPercentage(containerStatus.stats?.cpu_percent) }}</span>
</div>
<div class="container-metric">
<span class="metric-label">内存</span>
<span class="metric-value">
{{ formatBytes(containerStatus.stats?.memory?.used_bytes) }}
<template v-if="containerStatus.stats?.memory?.limit_bytes">
/ {{ formatBytes(containerStatus.stats.memory.limit_bytes) }}
</template>
</span>
<span class="metric-subtext" v-if="containerStatus.stats?.memory?.percent">
{{ formatPercentage(containerStatus.stats.memory.percent) }}
</span>
</div>
<div class="container-metric">
<span class="metric-label">网络</span>
<span class="metric-value">
↓{{ formatRate(containerNetRate.down_bps) }}
↑{{ formatRate(containerNetRate.up_bps) }}
</span>
</div>
<div class="container-metric">
<span class="metric-label">存储</span>
<span class="metric-value">
{{ formatBytes(projectStorage.used_bytes) }}
<template v-if="projectStorage.limit_bytes">
/ {{ formatBytes(projectStorage.limit_bytes) }}
</template>
</span>
<span class="metric-subtext" v-if="projectStorage.limit_bytes">
{{ formatPercentage(projectStorage.usage_percent) }}
</span>
</div>
</div>
</template>
<template v-else>
<div class="container-empty">
当前运行在宿主机模式,暂无容器指标。
</div>
</template>
</div>
</div>
</div>

View File

@ -2425,6 +2425,23 @@ o-files {
padding: 16px 36px;
}
.token-panel-layout {
display: flex;
flex-wrap: wrap;
gap: 32px;
align-items: stretch;
}
.token-card {
flex: 1 1 320px;
}
.container-stats-card {
flex: 1 1 320px;
padding-left: 24px;
border-left: 1px solid var(--claude-border);
}
.token-stats {
display: flex;
justify-content: space-between;
@ -2462,7 +2479,87 @@ o-files {
.token-value.input { color: var(--claude-success); }
.token-value.output { color: var(--claude-warning); }
.container-stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.status-pill {
padding: 2px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
background: rgba(125, 109, 94, 0.15);
color: var(--claude-text-secondary);
}
.status-pill--running {
background: rgba(118, 176, 134, 0.18);
color: var(--claude-success);
}
.status-pill--stopped {
background: rgba(217, 152, 69, 0.2);
color: var(--claude-warning);
}
.status-pill--host {
background: rgba(125, 109, 94, 0.12);
color: var(--claude-text-secondary);
}
.container-metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 14px 20px;
}
.container-metric {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric-label {
color: var(--claude-text-secondary);
font-size: 11px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.metric-value {
font-weight: 600;
color: var(--claude-text);
font-size: 16px;
font-variant-numeric: tabular-nums;
}
.metric-subtext {
color: var(--claude-text-secondary);
font-size: 12px;
}
.container-empty {
color: var(--claude-text-secondary);
font-size: 13px;
padding: 12px 0;
}
@media (max-width: 900px) {
.token-panel-layout {
flex-direction: column;
gap: 20px;
}
.container-stats-card {
border-left: none;
border-top: 1px solid var(--claude-border);
padding-left: 0;
padding-top: 16px;
}
.token-stats {
flex-direction: column;
gap: 16px;

13
test/all_icons/book.svg Normal file
View File

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20" />
</svg>

After

Width:  |  Height:  |  Size: 310 B

18
test/all_icons/bot.svg Normal file
View File

@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 8V4H8" />
<rect width="16" height="12" x="4" y="8" rx="2" />
<path d="M2 14h2" />
<path d="M20 14h2" />
<path d="M15 13v2" />
<path d="M9 13v2" />
</svg>

After

Width:  |  Height:  |  Size: 380 B

20
test/all_icons/brain.svg Normal file
View File

@ -0,0 +1,20 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 18V5" />
<path d="M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4" />
<path d="M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5" />
<path d="M17.997 5.125a4 4 0 0 1 2.526 5.77" />
<path d="M18 18a4 4 0 0 0 2-7.464" />
<path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517" />
<path d="M6 18a4 4 0 0 1-2-7.464" />
<path d="M6.003 5.125a4 4 0 0 0-2.526 5.77" />
</svg>

After

Width:  |  Height:  |  Size: 601 B

14
test/all_icons/camera.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z" />
<circle cx="12" cy="13" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 437 B

13
test/all_icons/check.svg Normal file
View File

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 6 9 17l-5-5" />
</svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" />
</svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1" />
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
</svg>

After

Width:  |  Height:  |  Size: 354 B

14
test/all_icons/eye.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
<circle cx="12" cy="12" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 360 B

14
test/all_icons/file.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
</svg>

After

Width:  |  Height:  |  Size: 373 B

13
test/all_icons/flag.svg Normal file
View File

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 22V4a1 1 0 0 1 .4-.8A6 6 0 0 1 8 2c3 0 5 2 7.333 2q2 0 3.067-.8A1 1 0 0 1 20 4v10a1 1 0 0 1-.4.8A6 6 0 0 1 16 16c-3 0-5-2-8-2a6 6 0 0 0-4 1.528" />
</svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path d="m6 14l1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>

After

Width:  |  Height:  |  Size: 395 B

13
test/all_icons/folder.svg Normal file
View File

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
</svg>

After

Width:  |  Height:  |  Size: 342 B

15
test/all_icons/globe.svg Normal file
View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" />
</svg>

After

Width:  |  Height:  |  Size: 331 B

15
test/all_icons/hammer.svg Normal file
View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m15 12-9.373 9.373a1 1 0 0 1-3.001-3L12 9" />
<path d="m18 15 4-4" />
<path d="m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172v-.344a2 2 0 0 0-.586-1.414l-1.657-1.657A6 6 0 0 0 12.516 3H9l1.243 1.243A6 6 0 0 1 12 8.485V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5" />
</svg>

After

Width:  |  Height:  |  Size: 483 B

14
test/all_icons/laptop.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 5a2 2 0 0 1 2 2v8.526a2 2 0 0 0 .212.897l1.068 2.127a1 1 0 0 1-.9 1.45H3.62a1 1 0 0 1-.9-1.45l1.068-2.127A2 2 0 0 0 4 15.526V7a2 2 0 0 1 2-2z" />
<path d="M20.054 15.987H3.946" />
</svg>

After

Width:  |  Height:  |  Size: 405 B

15
test/all_icons/menu.svg Normal file
View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 5h16" />
<path d="M4 12h16" />
<path d="M4 19h16" />
</svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="20" height="14" x="2" y="3" rx="2" />
<line x1="8" x2="16" y1="21" y2="21" />
<line x1="12" x2="12" y1="17" y2="21" />
</svg>

After

Width:  |  Height:  |  Size: 346 B

View File

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z" />
</svg>

After

Width:  |  Height:  |  Size: 458 B

14
test/all_icons/pencil.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
<path d="m15 5 4 4" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Python</title><path d="M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 19H4.815a1.83 1.83 0 0 1-1.57-.881 1.785 1.785 0 0 1-.004-1.784L7.196 9.5" />
<path d="M11 19h8.203a1.83 1.83 0 0 0 1.556-.89 1.784 1.784 0 0 0 0-1.775l-1.226-2.12" />
<path d="m14 16-3 3 3 3" />
<path d="M8.293 13.596 7.196 9.5 3.1 10.598" />
<path d="m9.344 5.811 1.093-1.892A1.83 1.83 0 0 1 11.985 3a1.784 1.784 0 0 1 1.546.888l3.943 6.843" />
<path d="m13.378 9.633 4.096 1.098 1.097-4.096" />
</svg>

After

Width:  |  Height:  |  Size: 630 B

15
test/all_icons/save.svg Normal file
View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" />
<path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7" />
<path d="M7 3v4a1 1 0 0 0 1 1h7" />
</svg>

After

Width:  |  Height:  |  Size: 417 B

14
test/all_icons/search.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>

After

Width:  |  Height:  |  Size: 275 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
<circle cx="12" cy="12" r="3" />
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" />
<path d="M20 2v4" />
<path d="M22 4h-4" />
<circle cx="4" cy="20" r="2" />
</svg>

After

Width:  |  Height:  |  Size: 567 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 9a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 15 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2z" />
<path d="M15 3v5a1 1 0 0 0 1 1h5" />
</svg>

After

Width:  |  Height:  |  Size: 376 B

View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 19h8" />
<path d="m4 17 6-6-6-6" />
</svg>

After

Width:  |  Height:  |  Size: 261 B

15
test/all_icons/trash.svg Normal file
View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
<path d="M3 6h18" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
</svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>

After

Width:  |  Height:  |  Size: 345 B

14
test/all_icons/user.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>

After

Width:  |  Height:  |  Size: 299 B

13
test/all_icons/wrench.svg Normal file
View File

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z" />
</svg>

After

Width:  |  Height:  |  Size: 417 B

14
test/all_icons/x.svg Normal file
View File

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>

After

Width:  |  Height:  |  Size: 260 B

1
test/all_icons/zap.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="opacity:1;"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg>

After

Width:  |  Height:  |  Size: 373 B

325
test/demo.html Normal file
View File

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水面上涨动画特效</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
background: #f5f5f5;
font-family: 'Arial', sans-serif;
display: flex;
justify-content: center;
align-items: center;
}
/* 示例内容 */
.content {
position: relative;
z-index: 1;
text-align: center;
color: #333;
text-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.content h1 {
font-size: 3em;
margin-bottom: 20px;
animation: fadeIn 1s ease-out;
}
.content p {
font-size: 1.2em;
animation: fadeIn 1.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
/* 水面容器 */
.water-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 0%;
pointer-events: none;
overflow: visible;
}
/* 波浪层 */
.wave {
position: absolute;
bottom: 0;
left: 0;
width: 200%;
height: 100%;
background-repeat: repeat-x;
background-position: bottom;
}
.wave1 { z-index: 1; }
.wave2 { z-index: 2; }
.wave3 { z-index: 3; }
/* 触发按钮 */
.trigger-btn {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
background: rgba(33, 150, 243, 0.8);
border: 2px solid #2196F3;
color: white;
font-size: 16px;
cursor: pointer;
border-radius: 25px;
z-index: 1000;
transition: all 0.3s;
backdrop-filter: blur(10px);
}
.trigger-btn:hover {
background: rgba(33, 150, 243, 1);
transform: scale(1.05);
}
</style>
</head>
<body>
<!-- 页面原本的内容 -->
<div class="content">
<h1>🌊 彩蛋特效</h1>
<p>水面正在缓缓上涨...</p>
<p style="margin-top: 10px; font-size: 0.9em; opacity: 0.8;">
每次刷新都有不同的波浪效果
</p>
</div>
<!-- 水面动画容器 -->
<div class="water-container" id="waterContainer">
<div class="wave wave1"></div>
<div class="wave wave2"></div>
<div class="wave wave3"></div>
</div>
<!-- 重新触发按钮 -->
<button class="trigger-btn" onclick="restartAnimation()">🔄 重播动画</button>
<script>
// 随机数工具函数
function random(min, max) {
return Math.random() * (max - min) + min;
}
function randomInt(min, max) {
return Math.floor(random(min, max));
}
function randomChoice(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
// 生成随机波浪SVG路径 - 相邻波振幅变化平滑
function generateWaveSVG(waveIndex) {
const baseHeight = 180 + waveIndex * 10; // 基准高度
const numWaves = 4; // 生成4个完整波段
let path = `M0,${baseHeight}`;
let currentX = 0;
// 第一个波的振幅随机
let prevAmplitude = random(40, 80);
// 生成多个完整的波段,每个包含波峰和波谷
for (let i = 0; i < numWaves; i++) {
// 每个波段的随机参数
const waveLength = random(700, 900); // 整个周期的长度700-900
// 振幅限制与前一个波的差值不超过20
let amplitude;
if (i === 0) {
amplitude = prevAmplitude;
} else {
const minAmp = Math.max(10, prevAmplitude - 20);
const maxAmp = Math.min(80, prevAmplitude + 20);
amplitude = random(minAmp, maxAmp);
}
prevAmplitude = amplitude;
const halfWave = waveLength / 2;
// 前半段:基线 → 波峰 → 基线
const peak1X = currentX + halfWave / 2;
const peak1Y = baseHeight - amplitude;
const mid1X = currentX + halfWave;
const mid1Y = baseHeight;
path += ` Q${peak1X},${peak1Y} ${mid1X},${mid1Y}`;
// 后半段:基线 → 波谷 → 基线
const trough1X = mid1X + halfWave / 2;
const trough1Y = baseHeight + amplitude;
const end1X = currentX + waveLength;
const end1Y = baseHeight;
path += ` Q${trough1X},${trough1Y} ${end1X},${end1Y}`;
currentX = end1X;
}
// 闭合路径
path += ` L${currentX},1000 L0,1000 Z`;
return {
path: path,
width: currentX,
waveWidth: currentX / numWaves // 平均波长
};
}
// 初始化水面动画
function initWaterAnimation() {
const container = document.getElementById('waterContainer');
const waves = document.querySelectorAll('.wave');
// 波浪颜色配置
const waveColors = [
'rgba(135, 206, 250, 0.4)',
'rgba(100, 181, 246, 0.5)',
'rgba(33, 150, 243, 0.4)'
];
// 创建动态样式表
const styleSheet = document.createElement('style');
styleSheet.id = 'dynamic-waves-style';
document.head.appendChild(styleSheet);
// 1. 随机水面上涨参数
const riseDuration = random(30, 40); // 8-12秒
const finalHeight = random(87, 93); // 87-93%
const riseEasing = randomChoice([
'ease-in-out',
'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'cubic-bezier(0.42, 0, 0.58, 1)'
]);
// 创建水面上涨动画
const riseKeyframes = `
@keyframes waterRise {
0% { height: 0%; }
100% { height: ${finalHeight}%; }
}
`;
styleSheet.sheet.insertRule(riseKeyframes, 0);
container.style.animation = `waterRise ${riseDuration}s ${riseEasing} forwards`;
// 随机生成方向组合:两左一右 或 两右一左
const directions = randomChoice([
[1, 1, -1], // 两右一左
[1, -1, -1], // 一右两左
[-1, 1, 1], // 一左两右
[-1, -1, 1] // 两左一右
]);
// 2. 为每个波浪生成随机参数
waves.forEach((wave, index) => {
// 生成随机波浪形状
const svgData = generateWaveSVG(index);
const color = waveColors[index];
// 创建SVG
const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ${svgData.width} 1000' preserveAspectRatio='none'><path d='${svgData.path}' fill='${color}'/></svg>`;
const dataUrl = `data:image/svg+xml,${encodeURIComponent(svg)}`;
// 应用背景
wave.style.backgroundImage = `url("${dataUrl}")`;
wave.style.backgroundSize = `${svgData.waveWidth}px 100%`;
// 为每层设置不同的速度范围,确保速度差异明显
let duration;
if (index === 0) {
duration = random(16, 22); // 第一层:慢速
} else if (index === 1) {
duration = random(11, 16); // 第二层:中速
} else {
duration = random(7, 12); // 第三层:快速
}
const direction = directions[index]; // 使用预设的方向
const distance = svgData.waveWidth * direction;
const initialPos = randomInt(-200, 200); // 随机初始位置
const delay = random(0, 1.5); // 随机延迟 0-1.5秒
// 创建波浪移动动画
const moveKeyframes = `
@keyframes wave${index + 1}Move {
0% { background-position-x: ${initialPos}px; }
100% { background-position-x: ${initialPos + distance}px; }
}
`;
styleSheet.sheet.insertRule(moveKeyframes, 0);
// 应用动画
wave.style.animation = `wave${index + 1}Move ${duration}s linear infinite`;
wave.style.animationDelay = `${delay}s`;
wave.style.backgroundPositionX = `${initialPos}px`;
});
console.log(`🌊 水面动画已生成
- 上涨时长: ${riseDuration.toFixed(1)}秒
- 最终高度: ${finalHeight.toFixed(1)}%
- 缓动效果: ${riseEasing}
- 波浪方向: [${directions.map(d => d > 0 ? '→' : '←').join(', ')}]`);
}
// 重新播放动画
function restartAnimation() {
// 移除旧的动态样式
const oldStyle = document.getElementById('dynamic-waves-style');
if (oldStyle) {
oldStyle.remove();
}
// 重置容器
const container = document.getElementById('waterContainer');
container.style.animation = 'none';
container.style.height = '0%';
// 重置所有波浪
const waves = document.querySelectorAll('.wave');
waves.forEach(wave => {
wave.style.animation = 'none';
wave.style.animationDelay = '0s';
wave.style.backgroundPositionX = '0px';
});
// 强制重绘
void container.offsetHeight;
// 重新初始化
setTimeout(() => {
initWaterAnimation();
}, 50);
}
// 页面加载时初始化
window.addEventListener('load', initWaterAnimation);
</script>
</body>
</html>

282
test/snake.html Normal file
View File

@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贪吃蛇动画</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #0a0a0a, #1a1a1a, #2d2d2d);
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
#snake-canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<canvas id="snake-canvas"></canvas>
<script>
const canvas = document.getElementById('snake-canvas');
const ctx = canvas.getContext('2d');
// 设置canvas尺寸为窗口大小
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
class Snake {
constructor() {
this.radius = 14; // 蛇的半径苹果也是14
this.path = [
{ x: canvas.width / 2, y: canvas.height / 2 }
];
this.targetLength = 28; // 初始长度为一个苹果直径
this.currentLength = 28;
this.speed = 2;
this.angle = 0;
this.targetAngle = 0;
this.hue = 30; // 橙黄色
this.currentTarget = null; // 当前追逐的苹果
this.targetStartTime = Date.now(); // 开始追逐当前目标的时间
this.targetTimeout = 10000; // 10秒超时
this.timedOut = false; // 是否刚刚超时
}
findNearestApple() {
const now = Date.now();
// 如果当前目标存在且未超时,继续追逐
if (this.currentTarget && (now - this.targetStartTime) < this.targetTimeout) {
// 检查当前目标是否还在苹果列表中
const targetStillExists = apples.some(apple =>
apple.x === this.currentTarget.x && apple.y === this.currentTarget.y
);
if (targetStillExists) {
const dx = this.currentTarget.x - this.path[0].x;
const dy = this.currentTarget.y - this.path[0].y;
this.targetAngle = Math.atan2(dy, dx);
this.timedOut = false;
return;
}
}
// 超时或目标不存在,寻找新目标
let targetApple = null;
let targetDistance = this.timedOut ? -Infinity : Infinity; // 超时后找最远的
apples.forEach(apple => {
const dx = apple.x - this.path[0].x;
const dy = apple.y - this.path[0].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (this.timedOut) {
// 超时了,找最远的苹果
if (distance > targetDistance) {
targetDistance = distance;
targetApple = apple;
}
} else {
// 正常情况,找最近的苹果
if (distance < targetDistance) {
targetDistance = distance;
targetApple = apple;
}
}
});
if (targetApple) {
// 如果切换了目标,重置计时器
if (!this.currentTarget ||
this.currentTarget.x !== targetApple.x ||
this.currentTarget.y !== targetApple.y) {
this.currentTarget = targetApple;
this.targetStartTime = now;
this.timedOut = false; // 重置超时标志
} else if ((now - this.targetStartTime) >= this.targetTimeout) {
// 刚好达到超时
this.timedOut = true;
}
const dx = targetApple.x - this.path[0].x;
const dy = targetApple.y - this.path[0].y;
this.targetAngle = Math.atan2(dy, dx);
}
}
update() {
// 平滑转向 - 降低转向速度,避免锐角转弯
let angleDiff = this.targetAngle - this.angle;
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
// 限制每帧最大转向角度
const maxTurnRate = 0.03; // 降低转向速度
this.angle += angleDiff * maxTurnRate;
// 移动头部
const head = { ...this.path[0] };
head.x += Math.cos(this.angle) * this.speed;
head.y += Math.sin(this.angle) * this.speed;
// 屏幕边界穿越
if (head.x < 0) head.x = canvas.width;
if (head.x > canvas.width) head.x = 0;
if (head.y < 0) head.y = canvas.height;
if (head.y > canvas.height) head.y = 0;
// 添加新头部
this.path.unshift(head);
// 检查是否吃到苹果
apples.forEach((apple, index) => {
const dx = head.x - apple.x;
const dy = head.y - apple.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.radius + 14) { // 14是苹果半径
apples[index] = createApple();
this.targetLength += 28; // 增加一个苹果直径的长度
// 吃到苹果后清除当前目标,重新寻找
this.currentTarget = null;
}
});
// 计算当前路径实际长度
let pathLength = 0;
for (let i = 0; i < this.path.length - 1; i++) {
const dx = this.path[i].x - this.path[i + 1].x;
const dy = this.path[i].y - this.path[i + 1].y;
pathLength += Math.sqrt(dx * dx + dy * dy);
}
// 从尾部裁剪多余的路径
while (this.path.length > 2 && pathLength > this.targetLength) {
const last = this.path[this.path.length - 1];
const secondLast = this.path[this.path.length - 2];
const dx = secondLast.x - last.x;
const dy = secondLast.y - last.y;
const segmentLength = Math.sqrt(dx * dx + dy * dy);
if (pathLength - segmentLength >= this.targetLength) {
this.path.pop();
pathLength -= segmentLength;
} else {
break;
}
}
this.currentLength = pathLength;
}
draw() {
if (this.path.length < 2) return;
// 绘制丝带状的蛇身
ctx.strokeStyle = `hsl(${this.hue}, 70%, 65%)`;
ctx.lineWidth = this.radius * 2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.shadowBlur = 20;
ctx.shadowColor = `hsl(${this.hue}, 80%, 55%)`;
ctx.beginPath();
ctx.moveTo(this.path[0].x, this.path[0].y);
for (let i = 1; i < this.path.length; i++) {
ctx.lineTo(this.path[i].x, this.path[i].y);
}
ctx.stroke();
// 重置阴影
ctx.shadowBlur = 0;
}
}
const apples = [];
function createApple() {
const margin = 50;
return {
x: margin + Math.random() * (canvas.width - margin * 2),
y: margin + Math.random() * (canvas.height - margin * 2)
};
}
// 初始化3个苹果
for (let i = 0; i < 3; i++) {
apples.push(createApple());
}
function drawApples() {
apples.forEach(apple => {
// 绘制光晕
const gradient = ctx.createRadialGradient(
apple.x, apple.y, 0,
apple.x, apple.y, 28
);
gradient.addColorStop(0, `hsla(${snake.hue}, 70%, 65%, 0.5)`);
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(apple.x, apple.y, 28, 0, Math.PI * 2);
ctx.fill();
// 绘制苹果本体
ctx.fillStyle = `hsl(${snake.hue}, 70%, 65%)`;
ctx.beginPath();
ctx.arc(apple.x, apple.y, 14, 0, Math.PI * 2);
ctx.fill();
});
}
const snake = new Snake();
let lastTime = 0;
const FPS = 60;
const frameInterval = 1000 / FPS;
function animate(currentTime) {
requestAnimationFrame(animate);
// 限制帧率为60FPS
const elapsed = currentTime - lastTime;
if (elapsed < frameInterval) {
return;
}
lastTime = currentTime - (elapsed % frameInterval);
ctx.clearRect(0, 0, canvas.width, canvas.height);
snake.findNearestApple();
snake.update();
drawApples();
snake.draw();
}
// 开始动画
animate(0);
</script>
</body>
</html>

View File

@ -44,7 +44,8 @@ from config import (
LOGS_DIR,
AGENT_VERSION,
THINKING_FAST_INTERVAL,
MAX_ACTIVE_USER_CONTAINERS
MAX_ACTIVE_USER_CONTAINERS,
PROJECT_MAX_STORAGE_MB,
)
from modules.user_manager import UserManager, UserWorkspace
from modules.gui_file_manager import GuiFileManager
@ -52,6 +53,7 @@ from modules.personalization_manager import (
load_personalization_config,
save_personalization_config,
)
from modules.user_container_manager import UserContainerManager
app = Flask(__name__, static_folder='static')
app.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_SIZE
@ -73,11 +75,11 @@ class ConversationIdConverter(BaseConverter):
app.url_map.converters['conv'] = ConversationIdConverter
user_manager = UserManager()
container_manager = UserContainerManager()
user_terminals: Dict[str, WebTerminal] = {}
terminal_rooms: Dict[str, set] = {}
connection_users: Dict[str, str] = {}
stop_flags: Dict[str, Dict[str, Any]] = {}
active_users: set = set()
DEFAULT_PORT = 8091
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
@ -230,11 +232,8 @@ def get_user_resources(username: Optional[str] = None) -> Tuple[Optional[WebTerm
username = (username or get_current_username())
if not username:
return None, None
if username not in active_users:
if len(active_users) >= MAX_ACTIVE_USER_CONTAINERS:
raise RuntimeError("资源繁忙:终端资源已用尽,请稍后重试。")
active_users.add(username)
workspace = user_manager.ensure_user_workspace(username)
container_handle = container_manager.ensure_container(username, str(workspace.project_path))
terminal = user_terminals.get(username)
if not terminal:
thinking_mode = session.get('thinking_mode', False)
@ -242,12 +241,14 @@ def get_user_resources(username: Optional[str] = None) -> Tuple[Optional[WebTerm
project_path=str(workspace.project_path),
thinking_mode=thinking_mode,
message_callback=make_terminal_callback(username),
data_dir=str(workspace.data_dir)
data_dir=str(workspace.data_dir),
container_session=container_handle
)
if terminal.terminal_manager:
terminal.terminal_manager.broadcast = terminal.message_callback
user_terminals[username] = terminal
else:
terminal.update_container_session(container_handle)
attach_user_broadcast(terminal, username)
return terminal, workspace
@ -486,7 +487,7 @@ def login():
if request.method == 'GET':
if is_logged_in():
return redirect('/new')
if len(active_users) >= MAX_ACTIVE_USER_CONTAINERS:
if not container_manager.has_capacity():
return app.send_static_file('resource_busy.html'), 503
return app.send_static_file('login.html')
@ -498,15 +499,16 @@ def login():
if not record:
return jsonify({"success": False, "error": "账号或密码错误"}), 401
if record.username not in active_users and len(active_users) >= MAX_ACTIVE_USER_CONTAINERS:
return jsonify({"success": False, "error": "资源繁忙,请稍后重试", "code": "resource_busy"}), 503
session['logged_in'] = True
session['username'] = record.username
session['thinking_mode'] = app.config.get('DEFAULT_THINKING_MODE', False)
session.permanent = True
user_manager.ensure_user_workspace(record.username)
active_users.add(record.username)
workspace = user_manager.ensure_user_workspace(record.username)
try:
container_manager.ensure_container(record.username, str(workspace.project_path))
except RuntimeError as exc:
session.clear()
return jsonify({"success": False, "error": str(exc), "code": "resource_busy"}), 503
return jsonify({"success": True})
@ -540,8 +542,8 @@ def logout():
session.clear()
if username and username in user_terminals:
user_terminals.pop(username, None)
if username in active_users:
active_users.discard(username)
if username:
container_manager.release_container(username, reason="logout")
return jsonify({"success": True})
@ -634,9 +636,46 @@ def get_status(terminal: WebTerminal, workspace: UserWorkspace, username: str):
print(f"[Status] 获取当前对话信息失败: {e}")
status['project_path'] = str(workspace.project_path)
try:
status['container'] = container_manager.get_container_status(username)
except Exception as exc:
status['container'] = {"success": False, "error": str(exc)}
status['version'] = AGENT_VERSION
return jsonify(status)
@app.route('/api/container-status')
@api_login_required
@with_terminal
def get_container_status_api(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""轮询容器状态(供前端用量面板定时刷新)。"""
try:
status = container_manager.get_container_status(username)
return jsonify({"success": True, "data": status})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/project-storage')
@api_login_required
@with_terminal
def get_project_storage(terminal: WebTerminal, workspace: UserWorkspace, username: str):
"""获取项目目录占用情况,供前端轮询。"""
try:
file_manager = getattr(terminal, 'file_manager', None)
if not file_manager:
return jsonify({"success": False, "error": "文件管理器未初始化"}), 500
used_bytes = file_manager._get_project_size()
limit_bytes = PROJECT_MAX_STORAGE_MB * 1024 * 1024 if PROJECT_MAX_STORAGE_MB else None
usage_percent = (used_bytes / limit_bytes * 100) if limit_bytes else None
data = {
"used_bytes": used_bytes,
"limit_bytes": limit_bytes,
"limit_label": f"{PROJECT_MAX_STORAGE_MB}MB" if PROJECT_MAX_STORAGE_MB else "未限制",
"usage_percent": usage_percent
}
return jsonify({"success": True, "data": data})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 500
@app.route('/api/thinking-mode', methods=['POST'])
@api_login_required
@with_terminal

View File

@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

View File

@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /opt/agent/venv)
else
# use the path as-is
export VIRTUAL_ENV=/opt/agent/venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

View File

@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /opt/agent/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash

View File

@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /opt/agent/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end

8
虚拟环境/venv/bin/distro Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from distro.distro import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

10
虚拟环境/venv/bin/docx2txt Executable file
View File

@ -0,0 +1,10 @@
#!/opt/agent/agents/agents/venv/bin/python3
import docx2txt
if __name__ == '__main__':
import sys
args = docx2txt.process_args()
text = docx2txt.process(args.docx, args.img_dir)
output = getattr(sys.stdout, 'buffer', sys.stdout)
output.write(text.encode('utf-8'))

8
虚拟环境/venv/bin/dotenv Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from dotenv.__main__ import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli())

8
虚拟环境/venv/bin/f2py Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from numpy.f2py.f2py2e import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
虚拟环境/venv/bin/flask Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from flask.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
虚拟环境/venv/bin/httpx Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from httpx import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from charset_normalizer.cli import cli_detect
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli_detect())

View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from numpy._configtool import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
虚拟环境/venv/bin/openai Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from openai.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
虚拟环境/venv/bin/pip Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
虚拟环境/venv/bin/pip3 Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
虚拟环境/venv/bin/pip3.12 Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -0,0 +1 @@
python3

View File

@ -0,0 +1 @@
/usr/bin/python3

View File

@ -0,0 +1 @@
python3

8
虚拟环境/venv/bin/tqdm Executable file
View File

@ -0,0 +1,8 @@
#!/opt/agent/agents/agents/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from tqdm.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@ -0,0 +1,21 @@
#define NO_PYGAME_C_API
#include "_surface.h"
/* The structure passed to the low level blit functions */
typedef struct {
int width;
int height;
Uint8 *s_pixels;
int s_pxskip;
int s_skip;
Uint8 *d_pixels;
int d_pxskip;
int d_skip;
SDL_PixelFormat *src;
SDL_PixelFormat *dst;
Uint8 src_blanket_alpha;
int src_has_colorkey;
Uint32 src_colorkey;
SDL_BlendMode src_blend;
SDL_BlendMode dst_blend;
} SDL_BlitInfo;

View File

@ -0,0 +1,26 @@
/*
pygame - Python Game Library
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef _CAMERA_H
#define _CAMERA_H
#include "_pygame.h"
#include "camera.h"
#endif

View File

@ -0,0 +1,374 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
/* This will use PYGAMEAPI_EXTERN_SLOTS instead
* of PYGAMEAPI_DEFINE_SLOTS for base modules.
*/
#ifndef _PYGAME_INTERNAL_H
#define _PYGAME_INTERNAL_H
#include "pgplatform.h"
/*
If PY_SSIZE_T_CLEAN is defined before including Python.h, length is a
Py_ssize_t rather than an int for all # variants of formats (s#, y#, etc.)
*/
#define PY_SSIZE_T_CLEAN
#include <Python.h>
/* Ensure PyPy-specific code is not in use when running on GraalPython (PR
* #2580) */
#if defined(GRAALVM_PYTHON) && defined(PYPY_VERSION)
#undef PYPY_VERSION
#endif
#include <SDL.h>
/* SDL 1.2 constants removed from SDL 2 */
typedef enum {
SDL_HWSURFACE = 0,
SDL_RESIZABLE = SDL_WINDOW_RESIZABLE,
SDL_ASYNCBLIT = 0,
SDL_OPENGL = SDL_WINDOW_OPENGL,
SDL_OPENGLBLIT = 0,
SDL_ANYFORMAT = 0,
SDL_HWPALETTE = 0,
SDL_DOUBLEBUF = 0,
SDL_FULLSCREEN = SDL_WINDOW_FULLSCREEN,
SDL_HWACCEL = 0,
SDL_SRCCOLORKEY = 0,
SDL_RLEACCELOK = 0,
SDL_SRCALPHA = 0,
SDL_NOFRAME = SDL_WINDOW_BORDERLESS,
SDL_GL_SWAP_CONTROL = 0,
TIMER_RESOLUTION = 0
} PygameVideoFlags;
/* the wheel button constants were removed from SDL 2 */
typedef enum {
PGM_BUTTON_LEFT = SDL_BUTTON_LEFT,
PGM_BUTTON_RIGHT = SDL_BUTTON_RIGHT,
PGM_BUTTON_MIDDLE = SDL_BUTTON_MIDDLE,
PGM_BUTTON_WHEELUP = 4,
PGM_BUTTON_WHEELDOWN = 5,
PGM_BUTTON_X1 = SDL_BUTTON_X1 + 2,
PGM_BUTTON_X2 = SDL_BUTTON_X2 + 2,
PGM_BUTTON_KEEP = 0x80
} PygameMouseFlags;
typedef enum {
/* Any SDL_* events here are for backward compatibility. */
SDL_NOEVENT = 0,
SDL_ACTIVEEVENT = SDL_USEREVENT,
SDL_VIDEORESIZE,
SDL_VIDEOEXPOSE,
PGE_MIDIIN,
PGE_MIDIOUT,
PGE_KEYREPEAT, /* Special internal pygame event, for managing key-presses
*/
/* DO NOT CHANGE THE ORDER OF EVENTS HERE */
PGE_WINDOWSHOWN,
PGE_WINDOWHIDDEN,
PGE_WINDOWEXPOSED,
PGE_WINDOWMOVED,
PGE_WINDOWRESIZED,
PGE_WINDOWSIZECHANGED,
PGE_WINDOWMINIMIZED,
PGE_WINDOWMAXIMIZED,
PGE_WINDOWRESTORED,
PGE_WINDOWENTER,
PGE_WINDOWLEAVE,
PGE_WINDOWFOCUSGAINED,
PGE_WINDOWFOCUSLOST,
PGE_WINDOWCLOSE,
PGE_WINDOWTAKEFOCUS,
PGE_WINDOWHITTEST,
PGE_WINDOWICCPROFCHANGED,
PGE_WINDOWDISPLAYCHANGED,
/* Here we define PGPOST_* events, events that act as a one-to-one
* proxy for SDL events (and some extra events too!), the proxy is used
* internally when pygame users use event.post()
*
* At a first glance, these may look redundant, but they are really
* important, especially with event blocking. If proxy events are
* not there, blocked events dont make it to our event filter, and
* that can break a lot of stuff.
*
* IMPORTANT NOTE: Do not post events directly with these proxy types,
* use the appropriate functions from event.c, that handle these proxy
* events for you.
* Proxy events are for internal use only */
PGPOST_EVENTBEGIN, /* mark start of proxy-events */
PGPOST_ACTIVEEVENT = PGPOST_EVENTBEGIN,
PGPOST_APP_TERMINATING,
PGPOST_APP_LOWMEMORY,
PGPOST_APP_WILLENTERBACKGROUND,
PGPOST_APP_DIDENTERBACKGROUND,
PGPOST_APP_WILLENTERFOREGROUND,
PGPOST_APP_DIDENTERFOREGROUND,
PGPOST_AUDIODEVICEADDED,
PGPOST_AUDIODEVICEREMOVED,
PGPOST_CLIPBOARDUPDATE,
PGPOST_CONTROLLERAXISMOTION,
PGPOST_CONTROLLERBUTTONDOWN,
PGPOST_CONTROLLERBUTTONUP,
PGPOST_CONTROLLERDEVICEADDED,
PGPOST_CONTROLLERDEVICEREMOVED,
PGPOST_CONTROLLERDEVICEREMAPPED,
PGPOST_CONTROLLERTOUCHPADDOWN,
PGPOST_CONTROLLERTOUCHPADMOTION,
PGPOST_CONTROLLERTOUCHPADUP,
PGPOST_CONTROLLERSENSORUPDATE,
PGPOST_DOLLARGESTURE,
PGPOST_DOLLARRECORD,
PGPOST_DROPFILE,
PGPOST_DROPTEXT,
PGPOST_DROPBEGIN,
PGPOST_DROPCOMPLETE,
PGPOST_FINGERMOTION,
PGPOST_FINGERDOWN,
PGPOST_FINGERUP,
PGPOST_KEYDOWN,
PGPOST_KEYMAPCHANGED,
PGPOST_KEYUP,
PGPOST_JOYAXISMOTION,
PGPOST_JOYBALLMOTION,
PGPOST_JOYHATMOTION,
PGPOST_JOYBUTTONDOWN,
PGPOST_JOYBUTTONUP,
PGPOST_JOYDEVICEADDED,
PGPOST_JOYDEVICEREMOVED,
PGPOST_LOCALECHANGED,
PGPOST_MIDIIN,
PGPOST_MIDIOUT,
PGPOST_MOUSEMOTION,
PGPOST_MOUSEBUTTONDOWN,
PGPOST_MOUSEBUTTONUP,
PGPOST_MOUSEWHEEL,
PGPOST_MULTIGESTURE,
PGPOST_NOEVENT,
PGPOST_QUIT,
PGPOST_RENDER_TARGETS_RESET,
PGPOST_RENDER_DEVICE_RESET,
PGPOST_SYSWMEVENT,
PGPOST_TEXTEDITING,
PGPOST_TEXTINPUT,
PGPOST_VIDEORESIZE,
PGPOST_VIDEOEXPOSE,
PGPOST_WINDOWSHOWN,
PGPOST_WINDOWHIDDEN,
PGPOST_WINDOWEXPOSED,
PGPOST_WINDOWMOVED,
PGPOST_WINDOWRESIZED,
PGPOST_WINDOWSIZECHANGED,
PGPOST_WINDOWMINIMIZED,
PGPOST_WINDOWMAXIMIZED,
PGPOST_WINDOWRESTORED,
PGPOST_WINDOWENTER,
PGPOST_WINDOWLEAVE,
PGPOST_WINDOWFOCUSGAINED,
PGPOST_WINDOWFOCUSLOST,
PGPOST_WINDOWCLOSE,
PGPOST_WINDOWTAKEFOCUS,
PGPOST_WINDOWHITTEST,
PGPOST_WINDOWICCPROFCHANGED,
PGPOST_WINDOWDISPLAYCHANGED,
PGE_USEREVENT, /* this event must stay in this position only */
PG_NUMEVENTS =
SDL_LASTEVENT /* Not an event. Indicates end of user events. */
} PygameEventCode;
/* SDL1 ACTIVEEVENT state attribute can take the following values */
/* These constant values are directly picked from SDL1 source */
#define SDL_APPMOUSEFOCUS 0x01
#define SDL_APPINPUTFOCUS 0x02
#define SDL_APPACTIVE 0x04
/* Surface flags: based on SDL 1.2 flags */
typedef enum {
PGS_SWSURFACE = 0x00000000,
PGS_HWSURFACE = 0x00000001,
PGS_ASYNCBLIT = 0x00000004,
PGS_ANYFORMAT = 0x10000000,
PGS_HWPALETTE = 0x20000000,
PGS_DOUBLEBUF = 0x40000000,
PGS_FULLSCREEN = 0x80000000,
PGS_SCALED = 0x00000200,
PGS_OPENGL = 0x00000002,
PGS_OPENGLBLIT = 0x0000000A,
PGS_RESIZABLE = 0x00000010,
PGS_NOFRAME = 0x00000020,
PGS_SHOWN = 0x00000040, /* Added from SDL 2 */
PGS_HIDDEN = 0x00000080, /* Added from SDL 2 */
PGS_HWACCEL = 0x00000100,
PGS_SRCCOLORKEY = 0x00001000,
PGS_RLEACCELOK = 0x00002000,
PGS_RLEACCEL = 0x00004000,
PGS_SRCALPHA = 0x00010000,
PGS_PREALLOC = 0x01000000
} PygameSurfaceFlags;
// TODO Implement check below in a way that does not break CI
/* New buffer protocol (PEP 3118) implemented on all supported Py versions.
#if !defined(Py_TPFLAGS_HAVE_NEWBUFFER)
#error No support for PEP 3118/Py_TPFLAGS_HAVE_NEWBUFFER. Please use a
supported Python version. #endif */
#define RAISE(x, y) (PyErr_SetString((x), (y)), NULL)
#define DEL_ATTR_NOT_SUPPORTED_CHECK(name, value) \
do { \
if (!value) { \
PyErr_Format(PyExc_AttributeError, "Cannot delete attribute %s", \
name); \
return -1; \
} \
} while (0)
#define DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value) \
do { \
if (!value) { \
PyErr_SetString(PyExc_AttributeError, "Cannot delete attribute"); \
return -1; \
} \
} while (0)
/*
* Initialization checks
*/
#define VIDEO_INIT_CHECK() \
if (!SDL_WasInit(SDL_INIT_VIDEO)) \
return RAISE(pgExc_SDLError, "video system not initialized")
#define JOYSTICK_INIT_CHECK() \
if (!SDL_WasInit(SDL_INIT_JOYSTICK)) \
return RAISE(pgExc_SDLError, "joystick system not initialized")
/* thread check */
#ifdef WITH_THREAD
#define PG_CHECK_THREADS() (1)
#else /* ~WITH_THREAD */
#define PG_CHECK_THREADS() \
(RAISE(PyExc_NotImplementedError, "Python built without thread support"))
#endif /* ~WITH_THREAD */
#define PyType_Init(x) (((x).ob_type) = &PyType_Type)
/* CPython 3.6 had initial and undocumented FASTCALL support, but we play it
* safe by not relying on implementation details */
#if PY_VERSION_HEX < 0x03070000
/* Macro for naming a pygame fastcall wrapper function */
#define PG_FASTCALL_NAME(func) _##func##_fastcall_wrap
/* used to forward declare compat functions */
#define PG_DECLARE_FASTCALL_FUNC(func, self_type) \
static PyObject *PG_FASTCALL_NAME(func)(self_type * self, PyObject * args)
/* Using this macro on a function defined with the FASTCALL calling convention
* adds a wrapper definition that uses regular python VARARGS convention.
* Since it is guaranteed that the 'args' object is a tuple, we can directly
* call PySequence_Fast_ITEMS and PyTuple_GET_SIZE on it (which are macros that
* assume the same, and don't do error checking) */
#define PG_WRAP_FASTCALL_FUNC(func, self_type) \
PG_DECLARE_FASTCALL_FUNC(func, self_type) \
{ \
return func(self, (PyObject *const *)PySequence_Fast_ITEMS(args), \
PyTuple_GET_SIZE(args)); \
}
#define PG_FASTCALL METH_VARARGS
#else /* PY_VERSION_HEX >= 0x03070000 */
/* compat macros are no-op on python versions that support fastcall */
#define PG_FASTCALL_NAME(func) func
#define PG_DECLARE_FASTCALL_FUNC(func, self_type)
#define PG_WRAP_FASTCALL_FUNC(func, self_type)
#define PG_FASTCALL METH_FASTCALL
#endif /* PY_VERSION_HEX >= 0x03070000 */
/*
* event module internals
*/
struct pgEventObject {
PyObject_HEAD int type;
PyObject *dict;
};
/*
* surflock module internals
*/
typedef struct {
PyObject_HEAD PyObject *surface;
PyObject *lockobj;
PyObject *weakrefs;
} pgLifetimeLockObject;
/*
* surface module internals
*/
struct pgSubSurface_Data {
PyObject *owner;
int pixeloffset;
int offsetx, offsety;
};
/*
* color module internals
*/
struct pgColorObject {
PyObject_HEAD Uint8 data[4];
Uint8 len;
};
/*
* include public API
*/
#include "include/_pygame.h"
/* Slot counts.
* Remember to keep these constants up to date.
*/
#define PYGAMEAPI_RECT_NUMSLOTS 5
#define PYGAMEAPI_JOYSTICK_NUMSLOTS 2
#define PYGAMEAPI_DISPLAY_NUMSLOTS 2
#define PYGAMEAPI_SURFACE_NUMSLOTS 4
#define PYGAMEAPI_SURFLOCK_NUMSLOTS 8
#define PYGAMEAPI_RWOBJECT_NUMSLOTS 6
#define PYGAMEAPI_PIXELARRAY_NUMSLOTS 2
#define PYGAMEAPI_COLOR_NUMSLOTS 5
#define PYGAMEAPI_MATH_NUMSLOTS 2
#define PYGAMEAPI_BASE_NUMSLOTS 27
#define PYGAMEAPI_EVENT_NUMSLOTS 6
#endif /* _PYGAME_INTERNAL_H */

View File

@ -0,0 +1,30 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
Copyright (C) 2007 Marcus von Appen
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
#ifndef _SURFACE_H
#define _SURFACE_H
#include "_pygame.h"
#include "surface.h"
#endif

View File

@ -0,0 +1,218 @@
#ifndef CAMERA_H
#define CAMERA_H
/*
pygame - Python Game Library
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "pygame.h"
#include "pgcompat.h"
#include "doc/camera_doc.h"
#if defined(__unix__)
#include <structmember.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <fcntl.h> /* low-level i/o */
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
/* on freebsd there is no asm/types */
#ifdef __linux__
#include <asm/types.h> /* for videodev2.h */
#include <linux/videodev2.h>
#endif
/* on openbsd and netbsd we need to include videoio.h */
#if defined(__OpenBSD__) || defined(__NetBSD__)
#include <sys/videoio.h>
#endif
#ifdef __FreeBSD__
#include <linux/videodev2.h>
#endif
#endif /* defined(__unix__) */
#if defined(__WIN32__)
#ifdef __MINGW32__
#undef WINVER
/** _WIN32_WINNT_WINBLUE sets minimum platform SDK to Windows 8.1. */
#define WINVER _WIN32_WINNT_WINBLUE
#endif
#include <mfapi.h>
#include <mfobjects.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <combaseapi.h>
#include <mftransform.h>
#endif
/* some constants used which are not defined on non-v4l machines. */
#ifndef V4L2_PIX_FMT_RGB24
#define V4L2_PIX_FMT_RGB24 'RGB3'
#endif
#ifndef V4L2_PIX_FMT_RGB444
#define V4L2_PIX_FMT_RGB444 'R444'
#endif
#ifndef V4L2_PIX_FMT_YUYV
#define V4L2_PIX_FMT_YUYV 'YUYV'
#endif
#ifndef V4L2_PIX_FMT_XBGR32
#define V4L2_PIX_FMT_XBGR32 'XR24'
#endif
#define CLEAR(x) memset(&(x), 0, sizeof(x))
#define SAT(c) \
if (c & (~255)) { \
if (c < 0) \
c = 0; \
else \
c = 255; \
}
#define SAT2(c) ((c) & (~255) ? ((c) < 0 ? 0 : 255) : (c))
#define DEFAULT_WIDTH 640
#define DEFAULT_HEIGHT 480
#define RGB_OUT 1
#define YUV_OUT 2
#define HSV_OUT 4
#define CAM_V4L \
1 /* deprecated. the incomplete support in pygame was removed */
#define CAM_V4L2 2
struct buffer {
void *start;
size_t length;
};
#if defined(__unix__)
typedef struct pgCameraObject {
PyObject_HEAD char *device_name;
int camera_type;
unsigned long pixelformat;
unsigned int color_out;
struct buffer *buffers;
unsigned int n_buffers;
int width;
int height;
int size;
int hflip;
int vflip;
int brightness;
int fd;
} pgCameraObject;
#else
/* generic definition.
*/
typedef struct pgCameraObject {
PyObject_HEAD char *device_name;
int camera_type;
unsigned long pixelformat;
unsigned int color_out;
struct buffer *buffers;
unsigned int n_buffers;
int width;
int height;
int size;
int hflip;
int vflip;
int brightness;
int fd;
} pgCameraObject;
#endif
/* internal functions for colorspace conversion */
void
colorspace(SDL_Surface *src, SDL_Surface *dst, int cspace);
void
rgb24_to_rgb(const void *src, void *dst, int length, SDL_PixelFormat *format);
void
bgr32_to_rgb(const void *src, void *dst, int length, SDL_PixelFormat *format);
void
rgb444_to_rgb(const void *src, void *dst, int length, SDL_PixelFormat *format);
void
rgb_to_yuv(const void *src, void *dst, int length, unsigned long source,
SDL_PixelFormat *format);
void
rgb_to_hsv(const void *src, void *dst, int length, unsigned long source,
SDL_PixelFormat *format);
void
yuyv_to_rgb(const void *src, void *dst, int length, SDL_PixelFormat *format);
void
yuyv_to_yuv(const void *src, void *dst, int length, SDL_PixelFormat *format);
void
uyvy_to_rgb(const void *src, void *dst, int length, SDL_PixelFormat *format);
void
uyvy_to_yuv(const void *src, void *dst, int length, SDL_PixelFormat *format);
void
sbggr8_to_rgb(const void *src, void *dst, int width, int height,
SDL_PixelFormat *format);
void
yuv420_to_rgb(const void *src, void *dst, int width, int height,
SDL_PixelFormat *format);
void
yuv420_to_yuv(const void *src, void *dst, int width, int height,
SDL_PixelFormat *format);
#if defined(__unix__)
/* internal functions specific to v4l2 */
char **
v4l2_list_cameras(int *num_devices);
int
v4l2_get_control(int fd, int id, int *value);
int
v4l2_set_control(int fd, int id, int value);
PyObject *
v4l2_read_raw(pgCameraObject *self);
int
v4l2_xioctl(int fd, int request, void *arg);
int
v4l2_process_image(pgCameraObject *self, const void *image, int buffer_size,
SDL_Surface *surf);
int
v4l2_query_buffer(pgCameraObject *self);
int
v4l2_read_frame(pgCameraObject *self, SDL_Surface *surf, int *errno_code);
int
v4l2_stop_capturing(pgCameraObject *self);
int
v4l2_start_capturing(pgCameraObject *self);
int
v4l2_uninit_device(pgCameraObject *self);
int
v4l2_init_mmap(pgCameraObject *self);
int
v4l2_init_device(pgCameraObject *self);
int
v4l2_close_device(pgCameraObject *self);
int
v4l2_open_device(pgCameraObject *self);
#endif
#endif /* !CAMERA_H */

View File

@ -0,0 +1,15 @@
#ifndef PGFONT_INTERNAL_H
#define PGFONT_INTERNAL_H
#include <SDL_ttf.h>
/* test font initialization */
#define FONT_INIT_CHECK() \
if (!(*(int *)PyFONT_C_API[2])) \
return RAISE(pgExc_SDLError, "font system not initialized")
#include "include/pygame_font.h"
#define PYGAMEAPI_FONT_NUMSLOTS 3
#endif /* ~PGFONT_INTERNAL_H */

View File

@ -0,0 +1,114 @@
/*
pygame - Python Game Library
Copyright (C) 2009 Vicent Marti
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef _PYGAME_FREETYPE_INTERNAL_H_
#define _PYGAME_FREETYPE_INTERNAL_H_
#include "pgcompat.h"
#include "pgplatform.h"
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_CACHE_H
#include FT_XFREE86_H
#include FT_TRIGONOMETRY_H
/**********************************************************
* Global module constants
**********************************************************/
/* Render styles */
#define FT_STYLE_NORMAL 0x00
#define FT_STYLE_STRONG 0x01
#define FT_STYLE_OBLIQUE 0x02
#define FT_STYLE_UNDERLINE 0x04
#define FT_STYLE_WIDE 0x08
#define FT_STYLE_DEFAULT 0xFF
/* Bounding box modes */
#define FT_BBOX_EXACT FT_GLYPH_BBOX_SUBPIXELS
#define FT_BBOX_EXACT_GRIDFIT FT_GLYPH_BBOX_GRIDFIT
#define FT_BBOX_PIXEL FT_GLYPH_BBOX_TRUNCATE
#define FT_BBOX_PIXEL_GRIDFIT FT_GLYPH_BBOX_PIXELS
/* Rendering flags */
#define FT_RFLAG_NONE (0)
#define FT_RFLAG_ANTIALIAS (1 << 0)
#define FT_RFLAG_AUTOHINT (1 << 1)
#define FT_RFLAG_VERTICAL (1 << 2)
#define FT_RFLAG_HINTED (1 << 3)
#define FT_RFLAG_KERNING (1 << 4)
#define FT_RFLAG_TRANSFORM (1 << 5)
#define FT_RFLAG_PAD (1 << 6)
#define FT_RFLAG_ORIGIN (1 << 7)
#define FT_RFLAG_UCS4 (1 << 8)
#define FT_RFLAG_USE_BITMAP_STRIKES (1 << 9)
#define FT_RFLAG_DEFAULTS \
(FT_RFLAG_HINTED | FT_RFLAG_USE_BITMAP_STRIKES | FT_RFLAG_ANTIALIAS)
#define FT_RENDER_NEWBYTEARRAY 0x0
#define FT_RENDER_NEWSURFACE 0x1
#define FT_RENDER_EXISTINGSURFACE 0x2
/**********************************************************
* Global module types
**********************************************************/
typedef struct _scale_s {
FT_UInt x, y;
} Scale_t;
typedef FT_Angle Angle_t;
struct fontinternals_;
struct freetypeinstance_;
typedef struct {
FT_Long font_index;
FT_Open_Args open_args;
} pgFontId;
typedef struct {
PyObject_HEAD pgFontId id;
PyObject *path;
int is_scalable;
int is_bg_col_set;
Scale_t face_size;
FT_Int16 style;
FT_Int16 render_flags;
double strength;
double underline_adjustment;
FT_UInt resolution;
Angle_t rotation;
FT_Matrix transform;
FT_Byte fgcolor[4];
FT_Byte bgcolor[4];
struct freetypeinstance_ *freetype; /* Personal reference */
struct fontinternals_ *_internals;
} pgFontObject;
#define pgFont_IS_ALIVE(o) (((pgFontObject *)(o))->_internals != 0)
/* import public API */
#include "include/pygame_freetype.h"
#define PYGAMEAPI_FREETYPE_NUMSLOTS 2
#endif /* ~_PYGAME_FREETYPE_INTERNAL_H_ */

View File

@ -0,0 +1,949 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
#ifndef _PYGAME_H
#define _PYGAME_H
/** This header file includes all the definitions for the
** base pygame extensions. This header only requires
** Python includes (and SDL.h for functions that use SDL types).
** The reason for functions prototyped with #define's is
** to allow for maximum Python portability. It also uses
** Python as the runtime linker, which allows for late binding.
'' For more information on this style of development, read
** the Python docs on this subject.
** http://www.python.org/doc/current/ext/using-cobjects.html
**
** If using this to build your own derived extensions,
** you'll see that the functions available here are mainly
** used to help convert between python objects and SDL objects.
** Since this library doesn't add a lot of functionality to
** the SDL library, it doesn't need to offer a lot either.
**
** When initializing your extension module, you must manually
** import the modules you want to use. (this is the part about
** using python as the runtime linker). Each module has its
** own import_xxx() routine. You need to perform this import
** after you have initialized your own module, and before
** you call any routines from that module. Since every module
** in pygame does this, there are plenty of examples.
**
** The base module does include some useful conversion routines
** that you are free to use in your own extension.
**/
#include "pgplatform.h"
#include <Python.h>
/* version macros (defined since version 1.9.5) */
#define PG_MAJOR_VERSION 2
#define PG_MINOR_VERSION 6
#define PG_PATCH_VERSION 1
#define PG_VERSIONNUM(MAJOR, MINOR, PATCH) \
(1000 * (MAJOR) + 100 * (MINOR) + (PATCH))
#define PG_VERSION_ATLEAST(MAJOR, MINOR, PATCH) \
(PG_VERSIONNUM(PG_MAJOR_VERSION, PG_MINOR_VERSION, PG_PATCH_VERSION) >= \
PG_VERSIONNUM(MAJOR, MINOR, PATCH))
#include "pgcompat.h"
/* Flag indicating a pg_buffer; used for assertions within callbacks */
#ifndef NDEBUG
#define PyBUF_PYGAME 0x4000
#endif
#define PyBUF_HAS_FLAG(f, F) (((f) & (F)) == (F))
/* Array information exchange struct C type; inherits from Py_buffer
*
* Pygame uses its own Py_buffer derived C struct as an internal representation
* of an imported array buffer. The extended Py_buffer allows for a
* per-instance release callback,
*/
typedef void (*pybuffer_releaseproc)(Py_buffer *);
typedef struct pg_bufferinfo_s {
Py_buffer view;
PyObject *consumer; /* Input: Borrowed reference */
pybuffer_releaseproc release_buffer;
} pg_buffer;
#include "pgimport.h"
/*
* BASE module
*/
#ifndef PYGAMEAPI_BASE_INTERNAL
#define pgExc_SDLError ((PyObject *)PYGAMEAPI_GET_SLOT(base, 0))
#define pg_RegisterQuit \
(*(void (*)(void (*)(void)))PYGAMEAPI_GET_SLOT(base, 1))
/**
* \brief Convert number like object *obj* to C int and in *val*.
*
* \param obj The Python object to convert.
* \param val A pointer to the C integer to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*
* \note This function will clear any Python errors.
* \note This function will convert floats to integers.
*/
#define pg_IntFromObj \
(*(int (*)(PyObject *, int *))PYGAMEAPI_GET_SLOT(base, 2))
/**
* \brief Convert number like object at position *i* in sequence *obj*
* to C int and place in argument *val*.
*
* \param obj The Python object to convert.
* \param i The index of the object to convert.
* \param val A pointer to the C integer to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*
* \note This function will clear any Python errors.
* \note This function will convert floats to integers.
*/
#define pg_IntFromObjIndex \
(*(int (*)(PyObject *, int, int *))PYGAMEAPI_GET_SLOT(base, 3))
/**
* \brief Convert the two number like objects in length 2 sequence *obj* to C
* int and place in arguments *val1* and *val2*.
*
* \param obj The Python two element sequence object to convert.
* \param val A pointer to the C integer to store the result.
* \param val2 A pointer to the C integer to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*
* \note This function will clear any Python errors.
* \note This function will convert floats to integers.
*/
#define pg_TwoIntsFromObj \
(*(int (*)(PyObject *, int *, int *))PYGAMEAPI_GET_SLOT(base, 4))
/**
* \brief Convert number like object *obj* to C float and in *val*.
*
* \param obj The Python object to convert.
* \param val A pointer to the C float to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*
* \note This function will clear any Python errors.
*/
#define pg_FloatFromObj \
(*(int (*)(PyObject *, float *))PYGAMEAPI_GET_SLOT(base, 5))
/**
* \brief Convert number like object at position *i* in sequence *obj* to C
* float and place in argument *val*.
*
* \param obj The Python object to convert.
* \param i The index of the object to convert.
* \param val A pointer to the C float to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*
* \note This function will clear any Python errors.
*/
#define pg_FloatFromObjIndex \
(*(int (*)(PyObject *, int, float *))PYGAMEAPI_GET_SLOT(base, 6))
/**
* \brief Convert the two number like objects in length 2 sequence *obj* to C
* float and place in arguments *val1* and *val2*.
*
* \param obj The Python two element sequence object to convert.
* \param val A pointer to the C float to store the result.
* \param val2 A pointer to the C float to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*
* \note This function will clear any Python errors.
*/
#define pg_TwoFloatsFromObj \
(*(int (*)(PyObject *, float *, float *))PYGAMEAPI_GET_SLOT(base, 7))
/**
* \brief Convert number like object *obj* to C Uint32 and in *val*.
*
* \param obj The Python object to convert.
* \param val A pointer to the C int to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*/
#define pg_UintFromObj \
(*(int (*)(PyObject *, Uint32 *))PYGAMEAPI_GET_SLOT(base, 8))
/**
* \brief Convert number like object at position *i* in sequence *obj* to C
* Uint32 and place in argument *val*.
*
* \param obj The Python object to convert.
* \param i The index of the object to convert.
* \param val A pointer to the C int to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*/
#define pg_UintFromObjIndex \
(*(int (*)(PyObject *, int, Uint32 *))PYGAMEAPI_GET_SLOT(base, 9))
/**
* \brief Initialize all of the pygame modules.
* \returns 1 on success, 0 on failure with PyErr set.
*/
#define pg_mod_autoinit (*(int (*)(const char *))PYGAMEAPI_GET_SLOT(base, 10))
/**
* \brief Quit all of the pygame modules.
*/
#define pg_mod_autoquit (*(void (*)(const char *))PYGAMEAPI_GET_SLOT(base, 11))
/**
* \brief Convert the color represented by object *obj* into a red, green,
* blue, alpha length 4 C array *RGBA*.
*
* The object must be a length 3 or 4 sequence of numbers having values between
* 0 and 255 inclusive. For a length 3 sequence an alpha value of 255 is
* assumed.
*
* \param obj The Python object to convert.
* \param RGBA A pointer to the C array to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*/
#define pg_RGBAFromObj \
(*(int (*)(PyObject *, Uint8 *))PYGAMEAPI_GET_SLOT(base, 12))
/**
* \brief Given a Py_buffer, return a python dictionary representing the array
* interface.
*
* \param view_p A pointer to the Py_buffer to convert to a dictionary.
*
* \returns A Python dictionary representing the array interface of the object.
*/
#define pgBuffer_AsArrayInterface \
(*(PyObject * (*)(Py_buffer *)) PYGAMEAPI_GET_SLOT(base, 13))
/**
* \brief Given a Py_buffer, return a python capsule representing the array
* interface.
*
* \param view_p A pointer to the Py_buffer to convert to a capsule.
*
* \returns A Python capsule representing the array interface of the object.
*/
#define pgBuffer_AsArrayStruct \
(*(PyObject * (*)(Py_buffer *)) PYGAMEAPI_GET_SLOT(base, 14))
/**
* \brief Get a buffer object from a given Python object.
*
* \param obj The Python object to get the buffer from.
* \param pg_view_p A pointer to a pg_buffer struct to store the buffer in.
* \param flags The desired buffer access mode.
*
* \returns 0 on success, -1 on failure.
*
* \note This function attempts to get a buffer object from a given Python
* object. If the object supports the buffer protocol, it will be used to
* create the buffer. If not, it will try to get an array interface or
* dictionary representation of the object and use that to create the buffer.
* If none of these methods work, it will raise a ValueError.
*
*/
#define pgObject_GetBuffer \
(*(int (*)(PyObject *, pg_buffer *, int))PYGAMEAPI_GET_SLOT(base, 15))
/**
* \brief Release a pg_buffer object.
*
* \param pg_view_p The pg_buffer object to release.
*
* \note This function releases a pg_buffer object.
* \note some calls to this function expect this function to not clear
* previously set errors.
*/
#define pgBuffer_Release (*(void (*)(pg_buffer *))PYGAMEAPI_GET_SLOT(base, 16))
/**
* \brief Write the array interface dictionary buffer description *dict* into a
* Pygame buffer description struct *pg_view_p*.
*
* \param pg_view_p The Pygame buffer description struct to write into.
* \param dict The array interface dictionary to read from.
* \param flags The PyBUF flags describing the view type requested.
*
* \returns 0 on success, or -1 on failure.
*/
#define pgDict_AsBuffer \
(*(int (*)(pg_buffer *, PyObject *, int))PYGAMEAPI_GET_SLOT(base, 17))
#define pgExc_BufferError ((PyObject *)PYGAMEAPI_GET_SLOT(base, 18))
/**
* \brief Get the default SDL window created by a pygame.display.set_mode()
* call, or *NULL*.
*
* \return The default window, or *NULL* if no window has been created.
*/
#define pg_GetDefaultWindow \
(*(SDL_Window * (*)(void)) PYGAMEAPI_GET_SLOT(base, 19))
/**
* \brief Set the default SDL window created by a pygame.display.set_mode()
* call. The previous window, if any, is destroyed. Argument *win* may be
* *NULL*. This function is called by pygame.display.set_mode().
*
* \param win The new default window. May be NULL.
*/
#define pg_SetDefaultWindow \
(*(void (*)(SDL_Window *))PYGAMEAPI_GET_SLOT(base, 20))
/**
* \brief Return a borrowed reference to the Pygame default window display
* surface, or *NULL* if no default window is open.
*
* \return The default renderer, or *NULL* if no renderer has been created.
*/
#define pg_GetDefaultWindowSurface \
(*(pgSurfaceObject * (*)(void)) PYGAMEAPI_GET_SLOT(base, 21))
/**
* \brief Set the Pygame default window display surface. The previous
* surface, if any, is destroyed. Argument *screen* may be *NULL*. This
* function is called by pygame.display.set_mode().
*
* \param screen The new default window display surface. May be NULL.
*/
#define pg_SetDefaultWindowSurface \
(*(void (*)(pgSurfaceObject *))PYGAMEAPI_GET_SLOT(base, 22))
/**
* \returns NULL if the environment variable PYGAME_BLEND_ALPHA_SDL2 is not
* set, otherwise returns a pointer to the environment variable.
*/
#define pg_EnvShouldBlendAlphaSDL2 \
(*(char *(*)(void))PYGAMEAPI_GET_SLOT(base, 23))
/**
* \brief Convert number like object *obj* to C double and in *val*.
*
* \param obj The Python object to convert.
* \param val A pointer to the C double to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*
* \note This function will clear any Python errors.
*/
#define pg_DoubleFromObj \
(*(int (*)(PyObject *, double *))PYGAMEAPI_GET_SLOT(base, 24))
/**
* \brief Convert number like object at position *i* in sequence *obj* to C
* double and place in argument *val*.
*
* \param obj The Python object to convert.
* \param i The index of the object to convert.
* \param val A pointer to the C double to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*
* \note This function will clear any Python errors.
*/
#define pg_DoubleFromObjIndex \
(*(int (*)(PyObject *, int, double *))PYGAMEAPI_GET_SLOT(base, 25))
/**
* \brief Convert the two number like objects in length 2 sequence *obj* to C
* double and place in arguments *val1* and *val2*.
*
* \param obj The Python two element sequence object to convert.
* \param val A pointer to the C double to store the result.
* \param val2 A pointer to the C double to store the result.
* \returns 1 if the conversion was successful, 0 otherwise.
*/
#define pg_TwoDoublesFromObj \
(*(int (*)(PyObject *, double *, double *))PYGAMEAPI_GET_SLOT(base, 26))
#define import_pygame_base() IMPORT_PYGAME_MODULE(base)
#endif /* ~PYGAMEAPI_BASE_INTERNAL */
typedef struct {
/**
* \brief The SDL rect wrapped by this object.
*/
PyObject_HEAD SDL_Rect r;
/**
* \brief A list of weak references to this rect.
*/
PyObject *weakreflist;
} pgRectObject;
/**
* \brief Convert a pgRectObject to an SDL_Rect.
*
* \param obj A pgRectObject instance.
* \returns the SDL_Rect field of *obj*, a pgRect_Type instance.
*
* \note SDL_Rect pgRect_AsRect(PyObject *obj)
*/
#define pgRect_AsRect(x) (((pgRectObject *)x)->r)
#ifndef PYGAMEAPI_RECT_INTERNAL
/**
* \brief The Pygame rectangle object type pygame.Rect.
*/
#define pgRect_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(rect, 0))
/**
* \brief Check if *obj* is a `pygame.Rect` instance.
*
* \returns true if *obj* is a `pygame.Rect` instance
*/
#define pgRect_Check(obj) ((obj)->ob_type == &pgRect_Type)
/**
* \brief Create a new `pygame.Rect` instance.
*
* \param r A pointer to an SDL_Rect struct.
* \returns a new `pygame.Rect` object for the SDL_Rect *r*.
* Returns *NULL* on error.
*
* \note PyObject* pgRect_New(SDL_Rect *r)
*/
#define pgRect_New (*(PyObject * (*)(SDL_Rect *)) PYGAMEAPI_GET_SLOT(rect, 1))
/**
* \brief Create a new `pygame.Rect` instance from x, y, w, h.
*
* \param x The x coordinate of the rectangle.
* \param y The y coordinate of the rectangle.
* \param w The width of the rectangle.
* \param h The height of the rectangle.
* \returns a new `pygame.Rect` object. Returns *NULL* on error.
*
* \note PyObject* pgRect_New4(int x, int y, int w, int h)
*/
#define pgRect_New4 \
(*(PyObject * (*)(int, int, int, int)) PYGAMEAPI_GET_SLOT(rect, 2))
/**
* \brief Convert a Python object to a `pygame.Rect` instance.
*
* \param obj A Python object.
* A rectangle can be a length 4 sequence integers (x, y, w, h), or a length 2
* sequence of position (x, y) and size (w, h), or a length 1 tuple containing
* a rectangle representation, or have a method *rect* that returns a
* rectangle.
*
* \param temp A pointer to an SDL_Rect struct to store the result in.
* \returns a pointer to the SDL_Rect field of the `pygame.Rect` instance
* *obj*. Returns *NULL* on error.
*
* \note This function will clear any Python errors.
* \note SDL_Rect* pgRect_FromObject(PyObject *obj, SDL_Rect *temp)
*/
#define pgRect_FromObject \
(*(SDL_Rect * (*)(PyObject *, SDL_Rect *)) PYGAMEAPI_GET_SLOT(rect, 3))
/**
* \brief Normalize a `pygame.Rect` instance. A rect with a negative size
* (negative width and/or height) will be adjusted to have a positive size.
*
* \param rect A pointer to a `pygame.Rect` instance.
* \returns *rect* normalized with positive values only.
*
* \note void pgRect_Normalize(SDL_Rect *rect)
*/
#define pgRect_Normalize (*(void (*)(SDL_Rect *))PYGAMEAPI_GET_SLOT(rect, 4))
#define import_pygame_rect() IMPORT_PYGAME_MODULE(rect)
#endif /* ~PYGAMEAPI_RECT_INTERNAL */
/*
* JOYSTICK module
*/
typedef struct pgJoystickObject {
PyObject_HEAD int id;
SDL_Joystick *joy;
/* Joysticks form an intrusive linked list.
*
* Note that we don't maintain refcounts for these so they are weakrefs
* from the Python side.
*/
struct pgJoystickObject *next;
struct pgJoystickObject *prev;
} pgJoystickObject;
#define pgJoystick_AsID(x) (((pgJoystickObject *)x)->id)
#define pgJoystick_AsSDL(x) (((pgJoystickObject *)x)->joy)
#ifndef PYGAMEAPI_JOYSTICK_INTERNAL
#define pgJoystick_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(joystick, 0))
#define pgJoystick_Check(x) ((x)->ob_type == &pgJoystick_Type)
#define pgJoystick_New (*(PyObject * (*)(int)) PYGAMEAPI_GET_SLOT(joystick, 1))
#define import_pygame_joystick() IMPORT_PYGAME_MODULE(joystick)
#endif
/*
* DISPLAY module
*/
typedef struct {
Uint32 hw_available : 1;
Uint32 wm_available : 1;
Uint32 blit_hw : 1;
Uint32 blit_hw_CC : 1;
Uint32 blit_hw_A : 1;
Uint32 blit_sw : 1;
Uint32 blit_sw_CC : 1;
Uint32 blit_sw_A : 1;
Uint32 blit_fill : 1;
Uint32 video_mem;
SDL_PixelFormat *vfmt;
SDL_PixelFormat vfmt_data;
int current_w;
int current_h;
} pg_VideoInfo;
/**
* A pygame object that wraps an SDL_VideoInfo struct.
* The object returned by `pygame.display.Info()`
*/
typedef struct {
PyObject_HEAD pg_VideoInfo info;
} pgVidInfoObject;
/**
* \brief Convert a pgVidInfoObject to an SDL_VideoInfo.
*
* \note SDL_VideoInfo pgVidInfo_AsVidInfo(PyObject *obj)
*
* \returns the SDL_VideoInfo field of *obj*, a pgVidInfo_Type instance.
* \param obj A pgVidInfo_Type instance.
*
* \note Does not check that *obj* is not `NULL` or an `pgVidInfoObject`
* object.
*/
#define pgVidInfo_AsVidInfo(x) (((pgVidInfoObject *)x)->info)
#ifndef PYGAMEAPI_DISPLAY_INTERNAL
/**
* \brief The pgVidInfoObject object Python type.
* \note pgVideoInfo_Type is used for the `pygame.display.Info()` object.
*/
#define pgVidInfo_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(display, 0))
/**
* \brief Check if *obj* is a pgVidInfoObject.
*
* \returns true if *x* is a `pgVidInfo_Type` instance
* \note Will return false if *x* is a subclass of `pgVidInfo_Type`.
* \note This macro does not check that *x* is not ``NULL``.
* \note int pgVidInfo_Check(PyObject *x)
*/
#define pgVidInfo_Check(x) ((x)->ob_type == &pgVidInfo_Type)
/**
* \brief Create a new pgVidInfoObject.
*
* \param i A pointer to an SDL_VideoInfo struct.
* \returns a new `pgVidInfoObject` object for the SDL_VideoInfo *i*.
*
* \note PyObject* pgVidInfo_New(SDL_VideoInfo *i)
* \note On failure, raise a Python exception and return `NULL`.
*/
#define pgVidInfo_New \
(*(PyObject * (*)(pg_VideoInfo *)) PYGAMEAPI_GET_SLOT(display, 1))
#define import_pygame_display() IMPORT_PYGAME_MODULE(display)
#endif /* ~PYGAMEAPI_DISPLAY_INTERNAL */
/*
* SURFACE module
*/
struct pgSubSurface_Data;
struct SDL_Surface;
/**
* \brief A pygame object that wraps an SDL_Surface. A `pygame.Surface`
* instance.
*/
typedef struct {
PyObject_HEAD struct SDL_Surface *surf;
/**
* \brief If true, the surface will be freed when the python object is
* destroyed.
*/
int owner;
/**
* \brief The subsurface data for this surface (if a subsurface).
*/
struct pgSubSurface_Data *subsurface;
/**
* \brief A list of weak references to this surface.
*/
PyObject *weakreflist;
/**
* \brief A list of locks for this surface.
*/
PyObject *locklist;
/**
* \brief Usually a buffer object which the surface gets its data from.
*/
PyObject *dependency;
} pgSurfaceObject;
/**
* \brief Convert a `pygame.Surface` instance to an SDL_Surface.
*
* \param x A `pygame.Surface` instance.
* \returns the SDL_Surface field of *x*, a `pygame.Surface` instance.
*
* \note SDL_Surface* pgSurface_AsSurface(PyObject *x)
*/
#define pgSurface_AsSurface(x) (((pgSurfaceObject *)x)->surf)
#ifndef PYGAMEAPI_SURFACE_INTERNAL
/**
* \brief The `pygame.Surface` object Python type.
*/
#define pgSurface_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(surface, 0))
/**
* \brief Check if *x* is a `pygame.Surface` instance.
*
* \param x The object to check.
* \returns true if *x* is a `pygame.Surface` instance
*
* \note Will return false if *x* is a subclass of `pygame.Surface`.
* \note This macro does not check that *x* is not ``NULL``.
* \note int pgSurface_Check(PyObject *x)
*/
#define pgSurface_Check(x) \
(PyObject_IsInstance((x), (PyObject *)&pgSurface_Type))
/**
* \brief Create a new `pygame.Surface` instance.
*
* \param s The SDL surface to wrap in a python object.
* \param owner If true, the surface will be freed when the python object is
* destroyed. \returns A new new pygame surface instance for SDL surface *s*.
* Returns *NULL* on error.
*
* \note pgSurfaceObject* pgSurface_New2(SDL_Surface *s, int owner)
*/
#define pgSurface_New2 \
(*(pgSurfaceObject * (*)(SDL_Surface *, int)) \
PYGAMEAPI_GET_SLOT(surface, 1))
/**
* \brief Sets the SDL surface for a `pygame.Surface` instance.
*
* \param self The `pygame.Surface` instance to set the surface for.
* \param s The SDL surface to set.
* \param owner If true, the surface will be freed when the python object is
* destroyed. \returns 0 on success, -1 on failure.
*
* \note int pgSurface_SetSurface(pgSurfaceObject *self, SDL_Surface *s, int
* owner)
*/
#define pgSurface_SetSurface \
(*(int (*)(pgSurfaceObject *, SDL_Surface *, int))PYGAMEAPI_GET_SLOT( \
surface, 3))
/**
* \brief Blit one surface onto another.
*
* \param dstobj The destination surface.
* \param srcobj The source surface.
* \param dstrect The destination rectangle.
* \param srcrect The source rectangle.
* \param the_args The blit flags.
* \return 0 for success, -1 or -2 for error.
*
* \note Is accessible through the C api.
* \note int pgSurface_Blit(PyObject *dstobj, PyObject *srcobj, SDL_Rect
* *dstrect, SDL_Rect *srcrect, int the_args)
*/
#define pgSurface_Blit \
(*(int (*)(pgSurfaceObject *, pgSurfaceObject *, SDL_Rect *, SDL_Rect *, \
int))PYGAMEAPI_GET_SLOT(surface, 2))
#define import_pygame_surface() \
do { \
IMPORT_PYGAME_MODULE(surface); \
if (PyErr_Occurred() != NULL) \
break; \
IMPORT_PYGAME_MODULE(surflock); \
} while (0)
#define pgSurface_New(surface) pgSurface_New2((surface), 1)
#define pgSurface_NewNoOwn(surface) pgSurface_New2((surface), 0)
#endif /* ~PYGAMEAPI_SURFACE_INTERNAL */
/*
* SURFLOCK module
* auto imported/initialized by surface
*/
#ifndef PYGAMEAPI_SURFLOCK_INTERNAL
#define pgLifetimeLock_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(surflock, 0))
#define pgLifetimeLock_Check(x) ((x)->ob_type == &pgLifetimeLock_Type)
#define pgSurface_Prep(x) \
if ((x)->subsurface) \
(*(*(void (*)(pgSurfaceObject *))PYGAMEAPI_GET_SLOT(surflock, 1)))(x)
#define pgSurface_Unprep(x) \
if ((x)->subsurface) \
(*(*(void (*)(pgSurfaceObject *))PYGAMEAPI_GET_SLOT(surflock, 2)))(x)
#define pgSurface_Lock \
(*(int (*)(pgSurfaceObject *))PYGAMEAPI_GET_SLOT(surflock, 3))
#define pgSurface_Unlock \
(*(int (*)(pgSurfaceObject *))PYGAMEAPI_GET_SLOT(surflock, 4))
#define pgSurface_LockBy \
(*(int (*)(pgSurfaceObject *, PyObject *))PYGAMEAPI_GET_SLOT(surflock, 5))
#define pgSurface_UnlockBy \
(*(int (*)(pgSurfaceObject *, PyObject *))PYGAMEAPI_GET_SLOT(surflock, 6))
#define pgSurface_LockLifetime \
(*(PyObject * (*)(PyObject *, PyObject *)) PYGAMEAPI_GET_SLOT(surflock, 7))
#endif
/*
* EVENT module
*/
typedef struct pgEventObject pgEventObject;
#ifndef PYGAMEAPI_EVENT_INTERNAL
#define pgEvent_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(event, 0))
#define pgEvent_Check(x) ((x)->ob_type == &pgEvent_Type)
#define pgEvent_New \
(*(PyObject * (*)(SDL_Event *)) PYGAMEAPI_GET_SLOT(event, 1))
#define pgEvent_New2 \
(*(PyObject * (*)(int, PyObject *)) PYGAMEAPI_GET_SLOT(event, 2))
#define pgEvent_FillUserEvent \
(*(int (*)(pgEventObject *, SDL_Event *))PYGAMEAPI_GET_SLOT(event, 3))
#define pg_EnableKeyRepeat (*(int (*)(int, int))PYGAMEAPI_GET_SLOT(event, 4))
#define pg_GetKeyRepeat (*(void (*)(int *, int *))PYGAMEAPI_GET_SLOT(event, 5))
#define import_pygame_event() IMPORT_PYGAME_MODULE(event)
#endif
/*
* RWOBJECT module
* the rwobject are only needed for C side work, not accessible from python.
*/
#ifndef PYGAMEAPI_RWOBJECT_INTERNAL
#define pgRWops_FromObject \
(*(SDL_RWops * (*)(PyObject *, char **)) PYGAMEAPI_GET_SLOT(rwobject, 0))
#define pgRWops_IsFileObject \
(*(int (*)(SDL_RWops *))PYGAMEAPI_GET_SLOT(rwobject, 1))
#define pg_EncodeFilePath \
(*(PyObject * (*)(PyObject *, PyObject *)) PYGAMEAPI_GET_SLOT(rwobject, 2))
#define pg_EncodeString \
(*(PyObject * (*)(PyObject *, const char *, const char *, PyObject *)) \
PYGAMEAPI_GET_SLOT(rwobject, 3))
#define pgRWops_FromFileObject \
(*(SDL_RWops * (*)(PyObject *)) PYGAMEAPI_GET_SLOT(rwobject, 4))
#define pgRWops_ReleaseObject \
(*(int (*)(SDL_RWops *))PYGAMEAPI_GET_SLOT(rwobject, 5))
#define import_pygame_rwobject() IMPORT_PYGAME_MODULE(rwobject)
#endif
/*
* PixelArray module
*/
#ifndef PYGAMEAPI_PIXELARRAY_INTERNAL
#define PyPixelArray_Type ((PyTypeObject *)PYGAMEAPI_GET_SLOT(pixelarray, 0))
#define PyPixelArray_Check(x) ((x)->ob_type == &PyPixelArray_Type)
#define PyPixelArray_New (*(PyObject * (*)) PYGAMEAPI_GET_SLOT(pixelarray, 1))
#define import_pygame_pixelarray() IMPORT_PYGAME_MODULE(pixelarray)
#endif /* PYGAMEAPI_PIXELARRAY_INTERNAL */
/*
* Color module
*/
typedef struct pgColorObject pgColorObject;
#ifndef PYGAMEAPI_COLOR_INTERNAL
#define pgColor_Type (*(PyObject *)PYGAMEAPI_GET_SLOT(color, 0))
#define pgColor_Check(x) ((x)->ob_type == &pgColor_Type)
#define pgColor_New (*(PyObject * (*)(Uint8 *)) PYGAMEAPI_GET_SLOT(color, 1))
#define pgColor_NewLength \
(*(PyObject * (*)(Uint8 *, Uint8)) PYGAMEAPI_GET_SLOT(color, 3))
#define pg_RGBAFromColorObj \
(*(int (*)(PyObject *, Uint8 *))PYGAMEAPI_GET_SLOT(color, 2))
#define pg_RGBAFromFuzzyColorObj \
(*(int (*)(PyObject *, Uint8 *))PYGAMEAPI_GET_SLOT(color, 4))
#define pgColor_AsArray(x) (((pgColorObject *)x)->data)
#define pgColor_NumComponents(x) (((pgColorObject *)x)->len)
#define import_pygame_color() IMPORT_PYGAME_MODULE(color)
#endif /* PYGAMEAPI_COLOR_INTERNAL */
/*
* Math module
*/
#ifndef PYGAMEAPI_MATH_INTERNAL
#define pgVector2_Check(x) \
((x)->ob_type == (PyTypeObject *)PYGAMEAPI_GET_SLOT(math, 0))
#define pgVector3_Check(x) \
((x)->ob_type == (PyTypeObject *)PYGAMEAPI_GET_SLOT(math, 1))
/*
#define pgVector2_New \
(*(PyObject*(*)) \
PYGAMEAPI_GET_SLOT(PyGAME_C_API, 1))
*/
#define import_pygame_math() IMPORT_PYGAME_MODULE(math)
#endif /* PYGAMEAPI_MATH_INTERNAL */
#define IMPORT_PYGAME_MODULE _IMPORT_PYGAME_MODULE
/*
* base pygame API slots
* disable slots with NO_PYGAME_C_API
*/
#ifdef PYGAME_H
PYGAMEAPI_DEFINE_SLOTS(base);
PYGAMEAPI_DEFINE_SLOTS(rect);
PYGAMEAPI_DEFINE_SLOTS(cdrom);
PYGAMEAPI_DEFINE_SLOTS(joystick);
PYGAMEAPI_DEFINE_SLOTS(display);
PYGAMEAPI_DEFINE_SLOTS(surface);
PYGAMEAPI_DEFINE_SLOTS(surflock);
PYGAMEAPI_DEFINE_SLOTS(event);
PYGAMEAPI_DEFINE_SLOTS(rwobject);
PYGAMEAPI_DEFINE_SLOTS(pixelarray);
PYGAMEAPI_DEFINE_SLOTS(color);
PYGAMEAPI_DEFINE_SLOTS(math);
#else /* ~PYGAME_H */
PYGAMEAPI_EXTERN_SLOTS(base);
PYGAMEAPI_EXTERN_SLOTS(rect);
PYGAMEAPI_EXTERN_SLOTS(cdrom);
PYGAMEAPI_EXTERN_SLOTS(joystick);
PYGAMEAPI_EXTERN_SLOTS(display);
PYGAMEAPI_EXTERN_SLOTS(surface);
PYGAMEAPI_EXTERN_SLOTS(surflock);
PYGAMEAPI_EXTERN_SLOTS(event);
PYGAMEAPI_EXTERN_SLOTS(rwobject);
PYGAMEAPI_EXTERN_SLOTS(pixelarray);
PYGAMEAPI_EXTERN_SLOTS(color);
PYGAMEAPI_EXTERN_SLOTS(math);
#endif /* ~PYGAME_H */
#endif /* PYGAME_H */
/* Use the end of this file for other cross module inline utility
* functions There seems to be no good reason to stick to macro only
* functions in Python 3.
*/
static PG_INLINE PyObject *
pg_tuple_couple_from_values_int(int val1, int val2)
{
/* This function turns two input integers into a python tuple object.
* Currently, 5th November 2022, this is faster than using Py_BuildValue
* to do the same thing.
*/
PyObject *tup = PyTuple_New(2);
if (!tup) {
return NULL;
}
PyObject *tmp = PyLong_FromLong(val1);
if (!tmp) {
Py_DECREF(tup);
return NULL;
}
PyTuple_SET_ITEM(tup, 0, tmp);
tmp = PyLong_FromLong(val2);
if (!tmp) {
Py_DECREF(tup);
return NULL;
}
PyTuple_SET_ITEM(tup, 1, tmp);
return tup;
}
static PG_INLINE PyObject *
pg_tuple_triple_from_values_int(int val1, int val2, int val3)
{
/* This function turns three input integers into a python tuple object.
* Currently, 5th November 2022, this is faster than using Py_BuildValue
* to do the same thing.
*/
PyObject *tup = PyTuple_New(3);
if (!tup) {
return NULL;
}
PyObject *tmp = PyLong_FromLong(val1);
if (!tmp) {
Py_DECREF(tup);
return NULL;
}
PyTuple_SET_ITEM(tup, 0, tmp);
tmp = PyLong_FromLong(val2);
if (!tmp) {
Py_DECREF(tup);
return NULL;
}
PyTuple_SET_ITEM(tup, 1, tmp);
tmp = PyLong_FromLong(val3);
if (!tmp) {
Py_DECREF(tup);
return NULL;
}
PyTuple_SET_ITEM(tup, 2, tmp);
return tup;
}

View File

@ -0,0 +1,171 @@
/*
Bitmask 1.7 - A pixel-perfect collision detection library.
Copyright (C) 2002-2005 Ulf Ekstrom except for the bitcount
function which is copyright (C) Donald W. Gillies, 1992.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef BITMASK_H
#define BITMASK_H
#ifdef __cplusplus
extern "C" {
#endif
#include <limits.h>
/* Define INLINE for different compilers. If your compiler does not
support inlining then there might be a performance hit in
bitmask_overlap_area().
*/
#ifndef INLINE
#ifdef __GNUC__
#define INLINE inline
#else
#ifdef _MSC_VER
#define INLINE __inline
#else
#define INLINE
#endif
#endif
#endif
#define BITMASK_W unsigned long int
#define BITMASK_W_LEN (sizeof(BITMASK_W) * CHAR_BIT)
#define BITMASK_W_MASK (BITMASK_W_LEN - 1)
#define BITMASK_N(n) ((BITMASK_W)1 << (n))
typedef struct bitmask {
int w, h;
BITMASK_W bits[1];
} bitmask_t;
/* Creates a bitmask of width w and height h, where
w and h must both be greater than or equal to 0.
The mask is automatically cleared when created.
*/
bitmask_t *
bitmask_create(int w, int h);
/* Frees all the memory allocated by bitmask_create for m. */
void
bitmask_free(bitmask_t *m);
/* Create a copy of the given bitmask. */
bitmask_t *
bitmask_copy(bitmask_t *m);
/* Clears all bits in the mask */
void
bitmask_clear(bitmask_t *m);
/* Sets all bits in the mask */
void
bitmask_fill(bitmask_t *m);
/* Flips all bits in the mask */
void
bitmask_invert(bitmask_t *m);
/* Counts the bits in the mask */
unsigned int
bitmask_count(bitmask_t *m);
/* Returns nonzero if the bit at (x,y) is set. Coordinates start at
(0,0) */
static INLINE int
bitmask_getbit(const bitmask_t *m, int x, int y)
{
return (m->bits[x / BITMASK_W_LEN * m->h + y] &
BITMASK_N(x & BITMASK_W_MASK)) != 0;
}
/* Sets the bit at (x,y) */
static INLINE void
bitmask_setbit(bitmask_t *m, int x, int y)
{
m->bits[x / BITMASK_W_LEN * m->h + y] |= BITMASK_N(x & BITMASK_W_MASK);
}
/* Clears the bit at (x,y) */
static INLINE void
bitmask_clearbit(bitmask_t *m, int x, int y)
{
m->bits[x / BITMASK_W_LEN * m->h + y] &= ~BITMASK_N(x & BITMASK_W_MASK);
}
/* Returns nonzero if the masks overlap with the given offset.
The overlap tests uses the following offsets (which may be negative):
+----+----------..
|A | yoffset
| +-+----------..
+--|B
|xoffset
| |
: :
*/
int
bitmask_overlap(const bitmask_t *a, const bitmask_t *b, int xoffset,
int yoffset);
/* Like bitmask_overlap(), but will also give a point of intersection.
x and y are given in the coordinates of mask a, and are untouched
if there is no overlap. */
int
bitmask_overlap_pos(const bitmask_t *a, const bitmask_t *b, int xoffset,
int yoffset, int *x, int *y);
/* Returns the number of overlapping 'pixels' */
int
bitmask_overlap_area(const bitmask_t *a, const bitmask_t *b, int xoffset,
int yoffset);
/* Fills a mask with the overlap of two other masks. A bitwise AND. */
void
bitmask_overlap_mask(const bitmask_t *a, const bitmask_t *b, bitmask_t *c,
int xoffset, int yoffset);
/* Draws mask b onto mask a (bitwise OR). Can be used to compose large
(game background?) mask from several submasks, which may speed up
the testing. */
void
bitmask_draw(bitmask_t *a, const bitmask_t *b, int xoffset, int yoffset);
void
bitmask_erase(bitmask_t *a, const bitmask_t *b, int xoffset, int yoffset);
/* Return a new scaled bitmask, with dimensions w*h. The quality of the
scaling may not be perfect for all circumstances, but it should
be reasonable. If either w or h is 0 a clear 1x1 mask is returned. */
bitmask_t *
bitmask_scale(const bitmask_t *m, int w, int h);
/* Convolve b into a, drawing the output into o, shifted by offset. If offset
* is 0, then the (x,y) bit will be set if and only if
* bitmask_overlap(a, b, x - b->w - 1, y - b->h - 1) returns true.
*
* Modifies bits o[xoffset ... xoffset + a->w + b->w - 1)
* [yoffset ... yoffset + a->h + b->h - 1). */
void
bitmask_convolve(const bitmask_t *a, const bitmask_t *b, bitmask_t *o,
int xoffset, int yoffset);
#ifdef __cplusplus
} /* End of extern "C" { */
#endif
#endif

View File

@ -0,0 +1,102 @@
#if !defined(PGCOMPAT_H)
#define PGCOMPAT_H
#include <Python.h>
/* In CPython, Py_Exit finalises the python interpreter before calling C exit()
* This does not exist on PyPy, so use exit() directly here */
#ifdef PYPY_VERSION
#define PG_EXIT(n) exit(n)
#else
#define PG_EXIT(n) Py_Exit(n)
#endif
/* define common types where SDL is not included */
#ifndef SDL_VERSION_ATLEAST
#ifdef _MSC_VER
typedef unsigned __int8 uint8_t;
typedef unsigned __int32 uint32_t;
#else
#include <stdint.h>
#endif
typedef uint32_t Uint32;
typedef uint8_t Uint8;
#endif /* no SDL */
#if defined(SDL_VERSION_ATLEAST)
#ifndef SDL_WINDOW_VULKAN
#define SDL_WINDOW_VULKAN 0
#endif
#ifndef SDL_WINDOW_ALWAYS_ON_TOP
#define SDL_WINDOW_ALWAYS_ON_TOP 0
#endif
#ifndef SDL_WINDOW_SKIP_TASKBAR
#define SDL_WINDOW_SKIP_TASKBAR 0
#endif
#ifndef SDL_WINDOW_UTILITY
#define SDL_WINDOW_UTILITY 0
#endif
#ifndef SDL_WINDOW_TOOLTIP
#define SDL_WINDOW_TOOLTIP 0
#endif
#ifndef SDL_WINDOW_POPUP_MENU
#define SDL_WINDOW_POPUP_MENU 0
#endif
#ifndef SDL_WINDOW_INPUT_GRABBED
#define SDL_WINDOW_INPUT_GRABBED 0
#endif
#ifndef SDL_WINDOW_INPUT_FOCUS
#define SDL_WINDOW_INPUT_FOCUS 0
#endif
#ifndef SDL_WINDOW_MOUSE_FOCUS
#define SDL_WINDOW_MOUSE_FOCUS 0
#endif
#ifndef SDL_WINDOW_FOREIGN
#define SDL_WINDOW_FOREIGN 0
#endif
#ifndef SDL_WINDOW_ALLOW_HIGHDPI
#define SDL_WINDOW_ALLOW_HIGHDPI 0
#endif
#ifndef SDL_WINDOW_MOUSE_CAPTURE
#define SDL_WINDOW_MOUSE_CAPTURE 0
#endif
#ifndef SDL_WINDOW_ALWAYS_ON_TOP
#define SDL_WINDOW_ALWAYS_ON_TOP 0
#endif
#ifndef SDL_WINDOW_SKIP_TASKBAR
#define SDL_WINDOW_SKIP_TASKBAR 0
#endif
#ifndef SDL_WINDOW_UTILITY
#define SDL_WINDOW_UTILITY 0
#endif
#ifndef SDL_WINDOW_TOOLTIP
#define SDL_WINDOW_TOOLTIP 0
#endif
#ifndef SDL_WINDOW_POPUP_MENU
#define SDL_WINDOW_POPUP_MENU 0
#endif
#ifndef SDL_MOUSEWHEEL_FLIPPED
#define NO_SDL_MOUSEWHEEL_FLIPPED
#endif
#endif /* defined(SDL_VERSION_ATLEAST) */
#endif /* ~defined(PGCOMPAT_H) */

View File

@ -0,0 +1,67 @@
#ifndef PGIMPORT_H
#define PGIMPORT_H
/* Prefix when importing module */
#define IMPPREFIX "pygame."
#include "pgcompat.h"
#define PYGAMEAPI_LOCAL_ENTRY "_PYGAME_C_API"
#define PG_CAPSULE_NAME(m) (IMPPREFIX m "." PYGAMEAPI_LOCAL_ENTRY)
/*
* fill API slots defined by PYGAMEAPI_DEFINE_SLOTS/PYGAMEAPI_EXTERN_SLOTS
*/
#define _IMPORT_PYGAME_MODULE(module) \
{ \
PyObject *_mod_##module = PyImport_ImportModule(IMPPREFIX #module); \
\
if (_mod_##module != NULL) { \
PyObject *_c_api = \
PyObject_GetAttrString(_mod_##module, PYGAMEAPI_LOCAL_ENTRY); \
\
Py_DECREF(_mod_##module); \
if (_c_api != NULL && PyCapsule_CheckExact(_c_api)) { \
void **localptr = (void **)PyCapsule_GetPointer( \
_c_api, PG_CAPSULE_NAME(#module)); \
_PGSLOTS_##module = localptr; \
} \
Py_XDECREF(_c_api); \
} \
}
#define PYGAMEAPI_IS_IMPORTED(module) (_PGSLOTS_##module != NULL)
/*
* source file must include one of these in order to use _IMPORT_PYGAME_MODULE.
* this is set by import_pygame_*() functions.
* disable with NO_PYGAME_C_API
*/
#define PYGAMEAPI_DEFINE_SLOTS(module) void **_PGSLOTS_##module = NULL
#define PYGAMEAPI_EXTERN_SLOTS(module) extern void **_PGSLOTS_##module
#define PYGAMEAPI_GET_SLOT(module, index) _PGSLOTS_##module[(index)]
/*
* disabled API with NO_PYGAME_C_API; do nothing instead
*/
#ifdef NO_PYGAME_C_API
#undef PYGAMEAPI_DEFINE_SLOTS
#undef PYGAMEAPI_EXTERN_SLOTS
#define PYGAMEAPI_DEFINE_SLOTS(module)
#define PYGAMEAPI_EXTERN_SLOTS(module)
/* intentionally leave this defined to cause a compiler error *
#define PYGAMEAPI_GET_SLOT(api_root, index)
#undef PYGAMEAPI_GET_SLOT*/
#undef _IMPORT_PYGAME_MODULE
#define _IMPORT_PYGAME_MODULE(module)
#endif /* NO_PYGAME_C_API */
#define encapsulate_api(ptr, module) \
PyCapsule_New(ptr, PG_CAPSULE_NAME(module), NULL)
#endif /* ~PGIMPORT_H */

View File

@ -0,0 +1,83 @@
/* platform/compiler adjustments */
#ifndef PG_PLATFORM_H
#define PG_PLATFORM_H
#if defined(HAVE_SNPRINTF) /* defined in python.h (pyerrors.h) and SDL.h \
(SDL_config.h) */
#undef HAVE_SNPRINTF /* remove GCC redefine warning */
#endif /* HAVE_SNPRINTF */
#ifndef PG_INLINE
#if defined(__clang__)
#define PG_INLINE __inline__ __attribute__((__unused__))
#elif defined(__GNUC__)
#define PG_INLINE __inline__
#elif defined(_MSC_VER)
#define PG_INLINE __inline
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
#define PG_INLINE inline
#else
#define PG_INLINE
#endif
#endif /* ~PG_INLINE */
// Worth trying this on MSVC/win32 builds to see if provides any speed up
#ifndef PG_FORCEINLINE
#if defined(__clang__)
#define PG_FORCEINLINE __inline__ __attribute__((__unused__))
#elif defined(__GNUC__)
#define PG_FORCEINLINE __inline__
#elif defined(_MSC_VER)
#define PG_FORCEINLINE __forceinline
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
#define PG_FORCEINLINE inline
#else
#define PG_FORCEINLINE
#endif
#endif /* ~PG_FORCEINLINE */
/* This is unconditionally defined in Python.h */
#if defined(_POSIX_C_SOURCE)
#undef _POSIX_C_SOURCE
#endif
#if defined(HAVE_SNPRINTF)
#undef HAVE_SNPRINTF
#endif
/* SDL needs WIN32 */
#if !defined(WIN32) && \
(defined(MS_WIN32) || defined(_WIN32) || defined(__WIN32) || \
defined(__WIN32__) || defined(_WINDOWS))
#define WIN32
#endif
#ifndef PG_TARGET_SSE4_2
#if defined(__clang__) || \
(defined(__GNUC__) && \
((__GNUC__ == 4 && __GNUC_MINOR__ >= 9) || __GNUC__ >= 5))
// The old gcc 4.8 on centos used by manylinux1 does not seem to get sse4.2
// intrinsics
#define PG_FUNCTION_TARGET_SSE4_2 __attribute__((target("sse4.2")))
// No else; we define the fallback later
#endif
#endif /* ~PG_TARGET_SSE4_2 */
#ifdef PG_FUNCTION_TARGET_SSE4_2
#if !defined(__SSE4_2__) && !defined(PG_COMPILE_SSE4_2)
#if defined(__x86_64__) || defined(__i386__)
#define PG_COMPILE_SSE4_2 1
#endif
#endif
#endif /* ~PG_TARGET_SSE4_2 */
/* Fallback definition of target attribute */
#ifndef PG_FUNCTION_TARGET_SSE4_2
#define PG_FUNCTION_TARGET_SSE4_2
#endif
#ifndef PG_COMPILE_SSE4_2
#define PG_COMPILE_SSE4_2 0
#endif
#endif /* ~PG_PLATFORM_H */

View File

@ -0,0 +1,34 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
/* To allow the Pygame C api to be globally shared by all code within an
* extension module built from multiple C files, only include the pygame.h
* header within the top level C file, the one which calls the
* 'import_pygame_*' macros. All other C source files of the module should
* include _pygame.h instead.
*/
#ifndef PYGAME_H
#define PYGAME_H
#include "_pygame.h"
#endif

View File

@ -0,0 +1,56 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
Copyright (C) 2007 Rene Dudfield, Richard Goedeken
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
/* Bufferproxy module C api. */
#if !defined(PG_BUFPROXY_HEADER)
#define PG_BUFPROXY_HEADER
#include <Python.h>
typedef PyObject *(*_pgbufproxy_new_t)(PyObject *, getbufferproc);
typedef PyObject *(*_pgbufproxy_get_obj_t)(PyObject *);
typedef int (*_pgbufproxy_trip_t)(PyObject *);
#ifndef PYGAMEAPI_BUFPROXY_INTERNAL
#include "pgimport.h"
PYGAMEAPI_DEFINE_SLOTS(bufferproxy);
#define pgBufproxy_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(bufferproxy, 0))
#define pgBufproxy_Check(x) ((x)->ob_type == &pgBufproxy_Type)
#define pgBufproxy_New (*(_pgbufproxy_new_t)PYGAMEAPI_GET_SLOT(bufferproxy, 1))
#define pgBufproxy_GetParent \
(*(_pgbufproxy_get_obj_t)PYGAMEAPI_GET_SLOT(bufferproxy, 2))
#define pgBufproxy_Trip \
(*(_pgbufproxy_trip_t)PYGAMEAPI_GET_SLOT(bufferproxy, 3))
#define import_pygame_bufferproxy() _IMPORT_PYGAME_MODULE(bufferproxy)
#endif /* ~PYGAMEAPI_BUFPROXY_INTERNAL */
#endif /* ~defined(PG_BUFPROXY_HEADER) */

View File

@ -0,0 +1,50 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
#include <Python.h>
#include "pgplatform.h"
struct TTF_Font;
typedef struct {
PyObject_HEAD TTF_Font *font;
PyObject *weakreflist;
unsigned int ttf_init_generation;
} PyFontObject;
#define PyFont_AsFont(x) (((PyFontObject *)x)->font)
#ifndef PYGAMEAPI_FONT_INTERNAL
#include "pgimport.h"
PYGAMEAPI_DEFINE_SLOTS(font);
#define PyFont_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(font, 0))
#define PyFont_Check(x) ((x)->ob_type == &PyFont_Type)
#define PyFont_New (*(PyObject * (*)(TTF_Font *)) PYGAMEAPI_GET_SLOT(font, 1))
/*slot 2 taken by FONT_INIT_CHECK*/
#define import_pygame_font() _IMPORT_PYGAME_MODULE(font)
#endif

View File

@ -0,0 +1,42 @@
/*
pygame - Python Game Library
Copyright (C) 2009 Vicent Marti
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef PYGAME_FREETYPE_H_
#define PYGAME_FREETYPE_H_
#include "pgplatform.h"
#include "pgimport.h"
#include "pgcompat.h"
#ifndef PYGAME_FREETYPE_INTERNAL
PYGAMEAPI_DEFINE_SLOTS(_freetype);
#define pgFont_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(_freetype, 0))
#define pgFont_Check(x) ((x)->ob_type == &pgFont_Type)
#define pgFont_New \
(*(PyObject * (*)(const char *, long)) PYGAMEAPI_GET_SLOT(_freetype, 1))
#define import_pygame_freetype() _IMPORT_PYGAME_MODULE(_freetype)
#endif /* PYGAME_FREETYPE_INTERNAL */
#endif /* PYGAME_FREETYPE_H_ */

View File

@ -0,0 +1,45 @@
/*
pygame - Python Game Library
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#ifndef PGMASK_H
#define PGMASK_H
#include <Python.h>
#include "bitmask.h"
typedef struct {
PyObject_HEAD bitmask_t *mask;
void *bufdata;
} pgMaskObject;
#define pgMask_AsBitmap(x) (((pgMaskObject *)x)->mask)
#ifndef PYGAMEAPI_MASK_INTERNAL
#include "pgimport.h"
PYGAMEAPI_DEFINE_SLOTS(mask);
#define pgMask_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(mask, 0))
#define pgMask_Check(x) ((x)->ob_type == &pgMask_Type)
#define import_pygame_mask() _IMPORT_PYGAME_MODULE(mask)
#endif /* ~PYGAMEAPI_MASK_INTERNAL */
#endif /* ~PGMASK_H */

View File

@ -0,0 +1,71 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
#ifndef PGMIXER_H
#define PGMIXER_H
#include <Python.h>
#include <structmember.h>
#include "pgcompat.h"
struct Mix_Chunk;
typedef struct {
PyObject_HEAD Mix_Chunk *chunk;
Uint8 *mem;
PyObject *weakreflist;
} pgSoundObject;
typedef struct {
PyObject_HEAD int chan;
} pgChannelObject;
#define pgSound_AsChunk(x) (((pgSoundObject *)x)->chunk)
#define pgChannel_AsInt(x) (((pgChannelObject *)x)->chan)
#include "pgimport.h"
#ifndef PYGAMEAPI_MIXER_INTERNAL
PYGAMEAPI_DEFINE_SLOTS(mixer);
#define pgSound_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(mixer, 0))
#define pgSound_Check(x) ((x)->ob_type == &pgSound_Type)
#define pgSound_New \
(*(PyObject * (*)(Mix_Chunk *)) PYGAMEAPI_GET_SLOT(mixer, 1))
#define pgSound_Play \
(*(PyObject * (*)(PyObject *, PyObject *)) PYGAMEAPI_GET_SLOT(mixer, 2))
#define pgChannel_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(mixer, 3))
#define pgChannel_Check(x) ((x)->ob_type == &pgChannel_Type)
#define pgChannel_New (*(PyObject * (*)(int)) PYGAMEAPI_GET_SLOT(mixer, 4))
#define import_pygame_mixer() _IMPORT_PYGAME_MODULE(mixer)
#endif /* PYGAMEAPI_MIXER_INTERNAL */
#endif /* ~PGMIXER_H */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
#ifndef PGMASK_INTERNAL_H
#define PGMASK_INTERNAL_H
#include "include/pygame_mask.h"
#define PYGAMEAPI_MASK_NUMSLOTS 1
#endif /* ~PGMASK_INTERNAL_H */

View File

@ -0,0 +1,14 @@
#ifndef MIXER_INTERNAL_H
#define MIXER_INTERNAL_H
#include <SDL_mixer.h>
/* test mixer initializations */
#define MIXER_INIT_CHECK() \
if (!SDL_WasInit(SDL_INIT_AUDIO)) \
return RAISE(pgExc_SDLError, "mixer not initialized")
#define PYGAMEAPI_MIXER_NUMSLOTS 5
#include "include/pygame_mixer.h"
#endif /* ~MIXER_INTERNAL_H */

View File

@ -0,0 +1,123 @@
/*
pygame - Python Game Library
Copyright (C) 2000-2001 Pete Shinners
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Pete Shinners
pete@shinners.org
*/
#ifndef PALETTE_H
#define PALETTE_H
#include <SDL.h>
/* SDL 2 does not assign a default palette color scheme to a new 8 bit
* surface. Instead, the palette is set all white. This defines the SDL 1.2
* default palette.
*/
static const SDL_Color default_palette_colors[] = {
{0, 0, 0, 255}, {0, 0, 85, 255}, {0, 0, 170, 255},
{0, 0, 255, 255}, {0, 36, 0, 255}, {0, 36, 85, 255},
{0, 36, 170, 255}, {0, 36, 255, 255}, {0, 73, 0, 255},
{0, 73, 85, 255}, {0, 73, 170, 255}, {0, 73, 255, 255},
{0, 109, 0, 255}, {0, 109, 85, 255}, {0, 109, 170, 255},
{0, 109, 255, 255}, {0, 146, 0, 255}, {0, 146, 85, 255},
{0, 146, 170, 255}, {0, 146, 255, 255}, {0, 182, 0, 255},
{0, 182, 85, 255}, {0, 182, 170, 255}, {0, 182, 255, 255},
{0, 219, 0, 255}, {0, 219, 85, 255}, {0, 219, 170, 255},
{0, 219, 255, 255}, {0, 255, 0, 255}, {0, 255, 85, 255},
{0, 255, 170, 255}, {0, 255, 255, 255}, {85, 0, 0, 255},
{85, 0, 85, 255}, {85, 0, 170, 255}, {85, 0, 255, 255},
{85, 36, 0, 255}, {85, 36, 85, 255}, {85, 36, 170, 255},
{85, 36, 255, 255}, {85, 73, 0, 255}, {85, 73, 85, 255},
{85, 73, 170, 255}, {85, 73, 255, 255}, {85, 109, 0, 255},
{85, 109, 85, 255}, {85, 109, 170, 255}, {85, 109, 255, 255},
{85, 146, 0, 255}, {85, 146, 85, 255}, {85, 146, 170, 255},
{85, 146, 255, 255}, {85, 182, 0, 255}, {85, 182, 85, 255},
{85, 182, 170, 255}, {85, 182, 255, 255}, {85, 219, 0, 255},
{85, 219, 85, 255}, {85, 219, 170, 255}, {85, 219, 255, 255},
{85, 255, 0, 255}, {85, 255, 85, 255}, {85, 255, 170, 255},
{85, 255, 255, 255}, {170, 0, 0, 255}, {170, 0, 85, 255},
{170, 0, 170, 255}, {170, 0, 255, 255}, {170, 36, 0, 255},
{170, 36, 85, 255}, {170, 36, 170, 255}, {170, 36, 255, 255},
{170, 73, 0, 255}, {170, 73, 85, 255}, {170, 73, 170, 255},
{170, 73, 255, 255}, {170, 109, 0, 255}, {170, 109, 85, 255},
{170, 109, 170, 255}, {170, 109, 255, 255}, {170, 146, 0, 255},
{170, 146, 85, 255}, {170, 146, 170, 255}, {170, 146, 255, 255},
{170, 182, 0, 255}, {170, 182, 85, 255}, {170, 182, 170, 255},
{170, 182, 255, 255}, {170, 219, 0, 255}, {170, 219, 85, 255},
{170, 219, 170, 255}, {170, 219, 255, 255}, {170, 255, 0, 255},
{170, 255, 85, 255}, {170, 255, 170, 255}, {170, 255, 255, 255},
{255, 0, 0, 255}, {255, 0, 85, 255}, {255, 0, 170, 255},
{255, 0, 255, 255}, {255, 36, 0, 255}, {255, 36, 85, 255},
{255, 36, 170, 255}, {255, 36, 255, 255}, {255, 73, 0, 255},
{255, 73, 85, 255}, {255, 73, 170, 255}, {255, 73, 255, 255},
{255, 109, 0, 255}, {255, 109, 85, 255}, {255, 109, 170, 255},
{255, 109, 255, 255}, {255, 146, 0, 255}, {255, 146, 85, 255},
{255, 146, 170, 255}, {255, 146, 255, 255}, {255, 182, 0, 255},
{255, 182, 85, 255}, {255, 182, 170, 255}, {255, 182, 255, 255},
{255, 219, 0, 255}, {255, 219, 85, 255}, {255, 219, 170, 255},
{255, 219, 255, 255}, {255, 255, 0, 255}, {255, 255, 85, 255},
{255, 255, 170, 255}, {255, 255, 255, 255}, {0, 0, 0, 255},
{0, 0, 85, 255}, {0, 0, 170, 255}, {0, 0, 255, 255},
{0, 36, 0, 255}, {0, 36, 85, 255}, {0, 36, 170, 255},
{0, 36, 255, 255}, {0, 73, 0, 255}, {0, 73, 85, 255},
{0, 73, 170, 255}, {0, 73, 255, 255}, {0, 109, 0, 255},
{0, 109, 85, 255}, {0, 109, 170, 255}, {0, 109, 255, 255},
{0, 146, 0, 255}, {0, 146, 85, 255}, {0, 146, 170, 255},
{0, 146, 255, 255}, {0, 182, 0, 255}, {0, 182, 85, 255},
{0, 182, 170, 255}, {0, 182, 255, 255}, {0, 219, 0, 255},
{0, 219, 85, 255}, {0, 219, 170, 255}, {0, 219, 255, 255},
{0, 255, 0, 255}, {0, 255, 85, 255}, {0, 255, 170, 255},
{0, 255, 255, 255}, {85, 0, 0, 255}, {85, 0, 85, 255},
{85, 0, 170, 255}, {85, 0, 255, 255}, {85, 36, 0, 255},
{85, 36, 85, 255}, {85, 36, 170, 255}, {85, 36, 255, 255},
{85, 73, 0, 255}, {85, 73, 85, 255}, {85, 73, 170, 255},
{85, 73, 255, 255}, {85, 109, 0, 255}, {85, 109, 85, 255},
{85, 109, 170, 255}, {85, 109, 255, 255}, {85, 146, 0, 255},
{85, 146, 85, 255}, {85, 146, 170, 255}, {85, 146, 255, 255},
{85, 182, 0, 255}, {85, 182, 85, 255}, {85, 182, 170, 255},
{85, 182, 255, 255}, {85, 219, 0, 255}, {85, 219, 85, 255},
{85, 219, 170, 255}, {85, 219, 255, 255}, {85, 255, 0, 255},
{85, 255, 85, 255}, {85, 255, 170, 255}, {85, 255, 255, 255},
{170, 0, 0, 255}, {170, 0, 85, 255}, {170, 0, 170, 255},
{170, 0, 255, 255}, {170, 36, 0, 255}, {170, 36, 85, 255},
{170, 36, 170, 255}, {170, 36, 255, 255}, {170, 73, 0, 255},
{170, 73, 85, 255}, {170, 73, 170, 255}, {170, 73, 255, 255},
{170, 109, 0, 255}, {170, 109, 85, 255}, {170, 109, 170, 255},
{170, 109, 255, 255}, {170, 146, 0, 255}, {170, 146, 85, 255},
{170, 146, 170, 255}, {170, 146, 255, 255}, {170, 182, 0, 255},
{170, 182, 85, 255}, {170, 182, 170, 255}, {170, 182, 255, 255},
{170, 219, 0, 255}, {170, 219, 85, 255}, {170, 219, 170, 255},
{170, 219, 255, 255}, {170, 255, 0, 255}, {170, 255, 85, 255},
{170, 255, 170, 255}, {170, 255, 255, 255}, {255, 0, 0, 255},
{255, 0, 85, 255}, {255, 0, 170, 255}, {255, 0, 255, 255},
{255, 36, 0, 255}, {255, 36, 85, 255}, {255, 36, 170, 255},
{255, 36, 255, 255}, {255, 73, 0, 255}, {255, 73, 85, 255},
{255, 73, 170, 255}, {255, 73, 255, 255}, {255, 109, 0, 255},
{255, 109, 85, 255}, {255, 109, 170, 255}, {255, 109, 255, 255},
{255, 146, 0, 255}, {255, 146, 85, 255}, {255, 146, 170, 255},
{255, 146, 255, 255}, {255, 182, 0, 255}, {255, 182, 85, 255},
{255, 182, 170, 255}, {255, 182, 255, 255}, {255, 219, 0, 255},
{255, 219, 85, 255}, {255, 219, 170, 255}, {255, 219, 255, 255},
{255, 255, 0, 255}, {255, 255, 85, 255}, {255, 255, 170, 255},
{255, 255, 255, 255}};
static const int default_palette_size =
(int)(sizeof(default_palette_colors) / sizeof(SDL_Color));
#endif

View File

@ -0,0 +1,26 @@
/* array structure interface version 3 declarations */
#if !defined(PG_ARRAYINTER_HEADER)
#define PG_ARRAYINTER_HEADER
static const int PAI_CONTIGUOUS = 0x01;
static const int PAI_FORTRAN = 0x02;
static const int PAI_ALIGNED = 0x100;
static const int PAI_NOTSWAPPED = 0x200;
static const int PAI_WRITEABLE = 0x400;
static const int PAI_ARR_HAS_DESCR = 0x800;
typedef struct {
int two; /* contains the integer 2 -- simple sanity check */
int nd; /* number of dimensions */
char typekind; /* kind in array -- character code of typestr */
int itemsize; /* size of each element */
int flags; /* flags indicating how the data should be */
/* interpreted */
Py_intptr_t *shape; /* A length-nd array of shape information */
Py_intptr_t *strides; /* A length-nd array of stride information */
void *data; /* A pointer to the first element of the array */
PyObject *descr; /* NULL or a data-description */
} PyArrayInterface;
#endif

Some files were not shown because too many files have changed in this diff Show More