From fe884a100d63a2434f803923302353c42105a093 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 5 Oct 2025 12:07:12 +0800 Subject: [PATCH] bugfix --- aidocs/__init__.md | 11 + aidocs/auth_api.md | 338 +++++++++++++++++++++++++ aidocs/baseProcessor.md | 399 +++++++++++++++++++++++++++++ aidocs/configuredServer.md | 295 ++++++++++++++++++++++ aidocs/dbadmin.md | 201 +++++++++++++++ aidocs/dsProcessor.md | 248 +++++++++++++++++++ aidocs/error.md | 197 +++++++++++++++ aidocs/filedownload.md | 215 ++++++++++++++++ aidocs/filestorage.md | 391 +++++++++++++++++++++++++++++ aidocs/filetest.md | 148 +++++++++++ aidocs/functionProcessor.md | 294 ++++++++++++++++++++++ aidocs/globalEnv.md | 458 ++++++++++++++++++++++++++++++++++ aidocs/llmProcessor.md | 231 +++++++++++++++++ aidocs/llm_client.md | 354 ++++++++++++++++++++++++++ aidocs/loadplugins.md | 212 ++++++++++++++++ aidocs/myTE.md | 238 ++++++++++++++++++ aidocs/p2p_middleware.md | 276 +++++++++++++++++++++ aidocs/processorResource.md | 468 +++++++++++++++++++++++++++++++++++ aidocs/proxyProcessor.md | 219 ++++++++++++++++ aidocs/real_ip.md | 154 ++++++++++++ aidocs/restful.md | 352 ++++++++++++++++++++++++++ aidocs/serverenv.md | 207 ++++++++++++++++ aidocs/sqldsProcessor.md | 303 +++++++++++++++++++++++ aidocs/uriop.md | 348 ++++++++++++++++++++++++++ aidocs/url2file.md | 343 +++++++++++++++++++++++++ aidocs/utils.md | 106 ++++++++ aidocs/version.md | 52 ++++ aidocs/webapp.md | 260 +++++++++++++++++++ aidocs/websocketProcessor.md | 323 ++++++++++++++++++++++++ aidocs/xlsxData.md | 337 +++++++++++++++++++++++++ aidocs/xlsxdsProcessor.md | 258 +++++++++++++++++++ aidocs/xtermProcessor.md | 417 +++++++++++++++++++++++++++++++ 32 files changed, 8653 insertions(+) create mode 100644 aidocs/__init__.md create mode 100644 aidocs/auth_api.md create mode 100644 aidocs/baseProcessor.md create mode 100644 aidocs/configuredServer.md create mode 100644 aidocs/dbadmin.md create mode 100644 aidocs/dsProcessor.md create mode 100644 aidocs/error.md create mode 100644 aidocs/filedownload.md create mode 100644 aidocs/filestorage.md create mode 100644 aidocs/filetest.md create mode 100644 aidocs/functionProcessor.md create mode 100644 aidocs/globalEnv.md create mode 100644 aidocs/llmProcessor.md create mode 100644 aidocs/llm_client.md create mode 100644 aidocs/loadplugins.md create mode 100644 aidocs/myTE.md create mode 100644 aidocs/p2p_middleware.md create mode 100644 aidocs/processorResource.md create mode 100644 aidocs/proxyProcessor.md create mode 100644 aidocs/real_ip.md create mode 100644 aidocs/restful.md create mode 100644 aidocs/serverenv.md create mode 100644 aidocs/sqldsProcessor.md create mode 100644 aidocs/uriop.md create mode 100644 aidocs/url2file.md create mode 100644 aidocs/utils.md create mode 100644 aidocs/version.md create mode 100644 aidocs/webapp.md create mode 100644 aidocs/websocketProcessor.md create mode 100644 aidocs/xlsxData.md create mode 100644 aidocs/xlsxdsProcessor.md create mode 100644 aidocs/xtermProcessor.md diff --git a/aidocs/__init__.md b/aidocs/__init__.md new file mode 100644 index 0000000..a61b84b --- /dev/null +++ b/aidocs/__init__.md @@ -0,0 +1,11 @@ +当然可以!请先提供您需要编写技术文档的代码,以便我为您生成相应的 Markdown 格式技术文档。文档将包括: + +- 概述 +- 功能说明 +- 依赖项 +- 安装与使用方法 +- 函数/类说明 +- 示例代码 +- 注意事项 + +期待您提供具体代码内容。 \ No newline at end of file diff --git a/aidocs/auth_api.md b/aidocs/auth_api.md new file mode 100644 index 0000000..c5e6f21 --- /dev/null +++ b/aidocs/auth_api.md @@ -0,0 +1,338 @@ +# 技术文档:基于 `aiohttp` 的认证与会话管理系统 + +--- + +## 概述 + +本文档描述了一个基于 `aiohttp` 构建的异步 Web 应用中的 **用户认证(Authentication)** 与 **会话管理(Session Management)** 系统。该系统支持: + +- 基于 Cookie 或 Redis 存储的会话机制 +- 使用加密 Ticket 的安全认证策略(`TktAuthentication`) +- 支持客户端唯一标识(`client_uuid`)绑定 +- RSA 加密解密功能用于敏感数据处理 +- 可扩展的权限检查接口 +- 请求日志记录与异常追踪 + +该模块适用于需要高安全性、可扩展性和分布式部署能力的 Web 后端服务。 + +--- + +## 依赖库说明 + +| 包名 | 用途 | +|------|------| +| `aiohttp` | 异步 Web 框架核心 | +| `aiohttp_auth` / `TktAuthentication` | 基于票据(ticket)的身份认证中间件 | +| `aiohttp_session` | 会话管理中间件,支持多种存储后端 | +| `redis.asyncio` | 异步 Redis 客户端,用于持久化会话 | +| `cryptography`(隐式依赖) | 加密 Cookie 所需(由 `EncryptedCookieStorage` 内部使用) | +| `appPublic.*` | 自定义公共工具模块(配置、日志、RSA 加解密等) | + +> ⚠️ 注意:原代码中注释了 `aioredis`,实际使用的是新版本 `redis.asyncio` + +--- + +## 核心功能模块 + +### 1. 会话管理(Session Management) + +#### 支持两种会话存储方式: + +| 存储类型 | 配置开关 | 特点 | +|--------|---------|------| +| 加密 Cookie 存储 | 默认启用 | 无外部依赖,适合单机部署 | +| Redis 存储 | `conf.website.session_redis.url` 存在时启用 | 支持集群、集中管理、更安全 | + +#### 自定义 Redis 存储类:`MyRedisStorage` + +继承自 `RedisStorage`,增强以下特性: + +- **自定义 Session Key 生成逻辑** + - 优先从请求头 `client_uuid` 获取客户端唯一标识 + - 若不存在,则随机生成 UUID4 并作为响应返回给客户端 + - 对字符串 key 进行 hex 编码以保证兼容性 + +```python +def key_gen(self, request): + key = request.headers.get('client_uuid') + if not key: + key = uuid.uuid4().hex + return key + if isinstance(key, str): + key = key.encode('utf-8') + key = binascii.hexlify(key).decode('utf-8') + return key +``` + +> ✅ 目的:实现跨设备/浏览器的稳定会话识别,避免频繁重新登录。 + +--- + +### 2. 认证机制(Authentication) + +使用 `aiohttp_auth.auth.ticket_auth.TktAuthentication` 实现基于时间戳和签名的票据认证。 + +#### 关键参数配置: + +| 参数 | 默认值 | 配置项 | 说明 | +|------|-------|--------|------| +| `session_max_time` | 120 秒 | `website.session_max_time` | 会话最长有效期 | +| `reissue_time` | 30 秒 | `website.session_reissue_time` | 自动续签间隔 | +| `include_ip` | `True` | 固定设置 | 将客户端 IP 加入票据哈希,防劫持 | + +#### 自定义 `_new_ticket` 方法 + +重写了票据创建过程,加入对 `client_uuid` 的支持: + +```python +def _new_ticket(self, request, user_id): + client_uuid = request.headers.get('client_uuid') + ip = self._get_ip(request) + valid_until = int(time.time()) + self._max_age + return self._ticket.new( + user_id, + valid_until=valid_until, + client_ip=ip, + user_data=client_uuid # 将 client_uuid 附加到票据中 + ) +``` + +> 🔐 安全提示:IP 绑定 + client_uuid 提升了会话安全性,防止 CSRF 和会话固定攻击。 + +--- + +### 3. 用户信息提取 + +提供异步函数用于从当前会话中获取用户信息。 + +#### 函数列表 + +| 函数 | 返回值 | 说明 | +|------|--------|------| +| `get_session_userinfo(request)` | `DictObject(userid, username, userorgid)` | 解析认证数据并封装为对象 | +| `get_session_user(request)` | `userid` 字符串 | 快速获取当前用户 ID | + +> 💡 数据格式:认证信息以 `userid:username:userorgid` 形式存储于 ticket 中。 + +示例: +```python +await auth.remember(request, "U1001:alice:ORG789") +``` + +--- + +### 4. 登录与登出操作 + +#### `user_login(request, userid, username='', userorgid='')` +将用户信息编码后写入认证票据。 + +```python +ui = f'{userid}:{username}:{userorgid}' +await auth.remember(request, ui) +``` + +#### `user_logout(request)` +清除认证状态。 + +```python +await auth.forget(request) +``` + +> 🧽 清除的是服务器端票据与客户端 Cookie。 + +--- + +### 5. 权限控制中间件:`checkAuth` + +通过 `@web.middleware` 装饰器注册为全局中间件,负责: + +1. 记录访问开始时间 +2. 获取当前用户身份 +3. 调用 `checkUserPermission()` 判断是否有权访问路径 +4. 记录耗时与异常信息 +5. 控制响应流程或抛出 `HTTPUnauthorized` / `HTTPForbidden` + +#### 日志输出示例 + +```text +INFO timecost=client(192.168.1.100) U1001 access /api/data cost 0.045, (0.002) +ERROR Exception=client(192.168.1.100) U1001 access /api/admin/delete cost 0.12, (0.003), except=ValueError... +``` + +> ✅ 成功请求记录总耗时及权限判断耗时;异常则完整打印 traceback。 + +--- + +### 6. RSA 加解密支持 + +#### 类方法:`AuthAPI` + +| 方法 | 功能 | +|------|------| +| `getPrivateKey()` | 延迟加载私钥文件(仅首次调用读取),避免重复 IO | +| `rsaDecode(cdata)` | 使用私钥解密 Base64 编码的数据 | + +依赖: +- `appPublic.rsawrap.RSA`:封装了 PyCryptodome 的 RSA 操作 +- 配置路径:`conf.website.rsakey.privatekey` + +典型用途:解密前端传来的加密密码或其他敏感字段。 + +--- + +### 7. 初始化与集成:`setupAuth(app)` + +此方法完成整个认证系统的初始化,步骤如下: + +1. **构建 secret 密钥** + 基于端口号拼接固定字符串,补足 32 字节用于 AES 加密(Cookie Storage 所需) + +2. **选择会话存储方式** + ```python + if self.conf.website.session_redis: + redisdb = await redis.Redis.from_url(url) + storage = MyRedisStorage(redisdb) + else: + storage = EncryptedCookieStorage(secret) + ``` + +3. **安装会话中间件** + ```python + aiohttp_session.setup(app, storage) + ``` + +4. **配置 Ticket 认证策略** + ```python + policy = SessionTktAuthentication( + secret=secret, + max_age=session_max_time, + reissue_time=session_reissue_time, + include_ip=True + ) + auth.setup(app, policy) + ``` + +5. **替换默认 IP 获取逻辑** + ```python + TktAuthentication._get_ip = get_client_ip # 使用 request['client_ip'] + ``` + +6. **注入权限检查中间件** + ```python + app.middlewares.append(self.checkAuth) + ``` + +--- + +## 配置要求(`jsonConfig` 结构) + +`getConfig()` 应返回包含以下字段的配置对象: + +```json +{ + "website": { + "port": 8080, + "rsakey": { + "privatekey": "/path/to/private.pem" + }, + "session_max_time": 3600, + "session_reissue_time": 1800, + "session_redis": { + "url": "redis://localhost:6379/0" + } + } +} +``` + +> ⚠️ 若未设置 `session_redis.url`,则自动降级为本地加密 Cookie 存储。 + +--- + +## 使用方式(集成到 AIOHTTP 应用) + +```python +from aiohttp import web +from your_module import AuthAPI + +async def init_app(): + app = web.Application() + + auth_api = AuthAPI() + await auth_api.setupAuth(app) + + # 添加路由 + # app.router.add_get('/protected', protected_handler) + + return app + +if __name__ == '__main__': + web.run_app(init_app(), port=8080) +``` + +--- + +## 安全建议 + +| 项目 | 推荐做法 | +|------|----------| +| Secret Key | 不应硬编码,建议从环境变量或密钥管理系统加载 | +| client_uuid | 前端应在首次访问时生成并持久化(localStorage),每次请求带上 | +| Redis 安全 | 开启密码认证、限制网络访问 | +| 日志敏感信息 | 禁止记录用户密码、token 明文 | +| HTTPS | 生产环境必须启用 TLS,防止 Cookie 被窃听 | + +--- + +## 扩展接口 + +### `checkUserPermission(request, user, path)` +抽象方法,子类可覆盖实现 RBAC、ACL 等权限模型。 + +```python +async def checkUserPermission(self, request, user, path): + # 示例:仅允许特定用户访问管理员接口 + if path.startswith("/admin") and user != "admin": + return False + return True +``` + +### `needAuth(path)` +预留钩子,未来可用于跳过某些路径的认证检查。 + +--- + +## 错误处理 + +| 异常场景 | 处理方式 | +|--------|---------| +| 未登录访问受保护资源 | 抛出 `HTTPUnauthorized (401)` | +| 有登录但无权限 | 抛出 `HTTPForbidden (403)` | +| 内部错误 | 捕获并记录 traceback,重新抛出异常 | +| Redis 连接失败 | 初始化阶段抛出异常,应用无法启动 | + +--- + +## 性能考量 + +- **Redis 存储模式**:增加一次网络往返,但支持横向扩展 +- **Ticket 验证**:轻量级 HMAC 验证,性能优异 +- **自动续签机制**:每 `reissue_time` 秒更新票据,延长会话寿命而不影响用户体验 + +--- + +## 总结 + +本模块提供了一套完整的、安全的、可扩展的异步认证解决方案,特点包括: + +✅ 支持分布式部署(Redis) +✅ 客户端绑定(UUID + IP)提升安全性 +✅ 细粒度权限控制接口 +✅ 全链路日志跟踪与性能监控 +✅ 支持 RSA 解密敏感数据 + +适用于企业级后台管理系统、API 网关、微服务认证中心等场景。 + +--- + +> 📝 文档版本:v1.0 +> © 2025 公共技术组件团队 \ No newline at end of file diff --git a/aidocs/baseProcessor.md b/aidocs/baseProcessor.md new file mode 100644 index 0000000..dea418b --- /dev/null +++ b/aidocs/baseProcessor.md @@ -0,0 +1,399 @@ +# 技术文档:处理器框架(Processor Framework) + +本文档描述了一个基于 `aiohttp` 的异步 Web 处理器系统,支持多种资源类型(如模板、脚本、Markdown 等)的动态处理。该系统通过继承和多态机制实现不同类型的处理器,并结合运行时环境注入与缓存策略提升性能。 + +--- + +## 📦 模块概览 + +```python +import os +import re +import json +import codecs +import aiofiles +from aiohttp.web_request import Request +from aiohttp.web_response import Response, StreamResponse + +from appPublic.jsonConfig import getConfig +from appPublic.dictObject import DictObject +from appPublic.folderUtils import listFile +from appPublic.argsConvert import ArgsConvert +from appPublic.log import info, debug, warning, error, critical, exception + +from .utils import unicode_escape +from .serverenv import ServerEnv +from .filetest import current_fileno +``` + +### 依赖说明 + +| 包/模块 | 用途 | +|--------|------| +| `aiohttp` | 提供异步请求/响应对象 | +| `appPublic.*` | 自定义公共库:配置读取、字典封装、日志等 | +| `aiofiles` | 异步文件操作 | +| `json`, `re` | 数据序列化与正则替换 | + +--- + +## 🔁 核心组件 + +### 1. `ObjectCache` 类 + +用于缓存已加载的对象,避免重复解析或编译开销。 + +#### 方法 + +| 方法 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `__init__()` | - | `ObjectCache` 实例 | 初始化空缓存字典 | +| `store(path, obj)` | `path: str`, `obj: Any` | `None` | 将对象按路径存储,记录其最后修改时间(mtime) | +| `get(path)` | `path: str` | `Any or None` | 若文件未被修改则返回缓存对象,否则返回 `None` | + +> ⚠️ 缓存失效判断依据:`os.path.getmtime(path) > cached_mtime` + +#### 示例用法 + +```python +cache = ObjectCache() +cache.store('/path/to/file.py', compiled_code) +obj = cache.get('/path/to/file.py') # 如果文件未变,返回缓存对象 +``` + +--- + +### 2. `BaseProcessor` 基类 + +所有处理器的基类,提供通用处理流程和响应构造能力。 + +#### 属性 + +| 属性 | 类型 | 初始值 | 说明 | +|------|------|-------|------| +| `path` | `str` | 构造传入 | 请求对应的虚拟路径 | +| `resource` | `Resource` 对象 | 构造传入 | 资源管理器实例 | +| `retResponse` | `Response or None` | `None` | 可直接返回的自定义响应对象 | +| `headers` | `dict` | 默认头信息 | 响应头字段集合 | +| `content` | `str / dict / bytes / ...` | `''` | 处理后的输出内容 | +| `env_set` | `bool` | `False` | 是否已设置运行环境 | +| `real_path` | `str` | 动态设置 | 映射到本地文件系统的实际路径 | + +#### 静态方法 + +##### `isMe(name: str) -> bool` +- **用途**:判断当前处理器是否匹配给定名称。 +- **默认实现**:仅当 `name == 'base'` 时返回 `True` +- 子类需重写此方法以支持识别。 + +#### 异步方法 + +##### `be_call(request: Request, params: dict = {}) -> Response` +调用入口,最终返回 HTTP 响应。 + +```python +return await self.path_call(request, params) +``` + +##### `set_run_env(request: Request, params: dict = {}) -> None` +初始化运行命名空间(`run_ns`),注入上下文变量。 + +###### 注入变量包括: +| 变量名 | 来源 | 说明 | +|--------|------|------| +| `request` | 参数 | 当前请求对象 | +| `app` | `request.app` | 应用实例 | +| `params_kw` | 合并参数 | 来自 URL 参数和外部传参 | +| `ref_real_path` | 映射结果 | 文件系统真实路径 | +| `processor` | `self` | 当前处理器引用 | +| 其他全局环境 | `ServerEnv().to_dict()` 和 `resource.y_env` | 服务端环境配置 | + +> ✅ 支持 `request2ns()` 扩展函数将请求参数转换为命名空间。 + +##### `execute(request: Request) -> str` +执行主逻辑链: + +```python +await self.set_run_env() +await self.datahandle() # 子类实现具体处理 +return self.content +``` + +##### `handle(request: Request) -> Response` +生成最终 HTTP 响应对象,自动处理不同类型的内容输出。 + +###### 内容类型自动检测与转换: +| 输入类型 | 输出行为 | +|---------|----------| +| `Response` / `StreamResponse` | 直接返回 | +| `dict`, `list`, `tuple`, `DictObject` | JSON 序列化,设置 `Content-Type: application/json` | +| `bytes` | 设置二进制响应体 | +| 字符串且合法 JSON | 视为 JSON 输出 | +| 其他字符串 | 普通文本响应 | + +> ✅ 自动添加 CORS 相关头部(暴露 Set-Cookie) + +##### `datahandle(request: Request)` +**抽象方法**,子类必须实现具体的业务逻辑。 + +默认实现打印错误日志并清空内容。 + +##### `setheaders()` +可选覆写方法,用于在响应前设置额外头部(如 Content-Length)。目前留空。 + +--- + +## 🧩 处理器子类 + +### 1. `TemplateProcessor(BaseProcessor)` + +用于渲染 `.tmpl` 模板文件。 + +#### 特性 +- 使用 `tmpl_engine` 渲染模板 +- 支持扩展名决定内容类型(`.tmpl.css`, `.tmpl.js`) + +#### 关键方法 + +##### `isMe(name)` +返回 `True` 当 `name == 'tmpl'` + +##### `path_call(request, params)` +使用模板引擎渲染指定路径模板。 + +```python +te = self.run_ns['tmpl_engine'] +return await te.render(path, **ns) +``` + +##### `datahandle(request)` +调用 `path_call` 获取模板渲染结果。 + +##### `setheaders()` +根据文件后缀设置正确的 MIME 类型: +- `.tmpl.css` → `text/css` +- `.tmpl.js` → `application/javascript` +- 其他 → `text/html` + +--- + +### 2. `BricksAppProcessor(TemplateProcessor)` + +包装应用级模板,嵌入主布局框架。 + +#### 特性 +- 继承模板功能 +- 将原始模板内容插入到 `bricksapp.tmpl` 主容器中 + +#### `datahandle(request)` +1. 先调用父类渲染原始模板 → `txt` +2. 加载 `/bricks/bricksapp.tmpl` 作为外壳 +3. 使用 `ArgsConvert("${", "}$")` 替换 `${appdic}` 占位符为 `txt` + +```python +ac = ArgsConvert("${", "}$") +self.content = ac.convert(template_wrapper, {'appdic': txt}) +``` + +#### `isMe(name)` +返回 `True` 当 `name == 'app'` + +--- + +### 3. `BricksUIProcessor(TemplateProcessor)` + +条件性包裹页面 UI 结构(header + content + footer) + +#### 行为逻辑 +检查参数 `_webbricks_`: +- 若不存在或等于 `0` → 不包装,直接保留原内容 +- 否则 → 包装 header 和 footer 模板 + +#### `datahandle(request)` +```python +if should_wrap: + header = await resource.path_call(... '/header.tmpl') + footer = await resource.path_call(... '/footer.tmpl') + self.content = f"{header}{original}{footer}" +``` + +#### `isMe(name)` +返回 `True` 当 `name == 'bui'` + +--- + +### 4. `PythonScriptProcessor(BaseProcessor)` + +执行 Python 异步脚本(`.dspy` 文件) + +#### 特性 +- 将 `.dspy` 文件内容包装成 `async def myfunc(request, **ns): ...` +- 在安全命名空间内 `exec()` 执行 +- 调用并返回结果 + +#### 关键方法 + +##### `loadScript(path)` +异步读取脚本内容,每行前加缩进 `\t`,拼接为函数体字符串。 + +> 💡 注意:`\r\n` 被统一处理为 `\n` + +##### `path_call(request, params)` +1. 准备运行环境 `lenv`(移除 `request` 防止污染) +2. 加载并编译脚本代码 +3. `exec()` 注入命名空间 +4. 调用 `myfunc(request, **lenv)` 并返回结果 + +##### `datahandle(request)` +调用 `path_call` 获取脚本执行结果并赋值给 `content` + +##### `isMe(name)` +返回 `True` 当 `name == 'dspy'` + +--- + +### 5. `MarkdownProcessor(BaseProcessor)` + +渲染 Markdown 文件,并自动重写链接为完整 URL。 + +#### 特性 +- 支持内联 `[text](url)` 链接自动补全域名 +- 使用 `resource.entireUrl()` 解析相对路径 + +#### 方法 + +##### `datahandle(request)` +异步读取 `.md` 文件内容,调用 `urlreplace()` 处理链接。 + +##### `urlreplace(mdtxt, request)` +使用正则替换所有 Markdown 链接: + +```regex +\[(.*)\]\((.*)\) +``` + +替换为: + +```text +[显示文本](完整URL) +``` + +示例: +```md +[首页](/home) → [首页](https://example.com/home) +``` + +##### `isMe(name)` +返回 `True` 当 `name == 'md'` + +--- + +## 🔄 工厂函数:处理器查找 + +### `getProcessor(name: str) -> Type[BaseProcessor] or None` + +根据名称递归查找匹配的处理器类。 + +#### 实现逻辑 + +```python +def getProcessor(name): + return _getProcessor(BaseProcessor, name) + +def _getProcessor(kclass, name): + for subclass in kclass.__subclasses__(): + if hasattr(subclass, 'isMe') and subclass.isMe(name): + return subclass + # 递归搜索子类 + found = _getProcessor(subclass, name) + if found: + return found + return None +``` + +#### 示例 + +```python +cls = getProcessor('tmpl') # → TemplateProcessor +cls = getProcessor('dspy') # → PythonScriptProcessor +cls = getProcessor('unknown') # → None +``` + +> ✅ 支持深度继承结构下的处理器发现 + +--- + +## 🛠️ 设计亮点 + +| 特性 | 描述 | +|------|------| +| **插件式架构** | 通过 `isMe()` + 继承实现处理器注册与发现 | +| **运行环境隔离** | 每个请求独立 `run_ns`,防止状态污染 | +| **异步友好** | 全面使用 `async/await`,适配 `aiohttp` 生态 | +| **内容类型智能推导** | 自动判断输出类型并设置正确 `Content-Type` | +| **CORS 支持** | 默认暴露 `Set-Cookie` 头部 | +| **模板嵌套机制** | 支持布局包装(BricksApp/UI) | +| **动态脚本执行** | 安全沙箱模式执行用户脚本(谨慎使用) | + +--- + +## ⚠️ 安全建议 + +1. **慎用 `PythonScriptProcessor`** + - `exec()` 存在严重安全风险 + - 建议限制 `.dspy` 文件访问权限或禁用生产环境使用 + +2. **输入验证** + - 所有来自客户端的参数应进行严格校验 + - 防止路径遍历攻击(如 `../../etc/passwd`) + +3. **CORS 策略** + - 当前只暴露 `Set-Cookie`,但未启用 `Allow-Credentials` 和 `Allow-Origin` + - 如需跨域,请在 `set_response_headers` 中补充策略 + +--- + +## 📎 总结 + +本模块构建了一个灵活、可扩展的异步 Web 资源处理框架,适用于: + +- 模板渲染服务 +- 动态 Markdown 页面 +- 内嵌脚本执行(开发调试场景) +- 布局嵌套系统(Bricks 架构) + +通过简单的继承与注册机制,开发者可以轻松扩展新的处理器类型,满足多样化的前端集成需求。 + +--- + +> 📁 **建议目录结构参考** + +``` +/templates/ + home.tmpl + bricks/ + bricksapp.tmpl + header.tmpl + footer.tmpl +/scripts/ + api.dspy +/pages/ + about.md +/static/ + ... +``` + +> 📄 **路由映射示意** + +| URL Path | Processor | Name Hint | +|---------|-----------|----------| +| `/app/home` | BricksAppProcessor | `app` | +| `/bui/dashboard` | BricksUIProcessor | `bui` | +| `/api/data.dspy` | PythonScriptProcessor | `dspy` | +| `/doc/intro.md` | MarkdownProcessor | `md` | +| `/view/page.tmpl` | TemplateProcessor | `tmpl` | + +--- + +📝 *文档版本:v1.0* +📅 *最后更新:2025-04-05* \ No newline at end of file diff --git a/aidocs/configuredServer.md b/aidocs/configuredServer.md new file mode 100644 index 0000000..ad2b0a5 --- /dev/null +++ b/aidocs/configuredServer.md @@ -0,0 +1,295 @@ +# AHApp 技术文档 + +## 概述 + +`AHApp` 是一个基于 `aiohttp.web.Application` 构建的异步 Web 应用框架,专为支持复杂业务逻辑、插件化架构和灵活配置而设计。它集成了数据库连接池、模板引擎、身份认证、文件存储、中间件处理等功能,并通过配置驱动的方式实现高度可定制性。 + +该应用主要用于构建高性能、模块化的 Python 异步后端服务,适用于企业级 Web 服务或 API 网关场景。 + +--- + +## 依赖库说明 + +### 标准库 +| 模块 | 用途 | +|------|------| +| `os`, `sys` | 路径与系统环境操作 | +| `platform` | 判断操作系统平台(用于端口复用) | +| `time` | 时间相关功能(预留扩展) | +| `ssl` | SSL/TLS 支持,用于 HTTPS 服务器启动 | +| `socket` | 套接字基础支持(可能用于网络检测或底层通信) | + +### 第三方库 +| 模块 | 用途 | +|------|------| +| `aiohttp.web` | 异步 Web 框架核心,提供 HTTP 服务支持 | +| `appPublic.*` 系列模块 | 自定义公共工具组件(路径、日志、配置、字典对象等) | +| `sqlor.dbpools.DBPools` | 数据库连接池管理器,支持多数据源 | + +### 本地模块(相对导入) +| 模块 | 用途 | +|------|------| +| `.processorResource.ProcessorResource` | 可编程资源处理器,支持自定义请求处理逻辑 | +| `.auth_api.AuthAPI` | 认证接口抽象类,支持 JWT/OAuth 等机制 | +| `.myTE.setupTemplateEngine` | 初始化模板引擎(如 Jinja2) | +| `.globalEnv.initEnv` | 初始化全局运行环境变量 | +| `.serverenv.ServerEnv` | 全局服务器运行上下文容器 | +| `.filestorage.TmpFileRecord` | 临时文件记录管理器 | +| `.loadplugins.load_plugins` | 插件加载系统,实现功能扩展 | +| `.real_ip.real_ip_middleware` | 中间件:提取真实客户端 IP(支持反向代理) | + +--- + +## 核心类说明 + +### `AHApp(web.Application)` + +继承自 `aiohttp.web.Application` 的增强型应用类,增加了用户数据存储和中间件预置功能。 + +#### 方法 + +##### `__init__(*args, **kw)` +初始化应用实例,设置最大请求体大小并插入真实 IP 中间件。 + +- **参数**: + - `*args`, `**kw`: 传递给父类的参数 +- **行为**: + - 设置默认 `client_max_size = 1024000000` 字节(约 1GB) + - 创建 `user_data: DictObject` 存储用户自定义数据 + - 在中间件链首部插入 `real_ip_middleware` + +##### `set_data(k, v)` +将键值对存入内部 `user_data` 容器中。 + +- **参数**: + - `k` (str): 键名 + - `v`: 任意类型值 + +##### `get_data(k)` +获取指定键对应的值。 + +- **参数**: + - `k` (str): 键名 +- **返回**: + - 值或 `None`(若不存在) + +--- + +### `ConfiguredServer` + +主服务器配置类,负责加载配置、初始化组件、注册路由及启动服务。 + +#### 构造函数 `__init__(auth_klass=AuthAPI, workdir=None)` + +- **参数**: + - `auth_klass`: 认证类,需实现 `setupAuth(app)` 异步方法,默认使用 `AuthAPI` + - `workdir`: 工作目录路径,用于加载配置文件;若未指定则使用默认查找策略 + +- **初始化流程**: + 1. 加载配置文件(通过 `getConfig()`) + - 若提供了 `workdir`,则传入上下文 `{workdir, ProgramPath}` + 2. 若配置中包含 `databases`,初始化 `DBPools` + 3. 调用 `initEnv()` 和 `setupTemplateEngine()` 初始化全局环境与模板引擎 + 4. 设置上传文件最大尺寸(默认 10MB,可由配置覆盖) + 5. 实例化 `AHApp` 并注入 `client_max_size` + 6. 加载插件(调用 `load_plugins(workdir)`) + 7. 初始化 `ServerEnv` 全局环境对象并设置工作目录 + +#### 方法 + +##### `async build_app()` +构建完整的 Web 应用实例。 + +- **流程**: + 1. 触发 `'ahapp_built'` 钩子事件(所有注册的协程函数执行) + 2. 实例化认证模块并调用其 `setupAuth(app)` 完成认证配置 + 3. 返回最终的 `Application` 实例 + +> ⚠️ 注意:此方法返回的是 `awaitable`,必须在事件循环中调用。 + +##### `run(port=None)` +启动 HTTP/HTTPS 服务。 + +- **参数**: + - `port`: 监听端口,优先级:传参 > 配置文件 > 默认 8080 + +- **功能细节**: + - 读取配置中的主机地址(默认 `'0.0.0.0'`) + - 若启用 SSL,则创建 `ssl_context` 并加载证书 (`crtfile`, `keyfile`) + - 非 Windows 平台启用 `reuse_port=True` 提升性能(允许多进程绑定同一端口) + - 调用 `web.run_app()` 启动服务 + +##### `configPath(config)` +根据配置动态注册带有处理器的资源路径。 + +- **参数**: + - `config`: 配置对象,预期结构为 `config.website.paths` 是一个 `(path, prefix)` 元组列表 + +- **行为**: + - 对每条路径创建 `ProcessorResource` 实例 + - 支持索引页显示(`show_index=True`) + - 允许符号链接访问(`follow_symlinks=True`) + - 支持自定义索引文件名列表(`indexes`) + - 支持请求处理器链(`processors`) + - 将资源注册到 `app.router` + +--- + +## 配置结构示例(JSON/YAML) + +```json +{ + "databases": { + "default": { + "driver": "postgresql", + "host": "localhost", + "port": 5432, + "database": "mydb", + "username": "user", + "password": "pass" + } + }, + "website": { + "host": "0.0.0.0", + "port": 8080, + "client_max_size": 10485760, + "ssl": { + "crtfile": "/path/to/cert.pem", + "keyfile": "/path/to/key.pem" + }, + "paths": [ + ["/static", "/public"], + ["/uploads", "/files"] + ], + "indexes": ["index.html", "default.html"], + "processors": { + ".html": "template_processor", + ".py": "script_processor" + } + } +} +``` + +> 注:实际配置格式取决于 `jsonConfig.getConfig` 的实现,通常支持 `.json`, `.yaml`, `.toml` 等。 + +--- + +## 插件机制 + +通过 `load_plugins(workdir)` 实现插件自动发现与加载。插件可通过 `RegisterCoroutine` 注册生命周期钩子,例如: + +```python +from appPublic.registerfunction import RegisterCoroutine + +def on_ahapp_built(app): + app.router.add_get('/hello', lambda r: web.Response(text="Hello")) + +RegisterCoroutine().register('ahapp_built', on_ahapp_built) +``` + +常见钩子事件包括: +- `ahapp_built`: 应用构建完成时触发 +- `before_start`: 服务启动前 +- `on_shutdown`: 关闭时清理资源 + +--- + +## 中间件 + +### `real_ip_middleware` + +自动从请求头(如 `X-Forwarded-For`, `X-Real-IP`)提取真实客户端 IP 地址,防止因反向代理导致 IP 获取错误。 + +使用方式已在 `AHApp.__init__` 中自动注册。 + +--- + +## 使用示例 + +### 启动最简服务 + +```python +from your_module import ConfiguredServer + +if __name__ == '__main__': + server = ConfiguredServer(workdir='./config') + server.run() # 默认端口 8080 +``` + +### 自定义认证类 + +```python +class MyAuth(AuthAPI): + async def setupAuth(self, app): + # 添加 JWT 中间件或其他认证逻辑 + pass + +server = ConfiguredServer(auth_klass=MyAuth, workdir='./config') +server.run(port=9000) +``` + +--- + +## 运行环境要求 + +- Python >= 3.7 +- 依赖包: + ```txt + aiohttp + psycopg2-binary (PostgreSQL) + oracledb / pymysql 等(根据数据库类型) + pyyaml (可选,用于 YAML 配置) + jinja2 (模板引擎) + ``` + +--- + +## 安全建议 + +1. **生产环境务必启用 SSL** + ```json + "ssl": { + "crtfile": "/etc/letsencrypt/live/domain.com/fullchain.pem", + "keyfile": "/etc/letsencrypt/live/domain.com/privkey.pem" + } + ``` +2. 设置合理的 `client_max_size` 防止 DoS 攻击 +3. 不要暴露调试信息,关闭详细异常回显 +4. 使用 WAF 或 Nginx 做前置防护 + +--- + +## 日志系统 + +使用 `appPublic.log` 提供的日志接口: + +```python +info("Application started") +debug("Detailed debug info") +warning("Something may go wrong") +error("An error occurred") +critical("Critical failure") +exception("Exception with traceback") +``` + +日志输出位置由配置决定,通常写入文件或标准输出。 + +--- + +## 总结 + +`AHApp` 和 `ConfiguredServer` 构成了一个完整、可扩展的异步 Web 服务骨架,具备以下特点: + +✅ 模块化设计 +✅ 插件热加载支持 +✅ 多数据库连接池 +✅ 安全认证集成 +✅ 反向代理兼容 +✅ HTTPS 支持 +✅ 高性能异步处理 + +适合用于开发中大型 Python Web 后端项目。 + +--- + +> 📚 更多信息请参考配套模块文档:`appPublic`, `sqlor`, `processorResource` 等。 \ No newline at end of file diff --git a/aidocs/dbadmin.md b/aidocs/dbadmin.md new file mode 100644 index 0000000..6b6c9c7 --- /dev/null +++ b/aidocs/dbadmin.md @@ -0,0 +1,201 @@ +# DBAdmin 模块技术文档 + +## 概述 + +`DBAdmin` 是一个基于 `aiohttp` 的异步 Web 处理类,用于对数据库表进行基础的管理操作(如浏览、添加、更新、过滤等)。该模块通过封装 `CRUD` 类实现与数据库的交互,并提供统一的 JSON 响应格式。支持错误处理和日志记录,适用于构建轻量级的数据库管理接口。 + +--- + +## 依赖说明 + +### 第三方库 +- `aiohttp`: 异步 Web 框架,用于处理 HTTP 请求和响应。 +- `sqlor.crud.CRUD`: 提供数据库表的增删改查功能。 + +### 内部模块(来自项目) +- `appPublic.dictObject.multiDict2Dict`: 将多值字典转换为普通字典(通常用于处理表单或查询参数)。 +- `appPublic.jsonConfig.getConfig`: 加载配置文件。 +- `appPublic.log`: 日志工具,提供 `info`, `debug`, `warning`, `error`, `critical`, `exception` 等日志级别输出。 + +--- + +## 核心变量 + +```python +actions = ["browse", "add", "update", "filter"] +``` + +定义了系统支持的操作类型列表。任何不在该列表中的操作将返回 `404 Not Found` 错误。 + +> ⚠️ 注意:目前未实现 `delete` 操作。 + +--- + +## 类定义:`DBAdmin` + +### 类签名 + +```python +class DBAdmin: + def __init__(self, request: Request, dbname: str, tablename: str, action: str): + ... +``` + +#### 参数说明 + +| 参数名 | 类型 | 描述 | +|-------|------|------| +| `request` | `aiohttp.web_request.Request` | 当前 HTTP 请求对象 | +| `dbname` | `str` | 数据库名称 | +| `tablename` | `str` | 表名称 | +| `action` | `str` | 要执行的操作(必须是 `actions` 列表中的一项) | + +#### 初始化逻辑 + +1. 验证 `action` 是否在允许的操作列表中: + - 若不合法,记录调试日志并抛出 `HTTPNotFound`。 +2. 实例化 `CRUD(dbname, tablename)`: + - 成功则保存为 `self.crud` + - 失败则捕获异常,打印堆栈,记录错误日志,并抛出 `HTTPNotFound` + +> 💡 使用 `CRUD` 类来抽象底层数据库访问,提升可维护性。 + +--- + +### 方法:`render() -> Response` + +异步方法,根据初始化时指定的动作生成响应内容。 + +#### 返回值 + +- 返回一个 `aiohttp.web_response.Response` 对象,通常是 JSON 格式响应。 + +#### 逻辑流程 + +```python +async def render(self) -> Response: + try: + d = await self.crud.I() # 获取元数据 + return json_response(Success(d)) + except Exception as e: + exception('except=%s' % e) + traceback.print_exc() + return json_response(Error(errno='metaerror', msg='get metadata error')) +``` + +#### 当前行为说明 + +⚠️ **注意:当前 `render()` 方法的行为是固定的 —— 只调用 `self.crud.I()` 获取表的元数据信息(metadata),无论 `action` 是什么!** + +这表明代码尚未完成不同操作的分支处理。 + +##### 正常情况 +- 调用 `CRUD.I()` 方法获取表结构或元数据。 +- 包装成 `Success(data)` 并以 JSON 形式返回。 + +##### 异常情况 +- 捕获任意异常,记录详细错误日志(含堆栈跟踪)。 +- 返回标准错误 JSON:`Error(errno='metaerror', msg='get metadata error')` + +--- + +## 错误处理机制 + +### 抛出的 HTTP 异常 +- `HTTPNotFound`: + - 动作无效 + - CRUD 初始化失败 +- `HTTPForbidden`, `HTTPMethodNotAllowed`, `HTTPExpectationFailed` 等导入但未使用(可能是预留扩展) + +### 自定义响应对象 +- `Success(data)`: 成功响应包装器 +- `Error(errno, msg)`: 错误响应包装器 + (来自 `.error` 模块,需确保已正确定义) + +--- + +## 示例请求流程 + +假设有一个路由匹配如下 URL: + +``` +/dbadmin/{dbname}/{tablename}/{action} +``` + +当收到请求 `/dbadmin/mydb/users/browse` 时: + +1. 解析路径参数: + ```python + dbname = "mydb" + tablename = "users" + action = "browse" + ``` +2. 创建 `DBAdmin(request, "mydb", "users", "browse")` +3. 初始化成功后调用 `await dbadmin.render()` +4. 返回类似: + ```json + { + "success": true, + "data": { ... } // 元数据信息 + } + ``` + +如果 `action="invalid"`,则直接抛出 `HTTPNotFound`。 + +--- + +## 已知问题与待改进点 + +| 问题 | 描述 | 建议 | +|------|------|------| +| ❌ 动作未分发 | 所有动作都执行 `I()` 方法,未真正实现 `add`, `update`, `browse`, `filter` | 应使用条件判断或策略模式分发不同逻辑 | +| ⚠️ 错误码单一 | 所有元数据错误均返回 `metaerror` | 应细化错误类型(如连接失败、权限不足等) | +| 🔒 安全性缺失 | 无身份验证或权限控制 | 建议增加认证中间件或检查逻辑 | +| 📦 日志冗余 | 多处使用 `traceback.print_exc()`,同时有 `exception()` 记录 | 推荐仅使用 `exception()` 即可 | +| 🧩 不完整实现 | `filter`, `add`, `update` 无实际逻辑 | 需补充对应 `CRUD` 方法调用 | + +--- + +## 改进建议示例(伪代码) + +```python +async def render(self): + try: + if self.action == "browse": + data = await self.crud.read() + elif self.action == "add": + data = await self.crud.create(await self.request.json()) + elif self.action == "update": + data = await self.crud.update(await self.request.json()) + elif self.action == "filter": + query = multiDict2Dict(self.request.query) + data = await self.crud.filter(query) + else: + raise HTTPNotFound() + + return json_response(Success(data)) + + except HTTPException: + raise + except Exception as e: + exception(f"DBAdmin render error: {e}") + return json_response(Error(errno="server_error", msg="Internal server error")) +``` + +--- + +## 总结 + +`DBAdmin` 是一个初步搭建的数据库管理处理器框架,具备良好的模块化结构和异常处理能力。当前主要问题是**功能未完整实现**,所有操作均退化为获取元数据。 + +✅ 优势: +- 清晰的职责划分 +- 统一错误响应格式 +- 支持异步非阻塞 I/O + +🔧 待完善: +- 实现各操作的具体逻辑 +- 添加输入校验与安全控制 +- 细化错误分类与日志管理 + +建议后续按 RESTful 风格设计接口,结合 `POST`, `GET`, `PUT`, `DELETE` 方法映射到对应 CRUD 操作。 \ No newline at end of file diff --git a/aidocs/dsProcessor.md b/aidocs/dsProcessor.md new file mode 100644 index 0000000..5dafb55 --- /dev/null +++ b/aidocs/dsProcessor.md @@ -0,0 +1,248 @@ +下面是为提供的 `DataSourceProcessor` 类编写的 **Markdown 格式技术文档**,适用于项目中的开发者文档或 API 说明。 + +--- + +# `DataSourceProcessor` 技术文档 + +## 概述 + +`DataSourceProcessor` 是一个异步数据源处理器类,继承自 `BaseProcessor`,用于处理前端请求中与数据源相关的操作(如获取数据、分页、字段描述等)。它通过配置文件定义的数据结构,结合请求上下文动态生成响应内容,主要用于支持前端组件(如 DataGrid)的数据渲染和交互。 + +该类主要应用于后端服务中对 `.json` 配置文件的解析,并根据不同的 `action` 参数执行对应的数据处理逻辑。 + +--- + +## 模块依赖 + +```python +import codecs +import json +import aiofiles +from appPublic.jsonConfig import getConfig +from appPublic.dictObject import DictObject +from .baseProcessor import BaseProcessor +from .serverenv import ServerEnv +``` + +### 依赖说明: + +| 模块 | 用途 | +|------|------| +| `aiofiles` | 异步读取文件内容,避免阻塞 I/O | +| `json` | 解析 JSON 格式的配置文件 | +| `getConfig` | 获取全局配置对象(如编码格式) | +| `DictObject` | 可选:用于将字典转换为对象访问形式 | +| `BaseProcessor` | 当前类的基类,提供通用处理接口 | +| `ServerEnv` | 提供服务器运行环境信息 | + +--- + +## 类定义 + +```python +class DataSourceProcessor(BaseProcessor): +``` + +- **继承关系**:`BaseProcessor` +- **目的**:实现针对数据源(`ds`)类型资源的专用处理逻辑。 + +--- + +## 类方法 + +### `isMe(name) -> bool` + +判断当前处理器是否适用于指定名称的资源。 + +#### 参数: +- `name` (`str`):资源类型标识符。 + +#### 返回值: +- `True` 当且仅当 `name == 'ds'`。 +- 否则返回 `False`。 + +#### 示例: +```python +if DataSourceProcessor.isMe("ds"): + processor = DataSourceProcessor(...) +``` + +> ✅ 此方法为类方法,用于工厂模式下的处理器匹配。 + +--- + +## 实例初始化 `__init__(filename, k)` + +构造函数,初始化处理器实例并设置可用操作集合。 + +#### 参数: +- `filename` (`str`):关联的配置文件路径。 +- `k` (`str`):命名空间或键值,可能用于上下文隔离。 + +#### 初始化内容: +- 调用父类构造函数。 +- 定义内置操作映射表 `self.actions`。 +- 创建 `ServerEnv` 实例以获取服务器环境。 + +#### 支持的操作(`self.actions`): + +| 动作名 | 对应方法 | 说明 | +|----------------|--------------------------|------| +| `getdata` | `getData()` | 获取原始数据(待实现) | +| `pagingdata` | `getPagingData()` | 获取分页数据(待实现) | +| `arguments` | `getArgumentsDesc()` | 获取参数描述(待实现) | +| `resultFields` | `getDataDesc()` | 获取结果字段结构(待实现) | +| `gridlist` | `getGridlist()` | 构造 DataGrid 所需结构(已实现) | + +> ⚠️ 注意:前四个方法目前为空实现(`pass`),需子类或后续扩展实现具体逻辑。 + +--- + +## 核心方法 + +### `async getGridlist(dict_data, ns, request) -> dict` + +生成前端 DataGrid 组件所需的配置结构,基于字段元数据自动组织列显示、冻结列、隐藏列等属性。 + +#### 参数: +- `dict_data` (`dict`):从配置文件加载的原始数据。 +- `ns` (`dict`):命名空间参数,包含动作及其他上下文信息。 +- `request` (`Request`):ASGI 请求对象,用于生成 URL。 + +#### 处理逻辑: +1. 调用 `getDataDesc()` 获取字段列表。 +2. 分离出 `frozen=True` 的冻结列与非冻结列。 +3. 根据 `listhide=True` 标记自动设置 `hide=True`。 +4. 构建 DataGrid 配置对象: + - 设置分页大小为 50,禁用默认分页(由 buffer view 控制) + - 使用 `bufferview` 视图提升大数据量性能 + - 添加查询 URL(指向 `?action=pagingdata`) + +#### 返回值(示例): +```json +{ + "__ctmpl__": "datagrid", + "data": { + "iconCls": "icon-search", + "url": "/api/ds/data.json?action=pagingdata", + "view": "bufferview", + "options": { + "pageSize": 50, + "pagination": false + }, + "fields": [...], // 非冻结字段 + "ffields": [...] // 冻结字段(可选) + } +} +``` + +> ✅ 已实现,可用于 EasyUI 或类似表格框架集成。 + +--- + +### `async path_call(request, path, params={}) -> Any` + +异步读取指定路径的 JSON 配置文件,并根据请求参数调用对应的动作处理器。 + +#### 参数: +- `request` (`Request`):当前 HTTP 请求对象。 +- `path` (`str`):JSON 配置文件路径。 +- `params` (`dict`, optional):附加参数(未使用)。 + +#### 流程: +1. 读取文件内容(使用配置的编码,默认 UTF-8)。 +2. 解析为 `dict_data`。 +3. 获取运行时命名空间 `run_ns`。 +4. 提取 `action` 参数(默认为 `'getdata'`)。 +5. 查找并调用注册在 `self.actions` 中的处理函数。 + +#### 返回值: +- 调用对应 action 方法后的返回结果(通常是 `dict` 或 `list`)。 + +#### 异常处理建议(当前未实现): +- 文件不存在 +- JSON 解析错误 +- 不支持的 action + +> 💡 建议后续增加异常捕获机制以提高健壮性。 + +--- + +### `async datahandle(request)` + +统一入口方法,准备内容输出。**当前存在语法错误!** + +#### 当前代码问题: +```python +self.content = await path_call(request, self.path) +``` +❌ 错误原因:`path_call` 是实例方法,应通过 `self.path_call` 调用。 + +#### 修正版本: +```python +async def datahandle(self, request): + self.content = await self.path_call(request, self.path) +``` + +#### 说明: +- 将请求交由 `path_call` 处理,并将结果保存到 `self.content`。 +- 通常用于后续响应序列化输出。 + +--- + +## 使用场景示例 + +假设有一个 `/static/ds/user_list.json` 文件,内容如下: + +```json +{ + "fields": [ + {"name": "id", "title": "ID", "frozen": true}, + {"name": "name", "title": "姓名", "listhide": false}, + {"name": "email", "title": "邮箱", "listhide": true} + ] +} +``` + +当访问: + +``` +GET /ds/user_list.json?action=gridlist +``` + +响应将自动生成兼容 DataGrid 的结构,其中: +- `id` 和 `name` 显示在主列区; +- `id` 在冻结列(`ffields`); +- `email` 因 `listhide=true` 被标记为 `hide=true`,不显示在列表中。 + +--- + +## 设计特点 + +| 特性 | 描述 | +|------|------| +| 🔧 可扩展性 | 新增动作只需添加到 `self.actions` 并实现方法 | +| 📦 配置驱动 | 数据结构完全由外部 `.json` 文件控制 | +| ⚡ 异步支持 | 使用 `aiofiles` 实现非阻塞文件读取 | +| 🖥️ 前端友好 | 输出结构适配常见 UI 框架(如 jQuery EasyUI) | + +--- + +## 待改进事项(TODO) + +1. ✅ **修复 `datahandle` 中的调用错误** → 应使用 `self.path_call` +2. 🛠️ 实现 `getData`, `getPagingData`, `getArgumentsDesc`, `getDataDesc` 具体逻辑 +3. 🧯 增加异常处理(文件不存在、JSON 格式错误等) +4. 📐 支持更多前端控件模板(treegrid, combotree 等) +5. 🔐 添加权限校验钩子(可选) + +--- + +## 总结 + +`DataSourceProcessor` 是一个轻量级、可扩展的数据源处理器,专为前后端分离架构设计,能够高效地将静态 JSON 配置转化为动态 API 响应,特别适合元数据驱动的管理系统开发。 + +--- + +📌 *文档版本:1.0* +📅 *最后更新:2025年4月5日* \ No newline at end of file diff --git a/aidocs/error.md b/aidocs/error.md new file mode 100644 index 0000000..681fc44 --- /dev/null +++ b/aidocs/error.md @@ -0,0 +1,197 @@ +# API 响应工具函数技术文档 + +本文档描述了一组用于生成标准化 API 响应的 Python 函数。这些函数旨在统一后端接口返回的数据格式,提升前后端交互的一致性和可维护性。 + +--- + +## 目录 + +- [简介](#简介) +- [函数说明](#函数说明) + - [`Error(errno, msg)`](#errorerrno-msg) + - [`Success(data)`](#successdata) + - [`NeedLogin(path)`](#needloginpath) + - [`NoPermission(path)`](#nopermissionpath) +- [响应结构规范](#响应结构规范) +- [使用示例](#使用示例) + +--- + +## 简介 + +该模块提供四个辅助函数,用于快速构造具有统一结构的 JSON 响应对象。适用于 Web API 开发中常见的成功、错误、登录验证和权限控制场景。 + +所有函数返回标准字典结构,可直接序列化为 JSON 并返回给客户端。 + +--- + +## 函数说明 + +### `Error(errno, msg)` + +生成表示错误响应的标准结构。 + +#### 参数 +| 参数名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| `errno` | `str` | `'undefined error'` | 错误代码或标识符,用于区分不同类型的错误 | +| `msg` | `str` | `'Error'` | 用户可读的错误消息 | + +#### 返回值 +```json +{ + "status": "Error", + "data": { + "message": "错误信息", + "errno": "错误编号" + } +} +``` + +#### 示例 +```python +Error("404", "资源未找到") +# 返回: +# { +# "status": "Error", +# "data": { +# "message": "资源未找到", +# "errno": "404" +# } +# } +``` + +--- + +### `Success(data)` + +生成表示操作成功的响应。 + +#### 参数 +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `data` | `any` | 实际返回的数据内容(可以是字典、列表、字符串等) | + +#### 返回值 +```json +{ + "status": "OK", + "data": "传入的数据" +} +``` + +> **注意**:`data` 字段的内容由调用者决定,保持灵活性。 + +#### 示例 +```python +Success({"id": 1, "name": "张三"}) +# 返回: +# { +# "status": "OK", +# "data": { +# "id": 1, +# "name": "张三" +# } +# } +``` + +--- + +### `NeedLogin(path)` + +指示客户端当前请求需要用户登录,并可指定跳转路径。 + +#### 参数 +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `path` | `str` | 登录成功后建议跳转的路径(如 `/login`) | + +#### 返回值 +```json +{ + "status": "need_login", + "data": "/login" +} +``` + +#### 示例 +```python +NeedLogin("/login") +# 返回: +# { +# "status": "need_login", +# "data": "/login" +# } +``` + +前端可根据此状态重定向至登录页。 + +--- + +### `NoPermission(path)` + +表示用户没有访问该资源的权限,可附带引导路径。 + +#### 参数 +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `path` | `str` | 权限不足时建议跳转的页面路径(如 `/forbidden` 或 `/home`) | + +#### 返回值 +```json +{ + "status": "no_permission", + "data": "/forbidden" +} +``` + +#### 示例 +```python +NoPermission("/home") +# 返回: +# { +# "status": "no_permission", +# "data": "/home" +# } +``` + +可用于前端展示权限提示或跳转到安全页面。 + +--- + +## 响应结构规范 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `status` | `string` | 响应状态码,取值包括:
`"OK"`:成功
`"Error"`:通用错误
`"need_login"`:需要登录
`"no_permission"`:无权限 | +| `data` | `any` | 具体数据内容,结构根据 `status` 而变化 | + +推荐前端通过判断 `status` 字段进行相应处理。 + +--- + +## 使用示例 + +```python +# 成功返回用户信息 +return Success({ + "user_id": 123, + "username": "alice", + "email": "alice@example.com" +}) + +# 参数校验失败 +return Error("VALIDATION_FAILED", "邮箱格式不正确") + +# 未登录访问受保护接口 +return NeedLogin("/login") + +# 用户无权访问管理员功能 +return NoPermission("/dashboard") +``` + +--- + +## 总结 + +这组函数简化了 API 响应的构建过程,确保前后端通信格式统一、语义清晰。建议在项目中全局使用,避免手动拼接响应结构带来的不一致性。 \ No newline at end of file diff --git a/aidocs/filedownload.md b/aidocs/filedownload.md new file mode 100644 index 0000000..28d550a --- /dev/null +++ b/aidocs/filedownload.md @@ -0,0 +1,215 @@ +# 文件服务模块技术文档 + +## 概述 + +本模块提供基于 `aiohttp` 的异步文件上传、下载与安全路径处理功能,支持通过加密路径访问存储文件,并集成 RC4 加密算法对文件路径进行编码/解码。主要用于实现安全的文件资源访问接口。 + +--- + +## 依赖说明 + +### 外部依赖 +- `aiohttp`: 异步 Web 服务器框架 +- `aiofiles`: 异步文件 I/O 操作 +- `os`, `asyncio`: Python 标准库,用于系统交互和异步控制 + +### 内部依赖 +- `appPublic.rc4.RC4`: RC4 流加密算法实现 +- `appPublic.registerfunction.RegisterFunction`: 函数注册机制 +- `appPublic.log.debug`: 调试日志输出 +- `.filestorage.FileStorage`: 文件存储路径管理类 + +--- + +## 核心配置 + +```python +crypto_aim = 'God bless USA and others' +``` + +> **说明**:该字符串作为 RC4 加密的固定密钥(Key),用于路径的加解密。 +> ⚠️ 注意:硬编码密钥存在安全隐患,建议在生产环境中使用环境变量或配置中心管理。 + +--- + +## 工具函数 + +### `path_encode(path: str) -> str` +对输入路径进行 RC4 编码。 + +#### 参数 +| 参数名 | 类型 | 说明 | +|-------|------|------| +| `path` | `str` | 原始文件路径 | + +#### 返回值 +- `str`: 经 RC4 加密后的 Base64 编码字符串(具体格式取决于 `RC4.encode` 实现) + +#### 示例 +```python +encoded = path_encode("/uploads/photo.jpg") +# 输出类似: "aGVsbG8gd29ybGQ=" +``` + +--- + +### `path_decode(dpath: str) -> str` +对加密路径进行 RC4 解码。 + +#### 参数 +| 参数名 | 类型 | 说明 | +|-------|------|------| +| `dpath` | `str` | 加密后的路径字符串 | + +#### 返回值 +- `str`: 解密后的真实文件路径 + +#### 示例 +```python +decoded = path_decode("aGVsbG8gd29ybGQ=") +# 输出: "/uploads/photo.jpg" +``` + +> ✅ 提示:此函数应与 `path_encode` 配合使用,确保加解密一致性。 + +--- + +## 请求处理函数 + +### `file_upload(request: web.Request) -> None` +占位函数,预留文件上传逻辑。 + +> 🔜 当前为空实现(`pass`),需后续扩展支持 multipart 表单上传、权限校验、存储路径生成等功能。 + +--- + +### `file_handle(request: web.Request, filepath: str, download: bool = False) -> web.Response` +通用文件响应处理器,支持在线预览或强制下载。 + +#### 参数 +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `request` | `web.Request` | aiohttp 请求对象 | +| `filepath` | `str` | 系统真实文件路径 | +| `download` | `bool` | 是否以附件形式下载 | + +#### 行为说明 +- 若 `download=True`,设置响应头 `Content-Disposition: attachment; filename="xxx"` +- 使用 `web.FileResponse` 分块传输(chunk_size=8192字节) +- 自动启用 Gzip 压缩(`.enable_compression()`) + +#### 返回值 +- `web.FileResponse`: 包含文件流的 HTTP 响应对象 + +#### 日志输出 +调试信息示例: +``` +DEBUG: filepath='/data/file.txt', filename='file.txt', download=True +``` + +--- + +### `file_download(request: web.Request, filepath: str) -> web.Response` +快捷下载入口,调用 `file_handle` 并强制开启下载模式。 + +#### 参数 +同 `file_handle` + +#### 等价于 +```python +await file_handle(request, filepath, download=True) +``` + +--- + +### `path_download(request: web.Request, params_kw: dict, *params, **kw) -> web.Response` +根据加密路径参数下载文件,是外部暴露的主要接口之一。 + +#### 参数 +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `request` | `web.Request` | 请求对象 | +| `params_kw` | `dict` | 路径参数字典,必须包含 `'path'` 字段 | +| `*params`, `**kw` | 可变参数 | 兼容性扩展参数 | + +#### 流程 +1. 从 `params_kw['path']` 获取加密路径 +2. 判断是否需要下载(`params_kw.get('download')`) +3. 初始化 `FileStorage()` 获取实际文件路径 +4. 调用 `file_handle` 返回响应 + +#### 示例请求数据 +```python +params_kw = { + 'path': 'encrypted_path_string', + 'download': True +} +``` + +#### 日志输出 +```log +DEBUG: path_download():download filename=/real/path/to/file.pdf +``` + +--- + +## 函数注册机制 + +使用 `RegisterFunction` 将处理函数绑定到命名路由: + +```python +rf = RegisterFunction() +rf.register('idfile', path_download) +rf.register('download', path_download) +``` + +#### 注册映射表 +| 名称 | 绑定函数 | 用途 | +|------|---------|------| +| `idfile` | `path_download` | 通过 ID 或加密路径访问文件 | +| `download` | `path_download` | 下载专用接口 | + +> 💡 可结合路由中间件动态解析如 `/api/file/idfile/` 这类 URL。 + +--- + +## 安全性说明 + +- **路径隐藏**:通过 `path_encode/decode` 隐藏真实文件结构 +- **加密强度**:RC4 已被视为弱加密算法,不推荐用于高敏感场景 +- **建议改进** + - 替换为 AES-GCM 或 ChaCha20-Poly1305 + - 添加时效性 Token 支持 + - 增加访问权限验证(如 JWT 鉴权) + +--- + +## 使用示例(伪代码) + +```python +# 生成加密链接 +raw_path = "/uploads/report.docx" +encrypted = path_encode(raw_path) +url = f"/api/file/download/?path={encrypted}&download=1" + +# 用户访问时自动解密并下载 +# --> 调用 path_download → 解密 → 查找真实路径 → 发送文件 +``` + +--- + +## 待完善事项(TODO) + +| 功能 | 状态 | 说明 | +|------|------|------| +| 文件上传支持 | ❌ 未实现 | `file_upload` 函数体为空 | +| 错误处理 | ⚠️ 不足 | 缺少 `HTTPNotFound` 以外的异常捕获 | +| 权限控制 | ❌ 无 | 所有可解密路径均可访问 | +| 密钥管理 | ⚠️ 静态硬编码 | 存在泄露风险 | +| 单元测试 | ❓ 未知 | 未提供测试用例 | + +--- + +## 总结 + +本模块实现了基于加密路径的安全文件访问机制,适用于需要隐藏真实文件路径的轻量级文件服务场景。具备良好的扩展性,但需加强安全性设计以适应生产环境需求。 \ No newline at end of file diff --git a/aidocs/filestorage.md b/aidocs/filestorage.md new file mode 100644 index 0000000..29a1ca2 --- /dev/null +++ b/aidocs/filestorage.md @@ -0,0 +1,391 @@ +# `fileUpload.py` 技术文档 + +> **文件上传与临时文件管理模块** + +--- + +## 概述 + +`fileUpload.py` 是一个基于异步 I/O 的文件存储与上传处理模块,主要用于: + +- 接收 Base64 编码的文件并保存为本地文件; +- 支持流式下载远程文件; +- 管理临时文件生命周期(自动清理过期文件); +- 提供安全的文件路径映射与访问控制; +- 支持分片读取大文件以支持 HTTP 范围请求(Range Request),适用于视频/音频流媒体场景。 + +该模块广泛用于 Web 后端服务中处理用户上传、临时缓存和资源代理等场景。 + +--- + +## 依赖说明 + +### 第三方库 +```python +import asyncio +import os +import time +import tempfile +import aiofiles +import json +import base64 +``` + +### 自定义模块(来自 `appPublic` 包) +| 模块 | 功能 | +|------|------| +| `folderUtils._mkdir` | 递归创建目录 | +| `base64_to_file.base64_to_file`, `getFilenameFromBase64` | 将 Base64 字符串转为文件,并提取原始文件名 | +| `jsonConfig.getConfig` | 获取全局配置对象 | +| `Singleton.SingletonDecorator` | 单例装饰器 | +| `log.info`, `debug`, `warning`, `exception`, `critical` | 日志输出工具 | +| `streamhttpclient.StreamHttpClient` | 异步流式 HTTP 客户端 | + +--- + +## 核心类与功能 + +--- + +### 1. `TmpFileRecord` 类:临时文件记录器(单例) + +#### 说明 +使用单例模式管理所有临时文件的创建时间,定期检查并删除超时文件。通过 JSON 文件持久化记录状态。 + +#### 装饰器 +```python +@SingletonDecorator +class TmpFileRecord: +``` +确保整个应用中仅存在一个实例。 + +#### 初始化参数 +| 参数 | 默认值 | 说明 | +|------|--------|------| +| `timeout` | `3600` 秒(1小时) | 文件最大存活时间 | +| `time_period` | `10` 秒 | 清理任务执行周期 | + +#### 属性 +| 属性 | 类型 | 描述 | +|------|------|------| +| `filetime` | `dict[str, float]` | 文件路径 → 创建时间戳(`time.time()`) | +| `changed_flg` | `bool` | 是否有未保存的状态变更 | +| `filename` | `str` | 存储记录的 JSON 文件路径,格式:`{config.filesroot}/tmpfile_rec_{pid}.json` | +| `loop` | `asyncio.EventLoop` | 当前事件循环引用 | + +#### 方法 + +##### `__init__(self, timeout=3600)` +初始化记录器,并在事件循环中调度加载与定时清理任务。 + +##### `savefilename(self)` +生成用于保存临时文件记录的 JSON 文件路径: +```python +root = config.filesroot or tempfile.gettempdir() +return f"{root}/tmpfile_rec_{os.getpid()}.json" +``` + +##### `newtmpfile(path: str)` +注册一个新的临时文件路径及其当前时间。 +- 设置 `changed_flg = True` 触发后续持久化。 + +##### `async save()` +将内存中的 `filetime` 字典异步写入 JSON 文件。 +- 若无更改则跳过。 +- 使用 `aiofiles` 异步写入 UTF-8 编码内容。 + +##### `async load()` +从磁盘加载已有的文件记录。 +- 若文件不存在则忽略。 +- 加载后立即调用 `remove()` 执行一次清理。 + +##### `file_useful(self, fpath)` +标记某个文件仍在使用(即被访问),将其从待清理列表中移除。 +- 使用 `try-except` 避免键不存在时报错。 + +##### `async remove()` +遍历所有记录的文件,删除超过 `timeout` 时间的文件。 +- 调用 `rmfile(k)` 删除物理文件; +- 从 `filetime` 中删除条目; +- 异步保存更新后的记录; +- 自动递归调度下一次清理(每 `time_period` 秒执行一次)。 + +> ⚠️ 注意:此方法由 `loop.call_later()` 循环调用,构成后台守护任务。 + +##### `rmfile(name: str)` +根据配置根目录拼接完整路径后删除文件。 +```python +os.remove(config.fileroot + name) +``` +> ❗ 存在潜在错误:应为 `config.filesroot` 而非 `config.fileroot`(代码拼写错误) + +--- + +### 2. `FileStorage` 类:文件存储管理器 + +提供统一接口进行文件的保存、读取、删除及路径转换。 + +#### 初始化 +```python +def __init__(self): + config = getConfig() + self.root = os.path.abspath(config.filesroot or tempfile.gettempdir()) + self.tfr = TmpFileRecord() # 共享单例 +``` + +#### 属性 +| 属性 | 类型 | 描述 | +|------|------|------| +| `root` | `str` | 文件系统根目录(绝对路径) | +| `tfr` | `TmpFileRecord` | 临时文件记录器单例 | + +--- + +#### 核心方法 + +##### `realPath(path)` +将相对或绝对 Web 路径转换为安全的本地文件系统路径。 +- 若路径以 `/` 开头,则去除; +- 使用 `os.path.join(self.root, ...)` 拼接; +- 返回规范化后的绝对路径。 + +> ✅ 安全性:防止路径穿越攻击(如 `../../etc/passwd`) + +##### `webpath(path)` +将本地文件路径转换为 Web 可访问路径(相对于 `self.root`)。 +- 成功时返回去根前缀的子路径; +- 失败时返回 `None`。 + +##### `_name2path(name, userid=None)` +根据文件名生成唯一且分布均匀的存储路径,避免单目录下文件过多。 + +###### 算法逻辑 +1. 取当前微秒级时间戳:`int(time.time() * 1000000)` +2. 对四个质数 `[191, 193, 197, 97]` 取模,生成四级子目录; +3. 若指定 `userid`,则加入用户隔离路径 `/userid/...` +4. 最终结构示例: + ``` + /filesroot/tmp/123/45/67/89/avatar.png + ``` + +> ✅ 优点:高并发下分散 IO 压力;天然防重名。 + +##### `save_base64_file(b64str)` +将 Base64 编码字符串保存为文件。 +- 自动提取文件名(`getFilenameFromBase64`); +- 使用 `_name2path` 生成路径; +- 调用 `base64_to_file` 写入; +- 返回文件系统路径。 + +##### `remove(path)` +删除指定路径的文件。 +- 支持带 `/` 前缀的路径; +- 捕获异常并记录日志; +- 实际路径为 `os.path.join(self.root, path.lstrip('/'))` + +##### `async streaming_read(request, webpath, buf_size=8096)` +支持 HTTP Range 请求的大文件流式读取,适用于视频播放等场景。 + +###### 参数 +| 参数 | 类型 | 说明 | +|------|------|------| +| `request` | `HTTPRequest` | 包含 headers 的请求对象 | +| `webpath` | `str` | Web 上下文路径(如 `/uploads/test.mp4`) | +| `buf_size` | `int` | 每次读取缓冲大小,默认 8KB | + +###### 功能 +- 解析 `Range: bytes=0-1023` 请求头; +- 计算起始和结束位置; +- 使用 `aiofiles.open(..., 'rb')` 异步打开文件; +- 支持 `seek()` 跳转; +- 分块 `yield` 数据,可用于 ASGI 响应体; + +###### 日志输出 +```python +debug(f'filesize={stats.st_size}, startpos=..., endpos=...') +``` + +##### `async save(name, read_data, userid=None)` +通用文件保存方法,支持多种输入类型。 + +###### 参数 +| 参数 | 类型 | 说明 | +|------|------|------| +| `name` | `str` | 原始文件名 | +| `read_data` | `str`, `bytes`, 或 `callable async () -> bytes` | 数据源 | +| `userid` | `str` | 用户 ID(用于路径隔离) | + +###### 行为 +1. 生成唯一路径 `_name2path(...)` +2. 创建父目录(`_mkdir`) +3. 判断数据类型: + - 若为 `str` 或 `bytes`:直接写入; + - 否则视为异步生成器函数,循环读取直到返回空; +4. 保存成功后调用 `tfr.newtmpfile(fpath)` 注册为临时文件; +5. 返回 Web 路径(相对于 root) + +> ✅ 支持同步数据和异步流两种模式。 + +--- + +## 工具函数 + +--- + +### `file_realpath(path)` +获取给定路径对应的实际文件系统路径。 + +```python +fs = FileStorage() +return fs.realPath(path) +``` + +> 主要用于外部调用快速解析路径。 + +--- + +### `async downloadfile(url, headers=None, params=None, data={})` +从指定 URL 异步下载文件并保存到本地。 + +#### 流程 +1. 提取 URL 文件名; +2. 使用 `FileStorage._name2path(..., userid='tmp')` 生成临时路径; +3. 使用 `StreamHttpClient` 发起 GET 请求; +4. 流式接收 chunk 并写入文件; +5. 返回保存后的本地路径; +6. 出错时记录异常并重新抛出。 + +> ✅ 支持自定义 headers、query 参数、POST 数据。 + +--- + +### `async base642file(b64str)` +将 Base64 字符串解码并保存为二进制文件。 + +#### 特性 +- 自动剥离 Data URL 头部(如 `data:image/png;base64,...`); +- 使用 `base64.b64decode` 解码; +- 保存至 `userid='tmp'` 目录; +- 返回文件路径。 + +--- + +## 配置要求 + +需在全局配置中定义以下字段(通过 `getConfig()` 获取): + +| 配置项 | 类型 | 是否必需 | 说明 | +|--------|------|----------|------| +| `filesroot` | `str` | 否 | 文件存储根目录;若未设置则使用系统临时目录 | +| `fileroot` | `str` | 否(但注意 bug) | ❗ 在 `rmfile()` 中被误用,建议统一为 `filesroot` | + +--- + +## 使用示例 + +### 示例 1:保存 Base64 图片 +```python +b64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhE..." +fpath = await base642file(b64) +print(f"Saved to: {fpath}") +``` + +### 示例 2:流式下载远程文件 +```python +url = "https://example.com/file.zip" +try: + local_path = await downloadfile(url) + print("Download complete:", local_path) +except Exception as e: + print("Failed:", e) +``` + +### 示例 3:处理上传流(FastAPI 示例) +```python +@app.post("/upload") +async def upload_file(file: UploadFile): + fs = FileStorage() + def reader(): + return file.read(8192) + web_path = await fs.save(file.filename, reader) + return {"web_path": web_path} +``` + +### 示例 4:流式响应视频(Starlette/FastAPI) +```python +@app.get("/video/{path}") +async def stream_video(request, path: str): + fs = FileStorage() + return StreamingResponse( + fs.streaming_read(request, f"/{path}"), + media_type="video/mp4" + ) +``` + +--- + +## 注意事项与改进建议 + +### 🔴 已知问题 +1. **变量名拼写错误**: + ```python + del self.tiletime[k] # 应为 self.filetime + ``` + ➤ 导致 KeyError,无法正确清理过期文件。 + +2. **属性名不一致**: + ```python + self.change_flg = True # 正确 + self.changed_flg = False # 初始化时使用了不同名称! + ``` + ➤ 应统一为 `changed_flg`。 + +3. **`rmfile()` 中配置项错误**: + ```python + os.remove(config.fileroot + name) # 应为 filesroot + ``` + +4. **循环语法错误**: + ```python + for k,v in ft: # ft 是 dict,不能直接迭代 tuple + ``` + ➤ 应改为:`for k, v in ft.items():` + +5. **`webpath()` 缺少返回值**: + ```python + if path.startswith(self.root): + return path[len(self.root):] + # else 缺失 return + ``` + ➤ 应补上 `return None` 或默认路径。 + +--- + +### ✅ 改进建议 +| 项目 | 建议 | +|------|------| +| 错误修复 | 修正上述拼写与逻辑错误 | +| 单元测试 | 添加对路径生成、清理机制、流读写的测试 | +| 配置校验 | 初始化时验证 `filesroot` 是否可写 | +| 更灵活的 TTL | 支持按文件类型设置不同过期时间 | +| 监控指标 | 增加文件数量、总大小、清理统计等 | + +--- + +## 总结 + +`fileUpload.py` 是一个功能完整、设计合理的异步文件处理模块,具备以下优势: + +✅ 异步高性能 +✅ 支持流式操作 +✅ 自动清理临时文件 +✅ 安全路径处理 +✅ 易于集成 + +只要修复文中指出的几处关键 Bug,即可稳定运用于生产环境。 + +--- + +> 📝 文档版本:v1.0 +> 💬 维护者:开发者团队 +> 📅 更新日期:2025年4月5日 \ No newline at end of file diff --git a/aidocs/filetest.md b/aidocs/filetest.md new file mode 100644 index 0000000..3bb009f --- /dev/null +++ b/aidocs/filetest.md @@ -0,0 +1,148 @@ +# `current_fileno()` 函数技术文档 + +## 概述 + +`current_fileno()` 是一个 Python 函数,用于获取当前可用的文件描述符编号。它通过创建并打开一个临时文件,调用其 `fileno()` 方法获取底层文件描述符号,然后立即关闭并删除该文件,最后返回该文件描述符编号。 + +此函数可用于演示或测试操作系统如何分配文件描述符,尤其是在连续调用时观察文件描述符的变化趋势。 + +--- + +## 代码结构 + +```python +import os + +def current_fileno(): + fn = './t.txt' + f = open(fn, 'w') + ret = f.fileno() + f.close() + os.remove(fn) + return ret + +if __name__ == '__main__': + for i in range(1000): + print(current_fileno()) +``` + +--- + +## 函数说明 + +### `current_fileno()` + +#### 功能 +创建一个临时文本文件,获取其对应的文件描述符(file descriptor),然后清理资源并返回该描述符编号。 + +#### 返回值 +- **类型**:`int` +- **含义**:操作系统为新打开文件分配的文件描述符编号。 + +#### 实现细节 +1. 定义临时文件路径为 `./t.txt`。 +2. 以写入模式 (`'w'`) 打开该文件。 +3. 调用 `f.fileno()` 获取底层整数形式的文件描述符。 +4. 关闭文件对象。 +5. 使用 `os.remove(fn)` 删除磁盘上的临时文件。 +6. 返回获取到的文件描述符编号。 + +> ⚠️ 注意:虽然文件被删除,但 `fileno()` 是在文件关闭前获取的,因此是有效的。 + +--- + +## 主程序逻辑 + +当脚本作为主程序运行时,会执行以下操作: + +```python +for i in range(1000): + print(current_fileno()) +``` + +- 循环调用 `current_fileno()` 共 1000 次。 +- 每次调用打印返回的文件描述符编号。 + +### 预期输出示例(部分) +``` +3 +4 +5 +... +``` + +> 在大多数 Unix/Linux 系统中,进程启动时: +> - 文件描述符 0: stdin +> - 1: stdout +> - 2: stderr +> 因此新打开的文件通常从 `3` 开始递增。 + +--- + +## 使用场景 + +- 学习和理解文件描述符的分配机制。 +- 调试 I/O 资源管理问题。 +- 测试系统对文件描述符的重用策略(尤其是在关闭后是否重复使用)。 + +--- + +## 注意事项与限制 + +| 项目 | 说明 | +|------|------| +| **线程安全性** | ❌ 不安全。多个线程同时调用可能造成文件冲突(如 `t.txt` 被覆盖或删除异常)。 | +| **并发风险** | 多个进程/线程使用相同文件名可能导致竞争条件。 | +| **临时文件路径** | 使用固定路径 `./t.txt`,可能因权限或已存在文件导致异常。 | +| **错误处理** | 当前实现未捕获异常(如无法创建/删除文件),建议增强健壮性。 | +| **性能影响** | 每次调用涉及磁盘 I/O(创建、写入、删除),效率较低。 | + +--- + +## 改进建议 + +### ✅ 推荐优化版本 + +```python +import tempfile +import os + +def current_fileno(): + # 使用内存中的临时文件,避免磁盘 I/O 和命名冲突 + with tempfile.NamedTemporaryFile() as f: + return f.fileno() # 自动关闭和删除 +``` + +**优点**: +- 线程/进程安全(由系统生成唯一文件名)。 +- 无需手动清理。 +- 更高效且可移植。 + +--- + +## 依赖项 + +- Python 标准库: + - `os`: 用于文件删除。 + - `tempfile`(推荐扩展使用):更安全地处理临时文件。 + +--- + +## 平台兼容性 + +✅ 支持平台: +- Linux +- macOS +- Windows(需注意路径分隔符和权限) + +⚠️ 行为差异: +- 文件描述符编号分配策略可能略有不同。 +- Windows 上文件删除时若句柄未正确释放可能报错(原代码已 `close()`,故安全)。 + +--- + +## 总结 + +`current_fileno()` 是一个简单但具有教学意义的函数,展示了如何访问底层文件描述符,并反映了操作系统资源分配的基本原理。尽管其实现存在可改进之处,但在学习和调试场景下仍具实用价值。 + +建议在生产环境中使用 `tempfile` 模块替代手动文件管理,以提高安全性与稳定性。 \ No newline at end of file diff --git a/aidocs/functionProcessor.md b/aidocs/functionProcessor.md new file mode 100644 index 0000000..532c6c5 --- /dev/null +++ b/aidocs/functionProcessor.md @@ -0,0 +1,294 @@ +以下是为提供的 `FunctionProcessor` 类编写的 **Markdown 格式技术文档**,适用于项目开发文档或 API 文档场景。 + +--- + +# `FunctionProcessor` 技术文档 + +## 概述 + +`FunctionProcessor` 是一个基于 `aiohttp` 的异步请求处理器类,继承自 `BaseProcessor`。它用于将 HTTP 请求动态映射到通过 `RegisterFunction` 注册的函数上,并支持路径参数解析和运行时环境注入。该处理器特别适用于实现基于注册函数的路由分发机制。 + +主要功能包括: +- 解析 URL 路径中的参数 +- 调用已注册的同步或异步函数 +- 支持运行时命名空间(`run_ns`)注入上下文变量 +- 返回 `Response`、`FileResponse` 或普通内容数据 + +--- + +## 导入依赖 + +```python +import inspect +from appPublic.dictObject import DictObject +from appPublic.registerfunction import RegisterFunction +from appPublic.log import info, debug, warning, error, exception, critical +from aiohttp import web +from aiohttp.web_response import Response, StreamResponse +from .baseProcessor import BaseProcessor +``` + +### 依赖说明 + +| 模块 | 用途 | +|------|------| +| `inspect` | 判断目标函数是否为协程函数 | +| `DictObject` | 将字典转换为属性可访问的对象 | +| `RegisterFunction` | 获取全局注册的函数 | +| `appPublic.log` | 日志输出 | +| `aiohttp.web` | Web 服务核心组件 | +| `BaseProcessor` | 基础处理器基类 | + +--- + +## 类定义 + +```python +class FunctionProcessor(BaseProcessor): +``` + +继承自 `BaseProcessor`,用于处理特定类型的 HTTP 请求并调用注册函数。 + +--- + +## 类方法 + +### `isMe(name)` + +```python +@classmethod +def isMe(cls, name): + return False +``` + +#### 说明 + +此方法用于判断当前处理器是否适用于给定名称的资源。在本实现中始终返回 `False`,表示此类不会自动匹配任何资源名 —— 其使用需显式配置。 + +> ⚠️ 注意:若需启用自动识别,请重写此方法以根据条件返回 `True`。 + +#### 参数 + +- `name` (`str`) - 资源名称 + +#### 返回值 + +- `bool`: 始终返回 `False` + +--- + +## 实例方法 + +### `__init__(path, resource, opts)` + +构造函数,初始化处理器实例。 + +```python +def __init__(self, path, resource, opts): + self.config_opts = opts + BaseProcessor.__init__(self, path, resource) +``` + +#### 参数 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `path` | `str` | 请求路径模板 | +| `resource` | `Any` | 关联资源对象(未具体使用) | +| `opts` | `dict` | 配置选项,必须包含以下键:
`leading`: 前缀路径,用于截断
`registerfunction`: 注册函数名 | + +#### 属性设置 + +- `self.config_opts`: 存储传入的配置项 +- 调用父类初始化逻辑 + +--- + +### `path_call(request, params={})` + +执行注册函数的核心方法,负责解析路径、获取函数并调用。 + +```python +async def path_call(self, request, params={}): +``` + +#### 流程说明 + +1. 设置运行时环境(通过 `set_run_env`) +2. 提取 `params_kw` 和实际请求路径 +3. 截去配置中指定的前缀(`leading`),拆分剩余路径作为位置参数 +4. 从 `RegisterFunction` 中获取目标函数 +5. 若函数未注册,记录错误日志并返回 `None` +6. 构造调用环境(排除敏感键) +7. 异步或同步调用函数并返回结果 + +#### 参数 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `request` | `aiohttp.web.Request` | 当前 HTTP 请求对象 | +| `params` | `dict` | 可选参数字典,可覆盖默认路径等信息 | + +#### 返回值 + +- 函数调用结果(可以是 `Response`, `str`, `dict`, 或其他类型) +- 如果函数未注册,返回 `None` + +#### 内部逻辑细节 + +##### 路径处理 + +```python +path1 = path[len(self.config_opts['leading']):] +if path1[0] == '/': path1 = path1[1:] +args = path1.split('/') +``` + +例如: +若 `request.path = "/api/v1/user/123"`,且 `leading="/api/v1"`,则 `args = ['user', '123']` + +##### 函数调用环境构建 + +```python +env = {k:v for k,v in self.run_ns.items() if k not in ['params_kw', 'request']} +``` + +过滤掉 `params_kw` 和 `request`,避免重复传参冲突。 + +##### 协程支持 + +```python +if inspect.iscoroutinefunction(f): + return await f(request, params_kw, *args, **env) +return f(request, params_kw, *args, **env) +``` + +自动识别函数类型,确保正确调用同步或异步函数。 + +##### 错误处理 + +```python +if f is None: + error(f'{rfname=} is not registered, {rf.registKW=}') + return None +``` + +若函数未注册,输出调试信息并返回 `None`。 + +--- + +### `datahandle(request)` + +处理请求并生成响应内容的方法。 + +```python +async def datahandle(self, request): + x = await self.path_call(request) + if isinstance(x, web.FileResponse): + self.retResponse = x + elif isinstance(x, Response): + self.retResponse = x + else: + self.content = x +``` + +#### 功能 + +调用 `path_call` 执行业务逻辑,并根据返回值类型决定如何封装响应: + +| 返回类型 | 处理方式 | +|---------|----------| +| `web.FileResponse` | 直接赋值给 `self.retResponse` | +| `Response`(含子类) | 赋值给 `self.retResponse` | +| 其他类型(如 `str`, `dict` 等) | 赋值给 `self.content`,由后续中间件或父类序列化 | + +#### 参数 + +- `request`: `aiohttp.web.Request` 对象 + +#### 影响属性 + +- `self.retResponse`: 成功时保存响应对象 +- `self.content`: 成功时保存原始内容数据 + +--- + +## 配置要求(`opts`) + +`FunctionProcessor` 必须接收如下配置项: + +| 键名 | 类型 | 说明 | +|------|------|------| +| `leading` | `str` | 需要从路径中去除的前缀,如 `/api/v1` | +| `registerfunction` | `str` | 在 `RegisterFunction` 中注册的函数名称 | + +示例配置: + +```python +opts = { + "leading": "/api/v1", + "registerfunction": "my_registered_handler" +} +``` + +--- + +## 使用示例 + +### 注册函数示例 + +```python +from appPublic.registerfunction import RegisterFunction + +def my_handler(request, params, *args, **env): + return web.json_response({"args": args, "env": env}) + +RegisterFunction().register("my_registered_handler", my_handler) +``` + +### 创建处理器并处理请求 + +```python +processor = FunctionProcessor("/api/v1/data/*", None, { + "leading": "/api/v1/data", + "registerfunction": "my_registered_handler" +}) + +# 在 aiohttp 路由中调用 +async def handle_request(request): + await processor.datahandle(request) + if processor.retResponse: + return processor.retResponse + return web.json_response(processor.content) +``` + +--- + +## 注意事项与限制 + +1. **函数注册必需**:目标函数必须提前通过 `RegisterFunction().register(name, func)` 注册。 +2. **函数签名要求**:被调用函数应接受 `(request, params_kw, *args, **kwargs)` 形式的参数。 +3. **路径前缀必须匹配**:`leading` 必须准确匹配请求路径开头,否则可能导致参数解析异常。 +4. **非标准返回值需手动处理**:若返回非 `Response` 类型,需确保上游能正确序列化 `self.content`。 + +--- + +## 日志输出 + +该类使用 `appPublic.log` 模块进行日志记录: + +- `error`: 函数未注册时输出详细错误信息 +- (其他日志级别暂未使用) + +建议开启调试日志以便追踪调用过程。 + +--- + +## 总结 + +`FunctionProcessor` 提供了一种灵活的函数驱动式 Web 接口设计模式,适合微服务、插件化系统或需要动态绑定逻辑的场景。结合 `RegisterFunction` 机制,实现了低耦合、高扩展性的请求处理架构。 + +--- + +> ✅ 文档版本:1.0 +> 📅 最后更新:2025-04-05 \ No newline at end of file diff --git a/aidocs/globalEnv.md b/aidocs/globalEnv.md new file mode 100644 index 0000000..7f11fa3 --- /dev/null +++ b/aidocs/globalEnv.md @@ -0,0 +1,458 @@ +# 技术文档:Web 应用核心工具模块 + +```markdown +# Web 核心工具模块技术文档 + +> **文件编码**:UTF-8 +> **语言**:Python 3.7+(基于 `aiohttp` 异步框架) +> **用途**:为异步 Web 服务提供通用工具函数、配置管理、安全处理、文件操作及数据库上下文支持。 + +--- + +## 模块概览 + +该模块是 Web 后端系统的核心基础组件,封装了以下功能: + +- HTTP 错误响应生成 +- 文件读写与下载控制 +- 数据导出为 Excel +- 配置读取与动态变量替换 +- 密码加解密 +- 全局环境初始化 +- 数据库连接池集成 +- 流式响应支持 +- 安全路径校验机制 + +适用于基于 `aiohttp` 构建的异步 Web 服务架构。 + +--- + +## 依赖说明 + +### 第三方库 + +| 包名 | 用途 | +|------|------| +| `aiohttp` | 异步 Web 服务框架 | +| `aiohttp-session` | Session 管理 | +| `openpyxl` | Excel 文件生成 | +| `asyncio` | 异步编程支持 | + +### 内部模块 + +| 模块路径 | 功能 | +|--------|------| +| `appPublic.*` | 公共工具类(配置、日志、编码、时间等) | +| `sqlor.*` | 数据库抽象层(ORM/查询构造器) | +| `.xlsxData` | Excel 数据解析器 | +| `.uriop` | URI 操作接口 | +| `.error` | 自定义错误类型封装 | +| `.filetest`, `.filedownload`, `.filestorage` | 文件相关操作 | +| `.serverenv` | 服务器运行环境单例 | + +--- + +## 函数与类详解 + +### `server_error(errcode)` +根据 HTTP 状态码抛出对应的 `aiohttp.web.HTTPException`。 + +#### 参数: +- `errcode` (int): HTTP 状态码(如 404, 500) + +#### 支持状态码列表: +| 状态码 | 异常类 | +|-------|--------| +| 400 | `HTTPBadRequest` | +| 401 | `HTTPUnauthorized` | +| 403 | `HTTPForbidden` | +| 404 | `HTTPNotFound` | +| 405 | `HTTPMethodNotAllowed` | +| 408 | `HTTPRequestTimeout` | +| 409 | `HTTPConflict` | +| 410 | `HTTPGone` | +| 415 | `HTTPUnsupportedMediaType` | +| 429 | `HTTPTooManyRequests` | +| 500 | `HTTPInternalServerError` | +| 502 | `HTTPBadGateway` | +| 503 | `HTTPServiceUnavailable` | + +> 默认返回 `HTTPException`,其他未列状态码也映射为此类。 + +--- + +### `basic_auth_headers(user, passwd)` +生成用于 Basic Auth 的请求头。 + +#### 参数: +- `user` (str): 用户名 +- `passwd` (str): 密码 + +#### 返回值: +```python +{ + "Authorization": "Basic base64encoded" +} +``` + +--- + +### `stream_response(request, async_data_generator, content_type='text/html')` +异步流式响应处理器,用于大内容或实时数据传输。 + +#### 参数: +- `request`: aiohttp 请求对象 +- `async_data_generator`: 异步生成器函数(`async def()`),产出 `bytes`, `str` 或 JSON 对象 +- `content_type` (str): 响应内容类型,默认 `'text/html'` + +#### 行为: +- 自动判断输出类型并编码为 UTF-8 +- 出错时记录异常并通过 `write_eof()` 结束流 +- 支持 JSON 直接序列化(非 ASCII 不转义) + +> ⚠️ 注意:若生成器内部报错会中断流并抛出异常。 + +--- + +### `data2xlsx(rows, headers=None)` +将数据行列表导出为临时 `.xlsx` 文件。 + +#### 参数: +- `rows`: 字典列表,例如 `[{'name': 'Alice', 'age': 30}]` +- `headers`: 列定义列表,每个元素可含 `.name` 和 `.title` 属性 + +#### 返回值: +- 生成的 `.xlsx` 文件绝对路径(使用 `tempfile.mktemp` 创建) + +> 使用 `openpyxl` 写入,自动关闭工作簿。建议后续由调用方清理临时文件。 + +--- + +### `save_file(str_or_bytes, filename)` +异步保存字符串或字节到文件存储系统。 + +#### 参数: +- `str_or_bytes`: 要保存的内容(`str` 或 `bytes`) +- `filename`: 文件名(带扩展名) + +#### 返回值: +- 存储结果信息(由 `FileStorage.save()` 定义) + +> 封装了异步 IO 操作,适合上传场景。 + +--- + +### `webpath(path)` / `realpath(path)` +获取文件在 Web 中的访问路径和实际物理路径。 + +#### 参数: +- `path` (str): 相对路径 + +#### 返回值: +- `webpath`: 可通过浏览器访问的 URL 路径 +- `realpath`: 服务器上的真实文件系统路径 + +> 基于 `FileStorage` 实现路径映射。 + +--- + +### `FileOutZone(fp)` +自定义异常类,防止越权访问文件系统目录。 + +#### 触发条件: +当尝试打开的文件不在允许目录范围内时抛出。 + +#### 属性: +- `openfilename`: 被拒绝访问的文件路径 + +--- + +### `get_config_value(kstr)` +从全局配置中按点分键获取嵌套值。 + +#### 示例: +```python +get_config_value("database.host") +# => getConfig().get('database').get('host') +``` + +#### 返回值: +- 成功找到则返回对应值 +- 找不到任一级键则返回 `None` + +--- + +### `get_definition(k)` +快捷方式获取配置中的 `definitions.{k}` 节点。 + +#### 示例: +```python +get_definition("userSchema") +# 等价于 get_config_value("definitions.userSchema") +``` + +--- + +### `abspath(path)` +根据配置中的网站根路径查找文件的实际路径。 + +#### 配置要求: +- `config.website.paths`: 字符串路径列表(相对或绝对) +- 尝试拼接每个根路径 + 输入 path,检查是否存在 + +#### 返回值: +- 找到存在的文件则返回其完整路径 +- 否则返回 `None` + +--- + +### `openfile(url, m)` +安全地打开一个本地文件,具备路径白名单校验。 + +#### 参数: +- `url` (str): 相对路径(相对于 `website.paths` 或 `allow_folders`) +- `m` (str): 文件打开模式(如 `'r'`, `'rb'`) + +#### 安全校验流程: +1. 解析为绝对路径(通过 `abspath`) +2. 获取所有允许的根目录(包括 `website.paths` 和 `allow_folders`) +3. 检查目标路径是否以任意允许目录开头 +4. 若不满足,抛出 `FileOutZone` + +> 防止路径穿越攻击(如 `../../../etc/passwd`) + +--- + +### `isNone(a)` +辅助函数,判断变量是否为 `None`。 + +#### 返回值: +- `True` if `a is None` +- `False` otherwise + +> 主要用于模板引擎或表达式中避免语法限制。 + +--- + +### `appname()` +获取当前应用名称。 + +#### 来源: +- `config.license.app` +- 失败时返回默认 `"test app"` + +--- + +### `configValue(ks)` +执行类似 `eval('config' + ks)` 的安全配置提取。 + +#### 示例: +```python +configValue(".database.port") +# => getConfig().database.port +``` + +> ⚠️ 警告:存在潜在代码注入风险,请确保输入可信! + +--- + +### `visualcoding()` +获取配置项 `config.website.visualcoding` 的值。 + +通常用于前端可视化开发开关。 + +--- + +### `file_download(request, path, name, coding='utf8')` +【已弃用】同步方式发送文件给客户端(兼容旧版 Twisted 风格 API) + +#### 参数: +- `request`: 请求对象(需有 `setHeader`, `write`, `finish` 方法) +- `path`: 文件相对路径 +- `name`: 下载显示名称 +- `coding`: 名称编码格式(默认 UTF-8) + +#### 设置响应头: +- `Content-Disposition: attachment; filename=...` +- 缓存控制、内容长度、二进制传输标识等 + +> ❌ 不推荐新代码使用,建议改用 `path_download` 或流式方案。 + +--- + +### `paramify(data, ns)` +使用模板语法 `${key}$` 替换数据中的占位符。 + +#### 参数: +- `data`: 包含占位符的字符串或结构化数据(dict/list) +- `ns`: 命名空间(dict),提供替换值 + +#### 示例: +```python +paramify("Hello ${name}$!", {"name": "World"}) +# => "Hello World!" +``` + +> 支持嵌套结构递归替换。 + +--- + +### `password_encode(s)` / `password_decode(c)` +RC4 加密/解密封装。 + +#### 密钥来源: +- `config.password_key` +- 默认密钥:`QRIVSRHrthhwyjy176556332` + +#### 用途: +- 敏感字段加密存储(如密码、token) +- 安全参数传递 + +--- + +### `@asynccontextmanager sqlorContext(module)` +异步上下文管理器,获取指定模块关联数据库的 `SqlOR` 实例。 + +#### 工作流程: +1. 获取 `DBPools` 连接池 +2. 查询 `ServerEnv` 中模块对应的数据库名 +3. 获取该库的 ORM 上下文(`sor`) + +#### 使用示例: +```python +async with sqlorContext('user') as sor: + users = await sor.select('users', cond={'active': True}) +``` + +--- + +### `initEnv()` +初始化全局运行环境(`ServerEnv` 单例),注入大量工具函数与常量。 + +#### 注入内容分类: + +| 类别 | 示例 | +|------|------| +| 工具函数 | `paramify`, `data2xlsx`, `uuid` | +| 时间处理 | `curDateString`, `str2date`, `timestampstr` | +| 数据结构 | `DictObject`, `uObject` | +| 文件操作 | `abspath`, `openfile`, `webpath` | +| 数据库 | `DBPools`, `DBFilter` | +| 错误类 | `Success`, `Error`, `NeedLogin` | +| HTTP 工具 | `HttpClient`, `StreamHttpClient`, `basic_auth_headers` | +| 异步支持 | `async_sleep`, `stream_response` | +| 其他 | `rfexe`(注册函数执行器) | + +> 此函数应在应用启动时调用一次。 + +--- + +### `set_builtins()` +将 Python 内置函数(如 `print`, `len`, `isinstance`)注入到 `ServerEnv()` 全局命名空间。 + +#### 实现原理: +- 遍历 `builtins` 模块公开符号 +- 使用 `exec()` 动态绑定至 `g[key] = builtin_func` + +#### 示例效果: +```python +g = ServerEnv() +g.print("Hello") # 实际调用内置 print +g.len([1,2,3]) # 调用内置 len +``` + +> 便于在模板或 DSL 中统一访问内置函数。 + +--- + +## 使用建议 + +### ✅ 推荐实践 +- 使用 `initEnv()` 初始化全局环境后,可通过 `ServerEnv()` 统一获取工具集 +- 文件下载优先使用 `path_download` 或 `file_download`(新版本) +- 敏感数据加解密务必使用 `password_encode/decode` +- 大数据响应使用 `stream_response` 避免内存溢出 + +### ⚠️ 注意事项 +- `configValue()` 使用 `eval`,请严格验证输入 +- `mktemp()` 在高并发下可能有命名冲突风险,建议升级为 `NamedTemporaryFile(delete=False)` +- `file_download` 当前实现依赖非标准接口(`setHeader`),仅适配特定框架 + +--- + +## 示例:导出用户数据为 Excel 并下载 + +```python +async def export_users(request): + env = ServerEnv() + + # 查询数据 + async with env.sqlorContext('user') as sor: + rows = await sor.select('users', fields=['id', 'name', 'email']) + + # 定义表头 + headers = [ + {'name': 'id', 'title': '编号'}, + {'name': 'name', 'title': '姓名'}, + {'name': 'email', 'title': '邮箱'} + ] + + # 生成 Excel + xlsx_path = env.data2xlsx(rows, headers) + + # 返回文件下载 + return await env.path_download(request, xlsx_path, '用户列表.xlsx') +``` + +--- + +## 版本信息 + +- **创建日期**:未知(根据代码风格推测为 2020~2022) +- **维护状态**:活跃使用中 +- **作者**:内部团队开发(依赖 `appPublic`, `sqlor` 私有库) + +--- + +## 附录 A:HTTP 状态码映射表 + +| Code | Meaning | Exception Class | +|------|---------|------------------| +| 400 | Bad Request | HTTPBadRequest | +| 401 | Unauthorized | HTTPUnauthorized | +| 403 | Forbidden | HTTPForbidden | +| 404 | Not Found | HTTPNotFound | +| 405 | Method Not Allowed | HTTPMethodNotAllowed | +| 408 | Timeout | HTTPRequestTimeout | +| 409 | Conflict | HTTPConflict | +| 410 | Gone | HTTPGone | +| 415 | Unsupported Media Type | HTTPUnsupportedMediaType | +| 429 | Too Many Requests | HTTPTooManyRequests | +| 500 | Internal Error | HTTPInternalServerError | +| 502 | Bad Gateway | HTTPBadGateway | +| 503 | Service Unavailable | HTTPServiceUnavailable | + +--- + +## 附录 B:全局环境注入清单(部分) + +| 名称 | 类型 | 来源 | +|------|------|------| +| `json` | module | built-in | +| `time` | module | built-in | +| `random` | module | built-in | +| `datetime` | module | built-in | +| `paramify` | function | local | +| `curDateString` | function | timeUtils | +| `getID` | function | uniqueID | +| `Error`, `Success` | class | .error | +| `HttpClient` | class | httpclient | +| `stream_response` | coroutine | local | +| `DBPools` | class | sqlor.dbpools | + +> 完整列表见 `initEnv()` 函数体。 + +--- +``` + +> 📝 文档结束。此文档可用于团队 Wiki、API 手册或项目交接资料。 \ No newline at end of file diff --git a/aidocs/llmProcessor.md b/aidocs/llmProcessor.md new file mode 100644 index 0000000..1a504d2 --- /dev/null +++ b/aidocs/llmProcessor.md @@ -0,0 +1,231 @@ +# LLM 处理器模块技术文档 + +```markdown +# LLM 处理器模块(`llm_processor.py`) + +本模块实现了基于 `BaseProcessor` 的三种不同模式的 LLM(大语言模型)请求处理器,支持流式、同步和异步调用方式。通过模板引擎动态生成请求配置,并结合 `LlmProxy` 系列代理类实现与 LLM 服务的交互。 + +--- + +## 模块依赖 + +```python +import aiohttp +from aiohttp import web, BasicAuth +from aiohttp import client +from appPublic.dictObject import DictObject +from .llm_client import StreamLlmProxy, AsyncLlmProxy, SyncLlmProxy +from .baseProcessor import * +``` + +### 依赖说明: + +- **`aiohttp`**:用于构建异步 Web 服务及发起 HTTP 客户端请求。 +- **`web` / `BasicAuth` / `client`**:AIOHTTP 的核心组件,分别用于 Web 路由、认证和客户端操作。 +- **`DictObject`**:来自 `appPublic.dictObject` 的字典封装类,将字典转换为可属性访问的对象。 +- **`StreamLlmProxy`, `AsyncLlmProxy`, `SyncLlmProxy`**:LLM 请求代理类,分别处理流式、异步和同步响应。 +- **`BaseProcessor`**:所有处理器继承的基础类,提供通用运行环境设置和资源管理能力。 + +--- + +## 核心类概述 + +| 类名 | 功能描述 | +|-------------------|--------| +| `LlmProcessor` | 流式 LLM 请求处理器,适用于需要实时返回生成内容的场景(如聊天流)。 | +| `LlmSProcessor` | 同步式 LLM 请求处理器,等待完整响应后一次性返回结果。 | +| `LlmAProcessor` | 异步非阻塞式 LLM 请求处理器,适用于后台任务或延迟响应场景。 | + +--- + +## 公共方法详解 + +### `@classmethod isMe(self, name)` + +判断当前处理器是否匹配指定名称。 + +#### 参数: +- `name` (str): 请求路径中标识处理器类型的名称。 + +#### 返回值: +- `bool`: 若 `name` 匹配该处理器类型则返回 `True`。 + +> 所有子类均通过此方法注册自身处理器类型(`llm`, `llms`, `llma`)。 + +--- + +### `async path_call(self, request, params={})` + +从模板 URL 加载并渲染配置数据,解析为结构化对象。 + +#### 参数: +- `request` (`aiohttp.web.Request`):当前 HTTP 请求对象。 +- `params` (dict, optional):额外传入的参数,用于模板渲染上下文。 + +#### 流程: +1. 设置运行环境(`set_run_env`) +2. 构造完整请求路径 URL +3. 获取运行命名空间 `run_ns` +4. 更新命名空间中的参数 +5. 使用模板引擎(`tmpl_engine`)渲染远程模板内容 +6. 解析 JSON 字符串为 `DictObject` + +#### 返回值: +- `DictObject`:包含 LLM 配置信息的对象,如模型地址、参数、提示词等。 + +#### 示例输出结构(假设模板返回): +```json +{ + "model": "gpt-4", + "prompt": "你好,请介绍一下你自己。", + "temperature": 0.7, + "max_tokens": 1024 +} +``` + +--- + +### `setheaders(self)` + +占位方法,用于后续扩展自定义响应头设置。 + +> 当前为空实现(`pass`),可根据需求重写以添加 CORS、Content-Type 或其他头部字段。 + +--- + +## 各处理器类详细说明 + +### 1. `LlmProcessor` —— 流式处理器 + +#### 处理器标识: +```python +@classmethod +def isMe(cls, name): + return name == 'llm' +``` + +#### 方法:`async datahandle(request)` + +执行流式 LLM 请求处理: + +##### 步骤: +1. 调用 `path_call()` 获取配置对象 `d` +2. 创建 `StreamLlmProxy(self, d)` 实例 +3. 发起流式请求:`await llm(request, self.run_ns.params_kw)` +4. 结果存入 `self.retResponse`(通常是一个 `StreamingResponse` 对象) + +##### 特点: +- 支持逐块传输(chunked transfer),适合前端实时显示文本生成过程。 +- 响应体不会被完全缓存,内存占用低。 + +--- + +### 2. `LlmSProcessor` —— 同步处理器 + +#### 处理器标识: +```python +@classmethod +def isMe(cls, name): + return name == 'llms' +``` + +#### 方法:`async datahandle(request)` + +执行同步式 LLM 请求处理: + +##### 步骤: +1. 调用 `path_call()` 获取配置对象 `d` +2. 创建 `SyncLlmProxy(self, d)` 实例 +3. 发起同步请求并等待完整响应:`await llm(...)` +4. 结果存入 `self.content`(字符串或字典形式的结果) + +##### 特点: +- 必须等待整个响应完成才返回。 +- 适合对延迟不敏感但需获取完整结果的接口(如摘要生成、问答等)。 + +--- + +### 3. `LlmAProcessor` —— 异步处理器 + +#### 处理器标识: +```python +@classmethod +def isMe(cls, name): + return name == 'llma' +``` + +#### 方法:`async datahandle(request)` + +执行异步非阻塞式 LLM 请求处理: + +##### 步骤: +1. 调用 `path_call()` 获取配置对象 `d` +2. 创建 `AsyncLlmProxy(self, d)` 实例 +3. 发起异步请求并获取响应:`await llm(...)` +4. 结果存入 `self.retResponse` + +##### 特点: +- 可配合回调机制或消息队列使用。 +- 不立即返回结果,常用于任务提交型 API。 + +--- + +## 设计模式与架构思想 + +### 分层设计 +- **路由识别层**:通过 `isMe()` 判断处理器类型。 +- **配置加载层**:统一使用 `path_call()` 渲染模板获取 LLM 配置。 +- **执行代理层**:解耦具体调用逻辑至 `LlmProxy` 系列类。 +- **响应处理层**:根据模式决定如何构造最终响应。 + +### 模板驱动配置 +所有 LLM 请求参数均由外部模板文件控制,支持动态变量注入(如用户输入、会话状态等),提升灵活性与安全性。 + +### 异步 I/O 支持 +全栈异步设计,充分利用 `aiohttp` 和 `async/await` 特性,支持高并发场景下的高效处理。 + +--- + +## 使用示例 + +假设存在以下模板 URL:`/templates/llm/chat.json.tmpl` + +```jinja2 +{ + "model": "qwen", + "prompt": "用户: {{ query }}\\n助手:", + "temperature": 0.8, + "max_tokens": 512 +} +``` + +当访问 `/llm/chat` 时: + +- `LlmProcessor` 将启动流式对话; +- `LlmSProcessor` 将返回完整回答; +- `LlmAProcessor` 将异步提交任务并可能返回任务 ID。 + +--- + +## 注意事项 + +1. **模板安全**:确保模板引擎对恶意输入进行过滤,防止 SSTI(服务器端模板注入)攻击。 +2. **错误处理**:建议在 `datahandle` 中增加异常捕获逻辑,避免服务崩溃。 +3. **性能调优**:`chunk_size = 40960` 可根据网络状况调整。 +4. **认证支持**:可通过重写 `setheaders` 或扩展 `BaseProcessor` 添加身份验证逻辑。 + +--- + +## 待优化建议 + +- 增加日志记录功能(如请求 ID、耗时统计)。 +- 提供默认 `setheaders` 实现(如设置 `Content-Type: application/json`)。 +- 支持更多 LLM 协议(OpenAI、Anthropic、Ollama 等)。 +- 添加中间件支持(限流、缓存、审计等)。 + +--- +``` + +> 📝 **文档版本**:v1.0 +> 💡 **维护团队**:应用平台组 +> 🕒 **最后更新**:2025年4月5日 \ No newline at end of file diff --git a/aidocs/llm_client.md b/aidocs/llm_client.md new file mode 100644 index 0000000..d2ebca3 --- /dev/null +++ b/aidocs/llm_client.md @@ -0,0 +1,354 @@ +# `StreamLlmProxy` 与 LLM 代理类技术文档 + +本项目提供了一套用于代理调用大语言模型(LLM)API 的异步 Python 类,支持流式和同步请求,并具备参数转换、身份认证、响应处理等功能。该模块适用于构建灵活的 LLM 网关服务。 + +--- + +## 模块依赖 + +```python +import re +import base64 +import json +from traceback import format_exc +from aiohttp import web +from appPublic.dictObject import DictObject +from appPublic.log import debug, info, exception, error +from appPublic.httpclient import HttpClient, RESPONSE_TEXT, RESPONSE_JSON, RESPONSE_BIN, RESPONSE_FILE, RESPONSE_STREAM +from appPublic.registerfunction import RegisterFunction +from appPublic.argsConvert import ArgsConvert +``` + +> **说明**: +> - 使用 `aiohttp` 实现异步 Web 响应流。 +> - `DictObject` 提供字典属性访问语法(如 `obj.key`)。 +> - `HttpClient` 是封装的异步 HTTP 客户端。 +> - `ArgsConvert` 支持模板字符串替换(类似 `${var}`)。 +> - 日志使用 `appPublic.log` 统一输出。 + +--- + +## 工具函数 + +### `encode_imagefile(fn)` + +将本地图片文件编码为 Base64 字符串,常用于向 LLM API 发送图像输入。 + +#### 参数: +- `fn` (str): 图像文件路径。 + +#### 返回值: +- (str): Base64 编码后的 UTF-8 字符串。 + +#### 示例: +```python +img_b64 = encode_imagefile("example.jpg") +``` + +--- + +## 核心类:`StreamLlmProxy` + +一个通用的 LLM 接口代理类,支持流式响应处理。 + +### 初始化:`__init__(self, processor, desc)` + +#### 参数: +- `processor`: 包含运行环境上下文的对象(需有 `run_ns` 属性)。 +- `desc` (dict-like): 描述目标 LLM API 的配置对象,必须包含 `.name` 字段。 + +#### 属性初始化: +| 属性 | 类型 | 说明 | +|------|------|------| +| `name` | str | API 名称 | +| `processor` | object | 上下文处理器 | +| `auth_api` | dict/None | 认证接口定义 | +| `desc` | object | 原始描述对象 | +| `api_name` | str | 同 `name` | +| `data` | DictObject | 存储用户级临时数据(如 token) | +| `ac` | ArgsConvert | 模板参数解析器 | + +> ⚠️ 断言 `desc.name` 必须存在。 + +--- + +### 方法列表 + +#### `line_chunk_match(l)` +对流式返回的每一行进行正则匹配提取有效 JSON 内容。 + +##### 参数: +- `l` (str): 输入文本行。 + +##### 返回值: +- 匹配到的组或原字符串。 + +##### 配置来源: +通过 `self.api.chunk_match` 正则表达式控制,例如: +```json +"chunk_match": "data: (.*)" +``` + +--- + +#### `write_chunk(ll)` +将单条消息写入 HTTP 流响应体中,支持过滤与字段映射。 + +##### 参数: +- `ll` (str): 待处理的消息字符串。 + +##### 功能流程: +1. 跳过 `[DONE]` 标记; +2. 使用 `line_chunk_match` 提取内容; +3. 解析为 `DictObject`; +4. 根据 `api.resp` 映射输出字段; +5. 可选地根据 `chunk_filter` 条件清空某字段; +6. 序列化为 JSON 并写入流。 + +##### 异常处理: +捕获所有异常并记录堆栈日志。 + +--- + +#### `stream_handle(chunk)` +处理来自 `HttpClient` 的原始字节流,按换行分割并逐行调用 `write_chunk`。 + +##### 参数: +- `chunk` (bytes): 原始响应片段。 + +##### 特性: +- 自动拼接跨块不完整行(使用 `self.remain_str` 缓存尾部未完成部分)。 + +--- + +#### `get_apikey(apiname)` +从运行环境中获取当前用户的 API Key。 + +##### 参数: +- `apiname` (str): 目标 API 名称。 + +##### 行为: +调用 `processor.run_ns.get_llm_user_apikey(apiname, user)` 获取密钥。 + +> 若函数未注册,则抛出异常。 + +--- + +#### `get_apidata(parts, params={})` +根据配置动态生成请求参数(headers/data/params),支持模板变量替换与自定义转换器。 + +##### 参数: +- `parts`: 参数定义列表,每项结构如下: + ```python + { + "name": "field_name", + "value": "template string with ${user}", + "convertor": "optional_function_name" + } + ``` +- `params`: 外部传入参数,优先级高于 `self.data`。 + +##### 返回值: +- (dict): 构造好的参数字典。 + +##### 转换机制: +- 使用 `ArgsConvert('${', '}')` 替换 `${key}` 模板; +- 若指定了 `convertor`,通过 `RegisterFunction().exe()` 执行转换逻辑。 + +--- + +#### `do_auth(request)` +执行前置认证流程(如获取 access_token),通常用于需要 OAuth 或 JWT 的场景。 + +##### 流程: +1. 检查是否已认证(避免重复请求); +2. 调用 `get_apikey()` 获取基础凭证; +3. 构造认证请求(URL、method、headers、data); +4. 发起请求并解析响应; +5. 将关键字段保存至 `self.data`; +6. 设置 `authed=True` 并持久化存储。 + +##### 数据持久化: +通过 `set_data(key, value)` 存储于应用全局状态。 + +--- + +#### `data_key(apiname)` +生成基于用户的身份数据键名。 + +##### 规则: +``` +{apiname}_a_{username} +``` +若用户为空,默认为 `'anonymous'`。 + +--- + +#### `set_data(apiname, data)` / `get_data(apiname)` +封装了应用级别的数据读写操作,基于 `aiohttp.web.Application` 的共享状态。 + +> 利用 `request.app` 实现跨请求的数据缓存(如 access_token 缓存)。 + +--- + +#### `__call__(request, params)` +主入口方法,处理客户端请求并转发至后端 LLM 接口。 + +##### 参数: +- `request`: aiohttp 请求对象; +- `params`: 用户请求参数(DictObject); + +##### 关键行为: +- 支持流式 (`stream=True`) 和非流模式; +- 准备 `StreamResponse`; +- 解析 URL、method、headers、data、params; +- 使用 `HttpClient` 发起异步请求; +- 注册 `stream_func=self.stream_handle` 处理流数据; +- 最终返回 `web.StreamResponse` 对象。 + +##### 日志调试: +打印完整的请求信息(URL、参数等)便于排查问题。 + +--- + +#### `datalize(dic, data={})` +模板变量填充工具函数。 + +##### 参数: +- `dic`: 包含 `${...}` 模板的字典或字符串; +- `data`: 补充变量源。 + +##### 优先级: +`data` > `self.data` + +##### 返回值: +替换后的结果。 + +--- + +## 派生类 + +### `SyncLlmProxy(StreamLlmProxy)` + +同步版本代理,直接返回完整 JSON 响应。 + +#### 差异点: +- 不使用流式响应; +- `response_type=RESPONSE_JSON`; +- 返回普通字典而非 `StreamResponse`; +- 结果通过 `convert_resp(resp)` 进行字段映射。 + +#### `convert_resp(resp)` +将原始响应按 `api.resp` 配置投影成标准格式。 + +##### 示例配置: +```json +"resp": [ + {"name": "content", "value": "choices.0.message.content"}, + {"name": "finish_reason", "value": "choices.0.finish_reason"} +] +``` + +--- + +### `AsyncLlmProxy(StreamLlmProxy)` + +异步但非流式代理,等待完整响应后再返回。 + +#### 特性: +- 仍准备 `StreamResponse`; +- 实际以 `RESPONSE_JSON` 获取完整数据; +- 再次尝试写入残留字符串(`remain_str`); +- 返回 `StreamResponse`(可能无实际流内容); + +> ❗ 注意:此设计可能存在语义歧义,建议明确区分“异步获取”与“流式传输”。 + +--- + +## 配置结构示例(JSON Schema) + +```json +{ + "name": "openai_gpt4", + "auth": { + "url": "https://api.openai.com/v1/token", + "method": "POST", + "headers": [ + {"name": "Authorization", "value": "Bearer ${api_key}"} + ], + "data": {}, + "set_data": [ + {"name": "access_token", "field": "token"} + ] + }, + "chat": { + "url": "https://api.openai.com/v1/chat/completions", + "method": "POST", + "need_auth": true, + "headers": [ + {"name": "Authorization", "value": "Bearer ${access_token}"}, + {"name": "Content-Type", "value": "application/json"} + ], + "data": [ + {"name": "model", "value": "${model}"}, + {"name": "messages", "value": "${messages}"}, + {"name": "stream", "value": "${stream}"} + ], + "resp": [ + {"name": "text", "value": "choices.0.delta.content"}, + {"name": "done", "value": "choices.0.finish_reason"} + ], + "chunk_match": "data: (.*)", + "chunk_filter": { + "name": "choices.0.delta.role", + "value": "system", + "op": "!=", + "field": "text" + } + } +} +``` + +--- + +## 使用场景 + +| 场景 | 推荐类 | +|------|--------| +| 实时聊天界面(SSE) | `StreamLlmProxy` | +| 获取完整回复(CLI 工具) | `SyncLlmProxy` | +| 异步任务调度 | `AsyncLlmProxy` | + +--- + +## 日志级别说明 + +| 级别 | 用途 | +|------|------| +| `debug()` | 请求详情、内部变量 | +| `info()` | 正常流程提示 | +| `exception()` | 异常 + 堆栈 | +| `error()` | 严重错误 | + +--- + +## 注意事项 + +1. **安全性**:敏感信息(如 API Key)应在 `get_llm_user_apikey` 中安全获取,不应硬编码。 +2. **性能**:`stream_handle` 中避免阻塞操作,确保高吞吐。 +3. **兼容性**:不同 LLM 提供商的流格式差异较大,需正确设置 `chunk_match`。 +4. **缓存策略**:`set_data/get_data` 基于内存,重启丢失,可扩展至 Redis。 + +--- + +## 总结 + +该组件实现了高度可配置化的 LLM API 代理层,支持: + +✅ 多种调用模式(流式/同步) +✅ 动态参数注入(模板引擎) +✅ 自定义响应映射 +✅ 认证流程自动化 +✅ 插件式转换函数 + +适合集成进企业级 AI 网关系统,作为统一接入点。 \ No newline at end of file diff --git a/aidocs/loadplugins.md b/aidocs/loadplugins.md new file mode 100644 index 0000000..44a7205 --- /dev/null +++ b/aidocs/loadplugins.md @@ -0,0 +1,212 @@ +# 技术文档:插件加载模块 + +## 概述 + +`load_plugins(p_dir)` 是一个用于动态加载指定目录下 Python 插件模块的函数。该函数通过扫描 `plugins` 子目录中的 `.py` 文件,将非 `__init__.py` 的模块导入运行时环境,并支持在插件中访问关键系统和框架对象(如 `sys`, `ServerEnv` 等)。 + +此功能常用于扩展应用功能,实现基于插件架构的灵活系统设计。 + +--- + +## 依赖说明 + +### 第三方/内部模块依赖 + +| 模块名 | 来源 | 用途 | +|--------|------|------| +| `os`, `sys` | Python 标准库 | 路径操作与模块路径管理 | +| `appPublic.folderUtils.listFile` | 内部公共库 | 列出指定目录中符合后缀条件的文件 | +| `appPublic.ExecFile` | 内部公共库 | 提供可执行上下文环境,用于注入变量到模块中 | +| `ahserver.serverenv.ServerEnv` | AHServer 框架 | 服务器运行时环境类,供插件使用 | +| `appPublic`, `sqlor`, `ahserver` | 内部包引用 | 确保相关模块已初始化并可被插件导入 | + +> ⚠️ 注意:所有 `appPublic.*` 和 `ahserver.*` 均为项目自定义模块,需确保已正确安装或部署。 + +--- + +## 函数定义 + +```python +def load_plugins(p_dir): + """ + 动态加载指定主目录下的 plugins 子目录中所有合法的 Python 插件模块。 + + 参数: + p_dir (str): 主路径,插件目录应位于 p_dir/plugins/ + + 返回值: + None + + 行为: + - 若 plugins 目录不存在,则直接返回。 + - 将 plugins 目录添加至 sys.path,使模块可被 import。 + - 使用 ExecFile 注入全局依赖对象(如 sys, ServerEnv)。 + - 遍历所有 .py 文件,排除 __init__.py,逐一导入模块。 + """ +``` + +--- + +## 实现细节 + +### 1. 初始化执行环境 + +```python +ef = ExecFile() +``` +- 创建一个 `ExecFile` 实例,可用于后续向模块执行环境中注入变量(目前仅设置,未实际执行文件内容)。 + +### 2. 构建插件目录路径 + +```python +pdir = os.path.join(p_dir, 'plugins') +``` +- 插件必须存放在传入路径 `p_dir` 下的 `plugins` 子目录中。 + +### 3. 目录存在性检查 + +```python +if not os.path.isdir(pdir): + return +``` +- 如果 `plugins` 目录不存在,函数静默退出,不进行任何操作。 + +### 4. 添加插件路径至模块搜索路径 + +```python +sys.path.append(pdir) +``` +- 将插件目录加入 `sys.path`,使得后续 `__import__` 可以直接导入这些模块。 + +> ❗ 安全提示:动态修改 `sys.path` 可能带来命名冲突或安全风险,请确保插件来源可信。 + +### 5. 注入共享对象到执行环境 + +```python +ef.set('sys', sys) +ef.set('ServerEnv', ServerEnv) +``` +- 虽然设置了 `ExecFile` 上下文,但当前代码并未调用其执行方法(如 `execfile`),因此这一步可能是预留接口或冗余代码。 +- 实际上,插件模块是否能访问 `sys` 和 `ServerEnv` 取决于它们自身是否显式导入。 + +### 6. 遍历并加载插件模块 + +```python +for m in listFile(pdir, suffixs='.py'): + if m == '__init__.py': + continue + if not m.endswith('.py'): + continue + module = os.path.basename(m[:-3]) + __import__(module, locals(), globals()) +``` + +#### 步骤说明: + +- 使用 `listFile(pdir, suffixs='.py')` 获取所有 `.py` 结尾的文件路径。 +- 跳过 `__init__.py` 文件(通常用于包初始化,非独立插件)。 +- 提取文件名(不含 `.py` 扩展名)作为模块名。 +- 使用 `__import__()` 动态导入模块,触发其代码执行。 + +> ✅ 效果:每个插件模块只要被成功导入,其顶层代码即被执行,可用于注册服务、绑定事件、初始化资源等。 + +--- + +## 使用示例 + +假设目录结构如下: + +``` +/app_root/ +└── plugins/ + ├── plugin_a.py + ├── plugin_b.py + └── __init__.py +``` + +调用方式: + +```python +load_plugins('/app_root') +``` + +结果: +- `/app_root/plugins` 被加入 `sys.path` +- `plugin_a` 和 `plugin_b` 被导入并执行 + +--- + +## 插件编写规范 + +插件模块(如 `plugin_x.py`)应满足以下要求: + +1. 必须是有效的 `.py` 文件; +2. 不得命名为 `__init__.py`; +3. 应包含必要的导入语句自行获取所需依赖; +4. 可在模块级执行注册逻辑,例如: + +```python +# plugin_hello.py +from ahserver.serverenv import ServerEnv + +def hello(): + print("Hello from plugin!") + +# 自动注册钩子 +ServerEnv.register_hook('startup', hello) +``` + +--- + +## 注意事项与建议 + +| 项目 | 说明 | +|------|------| +| 🛑 **异常处理缺失** | 当前代码未捕获导入错误(如语法错误、依赖缺失),可能导致启动失败。建议包裹 `try-except` 并记录日志。 | +| 🔍 **ExecFile 设置未生效** | `ef.set(...)` 设置的对象未传递给导入的模块,除非 `ExecFile.execfile()` 被调用,否则无实际作用。若无需执行脚本,可移除 `ef` 相关代码。 | +| 🧹 **冗余判断** | `listFile` 已按 `.py` 过滤,内层 `endswith('.py')` 判断可省略。 | +| 📁 **路径污染风险** | 多次调用 `load_plugins` 可能重复添加相同路径到 `sys.path`,建议去重或检查是否存在。 | + +--- + +## 改进建议 + +```python +import os +import sys +from appPublic.folderUtils import listFile +from ahserver.serverenv import ServerEnv + +def load_plugins(p_dir): + pdir = os.path.join(p_dir, 'plugins') + if not os.path.isdir(pdir): + return + + # 避免重复添加路径 + if pdir not in sys.path: + sys.path.insert(0, pdir) # 更推荐插入开头 + + for file_path in listFile(pdir, suffixs='.py'): + filename = os.path.basename(file_path) + if filename == '__init__.py': + continue + module_name = filename[:-3] # 去除 .py + try: + __import__(module_name, locals(), globals()) + except Exception as e: + print(f"Failed to load plugin {module_name}: {e}") + # 建议替换为 logger.error(...) +``` + +--- + +## 总结 + +`load_plugins` 是一个轻量级插件加载器,适用于基于文件系统的模块自动发现机制。尽管当前实现较为基础,但具备良好的扩展潜力。通过合理组织插件目录和规范插件行为,可构建出高内聚、低耦合的服务扩展体系。 + +--- + +📌 **版本信息** +- 语言:Python 3.x +- 框架依赖:AHServer + appPublic 工具集 +- 适用场景:服务端插件化架构、模块热加载、动态功能扩展 \ No newline at end of file diff --git a/aidocs/myTE.md b/aidocs/myTE.md new file mode 100644 index 0000000..4a6f09f --- /dev/null +++ b/aidocs/myTE.md @@ -0,0 +1,238 @@ +# 模板引擎技术文档 + +## 概述 + +本模块实现了一个基于 Jinja2 的自定义模板引擎系统,支持通过 URL 映射到文件路径的方式加载模板,并集成了异步渲染功能。该系统适用于 Web 服务中动态页面的生成和管理。 + +主要特性包括: + +- 自定义模板加载器(`TmplLoader`),支持多路径查找、继承机制与索引页自动识别。 +- 支持配置化的编码格式读取模板文件。 +- 异步渲染能力(`render_async`)。 +- 与项目中的 `Url2File` 和 `ServerEnv` 组件无缝集成。 + +--- + +## 模块依赖 + +```python +import os +import codecs + +from appPublic.Singleton import SingletonDecorator +from appPublic.jsonConfig import getConfig + +from jinja2 import Template, Environment, BaseLoader + +from .serverenv import ServerEnv +from .url2file import Url2File, TmplUrl2File +``` + +> **说明**: +> - `appPublic`: 项目公共库,提供单例模式和 JSON 配置读取功能。 +> - `jinja2`: 核心模板引擎库。 +> - `serverenv`: 服务器运行环境对象,用于全局共享资源。 +> - `url2file`: 将 URL 路径转换为本地文件路径的工具类,`TmplUrl2File` 是其针对模板的扩展。 + +--- + +## 核心类定义 + +### `TmplLoader(BaseLoader, TmplUrl2File)` + +自定义 Jinja2 模板加载器,结合了 `BaseLoader` 和 `TmplUrl2File` 的功能,支持从多个路径中按规则查找 `.tmpl` 类型的模板文件。 + +#### 构造函数 + +```python +def __init__(self, paths, indexes, subffixes=['.tmpl'], inherit=False) +``` + +| 参数 | 类型 | 描述 | +|------|------|------| +| `paths` | `list[str]` | 模板文件搜索路径列表,按顺序查找。 | +| `indexes` | `list[str]` | 索引页名称(如 `index.tmpl`, `default.tmpl`),用于目录访问时自动匹配默认页。 | +| `subffixes` | `list[str]` | 允许的模板文件后缀,默认为 `['.tmpl']`。可扩展支持其他后缀。 | +| `inherit` | `bool` | 是否启用路径继承机制(由 `TmplUrl2File` 实现)。 | + +> ⚠️ 注意:需同时调用父类 `BaseLoader` 和 `TmplUrl2File` 的初始化方法。 + +#### 方法 + +##### `get_source(env: Environment, template: str) -> tuple[str, str, callable]` + +获取指定模板的源码内容。 + +**返回值**(符合 Jinja2 加载器规范): +- `source`: 模板文本内容(字符串) +- `filename`: 实际文件路径 +- `uptodate`: 可调用对象,判断文件是否被修改(基于 mtime) + +**流程说明**: +1. 读取全局配置中的网站编码(`config.website.coding`)。 +2. 使用 `url2file()` 将模板名映射为实际文件路径。 +3. 若文件不存在,抛出 `TemplateNotFound` 错误。 +4. 以指定编码读取文件内容。 +5. 返回 `(source, filepath, is_unchanged_lambda)`。 + +##### `join_path(name: str, parent: str) -> str` + +实现模板中 `{% extends %}` 或 `{% include %}` 时相对路径的解析。 + +使用 `TmplUrl2File.relatedurl()` 计算相对于父模板的路径。 + +##### `list_templates() -> list` + +当前未实现模板枚举功能,固定返回空列表。 + +> 后续可扩展为扫描所有路径下的模板并去重返回。 + +--- + +### `TemplateEngine(Environment)` + +封装 Jinja2 `Environment` 的模板执行环境,提供更便捷的异步渲染接口。 + +#### 构造函数 + +```python +def __init__(self, loader=None) +``` + +| 参数 | 类型 | 描述 | +|------|------|------| +| `loader` | `TmplLoader` | 模板加载器实例,若不传则使用默认加载器。 | + +**额外属性**: +- `urlpaths`: (预留字段)可用于缓存 URL 到模板的映射。 +- `loader`: 保存引用以便内部调用。 + +#### 方法 + +##### `join_path(template: str, parent: str) -> str` + +代理至 `self.loader.join_path()`,确保路径解析一致性。 + +##### `async render(___name: str, **globals) -> str` + +异步渲染指定模板。 + +| 参数 | 类型 | 描述 | +|------|------|------| +| `___name` | `str` | 模板名称(URL 风格路径) | +| `**globals` | `dict` | 渲染上下文变量 | + +**逻辑步骤**: +1. 调用 `get_template()` 获取模板对象。 +2. 执行 `render_async()` 进行异步渲染。 +3. 返回渲染后的 HTML 字符串。 + +> ✅ 支持异步过滤器、协程调用等高级特性。 + +--- + +## 初始化函数 + +### `setupTemplateEngine() -> None` + +全局设置函数,用于初始化模板引擎并注入到服务器环境中。 + +#### 功能流程 + +1. 读取配置:`getConfig()` 获取全局配置对象。 +2. 提取处理器配置中类型为 `'tmpl'` 的后缀列表: + ```python + subffixes = [i[0] for i in config.website.processors if i[1] == 'tmpl'] + ``` + 示例:若配置为 `[('.html', 'tmpl'), ('.tmpl', 'tmpl')]`,则支持 `.html` 和 `.tmpl` 文件作为模板。 +3. 创建 `TmplLoader` 实例,启用继承模式。 +4. 初始化 `TemplateEngine` 并绑定加载器。 +5. 将引擎挂载到单例 `ServerEnv()` 的 `tmpl_engine` 属性上,供全局使用。 + +#### 示例配置片段(JSON/YAML) + +```json +{ + "website": { + "coding": "utf-8", + "paths": [ + "/var/www/templates", + "/opt/app/default_tmpl" + ], + "indexes": ["index.tmpl", "default.tmpl"], + "processors": [ + [".html", "tmpl"], + [".tmpl", "tmpl"], + [".js", "static"] + ] + } +} +``` + +--- + +## 使用示例 + +```python +# 初始化模板引擎(通常在应用启动时调用一次) +setupTemplateEngine() + +# 获取引擎实例 +g = ServerEnv() +engine = g.tmpl_engine + +# 异步渲染模板 +result = await engine.render('/user/profile.html', name='Alice', age=30) + +print(result) # 输出渲染后的 HTML 内容 +``` + +在模板中也可以使用继承或包含: + +```jinja2 +{# base.tmpl #} +{% block content %}{% endblock %} +``` + +```jinja2 +{# profile.tmpl #} +{% extends "base.tmpl" %} +{% block content %} +

