docs: refresh readme and phase2 summary
@ -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
@ -23,3 +23,4 @@ sub_agent/project/
|
||||
|
||||
# Misc
|
||||
*.pid
|
||||
.env
|
||||
|
||||
286
README.md
@ -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` 定义的 Emoji;Web 模式所有工具事件都会写入 `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 贡献。
|
||||
|
||||
@ -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", ""))
|
||||
|
||||
@ -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):
|
||||
"""保存状态"""
|
||||
|
||||
@ -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
@ -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`。
|
||||
@ -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` 仅检查 session;Socket.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/自建规则)、限制文件类型/数量,并将上传目录与执行目录隔离。 |
|
||||
|
||||
@ -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/*
|
||||
|
||||
495
modules/container_file_proxy.py
Normal 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]}",
|
||||
}
|
||||
161
modules/container_monitor.py
Normal 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"),
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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]:
|
||||
"""验证命令安全性"""
|
||||
|
||||
@ -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:
|
||||
|
||||
324
modules/user_container_manager.py
Normal 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()
|
||||
298
static/app.js
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
230
static/demo_stadium_transition.html
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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 |
15
test/all_icons/circle-alert.svg
Normal 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 |
14
test/all_icons/clipboard.svg
Normal 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
@ -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
@ -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
@ -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 |
1
test/all_icons/folder-open.svg
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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 |
15
test/all_icons/monitor.svg
Normal 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 |
13
test/all_icons/octagon.svg
Normal 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
@ -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 |
1
test/all_icons/python.svg
Normal 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 |
18
test/all_icons/recycle.svg
Normal 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
@ -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
@ -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 |
14
test/all_icons/settings.svg
Normal 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 |
16
test/all_icons/sparkles.svg
Normal 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 |
14
test/all_icons/sticky-note.svg
Normal 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 |
14
test/all_icons/terminal.svg
Normal 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
@ -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 |
15
test/all_icons/triangle-alert.svg
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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>
|
||||
@ -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
|
||||
|
||||
247
虚拟环境/venv/bin/Activate.ps1
Normal 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"
|
||||
70
虚拟环境/venv/bin/activate
Normal 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
|
||||
27
虚拟环境/venv/bin/activate.csh
Normal 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
|
||||
69
虚拟环境/venv/bin/activate.fish
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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())
|
||||
8
虚拟环境/venv/bin/normalizer
Executable 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())
|
||||
8
虚拟环境/venv/bin/numpy-config
Executable 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
@ -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
@ -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
@ -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
@ -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())
|
||||
1
虚拟环境/venv/bin/python
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
||||
1
虚拟环境/venv/bin/python3
Symbolic link
@ -0,0 +1 @@
|
||||
/usr/bin/python3
|
||||
1
虚拟环境/venv/bin/python3.12
Symbolic link
@ -0,0 +1 @@
|
||||
python3
|
||||
8
虚拟环境/venv/bin/tqdm
Executable 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())
|
||||
21
虚拟环境/venv/include/site/python3.12/pygame/_blit_info.h
Normal 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;
|
||||
26
虚拟环境/venv/include/site/python3.12/pygame/_camera.h
Normal 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
|
||||
374
虚拟环境/venv/include/site/python3.12/pygame/_pygame.h
Normal 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 */
|
||||
30
虚拟环境/venv/include/site/python3.12/pygame/_surface.h
Normal 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
|
||||
218
虚拟环境/venv/include/site/python3.12/pygame/camera.h
Normal 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 */
|
||||
15
虚拟环境/venv/include/site/python3.12/pygame/font.h
Normal 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 */
|
||||
114
虚拟环境/venv/include/site/python3.12/pygame/freetype.h
Normal 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_ */
|
||||
949
虚拟环境/venv/include/site/python3.12/pygame/include/_pygame.h
Normal 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;
|
||||
}
|
||||
171
虚拟环境/venv/include/site/python3.12/pygame/include/bitmask.h
Normal 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
|
||||
102
虚拟环境/venv/include/site/python3.12/pygame/include/pgcompat.h
Normal 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) */
|
||||
67
虚拟环境/venv/include/site/python3.12/pygame/include/pgimport.h
Normal 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 */
|
||||
@ -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 */
|
||||
34
虚拟环境/venv/include/site/python3.12/pygame/include/pygame.h
Normal 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
|
||||
@ -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) */
|
||||
@ -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
|
||||
@ -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_ */
|
||||
@ -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 */
|
||||
@ -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 */
|
||||
6203
虚拟环境/venv/include/site/python3.12/pygame/include/sse2neon.h
Normal file
7
虚拟环境/venv/include/site/python3.12/pygame/mask.h
Normal 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 */
|
||||
14
虚拟环境/venv/include/site/python3.12/pygame/mixer.h
Normal 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 */
|
||||
123
虚拟环境/venv/include/site/python3.12/pygame/palette.h
Normal 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
|
||||
26
虚拟环境/venv/include/site/python3.12/pygame/pgarrinter.h
Normal 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
|
||||