docs: refresh readme and phase2 summary
@ -31,7 +31,7 @@ TERMINAL_SANDBOX_IMAGE=python:3.11-slim
|
|||||||
TERMINAL_SANDBOX_MOUNT_PATH=/workspace
|
TERMINAL_SANDBOX_MOUNT_PATH=/workspace
|
||||||
TERMINAL_SANDBOX_SHELL=/bin/bash
|
TERMINAL_SANDBOX_SHELL=/bin/bash
|
||||||
# 资源与网络限制
|
# 资源与网络限制
|
||||||
TERMINAL_SANDBOX_NETWORK=none
|
TERMINAL_SANDBOX_NETWORK=bridge
|
||||||
TERMINAL_SANDBOX_CPUS=0.5
|
TERMINAL_SANDBOX_CPUS=0.5
|
||||||
TERMINAL_SANDBOX_MEMORY=1g
|
TERMINAL_SANDBOX_MEMORY=1g
|
||||||
# 附加绑定目录(逗号分隔,可留空)
|
# 附加绑定目录(逗号分隔,可留空)
|
||||||
|
|||||||
1
.gitignore
vendored
@ -23,3 +23,4 @@ sub_agent/project/
|
|||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
*.pid
|
*.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` 控制返回体量。 |
|
| **单用户-单容器** | `modules/user_container_manager.py` 会在登录时启动专属容器,并在退出或空闲超时后自动销毁。CLI/Web/Toolbox 复用同一容器,资源配额(默认 0.5 vCPU / 1GB RAM / 2GB 磁盘)由 `.env` 控制。 |
|
||||||
| `focus_file` / `unfocus_file` | 将 UTF-8 文本持续注入上下文(最多 3 个),适合频繁查看/修改的文件。 |
|
| **容器内文件代理** | `modules/container_file_proxy.py` 通过 `docker exec` 调用内置 Python 脚本,对 `create/read/search/write/modify` 等操作进行沙箱化处理,宿主机不再直写用户代码。 |
|
||||||
| `append_to_file` / `modify_file` | 双阶段写入、大块内容追加、结构化补丁替换。 |
|
| **实时监控面板** | Web “用量统计”抽屉实时展示 Token 消耗、容器 CPU/内存、网络上下行速率(0.5s 刷新)以及项目存储占用(5s 刷新)。CLI `/status` 命令也会附带容器状态。 |
|
||||||
| `terminal_session` / `terminal_input` / `run_command` / `run_python` | 管理多终端会话、发送命令或一次性脚本;Web 端可实时查看输出。 |
|
| **联网能力 + 最小工具集** | 终端镜像改为 `bridge` 网络并预装 `iputils-ping`,方便验证网络连通性;遇到受限环境可以随时在 `.env` 中切换网络模式。 |
|
||||||
| `web_search` / `extract_webpage` / `save_webpage` | 外部信息检索与网页内容提取/落盘。 |
|
|
||||||
| `todo_*` / `update_memory` | 记录待办事项、更新全局/任务记忆;适合长任务拆解与结果总结。 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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. 环境需求
|
||||||
1. `read_file type=search` 定位关键函数。
|
|
||||||
2. `read_file type=extract` 抽取需要改动的多段内容。
|
|
||||||
3. `focus_file` 保持核心文件常驻上下文,配合 `modify_file` 输出补丁。
|
|
||||||
|
|
||||||
### 2. 构建脚手架并验证
|
- Python 3.11+
|
||||||
1. `create_folder` / `create_file` / `append_to_file` 生成工程骨架。
|
- Docker 20+(需启用 cgroup v1/v2 任一,默认通过 `bridge` 网络运行容器)
|
||||||
2. `terminal_session` 启动开发服务器;`terminal_input` 查看日志。
|
- Node/Vue 非必需,前端为静态文件
|
||||||
3. `todo_*` 记录剩余任务,`update_memory` 写入结论。
|
|
||||||
|
|
||||||
### 3. 信息搜集与整理
|
### 2. 安装依赖
|
||||||
1. `web_search` 获取外部资料。
|
|
||||||
2. `extract_webpage` 抽取正文,或 `save_webpage` 落盘。
|
```bash
|
||||||
3. `read_file type=search/extract` 在本地笔记中快速定位与摘录。
|
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 日志。
|
- **Web 用量面板**:点击右下角「用量统计」可查看 Token/CPU/内存/网络/存储实时状态。
|
||||||
- **终端隔离**:默认通过 `TERMINAL_SANDBOX_*` 配置启动 Docker 容器执行命令,每个终端挂载独立工作区;若本地无容器运行时,可暂时将 `TERMINAL_SANDBOX_REQUIRE=0` 退回宿主机模式。
|
- **CLI `/status`**:显示当前会话、上下文大小、容器资源等,日志位于 `logs/debug_stream.log`。
|
||||||
- **快捷命令环境**:`run_command` / `run_python` 会在专用的“工具容器”中执行,复用同一虚拟环境与依赖,减少对宿主机的影响;空闲超过 `TOOLBOX_TERMINAL_IDLE_SECONDS` 会自动释放。
|
- **容器日志**:`logs/container_stats.log` 存储 `docker stats/inspect` 样本,可配合 `tail -f` 或外部监控系统。
|
||||||
- **资源保护**:容器默认限制为 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`)编写自定义脚本。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 已知限制
|
## 常见命令
|
||||||
|
|
||||||
- 缺乏文件管理器式的可视化浏览;需依赖终端与聚焦机制。
|
| 命令 | 说明 |
|
||||||
- 主要依赖手动测试,尚无完备的自动化/集成测试。
|
| --- | --- |
|
||||||
- Windows 路径偶有兼容性问题,建议在类 Unix 环境下运行。
|
| `/help` | CLI 指令列表 |
|
||||||
- 仍有部分旧代码需重构(异步策略、异常处理、配置热更新等)。
|
| `/status` | 查看系统/容器资源状态 |
|
||||||
|
| `read_file` / `modify_file` | 读写项目文件(自动通过容器代理) |
|
||||||
|
| `terminal_session` / `terminal_input` | 管理多终端会话 |
|
||||||
|
| `run_command` / `run_python` | 工具容器快速执行命令/Python 代码 |
|
||||||
|
| `todo_*`, `update_memory` | 维护待办与长期记忆 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 贡献方式
|
## 配置速览
|
||||||
|
|
||||||
- 提 Bug:附复现场景与日志,方便排查。
|
`config/terminal.py` 支持以下常用变量:
|
||||||
- 提需求:描述使用场景、期望行为与约束。
|
|
||||||
- 代码贡献:请遵循现有目录结构与模块职责,提交 PR 前确保基础功能可用。
|
|
||||||
- 文档贡献:欢迎补充 FAQ、最佳实践或脚本案例。
|
|
||||||
|
|
||||||
> 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。
|
1. **安全**:新增模块前请优先考虑是否可以在容器中实现,避免回退宿主机;如需联网,务必评估外部依赖。
|
||||||
- 致谢:感谢 DeepSeek、Qwen、Kimi 等兼容 OpenAI 的 API,及 Tavily 搜索服务提供的数据能力;同时参考了 Claude/ChatGPT 的交互体验设计。
|
2. **日志**:尽量使用 `utils.logger.setup_logger`,便于统一收集。
|
||||||
- 若本项目对你有帮助,欢迎 Star 或提交改进建议 🙌
|
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_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_MOUNT_PATH = os.environ.get("TERMINAL_SANDBOX_MOUNT_PATH", "/workspace")
|
||||||
TERMINAL_SANDBOX_SHELL = os.environ.get("TERMINAL_SANDBOX_SHELL", "/bin/bash")
|
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_CPUS = os.environ.get("TERMINAL_SANDBOX_CPUS", "")
|
||||||
TERMINAL_SANDBOX_MEMORY = os.environ.get("TERMINAL_SANDBOX_MEMORY", "")
|
TERMINAL_SANDBOX_MEMORY = os.environ.get("TERMINAL_SANDBOX_MEMORY", "")
|
||||||
TERMINAL_SANDBOX_BINDS = _parse_bindings(os.environ.get("TERMINAL_SANDBOX_BINDS", ""))
|
TERMINAL_SANDBOX_BINDS = _parse_bindings(os.environ.get("TERMINAL_SANDBOX_BINDS", ""))
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Set
|
from typing import Dict, List, Optional, Set, TYPE_CHECKING
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -54,11 +54,15 @@ from modules.personalization_manager import (
|
|||||||
load_personalization_config,
|
load_personalization_config,
|
||||||
build_personalization_prompt,
|
build_personalization_prompt,
|
||||||
)
|
)
|
||||||
|
from modules.container_monitor import collect_stats, inspect_state
|
||||||
from core.tool_config import TOOL_CATEGORIES
|
from core.tool_config import TOOL_CATEGORIES
|
||||||
from utils.api_client import DeepSeekClient
|
from utils.api_client import DeepSeekClient
|
||||||
from utils.context_manager import ContextManager
|
from utils.context_manager import ContextManager
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from modules.user_container_manager import ContainerHandle
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
# 临时禁用长度检查
|
# 临时禁用长度检查
|
||||||
DISABLE_LENGTH_CHECK = True
|
DISABLE_LENGTH_CHECK = True
|
||||||
@ -68,6 +72,7 @@ class MainTerminal:
|
|||||||
project_path: str,
|
project_path: str,
|
||||||
thinking_mode: bool = False,
|
thinking_mode: bool = False,
|
||||||
data_dir: Optional[str] = None,
|
data_dir: Optional[str] = None,
|
||||||
|
container_session: Optional["ContainerHandle"] = None,
|
||||||
):
|
):
|
||||||
self.project_path = project_path
|
self.project_path = project_path
|
||||||
self.thinking_mode = thinking_mode # False=快速模式, True=思考模式
|
self.thinking_mode = thinking_mode # False=快速模式, True=思考模式
|
||||||
@ -81,10 +86,14 @@ class MainTerminal:
|
|||||||
self.container_cpu_limit = TERMINAL_SANDBOX_CPUS or "未限制"
|
self.container_cpu_limit = TERMINAL_SANDBOX_CPUS or "未限制"
|
||||||
self.container_memory_limit = TERMINAL_SANDBOX_MEMORY 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 = 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.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.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)
|
self.ocr_client = OCRClient(project_path, self.file_manager)
|
||||||
|
|
||||||
# 新增:终端管理器
|
# 新增:终端管理器
|
||||||
@ -93,8 +102,10 @@ class MainTerminal:
|
|||||||
max_terminals=MAX_TERMINALS,
|
max_terminals=MAX_TERMINALS,
|
||||||
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
|
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
|
||||||
terminal_display_size=TERMINAL_DISPLAY_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.todo_manager = TodoManager(self.context_manager)
|
||||||
self.sub_agent_manager = SubAgentManager(
|
self.sub_agent_manager = SubAgentManager(
|
||||||
@ -145,6 +156,22 @@ class MainTerminal:
|
|||||||
#self.context_manager._web_terminal_callback = message_callback
|
#self.context_manager._web_terminal_callback = message_callback
|
||||||
#self.context_manager._focused_files = self.focused_files # 引用传递
|
#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):
|
def _ensure_conversation(self):
|
||||||
"""确保CLI模式下存在可用的对话ID"""
|
"""确保CLI模式下存在可用的对话ID"""
|
||||||
@ -856,7 +883,49 @@ class MainTerminal:
|
|||||||
主记忆: {memory_stats['main_memory']['lines']} 行
|
主记忆: {memory_stats['main_memory']['lines']} 行
|
||||||
任务记忆: {memory_stats['task_memory']['lines']} 行
|
任务记忆: {memory_stats['task_memory']['lines']} 行
|
||||||
"""
|
"""
|
||||||
|
container_report = self._container_status_report()
|
||||||
|
if container_report:
|
||||||
|
status_text += container_report
|
||||||
print(status_text)
|
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):
|
async def save_state(self):
|
||||||
"""保存状态"""
|
"""保存状态"""
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# core/web_terminal.py - Web终端(集成对话持久化)
|
# core/web_terminal.py - Web终端(集成对话持久化)
|
||||||
|
|
||||||
import json
|
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 core.main_terminal import MainTerminal
|
||||||
from utils.logger import setup_logger
|
from utils.logger import setup_logger
|
||||||
try:
|
try:
|
||||||
@ -15,6 +15,9 @@ except ImportError:
|
|||||||
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
|
from config import MAX_TERMINALS, TERMINAL_BUFFER_SIZE, TERMINAL_DISPLAY_SIZE
|
||||||
from modules.terminal_manager import TerminalManager
|
from modules.terminal_manager import TerminalManager
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from modules.user_container_manager import ContainerHandle
|
||||||
|
|
||||||
logger = setup_logger(__name__)
|
logger = setup_logger(__name__)
|
||||||
|
|
||||||
class WebTerminal(MainTerminal):
|
class WebTerminal(MainTerminal):
|
||||||
@ -46,10 +49,11 @@ class WebTerminal(MainTerminal):
|
|||||||
project_path: str,
|
project_path: str,
|
||||||
thinking_mode: bool = False,
|
thinking_mode: bool = False,
|
||||||
message_callback: Optional[Callable] = None,
|
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特有属性
|
# Web特有属性
|
||||||
self.message_callback = message_callback
|
self.message_callback = message_callback
|
||||||
@ -64,7 +68,8 @@ class WebTerminal(MainTerminal):
|
|||||||
max_terminals=MAX_TERMINALS,
|
max_terminals=MAX_TERMINALS,
|
||||||
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
|
terminal_buffer_size=TERMINAL_BUFFER_SIZE,
|
||||||
terminal_display_size=TERMINAL_DISPLAY_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}")
|
print(f"[WebTerminal] 初始化完成,项目路径: {project_path}")
|
||||||
@ -257,6 +262,7 @@ class WebTerminal(MainTerminal):
|
|||||||
conversation_stats = self.context_manager.get_conversation_statistics()
|
conversation_stats = self.context_manager.get_conversation_statistics()
|
||||||
|
|
||||||
# 构建状态信息
|
# 构建状态信息
|
||||||
|
limit_bytes = getattr(self, "project_storage_limit_bytes", None)
|
||||||
status = {
|
status = {
|
||||||
"project_path": self.project_path,
|
"project_path": self.project_path,
|
||||||
"thinking_mode": self.thinking_mode,
|
"thinking_mode": self.thinking_mode,
|
||||||
@ -271,7 +277,10 @@ class WebTerminal(MainTerminal):
|
|||||||
"terminals": terminal_status,
|
"terminals": terminal_status,
|
||||||
"project": {
|
"project": {
|
||||||
"total_files": structure['total_files'],
|
"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": {
|
"memory": {
|
||||||
"main": memory_stats['main_memory']['lines'],
|
"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` 页面提示。
|
- ✅ **资源与并发控制**:新增 `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 关键数据资产
|
### 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,删除仓库中的明文;在启动时校验缺省值并阻止运行。 |
|
| 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 源。 |
|
| 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。 |
|
| 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`,没有备份策略、并发安全或加密。 | 易被本地用户读取/篡改;当并发写入时有数据损坏风险,也无法满足审计/恢复。 | 引入关系型数据库或托管身份服务;对敏感字段做透明加密,提供备份与迁移策略。 |
|
| 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 等响应头。 |
|
| 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/自建规则)、限制文件类型/数量,并将上传目录与执行目录隔离。 |
|
| 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 \
|
zip \
|
||||||
unzip \
|
unzip \
|
||||||
locales \
|
locales \
|
||||||
tzdata && \
|
tzdata \
|
||||||
|
iputils-ping && \
|
||||||
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen && \
|
sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen && \
|
||||||
locale-gen && \
|
locale-gen && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
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 os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict, List, Tuple
|
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
try:
|
try:
|
||||||
from config import (
|
from config import (
|
||||||
@ -28,11 +28,44 @@ except ImportError: # 兼容全局环境中存在同名包的情况
|
|||||||
READ_TOOL_MAX_FILE_SIZE,
|
READ_TOOL_MAX_FILE_SIZE,
|
||||||
PROJECT_MAX_STORAGE_BYTES,
|
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
|
DISABLE_LENGTH_CHECK = True
|
||||||
class FileManager:
|
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.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:
|
def _get_project_size(self) -> int:
|
||||||
"""计算项目目录的总大小(字节)"""
|
"""计算项目目录的总大小(字节)"""
|
||||||
@ -94,6 +127,9 @@ class FileManager:
|
|||||||
return False, f"禁止访问系统目录: {forbidden}", None
|
return False, f"禁止访问系统目录: {forbidden}", None
|
||||||
|
|
||||||
return True, "", full_path
|
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:
|
def create_file(self, path: str, content: str = "", file_type: str = "txt") -> Dict:
|
||||||
"""创建文件"""
|
"""创建文件"""
|
||||||
@ -104,6 +140,7 @@ class FileManager:
|
|||||||
# 添加文件扩展名
|
# 添加文件扩展名
|
||||||
if not full_path.suffix:
|
if not full_path.suffix:
|
||||||
full_path = full_path.with_suffix(f".{file_type}")
|
full_path = full_path.with_suffix(f".{file_type}")
|
||||||
|
relative_path = self._relative_path(full_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if full_path.parent == self.project_path:
|
if full_path.parent == self.project_path:
|
||||||
@ -112,14 +149,21 @@ class FileManager:
|
|||||||
"error": "禁止在项目根目录直接创建文件,请先创建或选择子目录。",
|
"error": "禁止在项目根目录直接创建文件,请先创建或选择子目录。",
|
||||||
"suggestion": "创建文件所属文件夹,在其中创建新文件。"
|
"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)
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# 固定创建空文件,忽略传入内容
|
|
||||||
with open(full_path, 'w', encoding='utf-8') as f:
|
with open(full_path, 'w', encoding='utf-8') as f:
|
||||||
f.write("")
|
f.write("")
|
||||||
|
|
||||||
relative_path = str(full_path.relative_to(self.project_path))
|
|
||||||
print(f"{OUTPUT_FORMATS['file']} 创建文件: {relative_path}")
|
print(f"{OUTPUT_FORMATS['file']} 创建文件: {relative_path}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -143,7 +187,15 @@ class FileManager:
|
|||||||
return {"success": False, "error": "不是文件"}
|
return {"success": False, "error": "不是文件"}
|
||||||
|
|
||||||
try:
|
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()
|
full_path.unlink()
|
||||||
print(f"{OUTPUT_FORMATS['file']} 删除文件: {relative_path}")
|
print(f"{OUTPUT_FORMATS['file']} 删除文件: {relative_path}")
|
||||||
|
|
||||||
@ -176,10 +228,19 @@ class FileManager:
|
|||||||
return {"success": False, "error": "目标文件已存在"}
|
return {"success": False, "error": "目标文件已存在"}
|
||||||
|
|
||||||
try:
|
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)
|
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}")
|
print(f"{OUTPUT_FORMATS['file']} 重命名: {old_relative} -> {new_relative}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -201,8 +262,14 @@ class FileManager:
|
|||||||
return {"success": False, "error": "文件夹已存在"}
|
return {"success": False, "error": "文件夹已存在"}
|
||||||
|
|
||||||
try:
|
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)
|
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}")
|
print(f"{OUTPUT_FORMATS['file']} 创建文件夹: {relative_path}")
|
||||||
|
|
||||||
return {"success": True, "path": relative_path}
|
return {"success": True, "path": relative_path}
|
||||||
@ -222,8 +289,14 @@ class FileManager:
|
|||||||
return {"success": False, "error": "不是文件夹"}
|
return {"success": False, "error": "不是文件夹"}
|
||||||
|
|
||||||
try:
|
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)
|
shutil.rmtree(full_path)
|
||||||
relative_path = str(full_path.relative_to(self.project_path))
|
|
||||||
print(f"{OUTPUT_FORMATS['file']} 删除文件夹: {relative_path}")
|
print(f"{OUTPUT_FORMATS['file']} 删除文件夹: {relative_path}")
|
||||||
|
|
||||||
return {"success": True, "path": relative_path}
|
return {"success": True, "path": relative_path}
|
||||||
@ -280,6 +353,14 @@ class FileManager:
|
|||||||
if not full_path.is_file():
|
if not full_path.is_file():
|
||||||
return {"success": False, "error": "不是文件"}
|
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)
|
result = self._read_text_lines(full_path, size_limit=MAX_FILE_SIZE)
|
||||||
if not result["success"]:
|
if not result["success"]:
|
||||||
return result
|
return result
|
||||||
@ -311,6 +392,35 @@ class FileManager:
|
|||||||
if not full_path.is_file():
|
if not full_path.is_file():
|
||||||
return {"success": False, "error": "不是文件"}
|
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(
|
result = self._read_text_lines(
|
||||||
full_path,
|
full_path,
|
||||||
size_limit=size_limit or READ_TOOL_MAX_FILE_SIZE
|
size_limit=size_limit or READ_TOOL_MAX_FILE_SIZE
|
||||||
@ -505,6 +615,7 @@ class FileManager:
|
|||||||
print(f"{OUTPUT_FORMATS['warning']} 检测到大量转义字符,建议检查内容格式")
|
print(f"{OUTPUT_FORMATS['warning']} 检测到大量转义字符,建议检查内容格式")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
relative_path = self._relative_path(full_path)
|
||||||
current_size = self._get_project_size()
|
current_size = self._get_project_size()
|
||||||
existing_size = full_path.stat().st_size if full_path.exists() else 0
|
existing_size = full_path.stat().st_size if full_path.exists() else 0
|
||||||
if mode == "a":
|
if mode == "a":
|
||||||
@ -519,13 +630,23 @@ class FileManager:
|
|||||||
"project_size_bytes": current_size,
|
"project_size_bytes": current_size,
|
||||||
"attempt_size_bytes": len(content)
|
"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)
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
with open(full_path, mode, encoding='utf-8') as f:
|
with open(full_path, mode, encoding='utf-8') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
relative_path = str(full_path.relative_to(self.project_path))
|
|
||||||
action = "覆盖" if mode == "w" else "追加"
|
action = "覆盖" if mode == "w" else "追加"
|
||||||
print(f"{OUTPUT_FORMATS['file']} {action}文件: {relative_path}")
|
print(f"{OUTPUT_FORMATS['file']} {action}文件: {relative_path}")
|
||||||
|
|
||||||
@ -561,6 +682,13 @@ class FileManager:
|
|||||||
return {"success": False, "error": "不是文件"}
|
return {"success": False, "error": "不是文件"}
|
||||||
|
|
||||||
try:
|
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:
|
with open(full_path, 'r', encoding='utf-8') as f:
|
||||||
original_content = f.read()
|
original_content = f.read()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -740,6 +868,16 @@ class FileManager:
|
|||||||
return {"success": False, "error": "结束行号不能小于起始行号"}
|
return {"success": False, "error": "结束行号不能小于起始行号"}
|
||||||
|
|
||||||
try:
|
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:
|
with open(full_path, 'r', encoding='utf-8') as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
|
|||||||
@ -150,6 +150,7 @@ class PersistentTerminal:
|
|||||||
self.execution_mode = "host"
|
self.execution_mode = "host"
|
||||||
self.using_container = False
|
self.using_container = False
|
||||||
self._sandbox_bin_path = None
|
self._sandbox_bin_path = None
|
||||||
|
self._owns_container = False
|
||||||
|
|
||||||
def start(self) -> bool:
|
def start(self) -> bool:
|
||||||
"""启动终端进程(支持容器沙箱)"""
|
"""启动终端进程(支持容器沙箱)"""
|
||||||
@ -227,6 +228,7 @@ class PersistentTerminal:
|
|||||||
def _start_host_terminal(self):
|
def _start_host_terminal(self):
|
||||||
"""启动宿主机终端"""
|
"""启动宿主机终端"""
|
||||||
self.using_container = False
|
self.using_container = False
|
||||||
|
self._owns_container = False
|
||||||
self.is_windows = sys.platform == "win32"
|
self.is_windows = sys.platform == "win32"
|
||||||
shell_cmd = self.host_shell_command
|
shell_cmd = self.host_shell_command
|
||||||
if self.is_windows:
|
if self.is_windows:
|
||||||
@ -271,7 +273,7 @@ class PersistentTerminal:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _start_docker_terminal(self):
|
def _start_docker_terminal(self):
|
||||||
"""启动容器化终端"""
|
"""启动或连接容器化终端。"""
|
||||||
docker_bin = self.sandbox_options.get("bin") or "docker"
|
docker_bin = self.sandbox_options.get("bin") or "docker"
|
||||||
docker_path = shutil.which(docker_bin)
|
docker_path = shutil.which(docker_bin)
|
||||||
if not docker_path:
|
if not docker_path:
|
||||||
@ -281,6 +283,62 @@ class PersistentTerminal:
|
|||||||
print(f"{OUTPUT_FORMATS['warning']} {message}")
|
print(f"{OUTPUT_FORMATS['warning']} {message}")
|
||||||
return None
|
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")
|
image = self.sandbox_options.get("image")
|
||||||
if not image:
|
if not image:
|
||||||
raise RuntimeError("TERMINAL_SANDBOX_IMAGE 未配置")
|
raise RuntimeError("TERMINAL_SANDBOX_IMAGE 未配置")
|
||||||
@ -354,11 +412,43 @@ class PersistentTerminal:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
self.sandbox_container_name = container_name
|
self.sandbox_container_name = container_name
|
||||||
self._sandbox_bin_path = docker_path
|
|
||||||
self.shell_command = f"{shell_path} (sandbox:{image})"
|
self.shell_command = f"{shell_path} (sandbox:{image})"
|
||||||
self.using_container = True
|
self.using_container = True
|
||||||
self.is_windows = False
|
self.is_windows = False
|
||||||
|
self._owns_container = True
|
||||||
return process
|
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):
|
def _read_output(self):
|
||||||
"""后台线程:持续读取输出(修复版,正确处理编码)"""
|
"""后台线程:持续读取输出(修复版,正确处理编码)"""
|
||||||
@ -866,7 +956,7 @@ class PersistentTerminal:
|
|||||||
|
|
||||||
def _stop_sandbox_container(self, force: bool = False):
|
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
|
return
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
@ -881,3 +971,4 @@ class PersistentTerminal:
|
|||||||
print(f"{OUTPUT_FORMATS['warning']} 强制终止容器 {self.sandbox_container_name} 失败,可能已退出。")
|
print(f"{OUTPUT_FORMATS['warning']} 强制终止容器 {self.sandbox_container_name} 失败,可能已退出。")
|
||||||
finally:
|
finally:
|
||||||
self.sandbox_container_name = None
|
self.sandbox_container_name = None
|
||||||
|
self._owns_container = False
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# modules/terminal_manager.py - 终端会话管理器
|
# modules/terminal_manager.py - 终端会话管理器
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Dict, List, Optional, Callable
|
from typing import Dict, List, Optional, Callable, TYPE_CHECKING
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
try:
|
try:
|
||||||
@ -56,6 +56,9 @@ except ImportError:
|
|||||||
from modules.persistent_terminal import PersistentTerminal
|
from modules.persistent_terminal import PersistentTerminal
|
||||||
from utils.terminal_factory import TerminalFactory
|
from utils.terminal_factory import TerminalFactory
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from modules.user_container_manager import ContainerHandle
|
||||||
|
|
||||||
class TerminalManager:
|
class TerminalManager:
|
||||||
"""管理多个终端会话"""
|
"""管理多个终端会话"""
|
||||||
|
|
||||||
@ -67,7 +70,8 @@ class TerminalManager:
|
|||||||
terminal_display_size: int = None,
|
terminal_display_size: int = None,
|
||||||
broadcast_callback: Callable = None,
|
broadcast_callback: Callable = None,
|
||||||
sandbox_mode: Optional[str] = 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_lines = TERMINAL_SNAPSHOT_MAX_LINES
|
||||||
self.max_snapshot_chars = TERMINAL_SNAPSHOT_MAX_CHARS
|
self.max_snapshot_chars = TERMINAL_SNAPSHOT_MAX_CHARS
|
||||||
self.broadcast = broadcast_callback
|
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 = {
|
default_sandbox_options = {
|
||||||
"image": TERMINAL_SANDBOX_IMAGE,
|
"image": TERMINAL_SANDBOX_IMAGE,
|
||||||
"mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
|
"mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
|
||||||
@ -111,6 +116,10 @@ class TerminalManager:
|
|||||||
else:
|
else:
|
||||||
default_sandbox_options[key] = value
|
default_sandbox_options[key] = value
|
||||||
self.sandbox_options = default_sandbox_options
|
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] = {}
|
self.terminals: Dict[str, PersistentTerminal] = {}
|
||||||
@ -120,6 +129,48 @@ class TerminalManager:
|
|||||||
|
|
||||||
# 终端工厂(跨平台支持)
|
# 终端工厂(跨平台支持)
|
||||||
self.factory = TerminalFactory()
|
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(
|
def open_terminal(
|
||||||
self,
|
self,
|
||||||
@ -167,6 +218,7 @@ class TerminalManager:
|
|||||||
shell_command = self.factory.get_shell_command()
|
shell_command = self.factory.get_shell_command()
|
||||||
|
|
||||||
# 创建终端实例
|
# 创建终端实例
|
||||||
|
sandbox_options = self._build_sandbox_options()
|
||||||
terminal = PersistentTerminal(
|
terminal = PersistentTerminal(
|
||||||
session_name=session_name,
|
session_name=session_name,
|
||||||
working_dir=str(work_path),
|
working_dir=str(work_path),
|
||||||
@ -176,7 +228,7 @@ class TerminalManager:
|
|||||||
display_size=self.terminal_display_size,
|
display_size=self.terminal_display_size,
|
||||||
project_path=str(self.project_path),
|
project_path=str(self.project_path),
|
||||||
sandbox_mode=self.sandbox_mode,
|
sandbox_mode=self.sandbox_mode,
|
||||||
sandbox_options=self.sandbox_options
|
sandbox_options=sandbox_options
|
||||||
)
|
)
|
||||||
|
|
||||||
# 启动终端
|
# 启动终端
|
||||||
@ -293,6 +345,7 @@ class TerminalManager:
|
|||||||
terminal.close()
|
terminal.close()
|
||||||
del self.terminals[target_session]
|
del self.terminals[target_session]
|
||||||
|
|
||||||
|
sandbox_options = self._build_sandbox_options()
|
||||||
new_terminal = PersistentTerminal(
|
new_terminal = PersistentTerminal(
|
||||||
session_name=target_session,
|
session_name=target_session,
|
||||||
working_dir=working_dir,
|
working_dir=working_dir,
|
||||||
@ -302,7 +355,7 @@ class TerminalManager:
|
|||||||
display_size=self.terminal_display_size,
|
display_size=self.terminal_display_size,
|
||||||
project_path=str(self.project_path),
|
project_path=str(self.project_path),
|
||||||
sandbox_mode=self.sandbox_mode,
|
sandbox_mode=self.sandbox_mode,
|
||||||
sandbox_options=self.sandbox_options
|
sandbox_options=sandbox_options
|
||||||
)
|
)
|
||||||
|
|
||||||
if not new_terminal.start():
|
if not new_terminal.start():
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import asyncio
|
|||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Dict, Optional, Tuple, TYPE_CHECKING
|
||||||
try:
|
try:
|
||||||
from config import (
|
from config import (
|
||||||
CODE_EXECUTION_TIMEOUT,
|
CODE_EXECUTION_TIMEOUT,
|
||||||
@ -30,14 +30,18 @@ except ImportError:
|
|||||||
)
|
)
|
||||||
from modules.toolbox_container import ToolboxContainer
|
from modules.toolbox_container import ToolboxContainer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from modules.user_container_manager import ContainerHandle
|
||||||
|
|
||||||
class TerminalOperator:
|
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.project_path = Path(project_path).resolve()
|
||||||
self.process = None
|
self.process = None
|
||||||
# 自动检测Python命令
|
# 自动检测Python命令
|
||||||
self.python_cmd = self._detect_python_command()
|
self.python_cmd = self._detect_python_command()
|
||||||
print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}")
|
print(f"{OUTPUT_FORMATS['info']} 检测到Python命令: {self.python_cmd}")
|
||||||
self._toolbox: Optional[ToolboxContainer] = None
|
self._toolbox: Optional[ToolboxContainer] = None
|
||||||
|
self.container_session: Optional["ContainerHandle"] = container_session
|
||||||
|
|
||||||
def _detect_python_command(self) -> str:
|
def _detect_python_command(self) -> str:
|
||||||
"""
|
"""
|
||||||
@ -83,8 +87,16 @@ class TerminalOperator:
|
|||||||
self._toolbox = ToolboxContainer(
|
self._toolbox = ToolboxContainer(
|
||||||
project_path=str(self.project_path),
|
project_path=str(self.project_path),
|
||||||
idle_timeout=TOOLBOX_TERMINAL_IDLE_SECONDS,
|
idle_timeout=TOOLBOX_TERMINAL_IDLE_SECONDS,
|
||||||
|
container_session=self.container_session,
|
||||||
)
|
)
|
||||||
return self._toolbox
|
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]:
|
def _validate_command(self, command: str) -> Tuple[bool, str]:
|
||||||
"""验证命令安全性"""
|
"""验证命令安全性"""
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
import shlex
|
import shlex
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict
|
from typing import Optional, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
from modules.persistent_terminal import PersistentTerminal
|
from modules.persistent_terminal import PersistentTerminal
|
||||||
from config import (
|
from config import (
|
||||||
@ -24,15 +24,18 @@ from config import (
|
|||||||
TOOLBOX_TERMINAL_IDLE_SECONDS,
|
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
|
name_prefix = TERMINAL_SANDBOX_NAME_PREFIX
|
||||||
if name_prefix:
|
if name_prefix:
|
||||||
name_prefix = f"{name_prefix}-toolbox"
|
name_prefix = f"{name_prefix}-toolbox"
|
||||||
else:
|
else:
|
||||||
name_prefix = "toolbox-term"
|
name_prefix = "toolbox-term"
|
||||||
return {
|
options = {
|
||||||
"image": TERMINAL_SANDBOX_IMAGE,
|
"image": TERMINAL_SANDBOX_IMAGE,
|
||||||
"mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
|
"mount_path": TERMINAL_SANDBOX_MOUNT_PATH,
|
||||||
"shell": TERMINAL_SANDBOX_SHELL,
|
"shell": TERMINAL_SANDBOX_SHELL,
|
||||||
@ -45,6 +48,10 @@ def _build_sandbox_options() -> Dict:
|
|||||||
"env": dict(TERMINAL_SANDBOX_ENV),
|
"env": dict(TERMINAL_SANDBOX_ENV),
|
||||||
"require": TERMINAL_SANDBOX_REQUIRE,
|
"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:
|
class ToolboxContainer:
|
||||||
@ -56,10 +63,14 @@ class ToolboxContainer:
|
|||||||
sandbox_mode: Optional[str] = None,
|
sandbox_mode: Optional[str] = None,
|
||||||
sandbox_options: Optional[Dict] = None,
|
sandbox_options: Optional[Dict] = None,
|
||||||
idle_timeout: int = TOOLBOX_TERMINAL_IDLE_SECONDS,
|
idle_timeout: int = TOOLBOX_TERMINAL_IDLE_SECONDS,
|
||||||
|
container_session: Optional["ContainerHandle"] = None,
|
||||||
):
|
):
|
||||||
self.project_path = Path(project_path).resolve()
|
self.project_path = Path(project_path).resolve()
|
||||||
self.sandbox_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
|
self.default_mode = (sandbox_mode or TERMINAL_SANDBOX_MODE or "host").lower()
|
||||||
options = _build_sandbox_options()
|
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:
|
if sandbox_options:
|
||||||
for key, value in sandbox_options.items():
|
for key, value in sandbox_options.items():
|
||||||
if key == "binds" and isinstance(value, list):
|
if key == "binds" and isinstance(value, list):
|
||||||
@ -69,6 +80,7 @@ class ToolboxContainer:
|
|||||||
else:
|
else:
|
||||||
options[key] = value
|
options[key] = value
|
||||||
self.sandbox_options = options
|
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.idle_timeout = max(0, int(idle_timeout)) if idle_timeout is not None else 0
|
||||||
|
|
||||||
self._terminal: Optional[PersistentTerminal] = None
|
self._terminal: Optional[PersistentTerminal] = None
|
||||||
@ -76,6 +88,30 @@ class ToolboxContainer:
|
|||||||
self._session_name = f"toolbox-{uuid.uuid4().hex[:10]}"
|
self._session_name = f"toolbox-{uuid.uuid4().hex[:10]}"
|
||||||
self._last_used = 0.0
|
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 def _ensure_terminal(self) -> PersistentTerminal:
|
||||||
"""确保容器已启动。"""
|
"""确保容器已启动。"""
|
||||||
async with self._lock:
|
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面板折叠状态
|
// Token面板折叠状态
|
||||||
tokenPanelCollapsed: true,
|
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,
|
compressing: false,
|
||||||
@ -398,6 +412,8 @@ async function bootstrapApp() {
|
|||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.autoResizeInput();
|
this.autoResizeInput();
|
||||||
});
|
});
|
||||||
|
this.startContainerStatsPolling();
|
||||||
|
this.startProjectStoragePolling();
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
@ -420,6 +436,8 @@ async function bootstrapApp() {
|
|||||||
clearInterval(this.subAgentPollTimer);
|
clearInterval(this.subAgentPollTimer);
|
||||||
this.subAgentPollTimer = null;
|
this.subAgentPollTimer = null;
|
||||||
}
|
}
|
||||||
|
this.stopContainerStatsPolling();
|
||||||
|
this.stopProjectStoragePolling();
|
||||||
const cleanup = this.destroyEasterEggEffect(true);
|
const cleanup = this.destroyEasterEggEffect(true);
|
||||||
if (cleanup && typeof cleanup.catch === 'function') {
|
if (cleanup && typeof cleanup.catch === 'function') {
|
||||||
cleanup.catch(() => {});
|
cleanup.catch(() => {});
|
||||||
@ -816,7 +834,7 @@ async function bootstrapApp() {
|
|||||||
|
|
||||||
// 监听状态更新事件
|
// 监听状态更新事件
|
||||||
this.socket.on('status_update', (status) => {
|
this.socket.on('status_update', (status) => {
|
||||||
// 更新系统状态信息
|
this.applyStatusSnapshot(status);
|
||||||
if (status.conversation && status.conversation.current_id) {
|
if (status.conversation && status.conversation.current_id) {
|
||||||
this.currentConversationId = status.conversation.current_id;
|
this.currentConversationId = status.conversation.current_id;
|
||||||
}
|
}
|
||||||
@ -829,13 +847,14 @@ async function bootstrapApp() {
|
|||||||
this.socket.on('ai_message_start', () => {
|
this.socket.on('ai_message_start', () => {
|
||||||
console.log('AI消息开始');
|
console.log('AI消息开始');
|
||||||
this.cleanupStaleToolActions();
|
this.cleanupStaleToolActions();
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
actions: [],
|
actions: [],
|
||||||
streamingThinking: '',
|
streamingThinking: '',
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
currentStreamingType: null
|
currentStreamingType: null,
|
||||||
};
|
activeThinkingId: null
|
||||||
|
};
|
||||||
this.messages.push(newMessage);
|
this.messages.push(newMessage);
|
||||||
this.currentMessageIndex = this.messages.length - 1;
|
this.currentMessageIndex = this.messages.length - 1;
|
||||||
this.streamingMessage = true;
|
this.streamingMessage = true;
|
||||||
@ -863,6 +882,7 @@ async function bootstrapApp() {
|
|||||||
|
|
||||||
const blockId = action.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const blockId = action.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
action.blockId = blockId;
|
action.blockId = blockId;
|
||||||
|
msg.activeThinkingId = action.id;
|
||||||
this.expandedBlocks.add(blockId);
|
this.expandedBlocks.add(blockId);
|
||||||
// 开始思考时自动锁定滚动到底部
|
// 开始思考时自动锁定滚动到底部
|
||||||
this.autoScrollEnabled = true;
|
this.autoScrollEnabled = true;
|
||||||
@ -879,14 +899,14 @@ async function bootstrapApp() {
|
|||||||
const msg = this.messages[this.currentMessageIndex];
|
const msg = this.messages[this.currentMessageIndex];
|
||||||
msg.streamingThinking += data.content;
|
msg.streamingThinking += data.content;
|
||||||
|
|
||||||
const lastAction = msg.actions[msg.actions.length - 1];
|
const thinkingAction = this.getActiveThinkingAction(msg);
|
||||||
if (lastAction && lastAction.type === 'thinking') {
|
if (thinkingAction) {
|
||||||
lastAction.content += data.content;
|
thinkingAction.content += data.content;
|
||||||
}
|
}
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (lastAction && lastAction.blockId) {
|
if (thinkingAction && thinkingAction.blockId) {
|
||||||
this.scrollThinkingToBottom(lastAction.blockId);
|
this.scrollThinkingToBottom(thinkingAction.blockId);
|
||||||
}
|
}
|
||||||
this.conditionalScrollToBottom();
|
this.conditionalScrollToBottom();
|
||||||
});
|
});
|
||||||
@ -898,13 +918,13 @@ async function bootstrapApp() {
|
|||||||
console.log('思考结束');
|
console.log('思考结束');
|
||||||
if (this.currentMessageIndex >= 0) {
|
if (this.currentMessageIndex >= 0) {
|
||||||
const msg = this.messages[this.currentMessageIndex];
|
const msg = this.messages[this.currentMessageIndex];
|
||||||
const lastAction = msg.actions[msg.actions.length - 1];
|
const thinkingAction = this.getActiveThinkingAction(msg);
|
||||||
if (lastAction && lastAction.type === 'thinking') {
|
if (thinkingAction) {
|
||||||
lastAction.streaming = false;
|
thinkingAction.streaming = false;
|
||||||
lastAction.content = data.full_content;
|
thinkingAction.content = data.full_content;
|
||||||
const blockId = lastAction.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
const blockId = thinkingAction.blockId || `thinking-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||||
if (!lastAction.blockId) {
|
if (!thinkingAction.blockId) {
|
||||||
lastAction.blockId = blockId;
|
thinkingAction.blockId = blockId;
|
||||||
}
|
}
|
||||||
if (blockId) {
|
if (blockId) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -917,6 +937,7 @@ async function bootstrapApp() {
|
|||||||
}
|
}
|
||||||
msg.streamingThinking = '';
|
msg.streamingThinking = '';
|
||||||
msg.currentStreamingType = null;
|
msg.currentStreamingType = null;
|
||||||
|
msg.activeThinkingId = null;
|
||||||
this.$forceUpdate();
|
this.$forceUpdate();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1409,7 +1430,8 @@ async function bootstrapApp() {
|
|||||||
actions: [],
|
actions: [],
|
||||||
streamingThinking: '',
|
streamingThinking: '',
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
currentStreamingType: null
|
currentStreamingType: null,
|
||||||
|
activeThinkingId: null
|
||||||
};
|
};
|
||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
this.currentMessageIndex = this.messages.length - 1;
|
this.currentMessageIndex = this.messages.length - 1;
|
||||||
@ -1503,6 +1525,7 @@ async function bootstrapApp() {
|
|||||||
this.projectPath = statusData.project_path || '';
|
this.projectPath = statusData.project_path || '';
|
||||||
this.agentVersion = statusData.version || this.agentVersion;
|
this.agentVersion = statusData.version || this.agentVersion;
|
||||||
this.thinkingMode = !!statusData.thinking_mode;
|
this.thinkingMode = !!statusData.thinking_mode;
|
||||||
|
this.applyStatusSnapshot(statusData);
|
||||||
|
|
||||||
// 获取当前对话信息
|
// 获取当前对话信息
|
||||||
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
|
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
|
||||||
@ -1609,6 +1632,139 @@ async function bootstrapApp() {
|
|||||||
toggleTokenPanel() {
|
toggleTokenPanel() {
|
||||||
this.tokenPanelCollapsed = !this.tokenPanelCollapsed;
|
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 statusResponse = await fetch('/api/status');
|
||||||
const status = await statusResponse.json();
|
const status = await statusResponse.json();
|
||||||
console.log('系统状态:', status);
|
console.log('系统状态:', status);
|
||||||
|
this.applyStatusSnapshot(status);
|
||||||
|
|
||||||
// 如果状态中有对话历史字段
|
// 如果状态中有对话历史字段
|
||||||
if (status.conversation_history && Array.isArray(status.conversation_history)) {
|
if (status.conversation_history && Array.isArray(status.conversation_history)) {
|
||||||
@ -1824,7 +1981,8 @@ async function bootstrapApp() {
|
|||||||
actions: [],
|
actions: [],
|
||||||
streamingThinking: '',
|
streamingThinking: '',
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
currentStreamingType: null
|
currentStreamingType: null,
|
||||||
|
activeThinkingId: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3019,6 +3177,25 @@ async function bootstrapApp() {
|
|||||||
}
|
}
|
||||||
this.$forceUpdate();
|
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
|
// 修复:工具相关方法 - 接收tool对象而不是name
|
||||||
getToolIcon(tool) {
|
getToolIcon(tool) {
|
||||||
@ -3669,6 +3846,83 @@ async function bootstrapApp() {
|
|||||||
} else {
|
} else {
|
||||||
return (num / 1000000).toFixed(1) + 'M';
|
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-drawer" v-if="currentConversationId" :class="{ collapsed: tokenPanelCollapsed }">
|
||||||
<div class="token-display-panel">
|
<div class="token-display-panel">
|
||||||
<div class="token-panel-content">
|
<div class="token-panel-content">
|
||||||
<div class="token-stats">
|
<div class="token-panel-layout">
|
||||||
<div class="token-item">
|
<div class="token-card">
|
||||||
<span class="token-label">当前上下文</span>
|
<div class="token-stats">
|
||||||
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
|
<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>
|
||||||
<div class="token-item">
|
<div class="container-stats-card" v-if="containerStatus">
|
||||||
<span class="token-label">累计输入</span>
|
<div class="container-stats-header">
|
||||||
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
|
<span class="token-label">容器资源</span>
|
||||||
</div>
|
<span class="status-pill" :class="containerStatusClass()">{{ containerStatusText() }}</span>
|
||||||
<div class="token-item">
|
</div>
|
||||||
<span class="token-label">累计输出</span>
|
<template v-if="hasContainerStats()">
|
||||||
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2425,6 +2425,23 @@ o-files {
|
|||||||
padding: 16px 36px;
|
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 {
|
.token-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -2462,7 +2479,87 @@ o-files {
|
|||||||
.token-value.input { color: var(--claude-success); }
|
.token-value.input { color: var(--claude-success); }
|
||||||
.token-value.output { color: var(--claude-warning); }
|
.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) {
|
@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 {
|
.token-stats {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
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,
|
LOGS_DIR,
|
||||||
AGENT_VERSION,
|
AGENT_VERSION,
|
||||||
THINKING_FAST_INTERVAL,
|
THINKING_FAST_INTERVAL,
|
||||||
MAX_ACTIVE_USER_CONTAINERS
|
MAX_ACTIVE_USER_CONTAINERS,
|
||||||
|
PROJECT_MAX_STORAGE_MB,
|
||||||
)
|
)
|
||||||
from modules.user_manager import UserManager, UserWorkspace
|
from modules.user_manager import UserManager, UserWorkspace
|
||||||
from modules.gui_file_manager import GuiFileManager
|
from modules.gui_file_manager import GuiFileManager
|
||||||
@ -52,6 +53,7 @@ from modules.personalization_manager import (
|
|||||||
load_personalization_config,
|
load_personalization_config,
|
||||||
save_personalization_config,
|
save_personalization_config,
|
||||||
)
|
)
|
||||||
|
from modules.user_container_manager import UserContainerManager
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='static')
|
app = Flask(__name__, static_folder='static')
|
||||||
app.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_SIZE
|
app.config['MAX_CONTENT_LENGTH'] = MAX_UPLOAD_SIZE
|
||||||
@ -73,11 +75,11 @@ class ConversationIdConverter(BaseConverter):
|
|||||||
app.url_map.converters['conv'] = ConversationIdConverter
|
app.url_map.converters['conv'] = ConversationIdConverter
|
||||||
|
|
||||||
user_manager = UserManager()
|
user_manager = UserManager()
|
||||||
|
container_manager = UserContainerManager()
|
||||||
user_terminals: Dict[str, WebTerminal] = {}
|
user_terminals: Dict[str, WebTerminal] = {}
|
||||||
terminal_rooms: Dict[str, set] = {}
|
terminal_rooms: Dict[str, set] = {}
|
||||||
connection_users: Dict[str, str] = {}
|
connection_users: Dict[str, str] = {}
|
||||||
stop_flags: Dict[str, Dict[str, Any]] = {}
|
stop_flags: Dict[str, Dict[str, Any]] = {}
|
||||||
active_users: set = set()
|
|
||||||
|
|
||||||
DEFAULT_PORT = 8091
|
DEFAULT_PORT = 8091
|
||||||
THINKING_FAILURE_KEYWORDS = ["⚠️", "🛑", "失败", "错误", "异常", "终止", "error", "failed", "未完成", "超时", "强制"]
|
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())
|
username = (username or get_current_username())
|
||||||
if not username:
|
if not username:
|
||||||
return None, None
|
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)
|
workspace = user_manager.ensure_user_workspace(username)
|
||||||
|
container_handle = container_manager.ensure_container(username, str(workspace.project_path))
|
||||||
terminal = user_terminals.get(username)
|
terminal = user_terminals.get(username)
|
||||||
if not terminal:
|
if not terminal:
|
||||||
thinking_mode = session.get('thinking_mode', False)
|
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),
|
project_path=str(workspace.project_path),
|
||||||
thinking_mode=thinking_mode,
|
thinking_mode=thinking_mode,
|
||||||
message_callback=make_terminal_callback(username),
|
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:
|
if terminal.terminal_manager:
|
||||||
terminal.terminal_manager.broadcast = terminal.message_callback
|
terminal.terminal_manager.broadcast = terminal.message_callback
|
||||||
user_terminals[username] = terminal
|
user_terminals[username] = terminal
|
||||||
else:
|
else:
|
||||||
|
terminal.update_container_session(container_handle)
|
||||||
attach_user_broadcast(terminal, username)
|
attach_user_broadcast(terminal, username)
|
||||||
return terminal, workspace
|
return terminal, workspace
|
||||||
|
|
||||||
@ -486,7 +487,7 @@ def login():
|
|||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
if is_logged_in():
|
if is_logged_in():
|
||||||
return redirect('/new')
|
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('resource_busy.html'), 503
|
||||||
return app.send_static_file('login.html')
|
return app.send_static_file('login.html')
|
||||||
|
|
||||||
@ -498,15 +499,16 @@ def login():
|
|||||||
if not record:
|
if not record:
|
||||||
return jsonify({"success": False, "error": "账号或密码错误"}), 401
|
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['logged_in'] = True
|
||||||
session['username'] = record.username
|
session['username'] = record.username
|
||||||
session['thinking_mode'] = app.config.get('DEFAULT_THINKING_MODE', False)
|
session['thinking_mode'] = app.config.get('DEFAULT_THINKING_MODE', False)
|
||||||
session.permanent = True
|
session.permanent = True
|
||||||
user_manager.ensure_user_workspace(record.username)
|
workspace = user_manager.ensure_user_workspace(record.username)
|
||||||
active_users.add(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})
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
|
||||||
@ -540,8 +542,8 @@ def logout():
|
|||||||
session.clear()
|
session.clear()
|
||||||
if username and username in user_terminals:
|
if username and username in user_terminals:
|
||||||
user_terminals.pop(username, None)
|
user_terminals.pop(username, None)
|
||||||
if username in active_users:
|
if username:
|
||||||
active_users.discard(username)
|
container_manager.release_container(username, reason="logout")
|
||||||
return jsonify({"success": True})
|
return jsonify({"success": True})
|
||||||
|
|
||||||
|
|
||||||
@ -634,9 +636,46 @@ def get_status(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
|||||||
print(f"[Status] 获取当前对话信息失败: {e}")
|
print(f"[Status] 获取当前对话信息失败: {e}")
|
||||||
|
|
||||||
status['project_path'] = str(workspace.project_path)
|
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
|
status['version'] = AGENT_VERSION
|
||||||
return jsonify(status)
|
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'])
|
@app.route('/api/thinking-mode', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@with_terminal
|
@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
|
||||||