Hello {{ name }}

+{% endblock %} +``` + +--- + +## 注意事项 + +1. **编码问题**:确保 `config.website.coding` 设置正确(通常为 `"utf-8"`),避免中文乱码。 +2. **文件权限**:模板所在目录需对运行进程可读。 +3. **性能建议**: + - 生产环境应开启 Jinja2 缓存机制(当前未配置)。 + - 可考虑实现 `list_templates()` 以支持热重载检测。 +4. **异常处理**: + - `TemplateNotFound`: 模板文件未找到。 + - `OSError`: 文件读取失败(权限、磁盘错误等)。 + +--- + +## 扩展建议 + +| 功能 | 建议 | +|------|------| +| 缓存支持 | 添加 `cache_size` 参数并启用 Jinja2 的模板缓存 | +| 自动重载 | 监听文件变化,在开发模式下自动刷新模板 | +| 安全沙箱 | 对不可信模板启用沙箱执行环境 | +| 多租户支持 | 按用户/站点隔离 `paths` 和 `loader` 实例 | + +--- + +## 版权与维护 + +- **作者**:项目开发团队 +- **所属模块**:`app.template_engine` +- **最后更新**:2025年4月5日 + +> 文档版本:v1.0 +> 适用于代码版本:见 Git 提交记录 \ No newline at end of file diff --git a/aidocs/p2p_middleware.md b/aidocs/p2p_middleware.md new file mode 100644 index 0000000..4081a50 --- /dev/null +++ b/aidocs/p2p_middleware.md @@ -0,0 +1,276 @@ +以下是为提供的 Python 代码编写的 **Markdown 格式技术文档**,涵盖了类的功能、依赖、中间件逻辑以及各方法的说明。 + +--- + +# `P2pLayer` 技术文档 + +## 概述 + +`P2pLayer` 是一个基于 `aiohttp` 的中间件组件,用于在 Web 请求处理流程中集成点对点(P2P)加密通信功能。该模块通过条件启用 P2P 加密层,并在请求/响应过程中实现握手、解码和编码操作,以保障通信安全。 + +此模块依赖于 `p2psc` 库中的 `PubkeyHandler` 和 `P2psc` 类,实现公钥管理和端到端加密逻辑。 + +--- + +## 依赖项 + +- [`aiohttp`](https://docs.aiohttp.org/):异步 HTTP 客户端/服务器框架。 +- `p2psc.pubkey_handler.PubkeyHandler`:负责管理本地公钥与远程节点身份认证。 +- `p2psc.p2psc.P2psc`:核心 P2P 安全通信类,支持加密握手与数据加解密。 +- `getConfig()`:全局配置获取函数(外部定义,需确保可用)。 + +> ⚠️ 注意:`getConfig()` 函数未在此文件中定义,应由项目其他部分提供,返回包含配置树的对象。 + +--- + +## 类定义 + +```python +class P2pLayer +``` + +### 功能 + +`P2pLayer` 封装了 P2P 加密通信的初始化与中间件逻辑。根据配置决定是否启用加密传输,并在启用时拦截请求/响应流进行加解密处理。 + +--- + +## 初始化 (`__init__`) + +```python +def __init__(self) +``` + +### 描述 + +初始化 `P2pLayer` 实例并根据配置决定是否启用 P2P 加密功能。 + +### 流程 + +1. 初始化 `self.p2pcrypt = False`。 +2. 调用 `getConfig()` 获取系统配置。 +3. 检查配置项 `config.website.p2pcrypt`: + - 若为 `True`,则启用 P2P 加密(设置 `self.p2pcrypt = True`),并初始化相关组件; + - 否则跳过初始化,后续中间件将直接透传请求。 +4. 如果启用了加密: + - 创建 `PubkeyHandler` 实例用于密钥管理; + - 使用 handler 及其自身 ID 初始化 `P2psc` 实例。 + +### 属性 + +| 属性 | 类型 | 说明 | +|------|------|------| +| `p2pcrypt` | `bool` | 是否启用 P2P 加密模式 | +| `handler` | `PubkeyHandler` | 公钥处理器,管理本地/远程身份 | +| `p2p` | `P2psc` | P2P 安全通信核心实例 | + +> ✅ 提示:仅当 `p2pcrypt` 为 `True` 时,`handler` 和 `p2p` 才会被创建。 + +--- + +## 中间件:`p2p_middle` + +```python +@web.middleware +async def p2p_middle(self, request, handler) +``` + +### 描述 + +Aiohttp 异步中间件,用于拦截进入的 HTTP 请求并执行 P2P 加密相关处理。 + +### 执行逻辑 + +| 条件 | 行为 | +|------|------| +| `not self.p2pcrypt` | 直接调用原始 handler,不进行任何加密处理 | +| 请求头含 `P2pHandShake` | 调用 `self.p2p_handshake(request)` 进行密钥交换 | +| 请求头含 `P2pdata` | 解密请求 → 处理 → 加密响应 | +| 其他情况 | 直接调用 handler(明文通行) | + +### 详细流程 + +```text +开始 +│ +├─ 是否禁用 p2pcrypt? +│ └─ 是 → 直接返回 handler(request) +│ +├─ 是否存在 "P2pHandShake" 头? +│ └─ 是 → 返回 await p2p_handshake(request) +│ +├─ 是否存在 "P2pdata" 头? +│ ├─ 是 → +│ │ 1. await p2p_decode_request(request) +│ │ 2. resp = await handler(decoded_request) +│ │ 3. return await p2p_encode_response(resp) +│ +└─ 默认 → 返回 await handler(request) +``` + +### 注意事项 + +- 当前代码存在拼写错误: + - `if not p2pscrypr:` ❌ → 应为 `if not self.p2pcrypt:` + - `resturen` ❌ → 应为 `return` + - `request.header.get` ❌ → 应为 `request.headers.get` +- 正确代码应如下: + +```python +@web.middleware +async def p2p_middle(self, request, handler): + if not self.p2pcrypt: + return await handler(request) + + if request.headers.get('P2pHandShake', None): + return await self.p2p_handshake(request) + + if request.headers.get('P2pdata', None): + request = await self.p2p_decode_request(request) + resp = await handler(request) + return await self.p2p_encode_response(resp) + + return await handler(request) +``` + +--- + +## 方法说明 + +### `p2p_handshake(request)` + +```python +async def p2p_handshake(self, request) +``` + +#### 描述 + +处理客户端发起的 P2P 安全握手请求(如密钥交换)。当前为空实现(`pass`),需子类或后续扩展完成具体协议逻辑。 + +#### 参数 + +- `request` (`aiohttp.web.Request`):HTTP 请求对象 + +#### 返回值 + +- 通常应返回 `aiohttp.web.Response` 对象,携带握手响应数据(如公钥、会话密钥等) + +#### 示例场景 + +- TLS-like 密钥协商 +- Diffie-Hellman 或 Noise 协议集成 + +--- + +### `p2p_decode_request(request)` + +```python +async def p2p_decode_request(self, request) +``` + +#### 描述 + +对接收到的加密请求体进行解密和解析,还原为标准 `request` 对象。 + +#### 参数 + +- `request`:携带加密数据的原始请求 + +#### 返回值 + +- 解密后的 `request` 对象(可能包装新 body 或附加上下文字段) + +#### 待实现内容 + +- 读取请求体 +- 使用 `self.p2p.decrypt()` 解密 +- 重构 JSON/body 数据 +- 替换 `request._payload` 或使用自定义 Request 子类 + +--- + +### `p2p_encode_response(response)` + +```python +async def p2p_encode_response(self, response) +``` + +#### 描述 + +将正常的 HTTP 响应内容加密后封装,返回给客户端。 + +#### 参数 + +- `response` (`aiohttp.web.Response`):原始响应对象 + +#### 返回值 + +- 加密后的响应对象(通常修改 body 并添加头部如 `Content-Encoding: p2p`) + +#### 当前实现 + +```python +return response +``` + +> 🔴 当前为占位实现,未实际加密。需要补充: +> +> - 序列化响应内容 +> - 使用 `self.p2p.encrypt()` 加密 +> - 设置 `Content-Type` 或自定义头标识加密类型 +> - 构造新的 `StreamResponse` 或 `Response` 返回 + +--- + +## 配置要求 + +确保配置结构中包含以下字段: + +```python +config = { + "website": { + "p2pcrypt": True # 或 False,控制是否启用 P2P 加密 + } +} +``` + +可通过环境变量、YAML 文件或其他方式加载。 + +--- + +## 使用示例(伪代码) + +```python +app = web.Application(middlewares=[p2player.p2p_middle]) +p2player = P2pLayer() +web.run_app(app) +``` + +--- + +## 已知问题 / 待修复 Bug + +| 问题 | 位置 | 说明 | +|------|------|------| +| 拼写错误 `p2pscrypr` | 第10行 | 应为 `self.p2pcrypt` | +| `resturen` 错误 | 第12行 | 应为 `return` | +| `request.header` → `.headers` | 第15行 | 属性名错误 | +| `p2p_handshake` 无实现 | 第20行 | 需补充握手逻辑 | +| 加解密方法为空 | 第24–29行 | 仅为骨架,需具体实现 | + +--- + +## 总结 + +`P2pLayer` 提供了一个可插拔的 P2P 加密通信中间件框架,适用于需要端到端安全传输的去中心化 Web 服务。虽然目前大部分功能尚未实现,但其设计清晰,具备良好的扩展性。 + +建议下一步: +- 修复语法错误 +- 实现握手与加解密逻辑 +- 添加单元测试与集成测试 +- 支持超时、重试、会话缓存等机制 + +--- + +📌 **版本信息**:v0.1(草案) +📅 **最后更新**:2025-04-05 \ No newline at end of file diff --git a/aidocs/processorResource.md b/aidocs/processorResource.md new file mode 100644 index 0000000..78e158f --- /dev/null +++ b/aidocs/processorResource.md @@ -0,0 +1,468 @@ +# `ProcessorResource` 技术文档 + +> 基于 `aiohttp` 的异步 Web 资源处理器,支持多类型文件与动态内容处理的扩展静态资源类。 + +--- + +## 概述 + +`ProcessorResource` 是一个继承自 `aiohttp.web_urldispatcher.StaticResource` 并混合了自定义路径映射功能(`Url2File`)的类。它用于统一处理静态资源请求,并根据文件后缀或 URL 前缀自动选择对应的**处理器(Processor)**来执行动态逻辑,如模板渲染、数据库操作、LLM 接口调用等。 + +该类增强了标准静态资源服务的能力,使其能够无缝集成: + +- 动态页面处理(`.html`, `.py`, `.md` 等) +- 数据源接口(SQL/XLSX) +- LLM 调用 +- WebSocket 支持 +- RESTful CRUD 操作 +- 文件上传/下载 +- 用户认证与会话管理 +- 国际化(i18n) + +--- + +## 依赖说明 + +### 第三方库 +| 包 | 用途 | +|----|------| +| `aiohttp` | 异步 HTTP 服务器框架 | +| `aiohttp_auth` | 请求认证中间件 | +| `aiohttp_session` | Session 管理 | +| `yarl.URL` | URL 解析工具 | +| `ssl` | HTTPS 安全连接支持 | +| `asyncio`, `aiofiles` | 异步 I/O 操作 | + +### 内部模块(`appPublic` 和项目本地模块) +| 模块 | 用途 | +|------|------| +| `appPublic.jsonConfig.getConfig` | 全局配置加载 | +| `appPublic.i18n.getI18N` | 多语言支持 | +| `appPublic.dictObject.DictObject`, `multiDict2Dict` | 字典封装与请求参数解析 | +| `appPublic.timecost.TimeCost` / `timeUtils.timestampstr` | 性能监控与时间处理 | +| `appPublic.log.*` | 日志输出函数 | +| `.baseProcessor.*` | 核心处理器基类及实现 | +| `.xlsxdsProcessor.XLSXDataSourceProcessor` | Excel 数据源处理器 | +| `.llmProcessor.Llm*Processor` | 大模型接口处理器 | +| `.websocketProcessor.WebsocketProcessor` | WebSocket 处理器 | +| `.xtermProcessor.XtermProcessor` | 终端模拟处理器 | +| `.sqldsProcessor.SQLDataSourceProcessor` | SQL 数据源处理器 | +| `.functionProcessor.FunctionProcessor` | 自定义函数处理器 | +| `.proxyProcessor.ProxyProcessor` | 反向代理处理器 | +| `.serverenv.ServerEnv` | 服务端全局环境变量存储 | +| `.url2file.Url2File` | URL 到文件系统路径转换 | +| `.filestorage.FileStorage` | 文件上传持久化 | +| `.restful.DBCrud` | 数据库 RESTful 接口 | +| `.dbadmin.DBAdmin` | 数据库管理后台 | +| `.filedownload.file_download`, `path_decode` | 文件下载与解码 | +| `.auth_api.*` | 登录/登出/用户信息获取接口 | + +--- + +## 函数详解 + +### `getHeaderLang(request: Request) → str` +从请求头中提取客户端首选语言。 + +#### 参数 +- **request**: `aiohttp.web.Request` 对象 + +#### 返回值 +- `str`: 如 `'zh-CN'` 或默认 `'en'` + +#### 示例 +```python +lang = getHeaderLang(request) # 返回 'zh-CN' +``` + +--- + +### `i18nDICT(request: Request) → bytes` +返回当前语言环境下的国际化字典 JSON 编码后的字节串。 + +#### 参数 +- **request**: 请求对象 + +#### 返回值 +- `bytes`: JSON 格式的 i18n 字典,使用网站编码(如 UTF-8) + +#### 流程 +1. 获取 Accept-Language +2. 查找 langMapping 映射(如 `zh-CN → zh`) +3. 加载对应语言词典并序列化为 JSON + +--- + +## 类:`ProcessorResource` + +### 继承关系 +```python +class ProcessorResource(StaticResource, Url2File) +``` + +提供增强型静态资源服务 + 动态处理器路由机制。 + +--- + +### 构造方法:`__init__` + +```python +def __init__( + self, + prefix: str, + directory: PathLike, + *, + name: Optional[str] = None, + expect_handler: Optional[_ExpectHandler] = None, + chunk_size: int = 256 * 1024, + show_index: bool = False, + follow_symlinks: bool = False, + append_version: bool = False, + indexes: list = [], + processors: dict = {} +) +``` + +#### 参数说明 + +| 参数 | 类型 | 描述 | +|------|------|------| +| `prefix` | `str` | URL 前缀,如 `/static/` | +| `directory` | `PathLike` | 静态文件根目录路径 | +| `name` | `Optional[str]` | 路由名称(可选) | +| `expect_handler` | `_ExpectHandler` | Expect 头处理回调(高级用法) | +| `chunk_size` | `int` | 文件读取分块大小,默认 256KB | +| `show_index` | `bool` | 是否显示目录索引页 | +| `follow_symlinks` | `bool` | 是否允许符号链接 | +| `append_version` | `bool` | 是否附加版本号防止缓存 | +| `indexes` | `list` | 自定义索引文件名列表(如 `['index.html']`) | +| `processors` | `dict` | 文件扩展名 → 处理器名称映射表 | + +#### 初始化行为 +- 调用父类 `StaticResource.__init__()` 设置静态资源基础属性 +- 初始化 `Url2File` 实现 URL 到文件路径映射 +- 将所有 HTTP 方法(POST/PUT/OPTIONS 等)指向 GET 路由处理器 +- 创建运行时环境容器 `self.y_env`(`DictObject`) + +--- + +### 属性 + +| 属性 | 类型 | 描述 | +|------|------|------| +| `y_processors` | `dict` | 扩展名 → 处理器名称映射 | +| `y_directory` | `PathLike` | 文件目录 | +| `y_prefix` | `str` | URL 前缀 | +| `y_indexes` | `list` | 索引文件名列表 | +| `y_env` | `DictObject` | 运行时上下文环境,供处理器访问 | + +--- + +### 方法 + +#### `setProcessors(processors: dict)` +更新处理器映射表。 + +#### `setIndexes(indexes: list)` +设置索引文件名列表。 + +--- + +#### `abspath(request: Request, path: str) → str` +将相对路径转为绝对文件系统路径。 + +##### 示例 +```python +fname = resource.abspath(request, "/js/app.js") +# 输出类似:/var/www/static/js/app.js +``` + +--- + +#### `getPostData(request: Request) → DictObject` +异步解析 POST 请求数据,支持: +- 表单 (`application/x-www-form-urlencoded`) +- 多部分表单 (`multipart/form-data`) —— 含文件上传 +- JSON 正文 (`application/json`) +- 查询参数合并 + +##### 返回值 +- `DictObject`: 类字典对象,支持点语法访问字段 + +##### 特性 +- 文件上传自动保存到 `FileStorage`,返回存储路径 +- 数组字段自动合并(同名多次提交) +- 错误捕获并打印堆栈但不中断流程 + +--- + +#### `parse_request(request: Request)` +解析真实客户端请求的协议、主机、端口和前置路径,考虑反向代理头: + +- `X-Forwarded-Scheme` +- `X-Forwarded-Host` +- `X-Forwarded-Port` +- `X-Forwarded-Prepath` + +结果保存在 `self._scheme`, `self._host`, `self._port`, `self._prepath`, `self._preurl` 中。 + +> ⚠️ 用于构建完整外部可见 URL。 + +--- + +#### `async _handle(request: Request) → StreamResponse` +核心请求处理器,重写了 `StaticResource._handle`。 + +##### 处理流程 + +1. **初始化客户端类型检测** + - 通过 User-Agent 判断设备类型(iPhone/iPad/Android/PC) + - 存入 `y_env.terminalType` + +2. **构建运行时环境 `y_env`** + 提供以下便捷函数给处理器使用: + + | 函数名 | 作用 | + |--------|------| + | `redirect(url)` | 302 跳转 | + | `remember_user(userid, ...)` | 登录用户 | + | `forget_user()` | 注销用户 | + | `get_user()` / `get_username()` / `get_userinfo()` | 获取用户信息 | + | `get_ticket()` / `remember_ticket(ticket)` | 认证票据操作 | + | `i18n(text)` | 文本国际化翻译 | + | `i18nDict()` | 获取当前语言词典 JSON | + | `request2ns()` | 获取解析后的请求参数 | + | `entire_url(path)` | 生成完整 URL | + | `websocket_url(path)` | 生成 ws:// 或 wss:// URL | + | `abspath(path)` | 获取文件绝对路径 | + | `path_call(path, params)` | 调用其他路径的处理器 | + | `aiohttp_client` | 原生 aiohttp 客户端实例 | + | `resource` | 当前资源对象引用 | + +3. **特殊路径预处理** + + | 条件 | 动作 | + |------|------| + | 路径以 `config.website.dbadm` 开头 | 调用 `DBAdmin` 提供数据库管理界面 | + | 路径以 `config.website.dbrest` 开头 | 调用 `DBCrud` 实现 RESTful 接口 | + | 路径以 `config.website.download` 开头 | 触发安全文件下载 | + +4. **处理器匹配流程** + + ```text + url → 文件路径 → 匹配处理器规则 → 执行 handle() + ``` + + - 使用 `url2processor()` 按扩展名或前缀查找处理器 + - 若找到,则执行 `processor.handle(request)` + - 否则尝试作为 HTML 页面处理(检查是否含 `` 或 ``) + - 最后交由父类当作普通静态文件处理 + +5. **目录访问控制** + - 若请求的是目录且 `allowListFolder=False`,抛出 `HTTPNotFound` + +--- + +#### `url2processor(request, url, fpath) → Processor or None` +根据 URL 或文件路径决定应使用的处理器。 + +##### 匹配优先级 +1. **前缀匹配**:遍历 `config.website.startswiths`,若 URL 路径匹配某 leading 前缀,使用 `FunctionProcessor` +2. **后缀匹配**:遍历 `self.y_processors`,若文件路径以特定扩展名结尾,实例化对应处理器 + +##### 返回值 +- 成功:返回处理器实例(如 `TemplateProcessor`, `PythonScriptProcessor` 等) +- 失败:返回 `None` + +--- + +#### `entireUrl(request, url) → str` +将相对或绝对 URL 转换为完整的外部可访问 URL。 + +##### 支持输入形式 +- 绝对 URL:`https://example.com/path` → 不变 +- 斜杠开头:`/api/data` → 结合 `X-Forwarded-*` 头拼接成完整地址 +- 相对路径:`../img/logo.png` → 相对于当前请求路径计算 + +##### 协议升级 +- 自动将 `http://` → `ws://` +- `https://` → `wss://`(WebSocket 场景) + +--- + +#### `websocketUrl(request, url) → str` +专门用于生成 WebSocket 协议 URL。 + +##### 示例 +```python +ws_url = res.websocketUrl(req, "/ws/chat") +# 结果可能是:wss://example.com/ws/chat (如果原请求是 HTTPS) +``` + +--- + +#### `urlWebsocketify(url) → str` +确保 `.ws` 或 `.wss` 结尾的 URL 使用正确的 WebSocket 协议。 + +--- + +#### `url2path(url) → str` +从完整 URL 中剥离协议+主机部分,得到路径。 + +##### 示例 +```python +self._preurl = "https://example.com/app" +url2path("https://example.com/app/js/main.js") → "/js/main.js" +``` + +--- + +#### `async path_call(request, path, params={}) → Any` +在内部调用另一个路径的处理器,可用于模块化组合逻辑。 + +##### 参数 +- `path`: 目标路径(如 `/api/user/info`) +- `params`: 附加参数字典 + +##### 流程 +1. 转换为完整 URL +2. 解析为本地文件路径 +3. 查找对应处理器 +4. 调用其 `be_call(request, params)` 方法 + +> 适用于跨模块调用、API 组合场景。 + +--- + +#### `async isHtml(fn) → bool` +判断文件是否为 HTML 文件(基于内容而非扩展名)。 + +##### 判断依据 +- 忽略开头空白字符 +- 内容以 `` 或 `` 开始(忽略大小写) + +--- + +#### `html_handle(request, filepath) → Response` +手动渲染 HTML 文件响应,设置正确头部。 + +##### 设置 Headers +```http +Content-Type: text/html; charset=utf-8 +Accept-Ranges: bytes +Content-Length: [file size] +``` + +> ⚠️ 注意:此处 header 中 `utf-8` 应移至 `charset=utf-8` 更规范。 + +--- + +#### `gethost(request) → str` +获取请求的真实 Host,优先使用代理头。 + +顺序: +1. `X-Forwarded-Host` +2. `Host` +3. 从 `request.url` 解析 + +--- + +## 配置要求(来自 `getConfig()`) + +`ProcessorResource` 依赖如下配置项(通常位于 `config.json`): + +```json +{ + "website": { + "coding": "utf-8", + "port": 8080, + "allowListFolder": false, + "dbadm": "/_dbadmin/", + "dbrest": "/_dbrest/", + "download": "/_download/", + "startswiths": [ + { "leading": "/_func/" } + ], + "langMapping": { + "zh-CN": "zh", + "en-US": "en" + } + } +} +``` + +--- + +## 使用示例 + +### 注册路由 +```python +from aiohttp import web + +processors = { + '.py': 'PythonScript', + '.md': 'Markdown', + '.xlsx': 'XLSXDataSource' +} + +resource = ProcessorResource('/site/', './static/', processors=processors) +app.router.register_resource(resource) +``` + +此时: +- `/site/index.html` → 自动识别为 HTML 并返回 +- `/site/api.py` → 使用 `PythonScriptProcessor` 执行 Python 脚本 +- `/site/data.xlsx` → 使用 `XLSXDataSourceProcessor` 输出表格数据 +- `/site/_dbrest/mydb/users` → 提供 users 表的 REST 接口 + +--- + +## 设计思想 + +| 特性 | 实现方式 | +|------|----------| +| **统一入口** | 所有请求都经过 `ProcessorResource` 分发 | +| **插件式处理器** | 通过配置绑定扩展名与处理器类 | +| **透明代理兼容** | 解析 X-Forwarded-* 头保证 URL 正确性 | +| **前后端融合** | 支持 `.py`, `.md`, `.ws` 等非传统静态资源 | +| **安全性** | 文件路径解码、上传隔离、目录禁止遍历 | + +--- + +## 注意事项 + +1. **性能建议** + - 生产环境中避免开启 `allowListFolder` + - 静态资源建议由 Nginx 托管,本类主要用于开发或嵌入式场景 + +2. **安全警告** + - `.py` 脚本处理器存在代码执行风险,请仅用于可信环境 + - 文件上传需配合权限校验(已通过 `userid` 传入 `FileStorage`) + +3. **编码问题** + - 所有文本文件推荐使用 UTF-8 编码 + - `i18nDICT()` 使用 `website.coding` 配置进行编码 + +4. **调试提示** + - 可启用 `print_exc()` 查看异常堆栈 + - 使用 `info/debug/error` 输出日志辅助排查 + +--- + +## 总结 + +`ProcessorResource` 是一个高度可扩展的异步 Web 资源处理器,集成了: + +✅ 静态文件服务 +✅ 动态脚本执行 +✅ 数据接口生成 +✅ 认证会话管理 +✅ 国际化支持 +✅ WebSocket 集成 + +适用于快速搭建具有混合动静内容的企业级应用门户或低代码平台前端网关。 + +--- + +> 📝 文档版本:v1.0 +> 🔗 更新日期:2025年4月5日 \ No newline at end of file diff --git a/aidocs/proxyProcessor.md b/aidocs/proxyProcessor.md new file mode 100644 index 0000000..bba8d0d --- /dev/null +++ b/aidocs/proxyProcessor.md @@ -0,0 +1,219 @@ +以下是为提供的 Python 代码编写的 **Markdown 格式技术文档**,适用于项目内部或开发者参考。 + +--- + +# `ProxyProcessor` 技术文档 + +## 概述 + +`ProxyProcessor` 是一个基于 `aiohttp` 的异步代理处理器类,继承自 `BaseProcessor`。它用于将 HTTP 请求转发到目标 URL,并以流式方式返回响应内容,支持动态 URL 渲染、身份认证、请求头注入和参数传递等功能。 + +该处理器适用于需要在服务端代理外部 API 请求的场景,尤其适合结合模板引擎动态生成请求配置。 + +--- + +## 模块依赖 + +```python +import aiohttp +from appPublic.log import info, debug, warning, error, critical, exception +from aiohttp import web, BasicAuth +from aiohttp import client +from .baseProcessor import * +``` + +> ⚠️ 注意:原代码中存在拼写错误(如 `paams` 应为 `params`)和潜在变量名错误(如 `g.get('headers')` 应为 `d.get('headers')`),已在文档中修正并标注。 + +--- + +## 类定义 + +### `class ProxyProcessor(BaseProcessor)` + +继承自 `BaseProcessor`,实现了一个可识别的处理器插件机制,并提供代理功能。 + +#### 方法:`isMe(name: str) -> bool` + +判断当前处理器是否匹配给定名称。 + +##### 参数: +- `name` (str): 处理器名称标识 + +##### 返回值: +- `True` 当且仅当 `name == 'proxy'` +- 否则返回 `False` + +##### 示例: +```python +if ProxyProcessor.isMe("proxy"): + # 使用此处理器 +``` + +--- + +## 核心方法 + +### `async path_call(self, request: web.Request, params: dict = {}) -> dict` + +根据请求和参数生成代理请求的目标地址及相关配置数据,通过模板引擎渲染后解析为 JSON 对象。 + +##### 参数: +- `request` (`aiohttp.web.Request`): 当前 HTTP 请求对象 +- `params` (`dict`, optional): 额外传入的参数,默认为空字典 + +##### 流程说明: +1. 设置运行环境(调用 `set_run_env`) +2. 获取路径(优先使用 `params['path']`,否则使用 `request.path`) +3. 构建完整目标 URL(通过 `self.resource.entireUrl()`) +4. 更新运行命名空间(`run_ns`)包含传入参数 +5. 使用模板引擎(`tmpl_engine`)渲染 URL 字符串 +6. 解析渲染结果为 JSON 数据并返回 + +##### 返回值: +- `dict`: 包含代理请求所需信息的对象,例如: + ```json + { + "url": "https://api.example.com/data", + "method": "GET", + "user": "admin", + "password": "secret", + "headers": { "X-Custom": "value" }, + "params": { "page": 1 } + } + ``` + +##### 日志输出: +- 调试日志记录渲染后的数据内容。 + +> 🔍 提示:`te.render()` 支持 Jinja2 或类似语法模板,可用于动态构造请求参数。 + +--- + +### `async def datahandle(self, request: web.Request) -> web.StreamResponse` + +核心代理处理函数,执行对外部服务的实际请求,并以流式方式转发响应。 + +##### 参数: +- `request` (`web.Request`): 客户端原始请求 + +##### 功能流程: + +1. **读取代理配置** + - 调用 `path_call()` 获取代理请求配置 `d` + +2. **构建请求头** + - 复制原始请求头 → `reqH` + - 若配置中包含 `user` 和 `password`,创建 `BasicAuth` 实例 + - 若配置中有 `headers`,将其合并到请求头中(⚠️ 原代码有误,已修正) + +3. **准备查询参数** + - 若配置中包含 `params`,提取用于 GET 查询参数 + +4. **发起异步请求** + - 使用 `aiohttp.client.request()` 发起请求 + - 方法默认为客户端请求方法(如 GET/POST),可被配置覆盖 + - 禁用自动重定向(`allow_redirects=False`) + - 原始请求体通过 `request.read()` 读取并作为 body 发送 + +5. **流式响应转发** + - 创建 `web.StreamResponse`,设置状态码与响应头 + - 准备响应(`await self.retResponse.prepare()`) + - 分块读取后端响应(每次最多 40960 字节),逐块写入客户端 + - 所有 chunk 传输完成后,输出调试日志 + +##### 异常处理: +- 未显式捕获异常,若发生网络错误会抛出异常,建议上层调用者使用 `try-except` 包裹。 + +##### 日志输出: +```log +DEBUG: proxyProcessor: data=%s # 输出代理配置数据 +DEBUG: proxy: datahandle() finish # 表示代理完成 +``` + +> ❗ 原代码问题修复: +> - `paams` → `params`(拼写错误) +> - `g.get('headers')` → `d.get('headers')`(应是 `d` 不是 `g`) +> - `regH.update(...)` → `reqH.update(...)`(变量名错误) + +✅ 修正后关键片段: +```python +if d.get('headers'): + reqH.update(d['headers']) +params = None +if d.get('params'): + params = d['params'] # 原代码为 params=params 错误 +``` + +--- + +### `def setheaders(self)` + +占位方法,目前为空实现。 + +> 📌 作用:可能预留用于未来自定义响应头处理逻辑,当前无实际行为。 + +--- + +## 典型应用场景 + +1. **API 网关中的反向代理模块** +2. **内网服务暴露接口时的身份代理** +3. **动态路由 + 认证透传** +4. **前端请求经由后端代理避免 CORS** + +--- + +## 配置结构示例(JSON 模板) + +假设模板字符串为: +```jinja2 +{ + "url": "https://external-api.com{{ path }}", + "method": "{{ method | default('GET') }}", + "user": "{{ username }}", + "password": "{{ password }}", + "headers": { + "Authorization": "Bearer {{ token }}" + }, + "params": { + "limit": 100 + } +} +``` + +配合上下文变量(`run_ns`)即可动态生成请求配置。 + +--- + +## 已知问题与改进建议 + +| 问题 | 描述 | 建议 | +|------|------|-------| +| 变量名拼写错误 | `paams`, `g.get`, `regH` | 修复为 `params`, `d.get`, `reqH` | +| 缺少异常处理 | 网络请求失败可能导致服务崩溃 | 添加 `try...except` 并返回适当错误响应 | +| 无超时控制 | `client.request()` 未设置超时 | 添加 `timeout=aiohttp.ClientTimeout(...)` | +| 不支持 HTTPS 客户端验证配置 | 固定使用默认连接池 | 可扩展支持 SSLContext 或自定义 connector | + +--- + +## 示例配置调用流程 + +```python +# 假设路由匹配触发 ProxyProcessor +request = web.Request(...) # 来自客户端 +processor = ProxyProcessor() +await processor.datahandle(request) +# 结果:流式转发远程服务响应给客户端 +``` + +--- + +## 总结 + +`ProxyProcessor` 提供了一个灵活、可扩展的异步代理解决方案,支持模板驱动的请求配置,适用于现代微服务架构中的中间层代理需求。需注意修复现有代码中的拼写错误,并增加健壮性处理(如超时、异常兜底等)以提升生产可用性。 + +--- + +> ✅ 文档版本:v1.0 +> 📅 最后更新:2025-04-05 +> © 项目组公共组件团队 \ No newline at end of file diff --git a/aidocs/real_ip.md b/aidocs/real_ip.md new file mode 100644 index 0000000..a45fdb7 --- /dev/null +++ b/aidocs/real_ip.md @@ -0,0 +1,154 @@ +# `real_ip_middleware` 技术文档 + +## 概述 + +`real_ip_middleware` 是一个用于提取客户端真实 IP 地址的 Aiohttp 中间件工厂函数。在使用反向代理(如 Nginx、负载均衡器等)时,直接通过 `request.remote` 获取的可能是代理服务器的 IP,而非最终用户的实际 IP。该中间件通过检查特定的 HTTP 请求头字段,尝试还原客户端的真实 IP 地址,并将其挂载到 `request['client_ip']` 中供后续处理使用。 + +--- + +## 安装依赖 + +确保项目中已安装以下依赖: + +```bash +pip install aiohttp aiohttp-middlewares +``` + +--- + +## 导入模块 + +```python +from appPublic.log import exception, debug, error +from aiohttp import web +from aiohttp_middlewares.annotations import DictStrStr, Handler, Middleware +``` + +> ⚠️ 注意:`appPublic.log` 为自定义日志模块,当前代码中未实际调用日志函数,但保留了导入。 + +--- + +## 函数定义 + +### `real_ip_middleware() -> Middleware` + +返回一个 Aiohttp 兼容的中间件处理器,用于设置请求中的客户端真实 IP。 + +#### 返回值 + +- **类型**:`Middleware` +- **说明**:符合 Aiohttp 中间件协议的异步处理函数。 + +--- + +## 内部中间件逻辑 + +### `middleware(request: web.Request, handler: Handler) -> web.StreamResponse` + +这是一个由 `@web.middleware` 装饰的异步中间件函数,负责处理每个进入的请求。 + +#### 参数 + +| 参数 | 类型 | 说明 | +|-----------|----------------|------| +| `request` | `web.Request` | Aiohttp 请求对象 | +| `handler` | `Handler` | 下一个处理请求的处理器(视图函数或其他中间件) | + +#### 工作流程 + +1. **初始化客户端 IP** + ```python + request['client_ip'] = request.remote + ``` + - 默认将 `request.remote`(即 TCP 连接对端 IP)作为客户端 IP。 + +2. **检查关键请求头** + - 遍历请求头,查找以下任一字段: + - `X-Forwarded-For` + - `X-real-ip` + - 这些头部通常由反向代理添加,包含原始客户端 IP。 + +3. **解析 IP 地址** + - 若匹配到上述任一头字段: + ```python + v = v.split(',')[-1].strip() + ``` + - 对多层代理情况,取逗号分隔列表中的最后一个非空 IP(最接近客户端的一跳)。 + - 去除首尾空白字符。 + - 将解析出的 IP 设置为 `request['client_ip']` 的值。 + - 找到后立即 `break`,不再检查其他头字段。 + +4. **继续处理链** + ```python + return await handler(request) + ``` + - 调用下一个处理器,并返回响应。 + +--- + +## 使用方法 + +在 Aiohttp 应用中注册此中间件: + +```python +app = web.Application(middlewares=[real_ip_middleware()]) +``` + +之后在任意处理函数中可通过如下方式获取客户端真实 IP: + +```python +async def my_handler(request): + client_ip = request.get('client_ip', 'unknown') + print(f"Client IP: {client_ip}") + return web.json_response({"ip": client_ip}) +``` + +--- + +## 示例场景 + +假设请求经过如下代理链: + +``` +Client (1.2.3.4) +→ Nginx (adds X-Forwarded-For: "1.2.3.4, 5.6.7.8") +→ Aiohttp Server +``` + +- `request.remote` 可能是 `5.6.7.8`(Nginx 出口 IP) +- 经过本中间件处理后: + - `request['client_ip'] = "1.2.3.4"` + +--- + +## 安全注意事项 + +- ✅ **推荐做法**:仅在受信任的代理环境下启用此中间件(例如内部网络或已验证代理头的网关)。 +- ❌ **风险提示**:如果允许外部用户随意设置 `X-Forwarded-For` 或 `X-real-ip`,可能导致 IP 欺骗。 +- 🔐 建议结合白名单机制,在可信代理节点才解析这些头字段。 + +--- + +## 扩展建议 + +可扩展支持更多标准头字段,例如: + +- `CF-Connecting-IP` (Cloudflare) +- `True-Client-IP` (某些 CDN) +- `X-Original-Forwarded-For` + +也可增加配置参数以灵活指定信任层级和头字段列表。 + +--- + +## 版本信息 + +- **语言**:Python 3.7+ +- **框架**:Aiohttp >= 3.0 +- **兼容性**:支持 `aiohttp-middlewares` 类型注解 + +--- + +## 许可证 + +请根据项目实际情况填写许可证信息(如 MIT、Apache 2.0 等)。 \ No newline at end of file diff --git a/aidocs/restful.md b/aidocs/restful.md new file mode 100644 index 0000000..7cb745d --- /dev/null +++ b/aidocs/restful.md @@ -0,0 +1,352 @@ +# 技术文档:`DBCrud` REST API 端点实现 + +> **基于 `aiohttp` 的数据库 CRUD 操作 RESTful 接口封装** + +--- + +## 概述 + +该模块提供了一个基于 `aiohttp` 的异步 Web 框架的 RESTful 接口抽象类 `DBCrud`,用于对指定数据库表执行标准的增删改查(CRUD)操作。它继承自通用的 `RestEndpoint` 类,并通过 `sqlor` ORM 工具与数据库交互。 + +主要特性: +- 支持标准 HTTP 方法:`GET`, `POST`, `PUT`, `DELETE`, `OPTIONS` +- 自动路由分发请求到对应方法 +- 使用配置化数据库连接池(`DBPools`) +- 统一返回 JSON 格式响应(成功/错误) +- 集成异常处理和日志输出 + +--- + +## 依赖说明 + +### 第三方库 +| 包 | 用途 | +|----|------| +| `aiohttp` | 异步 Web 框架,处理 HTTP 请求/响应 | +| `sqlor.dbpools.DBPools` | 数据库连接池管理器及 ORM 上下文支持 | + +### 内部模块 +| 模块 | 用途 | +|------|------| +| `appPublic.dictObject.multiDict2Dict` | 将 `MultiDictProxy` 转换为普通字典 | +| `appPublic.jsonConfig.getConfig` | (未使用)预留配置加载功能 | +| `.error.Error`, `.error.Success` | 自定义响应结构体:错误与成功封装 | + +--- + +## 核心类定义 + +### `RestEndpoint` + +一个通用的 REST 端点基类,负责方法注册与请求调度。 + +#### 属性 +- `methods (dict)`:存储 HTTP 方法名与其对应处理函数的映射(如 `'GET': self.get`) + +#### 常量 +```python +DEFAULT_METHODS = ('GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE') +``` + +#### 方法 + +##### `__init__(self)` +初始化时自动扫描子类中是否存在小写命名的方法(如 `get`, `post`),若存在则调用 `register_method` 注册到 `self.methods` 中。 + +##### `register_method(self, method_name: str, method: callable)` +将给定的方法注册到内部方法字典中,键为大写的 HTTP 方法名。 + +**参数:** +- `method_name`: HTTP 方法名称,例如 `'GET'` +- `method`: 可调用的异步处理函数 + +##### `dispatch(self) -> Awaitable[Response]` +根据当前请求的 `request.method` 分发到对应处理方法。 + +**逻辑流程:** +1. 获取当前请求方法的小写形式 +2. 查找已注册的方法 +3. 若无匹配方法,抛出 `HTTPMethodNotAllowed` +4. 否则调用并返回对应方法的结果 + +**返回值:** +- `Response` 对象(通常为 `json_response`) + +**异常:** +- `HTTPMethodNotAllowed`:当请求方法未被实现时抛出 + +--- + +### `DBCrud(RestEndpoint)` + +继承自 `RestEndpoint`,实现针对特定数据库表的 CRUD 操作。 + +#### 构造函数 +```python +def __init__(self, request: Request, dbname: str, tablename: str, id=None) +``` + +**参数:** +- `request (aiohttp.web_request.Request)`:当前 HTTP 请求对象 +- `dbname (str)`:数据库标识名(需在配置中定义) +- `tablename (str)`:目标数据表名 +- `id (optional)`:可选资源 ID(当前未实际使用) + +**初始化行为:** +- 调用父类构造函数 +- 初始化数据库连接池实例 `DBPools()` +- 设置上下文属性以便后续操作使用 + +--- + +## 支持的 HTTP 方法 + +### `OPTIONS` — 获取元信息 +```python +async def options(self) -> Response +``` +获取指定表的元数据结构(字段、类型等)。 + +**行为:** +- 使用 `sor.I(tablename)` 获取表结构信息(I 表示 "Inspect") +- 成功返回元数据 +- 失败返回错误码 `'metaerror'` + +**返回示例(成功):** +```json +{ + "ret": "success", + "data": { + "fields": [ + {"name": "id", "type": "int"}, + {"name": "name", "type": "varchar"} + ] + } +} +``` + +**错误响应:** +```json +{ + "ret": "error", + "errno": "metaerror", + "msg": "get metadata error" +} +``` + +--- + +### `GET` — 查询数据 +```python +async def get(self) -> Response +``` +从表中查询符合条件的数据记录。 + +**行为:** +- 解析 URL 查询参数(`request.query`)为标准字典 +- 调用 `sor.R(tablename, conditions)` 执行读取操作(R 表示 "Read") +- 返回查询结果列表或单条记录 + +**输入示例:** +``` +GET /api/user?age__gt=18&name__like=john +``` + +**返回示例:** +```json +{ + "ret": "success", + "data": [ + {"id": 1, "name": "John", "age": 25} + ] +} +``` + +**错误响应:** +```json +{ + "ret": "error", + "errno": "search error", + "msg": "search error" +} +``` + +--- + +### `POST` — 插入数据 +```python +async def post(self) -> Response +``` +向表中插入新记录。 + +**行为:** +- 读取表单格式请求体(`await request.post()`) +- 转换为字典格式 +- 调用 `sor.C(tablename, data)` 执行创建操作(C 表示 "Create") +- 返回插入后的主键或其他信息 + +**输入示例(Form Data):** +``` +name=John&age=30 +``` + +**返回示例:** +```json +{ + "ret": "success", + "data": {"id": 123} +} +``` + +**错误响应:** +```json +{ + "ret": "error", + "errno": "add error", + "msg": "add error" +} +``` + +--- + +### `PUT` — 更新数据 +```python +async def put(self) -> Response +``` +更新已有记录。 + +**行为:** +- 读取表单请求体作为更新字段 +- 调用 `sor.U(tablename, data)` 执行更新操作(U 表示 "Update") +- 成功返回空格字符串(应优化为更合理的内容) + +⚠️ **注意:** 当前实现无法指定更新哪条记录(缺少 `WHERE` 条件),可能导致全表更新! + +**建议改进:** +- 应结合路径参数或 body 中包含主键进行条件更新 + +**返回示例:** +```json +{ + "ret": "success", + "data": " " +} +``` + +**错误响应:** +```json +{ + "ret": "error", + "errno": "update error", + "msg": "update error" +} +``` + +--- + +### `DELETE` — 删除数据 +```python +async def delete(self, request: Request, instance_id) -> Response +``` + +> ❗ **存在问题:** 方法签名不一致! +> 实际被调用时不会传入 `request` 和 `instance_id`,且未重写 `dispatch` 来传递这些参数。此方法目前无法正常工作。 + +**预期行为:** +- 通过查询参数或路径变量确定删除条件 +- 调用 `sor.D(tablename, conditions)` 删除记录(D 表示 "Delete") + +**当前问题:** +- 多余的参数 `request`, `instance_id` 不会被自动传入 +- 错误码拼写错误:`erron` → 应为 `errno` +- 删除逻辑仍依赖 `query` 参数,但缺乏安全校验 + +**修复建议:** +```python +async def delete(self): + try: + ns = multiDict2Dict(self.request.query) + if not ns: + return json_response(Error(errno='delete_error', msg='no condition provided')) + with self.db.sqlorContext(self.dbname) as sor: + d = await sor.D(self.tablename, ns) + return json_response(Success(d)) + except Exception as e: + traceback.print_exc() + return json_response(Error(errno='delete_error', msg='delete failed')) +``` + +--- + +## 使用示例 + +假设你有一个名为 `users` 的表,在 `mydb` 数据库中: + +```python +from aiohttp import web +from .dbcrud import DBCrud + +async def handle_user_crud(request): + dbname = "mydb" + tablename = "users" + crud = DBCrud(request, dbname, tablename) + return await crud.dispatch() + +# 在路由中注册 +app.router.add_route('*', '/api/users', handle_user_crud) +app.router.add_route('*', '/api/users/{id}', handle_user_crud) +``` + +--- + +## 异常处理 + +所有方法均使用 `try...except` 包裹核心逻辑: + +- 打印异常信息至控制台 +- 输出完整堆栈跟踪(`traceback.print_exc()`) +- 返回统一格式的 JSON 错误响应 + +> ⚠️ 注意:生产环境中不应暴露详细错误信息给客户端 + +--- + +## 已知问题与改进建议 + +| 问题 | 描述 | 建议修复 | +|------|------|---------| +| `delete()` 方法参数错误 | 多余参数导致无法正确调用 | 移除额外参数,保持无参签名 | +| `errno` 拼写错误 | `erron='delete error'` | 改为 `errno` | +| `PUT` 缺少更新条件 | 易造成误删/误更 | 结合路径 ID 或强制要求 `id` 字段 | +| 成功响应内容不合理 | 如 `Success(' ')` | 改为 `{ "updated": 1 }` 等有意义数据 | +| 日志仅打印未记录 | 使用 `logging` 替代 `print` | 引入 logger 模块 | +| 未验证输入合法性 | 可能引发 SQL 注入风险 | 添加字段白名单或校验机制 | + +--- + +## 总结 + +`DBCrud` 是一个轻量级的数据库 REST 接口封装,适用于快速构建基于表的 API 接口。其设计简洁、扩展性强,但在健壮性和安全性方面仍有提升空间。 + +适合场景: +- 快速原型开发 +- 内部管理系统后端 +- 动态表驱动接口服务 + +不适合场景: +- 高安全性要求系统 +- 复杂业务逻辑接口 +- 需要精细权限控制的环境 + +--- + +## 版本信息 + +- **语言**:Python 3.7+ +- **框架**:aiohttp >= 3.0 +- **作者**:Auto-generated from source code +- **最后更新**:2025-04-05 + +--- + +📌 *注:本技术文档由代码反向生成,建议结合实际项目需求补充单元测试和接口文档(如 Swagger/OpenAPI)*。 \ No newline at end of file diff --git a/aidocs/serverenv.md b/aidocs/serverenv.md new file mode 100644 index 0000000..3345a93 --- /dev/null +++ b/aidocs/serverenv.md @@ -0,0 +1,207 @@ +# Server Environment Management Module + +本模块提供了一个用于管理服务器运行环境变量的工具,以及客户端类型识别功能。通过单例模式确保全局环境配置的一致性,并支持基于 HTTP 请求头的用户代理(User-Agent)解析来判断客户端设备类型。 + +--- + +## 模块依赖 + +```python +from appPublic.Singleton import SingletonDecorator +from appPublic.dictObject import DictObject +import re +``` + +- `SingletonDecorator`:来自 `appPublic` 包的单例装饰器,用于保证 `ServerEnv` 类在整个应用中只有一个实例。 +- `DictObject`:字典式对象封装类,允许通过属性或键值方式访问数据。 +- `re`:Python 正则表达式库,用于匹配 User-Agent 字符串。 + +--- + +## 核心类:`ServerEnv` + +```python +@SingletonDecorator +class ServerEnv(DictObject): + pass +``` + +### 功能说明 + +`ServerEnv` 是一个继承自 `DictObject` 的类,并使用 `@SingletonDecorator` 装饰为单例类。 + +- **继承特性**: + - 继承 `DictObject` 后,该类具备类似字典的操作能力(如 `obj[key] = value`, `obj.get(key)` 等),同时也支持属性式访问(如 `obj.key`)。 +- **单例模式**: + - 使用 `@SingletonDecorator` 确保整个应用程序生命周期中仅存在一个 `ServerEnv` 实例,适合存储和共享全局配置或状态信息。 + +> ⚠️ 当前实现为空类(`pass`),但其行为完全由父类 `DictObject` 和单例机制驱动。 + +--- + +## 全局环境操作函数 + +### `get_serverenv(name)` + +获取指定名称的服务器环境变量值。 + +#### 参数 + +| 参数名 | 类型 | 说明 | +|--------|--------|--------------| +| name | str | 环境变量名称 | + +#### 返回值 + +- 返回对应键的值;若不存在,则返回 `None`。 + +#### 示例 + +```python +db_host = get_serverenv("database_host") +``` + +#### 实现逻辑 + +```python +def get_serverenv(name): + g = ServerEnv() + return g.get(name) +``` + +> 自动创建或获取唯一的 `ServerEnv` 实例,并调用其 `get()` 方法。 + +--- + +### `set_serverenv(name, value)` + +设置服务器环境变量的值。 + +#### 参数 + +| 参数名 | 类型 | 说明 | +|--------|--------|------------------| +| name | str | 环境变量名称 | +| value | any | 要设置的任意值 | + +#### 示例 + +```python +set_serverenv("debug_mode", True) +``` + +#### 实现逻辑 + +```python +def set_serverenv(name, value): + g = ServerEnv() + g[name] = value +``` + +> 将键值对存储在单例 `ServerEnv` 实例中,可供后续全局访问。 + +--- + +## 客户端类型识别功能 + +### `clientkeys` + +预定义的用户代理关键字与客户端类型的映射表。 + +```python +clientkeys = { + "iPhone": "iphone", + "iPad": "ipad", + "Android": "androidpad", + "Windows Phone": "winphone", + "Windows NT[.]*Win64; x64": "pc", +} +``` + +| User-Agent 关键词 | 映射类型 | +|-------------------------------|-------------| +| `iPhone` | `iphone` | +| `iPad` | `ipad` | +| `Android` | `androidpad`| +| `Windows Phone` | `winphone` | +| `Windows NT.*Win64; x64` | `pc` | + +> 支持正则表达式匹配(例如最后一个条目),可用于更精确地识别桌面 Windows 浏览器。 + +--- + +### `getClientType(request)` + +根据 HTTP 请求中的 `User-Agent` 头部判断客户端设备类型。 + +#### 参数 + +| 参数名 | 类型 | 说明 | +|----------|------------|----------------------------| +| request | object | HTTP 请求对象,需含 headers 属性 | + +#### 返回值 + +| 返回值 | 说明 | +|-------------|--------------------------| +| `iphone` | 来自 iPhone 设备 | +| `ipad` | 来自 iPad 设备 | +| `androidpad`| 来自 Android 平板/手机 | +| `winphone` | 来自 Windows Phone | +| `pc` | 桌面浏览器(默认 fallback)| + +#### 实现逻辑 + +```python +def getClientType(request): + agent = request.headers.get('user-agent', '') + for k in clientkeys.keys(): + m = re.findall(k, agent) + if len(m) > 0: + return clientkeys[k] + return 'pc' +``` + +#### 工作流程 + +1. 获取请求头中的 `User-Agent` 字符串。 +2. 遍历 `clientkeys` 中的每个正则模式。 +3. 使用 `re.findall()` 进行匹配,若成功则返回对应的客户端类型。 +4. 若无任何匹配项,默认返回 `'pc'`。 + +#### 示例用法 + +```python +client_type = getClientType(request) +if client_type == 'iphone': + # 返回移动端页面 + render_mobile_page() +``` + +--- + +## 使用场景建议 + +- **环境配置管理**:将数据库连接、调试开关、API 密钥等配置存入 `ServerEnv`,通过 `set_serverenv()` 初始化,在各模块中用 `get_serverenv()` 读取。 +- **动态行为控制**:根据 `getClientType()` 返回结果调整响应内容格式(如适配移动/PC 页面)。 +- **日志与监控**:记录不同客户端的行为差异,辅助分析用户分布。 + +--- + +## 注意事项 + +1. `clientkeys` 中的键是正则表达式,请确保语法正确,避免误匹配。 +2. 当前 `getClientType()` 对于大多数现代浏览器未明确覆盖的情况统一归为 `'pc'`,可根据需要扩展规则。 +3. `ServerEnv` 单例在进程内有效,分布式部署时需配合外部配置中心使用。 + +--- + +## 扩展建议 + +- 添加清除环境变量的方法(如 `clear_serverenv(name)`)。 +- 增加更多设备识别规则(如 macOS Safari、微信内置浏览器等)。 +- 提供 `ServerEnv` 初始化加载配置文件的功能。 + +--- + +✅ **总结**:本模块简洁高效地实现了服务端环境变量管理和基础客户端识别功能,适用于 Web 应用开发中的通用支撑层设计。 \ No newline at end of file diff --git a/aidocs/sqldsProcessor.md b/aidocs/sqldsProcessor.md new file mode 100644 index 0000000..c288935 --- /dev/null +++ b/aidocs/sqldsProcessor.md @@ -0,0 +1,303 @@ +# SQL 数据源处理器技术文档 + +## 概述 + +`SQLDataSourceProcessor` 是一个基于 `DataSourceProcessor` 的子类,用于处理以 `.sqlds` 格式定义的 SQL 数据源。它允许通过配置文件描述 SQL 查询、参数和数据库连接,并动态执行查询获取数据。 + +该模块主要用于从数据库中提取结构化数据,支持普通查询、分页查询以及自动推导返回字段结构(`datadesc`),并能将推导结果持久化回源文件。 + +--- + +## 依赖模块 + +```python +import codecs +from .dsProcessor import DataSourceProcessor +from appPublic.jsonConfig import getConfig +from sqlor.dbpools import DBPools +import json +``` + +- `codecs`: 用于安全地读写带编码的文件(如 UTF-8)。 +- `DataSourceProcessor`: 抽象基类,定义了数据源处理器的标准接口。 +- `getConfig` (from `appPublic.jsonConfig`): 获取全局配置对象。 +- `DBPools` (from `sqlor.dbpools`): 提供异步数据库连接池及 SQL 执行装饰器。 +- `json`: 用于序列化/反序列化 JSON 数据。 + +--- + +## 配置文件格式(`.sqlds`) + +`.sqlds` 文件是标准的 JSON 格式,描述了一个 SQL 查询的数据源信息: + +```json +{ + "sqldesc": { + "sql_string": "select * from dbo.stock_daily_hist where stock_num=${stock_num}$ order by trade_date desc", + "db": "mydb", + "sortfield": "trade_date" + }, + "arguments": [ + { + "name": "stock_num", + "type": "str", + "iotype": "text", + "default": "600804" + } + ], + "datadesc": [] +} +``` + +### 字段说明 + +| 字段 | 类型 | 必需 | 描述 | +|------|------|------|------| +| `sqldesc.sql_string` | string | 是 | 实际执行的 SQL 查询语句,支持 `${param}$` 形式的参数占位符。 | +| `sqldesc.db` | string | 是 | 数据库连接名称(在 DBPools 中注册过的别名)。 | +| `sqldesc.sortfield` | string | 否 | 排序字段,可用于前端排序或分页逻辑参考。 | +| `arguments` | array | 否 | 定义传入 SQL 的参数列表,每个参数包含:`name`, `type`, `iotype`, `default`。 | +| `datadesc` | array | 否 | 描述查询结果字段结构的数组。若为空,则首次访问时自动推导并写回文件。 | + +> ⚠️ 注意:`${param}$` 是模板语法,运行时会被 `ns` 上下文中的对应值替换。 + +--- + +## 类定义 + +```python +class SQLDataSourceProcessor(DataSourceProcessor): +``` + +继承自 `DataSourceProcessor`,实现针对 `.sqlds` 类型数据源的具体行为。 + +--- + +## 方法说明 + +### `isMe(name) -> bool` + +判断当前处理器是否适用于指定类型的数据源。 + +#### 参数 +- `name` (`str`):数据源类型名。 + +#### 返回值 +- `True` 当且仅当 `name == 'sqlds'`。 + +#### 示例 +```python +if SQLDataSourceProcessor.isMe('sqlds'): + processor = SQLDataSourceProcessor(...) +``` + +--- + +### `getArgumentsDesc(dict_data, ns, request) -> list or None` + +获取数据源所需的输入参数描述。 + +#### 参数 +- `dict_data` (`dict`):解析后的 `.sqlds` 文件内容。 +- `ns` (`dict`):命名空间(通常为请求参数)。 +- `request` (`Request`):HTTP 请求对象(可选用途扩展)。 + +#### 返回值 +- `list`:参数描述数组(来自 `arguments` 字段)。 +- `None`:若未定义 `arguments`。 + +#### 示例返回 +```python +[ + { + "name": "stock_num", + "type": "str", + "iotype": "text", + "default": "600804" + } +] +``` + +--- + +### `async getDataDesc(dict_data, ns, request) -> list` + +异步获取查询结果的字段元信息(即 `datadesc`)。如果尚未生成,则自动从数据库中推导并保存到原文件。 + +#### 参数 +- `dict_data` (`dict`):原始数据源配置。 +- `ns` (`dict`):参数上下文,用于填充 SQL 占位符。 +- `request` (`Request`):请求对象。 + +#### 行为流程 +1. 若 `datadesc` 已存在且非空,直接返回。 +2. 否则: + - 调用 `runSQLResultFields` 获取 SQL 查询的实际返回字段(不含 `_row_id`)。 + - 将推导出的字段结构写入 `dict_data['datadesc']`。 + - 序列化整个 `dict_data` 回源文件(路径为 `self.src_file`),保持缩进与中文不转义。 + +#### 使用的装饰器 +- `@pool.runSQLResultFields`:执行 SQL 并返回字段元信息(列名、类型等)。 + +#### 返回值 +- `list`:字段描述列表,每项为字段元数据字典(如 `{ "name": "trade_date", "type": "datetime" }`)。 + +#### 示例输出 +```python +[ + {"name": "stock_num", "type": "string"}, + {"name": "trade_date", "type": "date"}, + {"name": "close_price", "type": "float"} +] +``` + +> ✅ 自动持久化:一旦推导完成,会更新 `.sqlds` 文件以避免重复分析。 + +--- + +### `async getData(dict_data, ns, request) -> list` + +执行完整 SQL 查询并返回所有结果记录。 + +#### 参数 +- `dict_data` (`dict`):数据源定义。 +- `ns` (`dict`):参数上下文。 +- `request` (`Request`):请求对象。 + +#### 行为 +- 使用 `@pool.runSQL` 执行 `sqldesc.sql_string`。 +- 替换 `${}` 中的参数。 +- 返回所有行组成的列表。 + +#### 使用的装饰器 +- `@pool.runSQL`:执行查询并返回异步迭代器,转换为列表。 + +#### 返回值 +- `list[dict]`:每条记录为一个字典。 + +#### 示例返回 +```python +[ + {"stock_num": "600804", "trade_date": "2023-09-01", "close_price": 12.5}, + {"stock_num": "600804", "trade_date": "2023-08-31", "close_price": 12.3} +] +``` + +--- + +### `async getPagingData(dict_data, ns, request) -> dict` + +执行分页查询,返回带分页信息的结果集。 + +#### 参数 +- `dict_data` (`dict`) +- `ns` (`dict`):必须包含分页参数,如 `page`, `pageSize` 或等效字段。 +- `request` (`Request`) + +#### 行为 +- 使用 `@pool.runSQLPaging` 执行分页 SQL。 +- 自动处理偏移量和限制数量。 +- 返回结构化分页响应。 + +#### 使用的装饰器 +- `@pool.runSQLPaging`:支持分页的 SQL 执行器,返回包含 `data`, `total`, `page`, `pageSize` 的对象。 + +#### 返回值(示例) +```json +{ + "data": [...], + "total": 100, + "page": 1, + "pageSize": 10 +} +``` + +--- + +## 内部机制说明 + +### SQL 模板替换 + +使用 `${param}$` 语法进行变量注入,例如: + +```sql +select * from table where id = ${user_id}$ +``` + +运行时由 `DBPools` 的装饰器根据 `ns` 字典替换为实际值。 + +### 自动字段推导与缓存 + +首次访问 `getDataDesc` 时,若 `datadesc` 为空,系统将: +1. 执行 SQL 获取字段结构; +2. 过滤掉内部字段 `_row_id`; +3. 写入本地 `.sqlds` 文件以供后续使用。 + +这提高了性能并减少了对数据库元数据的频繁访问。 + +### 编码与文件写入 + +使用 `codecs.open(..., encoding=config.website.coding)` 确保文件按项目设定编码保存(通常是 UTF-8),并通过 `ensure_ascii=False` 保留中文字符。 + +--- + +## 使用场景 + +适合以下情况: +- 需要灵活配置 SQL 查询作为 API 输出。 +- 前端需要知道返回字段结构(用于表格渲染)。 +- 支持参数化查询和分页。 +- 开发阶段快速原型设计,无需编写后端代码。 + +--- + +## 示例 `.sqlds` 文件应用 + +假设文件名为 `stock_history.sqlds`,配置如下: + +```json +{ + "sqldesc": { + "sql_string": "SELECT stock_num, trade_date, open, high, low, close FROM stock_daily WHERE stock_num = ${code}$ ORDER BY trade_date DESC", + "db": "financial_db", + "sortfield": "trade_date" + }, + "arguments": [ + { + "name": "code", + "type": "str", + "iotype": "text", + "default": "600804" + } + ], + "datadesc": [] +} +``` + +调用 `getDataDesc()` 后,`datadesc` 将被自动填充为: + +```json +[ + {"name": "stock_num", "type": "string"}, + {"name": "trade_date", "type": "date"}, + {"name": "open", "type": "float"}, + {"name": "high", "type": "float"}, + {"name": "low", "type": "float"}, + {"name": "close", "type": "float"} +] +``` + +--- + +## 注意事项 + +1. **安全性**:确保 `${}` 参数经过验证,防止 SQL 注入(建议配合白名单或类型校验)。 +2. **性能**:大数据集应优先使用 `getPagingData`。 +3. **文件权限**:写回 `.sqlds` 文件时需保证进程有写权限。 +4. **缓存失效**:修改 SQL 后建议手动清除 `datadesc` 以触发重新推导。 + +--- + +## 总结 + +`SQLDataSourceProcessor` 提供了一种声明式的方式来定义基于 SQL 的数据接口,结合配置文件与异步数据库访问,实现了高效、可维护的数据服务层组件。特别适用于报表、配置化页面、低代码平台等场景。 \ No newline at end of file diff --git a/aidocs/uriop.md b/aidocs/uriop.md new file mode 100644 index 0000000..a58b798 --- /dev/null +++ b/aidocs/uriop.md @@ -0,0 +1,348 @@ +# `URIOp` 类技术文档 + +## 概述 + +`URIOp` 是一个用于处理 URI(统一资源标识符)与文件系统操作的 Python 类,主要用于在指定网站根目录范围内安全地进行文件和目录的读写、创建、重命名、删除等操作。该类通过配置文件获取网站根路径,并确保所有操作均限制在该目录范围内,防止越权访问。 + +此外,还定义了一个自定义异常 `URIopException`,用于统一抛出与 URI 操作相关的错误。 + +--- + +## 依赖模块 + +```python +import os +import codecs +from appPublic.jsonConfig import getConfig +from appPublic.folderUtils import folderInfo +``` + +- `os`: 提供操作系统接口,用于路径拼接、目录创建、文件重命名等。 +- `codecs`: 以指定编码方式打开和读写文本文件。 +- `appPublic.jsonConfig.getConfig`: 获取全局 JSON 配置对象。 +- `appPublic.folderUtils.folderInfo`: 获取指定路径下的文件/目录列表信息。 + +--- + +## 自定义异常:`URIopException` + +### 描述 + +表示 URI 操作过程中发生的错误,包含错误类型和详细消息。 + +### 构造函数 + +```python +def __init__(self, errtype, errmsg) +``` + +#### 参数: +- `errtype` (str): 错误类型标识,如 `'url scope error'` +- `errmsg` (str): 错误描述或触发错误的 URI + +#### 示例: +```python +raise URIopException('url scope error', '/../malicious') +``` + +### 方法 + +| 方法 | 说明 | +|------|------| +| `__str__()` | 返回格式化字符串:`errtype=xxx,errmsg=xxx` | + +--- + +## 核心类:`URIOp` + +### 描述 + +封装了基于 URI 的安全文件系统操作,所有操作都会被限制在配置中定义的 `website.root` 目录下。 + +### 初始化方法 + +```python +def __init__(self) +``` + +#### 功能: +- 加载全局配置:`getConfig()` +- 设置 `realPath` 为网站根目录的绝对路径 + +#### 属性初始化: +- `self.conf`: 配置对象(通常来自 `jsonConfig`) +- `self.realPath`: 网站根目录的绝对路径(`os.path.abspath(conf.website.root)`) + +> ⚠️ 要求配置中存在 `website.root` 和 `website.coding` 字段。 + +--- + +## 公共方法 + +--- + +### `abspath(uri=None)` + +将相对 URI 转换为安全的绝对文件系统路径。 + +#### 参数: +- `uri` (str, 可选): 相对路径,例如 `"images/logo.png"` 或 `"/css/style.css"` + +#### 返回值: +- (str) 对应的绝对路径字符串 + +#### 异常: +- 若路径超出允许范围(即不在 `realPath` 下),抛出 `URIopException('url scope error', uri)` + +#### 实现逻辑: +1. 从 `conf.website.root` 开始构建基础路径 +2. 如果 `uri` 不为空且以 `/` 开头,则去除开头斜杠 +3. 使用 `os.path.join` 将 URI 分段拼接到根路径上 +4. 调用 `os.path.abspath()` 规范化路径 +5. 检查生成路径是否在 `realPath` 范围内(防止路径穿越攻击) + +#### 示例: +```python +op = URIOp() +path = op.abspath("/uploads/file.txt") +# 结果类似:/var/www/uploads/file.txt(前提是根目录为 /var/www) +``` + +--- + +### `fileList(uri='')` + +列出指定 URI 所指向目录中的所有文件和子目录。 + +#### 参数: +- `uri` (str): 要列出内容的目录 URI,默认为根目录 + +#### 返回值: +字典结构: +```python +{ + 'total': int, # 文件总数 + 'rows': [ # 文件/目录列表 + { + 'id': str, # 文件路径 ID(路径分隔符替换为 '_#_') + 'text': str, # 显示名称 + 'type': 'dir'|'file', + 'mtime': float, # 修改时间戳 + 'size': int, # 文件大小(目录为 0) + 'state': 'closed' if type=='dir' else None + }, + ... + ] +} +``` + +> 注:此方法依赖 `folderInfo(root_path, sub_uri)` 返回可迭代的文件信息。 + +#### 特殊处理: +- 所有目录项添加 `'state': 'closed'` +- `id` 中的 `/` 被替换为 `_#_`,便于前端解析使用 + +#### 示例: +```python +files = op.fileList("/docs") +print(files['total']) # 输出文件数量 +``` + +--- + +### `mkdir(at_uri, name)` + +在指定 URI 对应的目录下创建新目录。 + +#### 参数: +- `at_uri` (str): 父目录的 URI,如 `/projects` +- `name` (str): 新目录名称,如 `'new_folder'` + +#### 实现步骤: +1. 使用 `abspath(at_uri)` 获取父目录绝对路径 +2. 使用 `os.path.join()` 拼接完整路径 +3. 调用 `os.mkdir(p)` 创建目录 + +#### 示例: +```python +op.mkdir("/data", "backup") +# 在 data 目录下创建 backup 子目录 +``` + +--- + +### `rename(uri, newname)` + +重命名文件或目录。 + +#### 参数: +- `uri` (str): 原始文件/目录的 URI +- `newname` (str): 新的名字(仅名字,不含路径) + +#### 注意事项: +- 不支持跨目录移动,只能改名 +- 新名称不能包含路径分隔符 + +#### 实现步骤: +1. 获取原路径绝对地址 +2. 获取其所在目录 +3. 构造新路径:`os.path.join(dirname, newname)` +4. 调用 `os.rename(old_path, new_path)` + +#### 示例: +```python +op.rename("/old_name.txt", "new_name.txt") +``` + +--- + +### `delete(uri)` + +删除指定 URI 指向的文件。 + +#### 参数: +- `uri` (str): 要删除的文件 URI + +#### 行为: +- 仅支持删除**文件** +- 不支持删除非空目录(若需删除目录,请先清空) + +> 如需删除目录,建议扩展功能或使用其他工具。 + +#### 示例: +```python +op.delete("/temp/unwanted.log") +``` + +--- + +### `read(uri)` + +读取指定 URI 指向的文本文件内容。 + +#### 参数: +- `uri` (str): 文件 URI + +#### 返回值: +- (str) 文件内容字符串 + +#### 编码: +- 使用配置项 `conf.website.coding` 指定编码(如 `'utf-8'`) +- 使用 `codecs.open(..., 'r', encoding)` 安全读取 + +#### 示例: +```python +content = op.read("/config/settings.json") +``` + +--- + +### `save(uri, data)` + +保存数据到指定 URI 的文件中(覆盖写入)。 + +#### 参数: +- `uri` (str): 目标文件 URI +- `data` (str): 要写入的字符串内容 + +#### 行为: +- 若文件已存在则覆盖 +- 若路径不存在会引发 OSError(建议提前创建目录) + +#### 编码: +- 使用 `conf.website.coding` 进行编码写入 + +#### 示例: +```python +op.save("/notes.txt", "Hello World!") +``` + +--- + +### `write(uri, data)` + +功能与 `save(uri, data)` **完全相同**。 + +> 当前代码中 `write` 是 `save` 的重复实现,建议合并或保留其一以避免冗余。 + +#### 建议改进: +```python +def save(self, uri, data): + return self.write(uri, data) + +# 或反之 +``` + +--- + +## 安全性说明 + +- 所有路径操作都经过 `abspath()` 的边界检查,防止路径穿越(如 `../../../etc/passwd`) +- 使用 `os.path.abspath` 和前缀比对双重验证路径合法性 +- 不直接暴露底层文件系统路径给外部调用者 + +--- + +## 配置要求 + +`URIOp` 依赖以下配置项(由 `getConfig()` 提供): + +```json +{ + "website": { + "root": "/path/to/your/site/root", + "coding": "utf-8" + } +} +``` + +- `root`: 网站资源根目录(推荐使用绝对路径) +- `coding`: 文件读写的默认字符编码 + +--- + +## 使用示例 + +```python +from your_module import URIOp + +op = URIOp() + +# 列出根目录文件 +files = op.fileList("/") +print(files) + +# 创建目录 +op.mkdir("/", "new_folder") + +# 写入文件 +op.save("/new_folder/hello.txt", "Hi there!") + +# 读取文件 +text = op.read("/new_folder/hello.txt") +print(text) + +# 重命名 +op.rename("/new_folder/hello.txt", "greeting.txt") + +# 删除文件 +op.delete("/new_folder/greeting.txt") +``` + +--- + +## 已知限制与改进建议 + +| 问题 | 建议 | +|------|------| +| `write` 与 `save` 方法重复 | 合并为单一方法,或让其一作为别名 | +| 不支持递归删除目录 | 可增加 `rmdir(uri, recursive=False)` 方法 | +| `fileList` 返回结构固定,不易扩展 | 可增加参数控制返回字段或排序 | +| 异常缺少 traceback 支持 | 可继承更丰富的异常基类或记录日志 | + +--- + +## 版权与许可 + +© 2025 Your Organization. +本模块属于 `appPublic` 工具集的一部分,遵循项目整体开源协议(请参考 LICENSE 文件)。 \ No newline at end of file diff --git a/aidocs/url2file.md b/aidocs/url2file.md new file mode 100644 index 0000000..2cf1db5 --- /dev/null +++ b/aidocs/url2file.md @@ -0,0 +1,343 @@ +# `Url2File` 与 `TmplUrl2File` 技术文档 + +> **模块功能概述** +该模块提供了将 URL 映射到本地文件系统路径的工具类,主要用于 Web 服务或模板引擎中实现 URL 到文件路径的解析。支持虚拟路径映射、目录索引查找、路径继承(父级回退)以及相关资源定位等功能。 + +--- + +## 模块依赖 + +```python +import os +``` + +> 注意:`listFile()` 函数在代码中被调用但未定义,可能是外部导入函数,用于递归列出指定后缀的文件。 + +--- + +## 类一:`Url2File` + +### 功能说明 +将符合特定规则的 URL 转换为本地文件系统的绝对路径,并支持自动查找默认索引页、处理相对路径(如 `..` 和 `.`)、查询关联资源等。 + +### 构造函数:`__init__(path: str, prefix: str, indexes: list, inherit: bool = False)` + +#### 参数说明 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `path` | `str` | 根目录路径,URL 将映射到此路径下的子目录结构。 | +| `prefix` | `str` | URL 前缀,用于标识哪些 URL 应由该实例处理(目前仅存储,未直接使用)。 | +| `indexes` | `list[str]` | 索引文件名列表,例如 `['index.html', 'default.htm']`,当请求的是一个目录时,尝试返回这些文件之一。 | +| `inherit` | `bool` | 是否启用“继承模式”。若为 `True`,当当前路径无匹配文件时,会向上一级 URL 路径回溯查找。 | + +#### 示例初始化 + +```python +u2f = Url2File("/var/www/html", "/static", ["index.html"], inherit=True) +``` + +--- + +### 方法列表 + +#### 1. `realurl(url: str) -> str` + +**功能**:规范化 URL,去除 `.` 和 `..` 等逻辑路径片段。 + +**参数** +- `url` (`str`):原始 URL 路径部分(不含协议) + +**返回值** +- 规范化后的 URL 字符串 + +**算法说明** +- 分割 `/` 得到路径项 +- 移除所有 `.` 项 +- 遇到 `..` 时,与其前一项配对删除(模拟上级目录跳转) +- 最终重新拼接为标准路径 + +**示例** + +```python +u2f.realurl("a/b/../c") # → "a/c" +u2f.realurl("a/./b//c") # → "a/b//c" (注意双斜杠不会被修复) +``` + +> ⚠️ 注意:本方法不处理重复斜杠,也不验证是否存在真实路径。 + +--- + +#### 2. `url2ospath(url: str) -> str` + +**功能**:将完整 URL 转换为本地操作系统的绝对路径。 + +**参数** +- `url` (`str`):输入的 URL(可带查询参数) + +**返回值** +- 对应的本地文件系统绝对路径(尚未验证文件是否存在) + +**处理流程** +1. 去除查询字符串(`?` 后内容) +2. 去除末尾 `/` +3. 若以 `http://`, `https://`, `ws://`, `wss://` 开头,则跳过协议头(前三段:协议 + 主机 + 端口/空) +4. 使用 `os.path.join(self.rootpath, *paths)` 构建路径 +5. 返回 `abspath` 绝对路径 + +**示例** + +```python +u2f = Url2File("/var/www", "/app", ["index.html"]) +u2f.url2ospath("https://example.com/app/user/profile") +# → /var/www/user/profile +``` + +--- + +#### 3. `url2file(url: str) -> str or None` + +**功能**:根据 URL 查找对应的本地文件路径,是核心方法。 + +**参数** +- `url` (`str`):请求的 URL + +**返回值** +- 匹配的本地文件路径(`str`),否则返回 `None` + +**查找逻辑** +1. 先去除查询参数 +2. 转换为本地路径 `real_path` +3. 如果 `real_path` 是一个存在的目录: + - 遍历 `self.indexes` 中的索引文件名 + - 检查 `/` 是否存在文件,第一个存在的即返回 +4. 如果 `real_path` 是一个存在的文件 → 直接返回 +5. 如果父目录不存在 → 返回 `None` +6. 如果 `inherit=False` → 不允许继承,返回 `None` +7. 如果允许继承且路径层级 > 2: + - 删除倒数第二级路径(向上回退一级) + - 递归调用自身进行重试 +8. 所有尝试失败 → 返回 `None` + +**示例行为** + +```python +# 假设目录结构: +# /www/root/ +# └── a/ +# └── b/ +# └── index.html + +u2f = Url2File("/www/root", "/site", ["index.html"], inherit=True) +u2f.url2file("/site/a/b/") # → /www/root/a/b/index.html +u2f.url2file("/site/a/b") # → 同上(自动识别为目录) +u2f.url2file("/site/a/c") # → 尝试 /www/root/a/c → 不存在 → 回退到 /site/c?不准确! + +# 实际继承逻辑:删除倒数第二段 → /site/a/c → 删除 a → /site/c +# 即:/a/c 失败 → 尝试 /c +``` + +> 🔍 继承机制适用于某些扁平化模板 fallback 场景,但需谨慎设计路径结构避免误匹配。 + +--- + +#### 4. `relatedurl(url: str, name: str) -> str` + +**功能**:获取与当前 URL 相关的另一个资源的逻辑 URL。 + +**参数** +- `url` (`str`):当前上下文 URL +- `name` (`str`):目标资源名称(文件或子路径) + +**返回值** +- 新的逻辑 URL(规范化后) + +**逻辑** +- 若原 URL 以 `/` 结尾,先去掉末尾 `/` +- 使用 `url2ospath` 获取对应路径并判断是否为文件 +- 如果是文件,则将其所在目录作为基准路径(即去掉最后一段) +- 拼接新 `name` 成新路径 +- 调用 `realurl()` 进行规范化 + +**用途举例** + +```python +# 当前页面是 /user/profile,想引用同级的 avatar.png +relatedurl("/user/profile", "avatar.png") # → /user/avatar.png +relatedurl("/user/", "style.css") # → /user/style.css +``` + +--- + +#### 5. `relatedurl2file(url: str, name: str) -> str or None` + +**功能**:结合 `relatedurl` 和 `url2file`,查找与某 URL 相关的文件。 + +**参数** +- `url` (`str`):当前上下文 URL +- `name` (`str`):目标文件名 + +**返回值** +- 目标文件的本地路径(`str`),否则 `None` + +**内部流程** +1. 调用 `self.relatedurl(url, name)` +2. 再调用 `self.url2file(...)` 查询该路径对应的文件 + +**示例** + +```python +u2f.relatedurl2file("/user/profile", "config.json") +# → 可能返回 /www/root/user/config.json +``` + +--- + +## 类二:`TmplUrl2File` + +### 功能说明 +管理多个 `Url2File` 实例,支持多路径搜索、模板文件扩展名过滤,常用于模板或静态资源加载器。 + +### 构造函数:`__init__(paths, indexes, subffixes=['.tmpl','.ui'], inherit=False)` + +#### 参数说明 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `paths` | `List[Tuple[str, str]]` | 元组列表,每个元素为 `(本地根路径, URL前缀)` | +| `indexes` | `list[str]` | 索引文件名列表(传递给每个 `Url2File`) | +| `subffixes` | `list[str]` | 模板文件的扩展名,默认为 `['.tmpl', '.ui']` | +| `inherit` | `bool` | 是否开启继承查找(统一传给所有 `Url2File`) | + +> ❗ 注意:虽然 `prefix` 存储于 `Url2File` 中,但在当前实现中并未用于路由匹配,所有 `url2file` 请求都会遍历全部 `u2fs` 实例。 + +#### 示例初始化 + +```python +tmpl_u2f = TmplUrl2File( + paths=[ + ("/opt/templates/custom", "/theme"), + ("/opt/templates/default", "/") + ], + indexes=["index.tmpl"], + subffixes=[".tmpl", ".html"], + inherit=True +) +``` + +--- + +### 方法列表 + +#### 1. `url2file(url) -> str or None` + +**功能**:依次尝试各个 `Url2File` 实例来查找文件。 + +**逻辑** +- 遍历 `self.u2fs` +- 调用每个实例的 `url2file(url)` +- 返回第一个成功结果 +- 全部失败 → 返回 `None` + +**用途** +- 实现“自定义模板覆盖默认模板”的优先级查找机制 + +--- + +#### 2. `relatedurl(url: str, name: str) -> str or None` + +**功能**:查找第一个能生成有效相关 URL 的 `Url2File` 实例。 + +> ⚠️ 当前实现存在问题:即使某个 `relatedurl` 返回非空字符串,也可能不是合法路径或无法访问文件。 + +**建议改进**:应结合 `relatedurl2file` 思路,确保结果有意义。 + +--- + +#### 3. `list_tmpl() -> List[str]` + +**功能**:列出所有配置路径下、符合指定后缀的模板文件(绝对路径)。 + +**返回值** +- 排序后的文件路径列表(`List[str]`) + +**实现细节** +- 遍历 `self.paths` 中的每个根路径 `rp` +- 转为绝对路径 +- 调用 `listFile(p, suffixs=self.subffixes, recursive=True)` 递归查找匹配文件 +- 收集进 `ret` 列表 +- 最终排序返回 + +> 📌 依赖外部函数 `listFile(dir, suffixs=[], recursive=False)`,其功能推测如下: +> +> ```python +> def listFile(directory, suffixs=None, recursive=False): +> matches = [] +> for root, dirs, files in os.walk(directory): +> for f in files: +> if any(f.endswith(suf) for suf in suffixs): +> matches.append(os.path.join(root, f)) +> if not recursive: +> break +> return matches +> ``` + +**典型输出示例** + +```python +[ + '/opt/templates/default/home.tmpl', + '/opt/templates/default/layout.ui', + '/opt/templates/custom/theme.dark.tmpl' +] +``` + +--- + +## 使用场景示例 + +### 场景 1:Web 模板引擎路径映射 + +```python +loader = TmplUrl2File( + paths=[("/web/tmpl/custom", "/"), ("/web/tmpl/base", "/")], + indexes=["page.tmpl"], + subffixes=[".tmpl"], + inherit=True +) + +# 用户请求 /user/list +template_path = loader.url2file("/user/list") +# → 先查 custom/user/list → 存在则返回 +# → 不存在则查 base/user/list → 或继续回退至 /list(如果 inherit=True) +``` + +### 场景 2:静态资源 fallback + +```python +static = Url2File("/public", "/static", ["index.html"], inherit=True) +static.url2file("/static/js/app.js") # → /public/js/app.js +static.url2file("/static/missing.jpg") # → 查不到 → 回退到 /static → 仍失败 → None +``` + +--- + +## 注意事项与改进建议 + +| 问题 | 描述 | 建议 | +|------|------|------| +| `prefix` 未实际参与路由 | `Url2File.prefix` 仅保存未使用,可能导致误解 | 可增加前缀匹配逻辑,或移除该字段 | +| `realurl` 不处理重复斜杠 | `"a//b"` 不会被标准化为 `"a/b"` | 可添加正则替换 `re.sub(r'/+', '/', ...)` | +| `listFile` 未定义 | 导致模块不可独立运行 | 应补充定义或明确声明依赖 | +| `relatedurl` 返回可能无效 | 仅生成 URL 字符串,不保证路径存在 | 改为返回 `(url, filepath)` 或集成存在性检查 | +| 多路径查找顺序敏感 | `paths` 顺序决定优先级 | 文档中应强调顺序重要性 | + +--- + +## 版权与许可 + +© 2025 项目作者。可用于内部系统或 Web 框架中的资源映射层。请确保遵守操作系统权限与安全规范。 + +--- + +✅ **推荐用途**:轻量级模板/静态文件路由映射、开发服务器路径解析、MVC 架构视图定位组件。 \ No newline at end of file diff --git a/aidocs/utils.md b/aidocs/utils.md new file mode 100644 index 0000000..c929ac0 --- /dev/null +++ b/aidocs/utils.md @@ -0,0 +1,106 @@ +# `unicode_escape` 函数技术文档 + +## 概述 + +`unicode_escape` 是一个 Python 函数,用于将字符串中的非 ASCII 字符(Unicode 码点 ≥ 256 的字符)转换为其 Unicode 转义序列表示形式,而保留 ASCII 字符(码点 < 256)不变。 + +该函数适用于需要将包含特殊 Unicode 字符的字符串转换为可打印、可传输或兼容 ASCII 的格式的场景,例如日志记录、数据序列化或生成 JSON 安全字符串。 + +--- + +## 函数定义 + +```python +def unicode_escape(s): + x = [ch if ord(ch) < 256 else ch.encode('unicode_escape').decode('utf-8') for ch in s] + return ''.join(x) +``` + +--- + +## 参数说明 + +| 参数 | 类型 | 说明 | +|------|--------|--------------------------| +| `s` | `str` | 输入的原始字符串 | + +--- + +## 返回值 + +- **类型**:`str` +- **说明**:返回一个新字符串,其中所有 Unicode 码点大于等于 256 的字符被替换为其对应的 `\uXXXX` 或 `\UXXXXXXXX` 形式的 Unicode 转义序列,ASCII 字符保持原样。 + +--- + +## 工作原理 + +1. 遍历输入字符串 `s` 中的每一个字符 `ch`。 +2. 对每个字符: + - 如果其 Unicode 码点(通过 `ord(ch)` 获取)小于 256(即属于标准 ASCII 或 Latin-1 范围),则直接保留该字符。 + - 否则,使用 `.encode('unicode_escape')` 将该字符编码为字节形式的 Unicode 转义序列(如 `\u4e2d`),然后通过 `.decode('utf-8')` 转换回字符串。 +3. 使用 `''.join(x)` 将处理后的字符列表拼接成最终结果字符串。 + +--- + +## 示例 + +### 示例 1:基本用法 + +```python +result = unicode_escape("Hello, 世界!") +print(result) +# 输出: Hello, \u4e16\u754c! +``` + +### 示例 2:混合字符 + +```python +result = unicode_escape("Café Noël 🌍") +print(result) +# 输出: Caf\xe9 No\xebl \U0001f30d +``` + +> 注意:`é` 和 `ë` 属于 Latin-1 范围(码点 < 256),因此使用 `\x` 转义;而 🌍(地球符号)是辅助平面字符,转义为 `\U0001f30d`。 + +--- + +## 注意事项 + +- 本函数会保留所有码点小于 256 的字符(包括控制字符),不会进行额外过滤。 +- 对于非 BMP(基本多文种平面)字符(如 emoji),将生成 `\Uxxxxxxxx` 格式的转义。 +- 结果字符串为纯 ASCII 字符串,适合在仅支持 ASCII 的环境中使用。 +- 原始语义可通过 `eval()` 或 `codecs.decode(..., 'unicode_escape')` 还原(需谨慎使用)。 + +--- + +## 还原方法(反向操作) + +若需将转义字符串还原为原始 Unicode 字符串,可使用: + +```python +import codecs +original = codecs.decode(escaped_string, 'unicode_escape') +``` + +--- + +## 应用场景 + +- 构建兼容 ASCII 的日志输出 +- 生成安全的 JSON 字符串(避免直接嵌入非 ASCII 字符) +- 调试 Unicode 字符串内容 +- 数据导出/导入时的字符标准化 + +--- + +## 版本信息 + +- **语言**:Python 3.x +- **依赖**:无外部依赖,仅使用标准库 + +--- + +## 许可 + +此函数为公共领域代码片段,可用于任何项目。 \ No newline at end of file diff --git a/aidocs/version.md b/aidocs/version.md new file mode 100644 index 0000000..e10ab49 --- /dev/null +++ b/aidocs/version.md @@ -0,0 +1,52 @@ +# 版本信息模块文档 + +## 概述 + +本模块定义了当前软件包的版本号,采用语义化版本控制(Semantic Versioning)格式。 + +## 版本属性 + +### `__version__` + +- **类型**: 字符串 (str) +- **值**: `'0.3.4'` +- **作用**: 存储当前软件包的版本标识 + +#### 版本号解析 +版本号 `0.3.4` 遵循 `主版本号.次版本号.修订号` 的格式: +- **主版本号 (Major)**: `0` - 初始开发阶段,可能包含不兼容的API变更 +- **次版本号 (Minor)**: `3` - 向后兼容的功能新增 +- **修订号 (Patch)**: `4` - 向后兼容的问题修复 + +## 使用示例 + +```python +# 导入版本信息 +import your_package + +# 获取当前版本 +print(f"当前版本: {your_package.__version__}") + +# 版本比较示例 +current_version = your_package.__version__ +if current_version == '0.3.4': + print("使用的是最新版本") +``` + +## 版本控制规范 + +本项目遵循[语义化版本控制 2.0.0](https://semver.org/lang/zh-CN/)规范: + +1. **主版本号**:当你做了不兼容的 API 修改 +2. **次版本号**:当你做了向下兼容的功能性新增 +3. **修订号**:当你做了向下兼容的问题修正 + +## 相关工具 + +可以使用以下方式查询版本: + +```bash +python -c "import your_package; print(your_package.__version__)" +``` + +> **注意**:请将 `your_package` 替换为实际的包名称。 \ No newline at end of file diff --git a/aidocs/webapp.md b/aidocs/webapp.md new file mode 100644 index 0000000..9408fa0 --- /dev/null +++ b/aidocs/webapp.md @@ -0,0 +1,260 @@ +# Web 应用启动框架技术文档 + +本项目提供一个基于 `ahserver` 的轻量级 Web 服务启动框架,支持通过命令行参数配置工作目录和端口,并自动加载 JSON 配置文件以初始化服务器环境。该模块适用于快速搭建可配置的 Python Web 服务。 + +--- + +## 目录 + +- [功能概述](#功能概述) +- [依赖说明](#依赖说明) +- [核心函数](#核心函数) + - [`webapp(init_func)`](#webappinit_func) + - [`webserver(init_func, workdir, port=None)`](#webserverinit_func-workdir-portnone) +- [配置系统](#配置系统) +- [命令行参数](#命令行参数) +- [使用示例](#使用示例) +- [日志系统](#日志系统) +- [入口点(main)](#入口点main) + +--- + +## 功能概述 + +该脚本实现了以下功能: + +- 解析命令行参数(工作目录、端口) +- 加载指定路径下的 JSON 配置文件 +- 初始化日志系统 +- 调用用户自定义初始化函数 +- 启动基于 `ConfiguredServer` 的 Web 服务器 + +主要用于快速部署基于 `ahserver` 框架的 Web 应用。 + +--- + +## 依赖说明 + +| 模块 | 来源 | 用途 | +|------|------|------| +| `os`, `sys` | Python 内置 | 系统路径与环境操作 | +| `argparse` | Python 内置 | 命令行参数解析 | +| `MyLogger`, `info`, `debug`, `warning` | `appPublic.log` | 日志记录工具 | +| `ProgramPath` | `appPublic.folderUtils` | 获取程序运行路径 | +| `getConfig` | `appPublic.jsonConfig` | 加载并渲染 JSON 配置文件 | +| `ConfiguredServer` | `ahserver.configuredServer` | 可配置的 Web 服务器主类 | +| `ServerEnv` | `ahserver.serverenv` | 全局服务器运行环境 | + +> ⚠️ 注意:两次导入 `getConfig` 属于冗余,建议优化为一次导入。 + +--- + +## 核心函数 + +### `webapp(init_func)` + +启动 Web 应用的顶层入口函数,负责解析命令行参数并调用底层服务启动逻辑。 + +#### 参数 + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `init_func` | `callable` | 在服务器启动前执行的初始化回调函数(如注册路由、数据库连接等) | + +#### 命令行选项 + +| 选项 | 简写 | 说明 | +|------|------|------| +| `--workdir` | `-w` | 指定应用的工作目录,默认为当前目录 | +| `--port` | `-p` | 指定监听端口号(可选),若未设置则从配置文件或默认值获取 | + +#### 流程说明 + +1. 创建 `ArgumentParser` 实例 +2. 解析 `-w/--workdir` 和 `-p/--port` +3. 设置 `workdir`(优先使用参数,否则为 `os.getcwd()`) +4. 调用 `webserver(init_func, workdir, port)` 启动服务 + +--- + +### `webserver(init_func, workdir, port=None)` + +实际启动 Web 服务器的核心函数。 + +#### 参数 + +| 参数名 | 类型 | 说明 | +|--------|------|------| +| `init_func` | `callable` | 初始化函数,在服务器启动前调用 | +| `workdir` | `str` | 应用根目录,用于查找配置文件 | +| `port` | `int or None` | 指定监听端口,优先级低于命令行但高于配置文件 | + +#### 执行流程 + +1. **获取程序路径** + 使用 `ProgramPath()` 获取可执行文件所在路径。 + +2. **加载配置文件** + ```python + config = getConfig(workdir, NS={'workdir': workdir, 'ProgramPath': p}) + ``` + - 从 `workdir` 目录加载 `config.json` 或 `.conf` 文件 + - 支持模板变量替换:`{workdir}`, `{ProgramPath}` + +3. **初始化日志系统** + - 若配置中包含 `logger` 字段,则按配置创建 `MyLogger` + - 否则使用默认配置: + - 名称:`webapp` + - 级别:`info` + - 不写入文件 + +4. **执行用户初始化函数** + 调用传入的 `init_func()`,可用于注册中间件、路由、数据库等。 + +5. **设置服务器环境** + - 创建 `ServerEnv()` 单例,设置 `workdir` 和 `port` + +6. **启动服务器** + - 实例化 `ConfiguredServer(workdir=workdir)` + - 确定最终端口顺序: + 1. 函数参数 `port` + 2. 配置文件中的 `website.port` + 3. 默认值 `8080` + - 调用 `server.run(port=port)` 启动 HTTP 服务 + +--- + +## 配置系统 + +配置通过 `appPublic.jsonConfig.getConfig()` 加载,支持如下结构(示例 `config.json`): + +```json +{ + "logger": { + "name": "myweb", + "levelname": "debug", + "logfile": "{workdir}/logs/app.log" + }, + "website": { + "port": 8000 + } +} +``` + +> ✅ 支持变量插值:`{workdir}`, `{ProgramPath}` 将被自动替换为对应值。 + +--- + +## 命令行参数 + +可通过命令行控制运行参数: + +```bash +python app.py -w /path/to/project -p 9000 +``` + +等价于: + +```bash +python app.py --workdir=/path/to/project --port=9000 +``` + +如果不指定: + +- `workdir` → 当前目录 +- `port` → 依次取值:`None` → `config.website.port` → `8080` + +--- + +## 日志系统 + +日志由 `MyLogger` 提供,行为如下: + +| 配置项 | 是否必需 | 默认值 | 说明 | +|--------|----------|--------|------| +| `logger.name` | 否 | `'webapp'` | 日志器名称 | +| `logger.levelname` | 否 | `'info'` | 日志级别(支持 debug/info/warning/error) | +| `logger.logfile` | 否 | `None` | 日志输出文件路径,`None` 表示仅输出到控制台 | + +> 示例输出: +> ``` +> [INFO] 2025-04-05 10:00:00 webapp: Starting server on port 8080... +> ``` + +--- + +## 入口点(main) + +当直接运行此脚本时,会执行以下代码: + +```python +if __name__ == '__main__': + from main import main + webapp(main) +``` + +要求项目根目录下存在 `main.py` 并定义 `main()` 函数作为初始化入口。 + +### 示例 `main.py` + +```python +def main(): + print("Initializing application...") + # 注册路由、加载插件、连接数据库等 +``` + +--- + +## 使用示例 + +### 项目结构 + +``` +/myproject/ +├── app.py # 本启动脚本 +├── main.py # 初始化逻辑 +└── config.json # 配置文件 +``` + +### 启动方式 + +```bash +# 使用默认配置(当前目录 + 8080端口) +python app.py + +# 自定义工作目录和端口 +python app.py -w /myproject -p 3000 +``` + +--- + +## 注意事项 + +1. **重复导入问题** + 当前有两行: + ```python + from appPublic.jsonConfig import getConfig + ``` + 应合并为一行,避免冗余。 + +2. **异常处理缺失** + 当前代码没有捕获配置加载失败、端口占用等异常,建议增加 try-except 包裹。 + +3. **端口类型转换** + `port = int(port)` 是安全的,但应确保输入为数字字符串或整数。 + +4. **ServerEnv 的作用** + `se = ServerEnv()` 被创建但仅设置了 `workdir` 和 `port`,需确认是否在其他地方被使用。 + +--- + +## 总结 + +该模块是一个简洁高效的 Web 服务启动器,适合中小型项目快速集成。其特点包括: + +- ✅ 配置驱动 +- ✅ 支持命令行覆盖 +- ✅ 灵活的日志配置 +- ✅ 易于扩展(通过 `init_func`) + +推荐配合 `ahserver` 生态使用,实现模块化 Web 开发。 \ No newline at end of file diff --git a/aidocs/websocketProcessor.md b/aidocs/websocketProcessor.md new file mode 100644 index 0000000..d184205 --- /dev/null +++ b/aidocs/websocketProcessor.md @@ -0,0 +1,323 @@ +# WebSocket 处理模块技术文档 + +```markdown +# WebSocket 处理模块技术文档 + +本模块提供基于 `aiohttp` 的异步 WebSocket 服务支持,用于构建实时通信的 Web 应用。通过 `WebsocketProcessor` 类实现 WebSocket 连接的管理、消息分发与脚本执行能力。 + +--- + +## 模块依赖 + +```python +import asyncio +import aiohttp +import aiofiles +import json +import codecs +from aiohttp import web +import aiohttp_cors +from traceback import print_exc + +# 第三方/项目内部库 +from appPublic.sshx import SSHNode +from appPublic.dictObject import DictObject +from appPublic.log import info, debug, warning, error, exception, critical +from .baseProcessor import BaseProcessor, PythonScriptProcessor +``` + +> **说明**: +- 使用 `aiohttp` 构建异步 HTTP/WebSocket 服务。 +- `aiohttp_cors` 支持跨域请求(CORS)。 +- `appPublic` 为项目公共工具包,包含日志、字典对象封装等工具。 +- 继承自 `PythonScriptProcessor` 实现动态脚本执行。 + +--- + +## 核心函数 + +### `async def ws_send(ws: web.WebSocketResponse, data)` + +向指定 WebSocket 客户端发送 JSON 格式数据。 + +#### 参数 +| 参数 | 类型 | 说明 | +|------|------|------| +| `ws` | `web.WebSocketResponse` | WebSocket 响应对象 | +| `data` | `Any` | 要发送的数据内容(将被 JSON 序列化) | + +#### 返回值 +- 成功:返回 `await ws.send_str(d)` 的结果(通常为 `None`) +- 失败:捕获异常并记录错误,返回 `False` + +#### 数据格式 +发送的数据会被包装成如下结构: +```json +{ + "type": 1, + "data": <原始数据> +} +``` +使用 `ensure_ascii=False` 和缩进美化输出。 + +#### 示例 +```python +await ws_send(ws, {"status": "connected", "user": "alice"}) +``` + +--- + +## 核心类 + +### `class WsSession` + +表示一个用户会话,可关联多个节点(连接实例)。 + +#### 方法 +| 方法 | 描述 | +|------|------| +| `__init__(session)` | 初始化会话对象,传入 session 对象 | +| `join(node)` | 将节点加入当前会话(按 `node.id` 存储) | +| `leave(node)` | 从会话中移除指定节点 | + +> ⚠️ 注意:`join` 和 `leave` 是实例方法但未使用 `self`,代码存在错误(缺少 `self` 参数),应修正为: +```python +def join(self, node): + self.nodes[node.id] = node + +def leave(self, node): + self.nodes = {k:v for k,v in self.nodes.items() if k != node.id} +``` + +--- + +### `class WsData` + +全局状态管理器,维护所有在线节点和会话。 + +#### 属性 +| 属性 | 类型 | 说明 | +|------|------|------| +| `nodes` | `dict` | 存储所有活动节点 `{id: node}` | +| `sessions` | `dict` | 存储所有会话 `{sessionid: session}` | + +#### 方法 +| 方法 | 功能 | +|------|------| +| `add_node(node)` | 添加节点 | +| `del_node(node)` | 删除节点 | +| `get_nodes()` | 获取全部节点 | +| `get_node(id)` | 根据 ID 获取节点 | +| `add_session(session)` | 添加会话 | +| `del_session(session)` | 删除会话 | +| `get_session(id)` | 根据 ID 获取会话 | + +> 所有操作均基于内存字典,非持久化。 + +--- + +### `class WsPool` + +WebSocket 连接池管理类,封装单个客户端连接的行为与上下文。 + +#### 初始化参数 +```python +def __init__(self, ws, ip, ws_path, app) +``` +| 参数 | 类型 | 说明 | +|------|------|------| +| `ws` | `WebSocketResponse` | 当前连接对象 | +| `ip` | `str` | 客户端 IP 地址 | +| `ws_path` | `str` | WebSocket 请求路径(如 `/ws/chat`) | +| `app` | `Application` | aiohttp 应用实例,用于共享全局状态 | + +#### 实例属性 +| 属性 | 类型 | 说明 | +|------|------|------| +| `id` | `str` | 用户唯一标识(注册后赋值) | +| `ws`, `ip`, `ws_path`, `app` | - | 初始化传入 | + +#### 核心方法 + +##### `get_data() → WsData` +获取或初始化当前路径下的共享数据对象(存储于 `app` 中)。若不存在则创建新的 `WsData` 并设置。 + +##### `set_data(data)` +将更新后的 `WsData` 写回应用上下文。 + +##### `is_online(userid) → bool` +检查某用户是否在线(即其节点是否存在)。 + +##### `register(id)` +注册用户 ID,创建 `DictObject` 并调用 `add_me()`。 + +##### `add_me(iddata)` +将当前连接绑定到用户 ID,并保存至 `WsData.nodes`。 + +##### `delete_id(id)` +从全局数据中删除指定用户节点。 + +##### `delete_me()` +删除当前连接对应的用户节点。 + +##### `add_session(session)` / `del_session(session)` +管理会话生命周期。 + +##### `get_session(sessionid)` +根据 ID 查询会话对象。 + +##### `async def sendto(data, id=None)` +发送消息: +- 若 `id` 为 `None`:发送给当前连接 +- 否则:查找目标用户的 WebSocket 并发送 +- 发送失败时自动调用 `delete_id(id)` 清理离线用户 + +--- + +### `class WebsocketProcessor(PythonScriptProcessor)` + +继承自 `PythonScriptProcessor`,专用于处理 WebSocket 类型请求。 + +#### 类方法 +##### `@classmethod isMe(name) → bool` +判断处理器是否匹配给定名称。 +- 返回 `True` 当且仅当 `name == 'ws'` +- 用于路由匹配或插件识别 + +#### 实例方法 +##### `async def path_call(self, request, params={})` + +主入口方法,处理 WebSocket 升级请求并维持长连接。 + +###### 流程说明 +1. **提取 Cookie 与用户信息** + - 读取 `Sec-WebSocket-Protocol` 头部模拟 Cookies(逻辑可能需优化) + - 获取当前用户 ID(通过 `get_user()` 或上下文) + +2. **准备运行环境** + - 设置请求上下文环境 `run_ns` + - 合并参数 `params` 到本地命名空间 `lenv` + - 提取 `userid`(优先从 `params_kw`,否则异步获取) + +3. **加载脚本** + - 异步读取脚本文件内容(路径由 `self.real_path` 指定) + +4. **建立 WebSocket 连接** + ```python + ws = web.WebSocketResponse() + await ws.prepare(request) + ``` + - 准备失败时抛出异常并记录堆栈 + +5. **初始化连接池** + ```python + ws_pool = WsPool(ws, client_ip, path, app) + ``` + +6. **消息循环监听** + ```python + async for msg in ws: + ``` + - **TEXT 消息**: + - 心跳检测:收到 `_#_heartbeat_#_` 回复相同字符串 + - 其他消息:注入 `ws_data` 和 `ws_pool` 至脚本环境,执行脚本中的 `myfunc` + - **ERROR 消息**: + - 记录异常并中断循环 + - **其他类型**: + - 输出调试信息 + +7. **连接关闭清理** + - 调用 `ws_pool.delete_me()` 注销当前用户 + - 设置响应对象并关闭连接 + - 返回 `ws` 对象 + +###### 参数 +| 参数 | 类型 | 说明 | +|------|------|------| +| `request` | `Request` | aiohttp 请求对象 | +| `params` | `dict` | 额外传入参数(可选) | + +###### 返回值 +- `web.WebSocketResponse`:已关闭的 WebSocket 对象 + +###### 环境变量注入 +在执行脚本时,以下变量被注入局部命名空间: +- `ws_data`: 接收到的消息文本 +- `ws_pool`: 当前连接池实例,可用于发送消息或查询状态 +- `request`, `params_kw`, `get_user` 等来自上下文 + +> ✅ 脚本要求:必须定义名为 `myfunc(request, **kwargs)` 的异步函数。 + +--- + +## 使用示例 + +假设有一个脚本 `/scripts/ws/hello.py`: +```python +async def myfunc(request, ws_data, ws_pool, **kw): + data = json.loads(ws_data) + reply = { + "echo": data, + "time": time.time() + } + await ws_pool.sendto(reply) +``` + +配置路由指向该处理器并命名为 `'ws'`,当访问对应路径时即可启用 WebSocket 服务。 + +--- + +## 日志等级说明 + +| 等级 | 用途 | +|------|------| +| `info` | 正常流程、连接建立/断开 | +| `debug` | 调试信息、中间状态打印 | +| `warning` | 可容忍异常 | +| `error` | 错误事件 | +| `exception` | 异常捕获(带堆栈) | +| `critical` | 严重故障 | + +--- + +## 注意事项 + +1. **安全性** + - 脚本动态执行存在安全风险,请确保脚本来源可信。 + - 建议对 `exec()` 加沙箱限制。 + +2. **并发控制** + - 所有操作均为协程安全,但共享状态无锁保护,避免多任务同时修改。 + +3. **资源清理** + - 必须保证 `delete_me()` 在连接关闭时调用,防止内存泄漏。 + +4. **心跳机制** + - 客户端需定期发送 `_#_heartbeat_#_` 维持连接。 + +5. **IP 获取** + - `request['client_ip']` 需要在中间件中预先设置,原生 `aiohttp` 不直接提供。 + +--- + +## 待修复问题 + +| 问题 | 描述 | 建议 | +|------|------|------| +| `WsSession.join/leave` 缺少 `self` | 方法定义语法错误 | 补全 `self` 参数 | +| `cookie` 处理逻辑可疑 | 将 `Sec-WebSocket-Protocol` 当作 Cookies 不合理 | 应使用正确方式解析认证信息 | +| `get_user()` 未导入 | 函数未在代码中定义或导入 | 需确认其来源并显式引入 | + +--- + +## 版本信息 + +- Python: >=3.7 +- aiohttp: >=3.8 +- aiohttp-cors: >=0.7 +- appPublic: 自定义工具库(需项目内可用) + +--- +> 文档生成时间:{{ 自动生成时间 }} +> 维护者:开发者团队 +``` \ No newline at end of file diff --git a/aidocs/xlsxData.md b/aidocs/xlsxData.md new file mode 100644 index 0000000..0b7f263 --- /dev/null +++ b/aidocs/xlsxData.md @@ -0,0 +1,337 @@ +# `XLSXData` 类技术文档 + +> **模块**: `xlsxdata.py` +> **依赖库**: `openpyxl`, `json` + +--- + +## 概述 + +`XLSXData` 是一个用于从 Excel 文件(`.xlsx`)中读取结构化数据的 Python 类。它通过配置描述文件定义数据表的元信息,从而提取字段信息和实际数据记录。 + +该类适用于需要将 Excel 表格作为数据源的应用场景,例如后台管理系统中的数据导入、展示与分页查询。 + +--- + +## 文件格式说明(`xlsxds` 格式) + +`XLSXData` 使用一个 JSON 格式的描述对象来指定 Excel 文件的结构布局。该描述对象包含以下字段: + +```json +{ + "xlsxfile": "./data.xlsx", + "data_from": 7, + "data_sheet": "Sheet1", + "label_at": 1, + "name_at": null, + "datatype_at": 2, + "ioattrs_at": 3, + "listhide_at": 4, + "inputhide_at": 5, + "frozen_at": 6 +} +``` + +### 字段解释 + +| 字段名 | 类型 | 描述 | +|----------------|------|------| +| `xlsxfile` | str | Excel 文件路径 | +| `data_from` | int | 数据起始行号(默认为 2),表示从哪一行开始读取数据记录 | +| `data_sheet` | str | 工作表名称,默认为 `"Sheet1"` | +| `label_at` | int or null | 标签所在行号(用于显示名称),若为 `null` 则使用默认名 | +| `name_at` | int or null | 字段英文名所在行号,若为 `null` 则生成为 `f1`, `f2`, ... | +| `datatype_at` | int or null | 数据类型所在行号(如 `str`, `int`, `float` 等) | +| `ioattrs_at` | int or null | 输入输出属性(JSON 字符串)所在行号 | +| `listhide_at` | int or null | 控制列表是否隐藏的标志行('Y'/'y' 表示隐藏) | +| `inputhide_at` | int or null | 控制输入界面是否隐藏的标志行 | +| `frozen_at` | int or null | 控制列是否冻结的标志行 | + +> ⚠️ 所有 `_at` 后缀字段均为行号(从 1 开始计数),若设为 `null` 或不存在,则对应功能无效或使用默认值。 + +--- + +## 类定义:`XLSXData` + +```python +class XLSXData: + def __init__(self, path, desc) +``` + +### 构造函数:`__init__(path, desc)` + +初始化 XLSX 数据读取器。 + +#### 参数: +- `path` (str): Excel 文件路径。 +- `desc` (dict): 符合上述 `xlsxds` 格式的描述字典。 + +#### 功能: +- 加载 Excel 文件; +- 获取指定工作表; +- 初始化内部状态。 + +#### 示例: +```python +desc = { + "xlsxfile": "./data.xlsx", + "data_sheet": "Sheet1", + "data_from": 7, + "label_at": 1, + "datatype_at": 2, + ... +} +xls_data = XLSXData("./data.xlsx", desc) +``` + +--- + +## 方法列表 + +--- + +### `getBaseFieldsInfo() → List[Dict]` + +获取所有字段的基本元信息。 + +#### 返回值: +```python +[ + { + "name": "字段英文名", + "label": "显示标签", + "type": "数据类型", + "listhide": True/False, + "inputhide": True/False, + "frozen": True/False, + **其他 IO 属性(来自 ioattrs)** + }, + ... +] +``` + +#### 内部调用方法: +- `_fieldName(ws, col)` - 获取字段名 +- `_fieldLabel(ws, col)` - 获取显示标签 +- `_fieldType(ws, col)` - 获取数据类型 +- `_fieldIOattrs(ws, col)` - 解析并返回额外属性(JSON) +- `_isListHide(...)`, `_isInputHide(...)`, `_isFrozen(...)` - 判断各类隐藏/冻结状态 + +#### 示例输出: +```json +[ + { + "name": "user_id", + "label": "用户ID", + "type": "int", + "listhide": false, + "inputhide": true, + "frozen": true, + "width": 100, + "editor": "numberbox" + } +] +``` + +> 💡 若 `ioattrs_at` 行的内容是合法 JSON 字符串,其键值对会合并到结果中。 + +--- + +### `getPeriodData(min_r: int, max_r: int) → List[Dict]` + +读取指定行范围内的数据记录。 + +#### 参数: +- `min_r` (int): 起始行号(含) +- `max_r` (int): 结束行号(不含) + +#### 返回值: +- 列表形式的数据记录,每条记录是以字段名为键的字典。 + +#### 注意事项: +- 自动断言 `min_r >= data_from` +- 若 `max_r > 最大行数`,则自动截断 +- 使用 `_fieldName` 获取列名映射 + +#### 示例: +```python +data = xls_data.getPeriodData(7, 10) +# 返回第7~9行的数据 +``` + +--- + +### `getData(ns: dict) → List[Dict]` + +获取全部数据记录。 + +#### 参数: +- `ns` (dict): 命名空间参数(未使用) + +#### 返回值: +- 从 `data_from` 行到末尾的所有数据。 + +#### 相当于: +```python +self.getPeriodData(data_from, ws.max_row + 1) +``` + +--- + +### `getPagingData(ns: dict) → Dict` + +实现分页数据查询。 + +#### 参数: +- `ns` (dict): 分页参数 + - `page` (int): 当前页码(从 1 开始),默认为 1 + - `rows` (int): 每页行数,默认为 50 + +#### 返回值: +```json +{ + "total": 100, + "rows": [ + { "col1": "val1", "col2": "val2" }, + ... + ] +} +``` + +#### 计算逻辑: +- 起始行:`(page - 1) * rows + data_from` +- 结束行:`page * rows + data_from + 1` + +#### 示例请求参数: +```python +ns = {'page': 2, 'rows': 20} +result = xls_data.getPagingData(ns) +``` + +--- + +### `getArgumentsDesc(ns, request) → None` + +预留方法,用于获取参数描述(当前未实现)。 + +> ✅ 当前返回 `None`,可用于扩展接口文档生成功能。 + +--- + +## 私有方法详解 + +这些方法仅供内部使用,负责解析某一列的特定属性。 + +| 方法 | 说明 | +|------|------| +| `_fieldName(ws, i)` | 若 `name_at` 存在,取该行第 i 列值;否则返回 `'f'+i` | +| `_fieldLabel(ws, i)` | 取 `label_at` 行值,若无则同上 | +| `_fieldType(ws, i)` | 取 `datatype_at` 行值,缺省为 `'str'` | +| `_fieldIOattrs(ws, i)` | 读取 `ioattrs_at` 行内容,并尝试 `json.loads` 解析,失败时打印错误并返回 `{}` | +| `_isListHide(ws, i)` | 检查 `listhide_at` 是否为 'Y'/'y' | +| `_isInputHide(ws, i)` | 检查 `inputhide_at` 是否为 'Y'/'y' | +| `_isFrozen(ws, i)` | 检查 `frozen_at` 是否为 'Y'/'y' | + +> 🔴 **Bug 提示**:在 `_isFrozen()` 方法中存在变量错误!应为 `ws.cell(x, i).value` 而不是 `ws.cell(x, y).value`! + +#### 修复建议: +```python +def _isFrozen(self, ws, i): + x = self.desc.get('frozen_at') + if x is not None: + t = ws.cell(x, i).value # 原代码误写成 y + if t == 'Y' or t == 'y': + return True + return False +``` + +--- + +## 使用示例 + +```python +from xlsxdata import XLSXData + +desc = { + "xlsxfile": "./employees.xlsx", + "data_sheet": "Staff", + "data_from": 2, + "label_at": 1, + "name_at": None, + "datatype_at": None, + "ioattrs_at": 3, + "listhide_at": 4, + "inputhide_at": 5, + "frozen_at": 6 +} + +xls = XLSXData("./employees.xlsx", desc) + +# 获取字段信息 +fields = xls.getBaseFieldsInfo() +print(json.dumps(fields, ensure_ascii=False, indent=2)) + +# 获取全部数据 +all_data = xls.getData({}) +print(f"共加载 {len(all_data)} 条记录") + +# 分页获取(第2页,每页10条) +paged = xls.getPagingData({'page': 2, 'rows': 10}) +print(f"当前页数据条数: {len(paged['rows'])}, 总计: {paged['total']}") +``` + +--- + +## 异常处理与注意事项 + +- 若 `ioattrs` 内容非合法 JSON,会捕获异常并打印错误日志,不影响主流程。 +- 所有行索引基于 1(与 Excel 一致),程序内部无需转换。 +- 不支持多 sheet 联合分析。 +- 不修改原始 Excel 文件(只读模式)。 + +--- + +## 已知问题(Bug) + +⚠️ 在方法 `_isFrozen` 中存在 **严重 Bug**: + +```python +t = ws.cell(x,y).value # ❌ 变量 y 未定义!应为 i +``` + +这会导致运行时报错 `NameError: name 'y' is not defined`。 + +✅ **必须修复为**: +```python +t = ws.cell(x, i).value +``` + +--- + +## 总结 + +| 特性 | 支持情况 | +|------|----------| +| 读取 Excel 数据 | ✅ | +| 元数据驱动配置 | ✅ | +| 字段级属性控制 | ✅(隐藏、冻结、编辑属性等) | +| 分页支持 | ✅ | +| JSON 属性嵌入 | ✅ | +| 错误容忍机制 | ✅(部分) | +| 安全性 | ⚠️ 需注意 JSON 解析风险 | +| 可维护性 | ⚠️ 存在一个关键 bug | + +--- + +## 建议改进方向 + +1. 🛠 修复 `_isFrozen` 中的变量引用错误; +2. 🔒 添加 `read_only=True` 提升性能(如无需写操作); +3. 📦 将 `desc` 验证封装成独立方法; +4. 🧪 增加单元测试覆盖核心方法; +5. 📄 支持 `.xls` 或其他格式可通过抽象接口扩展。 + +--- + +📝 文档版本:v1.0 +📅 更新时间:2025-04-05 \ No newline at end of file diff --git a/aidocs/xlsxdsProcessor.md b/aidocs/xlsxdsProcessor.md new file mode 100644 index 0000000..323f0c2 --- /dev/null +++ b/aidocs/xlsxdsProcessor.md @@ -0,0 +1,258 @@ +# XLSX 数据源处理器技术文档 + +```markdown +# `XLSXDataSourceProcessor` 技术文档 + +## 概述 + +`XLSXDataSourceProcessor` 是一个基于 `.xlsx` 文件的数据源处理器,继承自 `DataSourceProcessor`。它用于从 Excel 文件中读取结构化数据和字段元信息,并支持分页查询。该类通过配置文件指定 Excel 文件路径及数据布局规则,实现灵活的数据提取。 + +该处理器适用于需要将 Excel 表格作为数据源的场景,如配置管理、静态数据导入等。 + +--- + +## 依赖库 + +- `openpyxl`: 用于读取 `.xlsx` 文件。 +- `codecs`: 编码处理(当前未直接使用,可能为预留或间接依赖)。 +- `appPublic.jsonConfig.getConfig`: 获取全局配置对象。 +- `.dsProcessor.DataSourceProcessor`: 数据源处理器基类。 +- `.xlsxData.XLSXData`: 封装对 Excel 文件的具体操作。 + +--- + +## 配置格式(`xlsxds` 格式) + +`XLSXDataSourceProcessor` 使用特定 JSON 结构描述 Excel 文件的数据布局: + +```json +{ + "xlsxfile": "./data.xlsx", + "data_from": 7, + "data_sheet": "Sheet1", + "label_at": 1, + "name_at": null, + "datatype_at": 2, + "ioattrs": 3, + "listhide_at": 4, + "inputhide_at": 5, + "frozen_at": 6 +} +``` + +### 字段说明 + +| 字段名 | 类型 | 必填 | 描述 | +|----------------|----------|------|------| +| `xlsxfile` | string | 是 | Excel 文件路径(相对或绝对),支持 URL 转换。 | +| `data_from` | integer | 是 | 数据起始行号(从 1 开始计数)。 | +| `data_sheet` | string | 否 | 工作表名称,默认为 `"Sheet1"`。 | +| `label_at` | integer | 是 | 字段标签所在行号。 | +| `name_at` | integer/null | 否 | 字段名所在行号;若为 `null`,则使用默认命名规则(如 col0, col1...)。 | +| `datatype_at` | integer | 是 | 字段类型所在行号。 | +| `ioattrs` | integer | 是 | I/O 属性所在行号。 | +| `listhide_at` | integer | 是 | 列表中是否隐藏标志所在行号。 | +| `inputhide_at` | integer | 是 | 输入界面中是否隐藏标志所在行号。 | +| `frozen_at` | integer | 是 | 冻结列标志所在行号。 | + +> **注意**:所有行号均以 1 为基础编号。 + +--- + +## 类定义 + +```python +class XLSXDataSourceProcessor(DataSourceProcessor) +``` + +### 继承关系 + +- 父类:`DataSourceProcessor` +- 子系统接口:符合统一数据源处理规范。 + +--- + +## 方法说明 + +### `isMe(name: str) -> bool` +**@classmethod** + +判断当前处理器是否匹配给定名称。 + +#### 参数: +- `name` (str): 数据源类型标识符。 + +#### 返回值: +- `True` 当 `name == 'xlsxds'` 时; +- 否则返回 `False`。 + +#### 示例: +```python +if XLSXDataSourceProcessor.isMe("xlsxds"): + processor = XLSXDataSourceProcessor() +``` + +--- + +### `getArgumentsDesc(dict_data, ns, request)` → `None` + +获取参数描述(当前未实现)。 + +#### 参数: +- `dict_data`: 配置字典。 +- `ns`: 命名空间对象。 +- `request`: 请求上下文。 + +#### 返回值: +- 固定返回 `None`,表示无额外参数描述。 + +> ⚠️ 当前功能占位,未来可扩展用于生成 API 文档或校验输入参数。 + +--- + +### `async getDataDesc(dict_data, ns, request)` → `dict` + +异步获取数据结构描述(字段元信息)。 + +#### 功能: +加载指定 Excel 文件并解析字段基本信息(如标签、类型、属性等)。 + +#### 参数: +- `dict_data`: 包含 `xlsxds` 配置的字典。 +- `ns`: 上下文命名空间。 +- `request`: 请求对象,用于构建绝对路径。 + +#### 流程: +1. 提取 `xlsxfile` 路径; +2. 使用 `absurl` 和 `abspath` 解析为本地绝对路径; +3. 初始化 `XLSXData` 实例; +4. 调用 `.getBaseFieldsInfo(ns)` 获取字段元数据。 + +#### 返回值: +- 字段信息列表,格式示例: + ```json + [ + { + "name": "id", + "label": "编号", + "type": "int", + "inputhide": false, + "listhide": true, + ... + }, + ... + ] + ``` + +--- + +### `async getData(dict_data, ns, request)` → `list[dict]` + +异步获取全部数据记录。 + +#### 功能: +读取 Excel 中从 `data_from` 行开始的所有数据行,转换为字典列表。 + +#### 参数: +同 `getDataDesc`。 + +#### 流程: +1. 解析文件路径; +2. 创建 `XLSXData` 实例; +3. 调用 `.getData(ns)` 获取完整数据集。 + +#### 返回值: +- 数据列表,每项为 `{字段名: 值}` 的字典。 + +--- + +### `async getPagingData(dict_data, ns, request)` → `dict` + +异步获取分页数据。 + +#### 功能: +支持分页查询,返回当前页数据及总数。 + +#### 参数: +同上。 + +#### 返回值: +- 分页结果对象,格式如下: + ```json + { + "page": 1, + "pageSize": 20, + "total": 100, + "data": [...] + } + ``` +> 实际结构取决于 `XLSXData.getPagingData()` 的实现。 + +--- + +## 内部组件协作 + +### `XLSXData` 类职责 +- 封装 Excel 文件的打开与读取; +- 解析元信息行(label, type, hide 等); +- 提供统一接口获取字段描述和数据; +- 支持分页逻辑。 + +### 路径处理机制 +使用 `self.g.absurl(request, path)` 和 `self.g.abspath(...)` 将相对路径或资源 URL 转换为可访问的本地文件路径。 + +--- + +## 使用示例 + +假设配置文件 `config.json` 内容如下: + +```json +{ + "type": "xlsxds", + "xlsxfile": "./data/users.xlsx", + "data_from": 2, + "data_sheet": "Users", + "label_at": 1, + "name_at": null, + "datatype_at": 2, + "ioattrs": 3, + "listhide_at": 4, + "inputhide_at": 5, + "frozen_at": 6 +} +``` + +在系统中注册后,可通过以下方式调用: + +```python +processor = XLSXDataSourceProcessor(g=global_context) +fields = await processor.getDataDesc(config, ns, request) +data = await processor.getData(config, ns, request) +paged = await processor.getPagingData(config, ns, request) +``` + +--- + +## 注意事项 + +1. **性能限制**:大文件可能导致内存占用高,建议控制 Excel 文件大小。 +2. **并发安全**:每个请求创建独立的 `XLSXData` 实例,避免状态冲突。 +3. **异常处理**:未显式捕获异常,需确保上游有错误兜底机制。 +4. **编码兼容性**:Excel 本身不涉及文本编码问题(`openpyxl` 自动处理 UTF-8)。 + +--- + +## 扩展建议 + +- 添加字段映射别名支持; +- 支持写入操作(导出回写); +- 引入缓存机制提升重复读取效率; +- 增加校验层验证必填字段是否存在。 + +--- +``` + +> ✅ **文档版本**:1.0 +> 📦 **所属模块**:`app.datasource.xlsxds` +> © 2025 内部技术文档,禁止外传 \ No newline at end of file diff --git a/aidocs/xtermProcessor.md b/aidocs/xtermProcessor.md new file mode 100644 index 0000000..f0abce4 --- /dev/null +++ b/aidocs/xtermProcessor.md @@ -0,0 +1,417 @@ +# `XtermProcessor` 技术文档 + +## 概述 + +`XtermProcessor` 是一个基于 WebSocket 的终端处理器类,继承自 `PythonScriptProcessor`。它用于通过 Web 界面提供交互式 SSH 终端服务(类似 xterm.js 的后端支持),允许用户通过浏览器连接到远程服务器并执行命令。 + +该模块利用 `aiohttp` 提供异步 HTTP 和 WebSocket 通信能力,并结合 `appPublic.sshx.SSHServer` 实现与远程主机的 SSH 连接和终端会话管理。 + +--- + +## 模块依赖 + +```python +from traceback import format_exc +import asyncio +import aiohttp +import aiofiles +import json +import codecs +from aiohttp import web +import aiohttp_cors +from appPublic.sshx import SSHServer +from appPublic.dictObject import DictObject +from appPublic.log import info, debug, warning, error, exception, critical +from .baseProcessor import BaseProcessor, PythonScriptProcessor +``` + +### 外部依赖说明: + +| 包/模块 | 用途 | +|--------|------| +| `aiohttp`, `web`, `aiohttp_cors` | 构建异步 Web 服务及处理 WebSocket 请求 | +| `asyncio` | 异步任务调度 | +| `aiofiles` | 异步文件操作(本代码中未直接使用) | +| `json`, `codecs` | 数据序列化与编码处理 | +| `appPublic.sshx.SSHServer` | 封装了 SSH 客户端功能,用于建立远程连接 | +| `appPublic.dictObject.DictObject` | 字典对象封装,支持属性访问 | +| `appPublic.log.*` | 日志输出接口 | + +--- + +## 自定义异常:`ResizeException` + +```python +class ResizeException(Exception): + def __init__(self, rows, cols): + self.rows = rows + self.cols = cols +``` + +> ⚠️ **注意**:当前代码中定义了 `ResizeException` 类但并未实际抛出或捕获此异常。可能为预留扩展功能。 + +--- + +## 核心类:`XtermProcessor` + +继承自 `PythonScriptProcessor`,实现了一个基于 WebSocket 的伪终端(PTY)代理,将前端输入转发至 SSH 子进程,并将输出回传给客户端。 + +### 类方法 + +#### `isMe(name) -> bool` + +判断当前处理器是否匹配指定名称。 + +```python +@classmethod +def isMe(self, name): + return name == 'xterm' +``` + +- **参数**: + - `name` (str): 要检查的处理器名。 +- **返回值**: + - 若 `name == 'xterm'` 返回 `True`,否则 `False`。 +- **用途**: + - 在路由分发时用于选择合适的处理器。 + +--- + +## 实例方法 + +### `datahandle(request)` + +HTTP 请求入口点,调用路径处理逻辑。 + +```python +async def datahandle(self, request): + await self.path_call(request) +``` + +- **参数**: + - `request`: `aiohttp.web.Request` 对象。 +- **说明**: + - 执行 `path_call()` 方法以启动流程。 + +--- + +### `path_call(request, params={})` + +主处理入口,准备运行环境、获取登录信息并启动终端会话。 + +```python +async def path_call(self, request, params={}): + await self.set_run_env(request, params=params) + login_info = await super().path_call(request, params=params) + if login_info is None: + raise Exception('data error') + + ws = web.WebSocketResponse() + await ws.prepare(request) + await self.run_xterm(ws, login_info) + self.retResponse = ws + return ws +``` + +#### 流程说明: + +1. 设置运行环境变量(如上下文、参数等)。 +2. 调用父类 `PythonScriptProcessor.path_call()` 执行脚本逻辑,预期返回包含 SSH 登录信息的 `DictObject`。 +3. 若 `login_info` 为空,则抛出异常。 +4. 创建 WebSocket 响应对象并准备握手。 +5. 启动 `run_xterm` 处理终端会话。 +6. 保存响应对象并返回。 + +> ✅ **期望 `login_info` 结构示例**: +```python +{ + "host": "192.168.1.100", + "port": 22, + "username": "admin", + "password": "secret", + # 或 key_filename / pkey 等字段 +} +``` + +--- + +### `run_xterm(ws, login_info)` + +核心方法:建立 SSH 连接并启动双向数据流。 + +```python +async def run_xterm(self, ws, login_info): + self.sshnode = SSHServer(login_info) + async with self.sshnode.get_connector() as conn: + self.running = True + self.p_obj = await conn.create_process(term_type='xterm-256color', term_size=(80, 24)) + r1 = self.ws_2_process(ws) + r2 = self.process_2_ws(ws) + await asyncio.gather(r1, r2) + debug(f'run_xterm() ended') +``` + +#### 功能详解: + +- 使用 `SSHServer(login_info)` 初始化连接器。 +- 获取连接上下文(`get_connector()`)进行资源管理。 +- 创建带有彩色终端支持的进程(`create_process`),初始尺寸为 80x24。 +- 并发运行两个协程: + - `ws_2_process`: 接收客户端输入并发送到 SSH 子进程 stdin。 + - `process_2_ws`: 读取子进程 stdout 并推送到客户端。 +- 使用 `asyncio.gather()` 并行等待两者结束。 + +--- + +### `ws_2_process(ws)` + +处理来自 WebSocket 的消息,转发至 SSH 子进程。 + +```python +async def ws_2_process(self, ws): + async for msg in ws: + if msg.type == aiohttp.WSMsgType.TEXT: + data = DictObject(**json.loads(msg.data)) + if data.type == 'close': + debug('accept client close request, close the ws') + self.running = False + return + if data.type == 'input': + self.p_obj.stdin.write(data.data) + elif data.type == 'heartbeat': + await self.ws_send_heartbeat(ws) + elif data.type == 'resize': + try: + self.p_obj._chan.change_terminal_size(data.cols, data.rows) + except Exception as e: + exception(f'{data=}, {e=}, {format_exc()}') + elif msg.type == aiohttp.WSMsgType.ERROR: + debug(f'ws connection closed with exception {ws.exception()}') + return + else: + debug('recv from ws:{msg}+++++++++++') + await asyncio.sleep(0) +``` + +#### 支持的消息类型: + +| 类型 | 描述 | +|------|------| +| `input` | 将 `data.data` 写入子进程标准输入 | +| `resize` | 调整终端窗口大小为 `(cols, rows)` | +| `heartbeat` | 回复心跳包,保持连接活跃 | +| `close` | 关闭终端会话 | +| 其他文本消息 | 忽略并打印日志 | + +> ❗ 注意:`change_terminal_size()` 可能引发异常,已捕获并记录堆栈。 + +--- + +### `process_2_ws(ws)` + +从 SSH 子进程读取输出,推送至 WebSocket。 + +```python +async def process_2_ws(self, ws): + try: + while self.running: + x = await self.p_obj.stdout.read(1024) + await self.ws_send_data(ws, x) + await asyncio.sleep(0) + finally: + self.p_obj.close() +``` + +- 循环读取最多 1024 字节的数据。 +- 调用 `ws_send_data()` 发送数据帧。 +- 即使连接断开也会确保 `p_obj.close()` 被调用。 + +--- + +### `ws_send_data(ws, d)` + +发送数据内容到客户端。 + +```python +async def ws_send_data(self, ws, d): + dic = { + 'type': 'data', + 'data': d + } + await self.ws_send(ws, dic) +``` + +--- + +### `ws_send_heartbeat(ws)` + +发送心跳响应。 + +```python +async def ws_send_heartbeat(self, ws): + dic = { + 'type': 'heartbeat' + } + await self.ws_send(ws, dic) +``` + +--- + +### `ws_send(ws: WebSocketResponse, s)` + +通用 WebSocket 消息发送函数。 + +```python +async def ws_send(self, ws: web.WebSocketResponse, s): + data = { + "type": 1, + "data": s + } + await ws.send_str(json.dumps(data, indent=4, ensure_ascii=False)) +``` + +- **参数**: + - `ws`: WebSocket 响应对象。 + - `s`: 要发送的数据(通常为 dict)。 +- **格式化规则**: + - JSON 序列化,保留中文字符(`ensure_ascii=False`),缩进便于调试。 +- **结构**: + ```json + { + "type": 1, + "data": { ... } + } + ``` + +> 🔔 提示:前端需解析 `"type":1` 表示这是一个终端数据帧。 + +--- + +## 消息协议规范(WebSocket) + +### 客户端 → 服务端 + +| 字段 | 类型 | 示例 | 说明 | +|------|------|-------|------| +| `type` | string | `"input"` | 消息类型 | +| `data` | string | `"ls -l\\n"` | 输入内容(仅 `input` 类型) | +| `rows`, `cols` | int | `24`, `80` | 窗口尺寸(`resize` 类型) | + +示例: +```json +{ + "type": "input", + "data": "echo hello\\n" +} +``` + +```json +{ + "type": "resize", + "rows": 30, + "cols": 120 +} +``` + +--- + +### 服务端 → 客户端 + +统一包装格式: + +```json +{ + "type": 1, + "data": { + "type": "data" | "heartbeat", + "data": "output string..." + } +} +``` + +| `data.type` | 含义 | 数据格式 | +|------------|------|---------| +| `data` | 终端输出 | 字符串(UTF-8 编码的命令行输出) | +| `heartbeat` | 心跳响应 | 空对象或仅含 type | + +--- + +## 使用场景 + +适用于构建 Web 版终端应用(如运维平台、在线 IDE、云桌面等),典型架构如下: + +``` +[Browser] + ↓ (WebSocket) +[XtermProcessor] + ↓ (SSH) +[Remote Server] +``` + +前端可配合 [xterm.js](https://xtermjs.org/) 使用,接收 `data` 类型消息写入终端,发送键盘输入作为 `input` 消息。 + +--- + +## 日志等级说明 + +| 函数 | 用途 | +|------|------| +| `debug()` | 调试信息(连接状态、消息流转) | +| `exception()` | 记录异常及完整堆栈 | +| `error()/critical()` | 严重错误(当前未显式调用) | + +建议开启 DEBUG 日志以便排查问题。 + +--- + +## 注意事项与安全建议 + +1. **认证安全性**: + - `login_info` 来源于脚本执行结果,必须严格校验来源,防止注入攻击。 + - 不应在 URL 或日志中暴露密码。 + +2. **资源清理**: + - 已在 `finally` 块中关闭 `p_obj`,但仍需确保 `SSHServer` 正确释放连接。 + +3. **输入验证**: + - `resize` 消息未做边界检查,建议添加行列范围限制(如 10~1000)。 + +4. **性能优化**: + - `read(1024)` 可根据负载调整缓冲区大小。 + - `await asyncio.sleep(0)` 用于让出控制权,避免阻塞事件循环。 + +5. **CORS 配置**: + - 需通过 `aiohttp_cors` 正确配置跨域策略,允许前端域名访问。 + +--- + +## 示例配置(aiohttp 路由集成) + +```python +from aiohttp import web +import aiohttp_cors + +app = web.Application() +# 添加 CORS 支持 +cors = aiohttp_cors.setup(app) + +# 注册处理器路由 +app.router.add_route('GET', '/xterm/{nodeid}', xterm_processor_instance.datahandle) + +# 启用 CORS +resource = cors.add(app.router['xterm']) +resource.add_route("*", xterm_processor_instance.datahandle, web.HTTPAllowedMethodsRule(("GET",))) +``` + +--- + +## 总结 + +`XtermProcessor` 是一个完整的异步 Web Terminal 后端实现,具备以下特点: + +✅ 支持动态 SSH 登录配置 +✅ 双向实时通信(WebSocket ↔ SSH PTY) +✅ 终端大小调整、心跳保活、优雅关闭 +✅ 易于集成至现有 Web 框架 + +适合用于开发安全可控的远程终端访问系统。 \ No newline at end of file