feat(openwebui-skills-manager): enhance auto-discovery and structural refactoring

- Enable default overwrite installation policy for overlapping skills
- Support deep recursive GitHub trees discovery mechanism to resolve #58
- Refactor internal architecture to fully decouple stateless helper logic
- READMEs and docs synced (v0.3.0)
This commit is contained in:
fujie
2026-03-08 18:21:21 +08:00
parent 55a9c6ffb5
commit d29c24ba4a
30 changed files with 5417 additions and 598 deletions

View File

@@ -0,0 +1,65 @@
# 🔗 Chat Session Mapping Filter
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.1.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions)
Automatically tracks and persists the mapping between user IDs and chat IDs for seamless session management.
## Key Features
🔄 **Automatic Tracking** - Captures user_id and chat_id on every message without manual intervention
💾 **Persistent Storage** - Saves mappings to JSON file for session recovery and analytics
🛡️ **Atomic Operations** - Uses temporary file writes to prevent data corruption
⚙️ **Configurable** - Enable/disable tracking via Valves setting
🔍 **Smart Context Extraction** - Safely extracts IDs from multiple source locations (body, metadata, __metadata__)
## How to Use
1. **Install the filter** - Add it to your OpenWebUI plugins
2. **Enable globally** - No configuration needed; tracking is enabled by default
3. **Monitor mappings** - Check `copilot_workspace/api_key_chat_id_mapping.json` for stored mappings
## Configuration
| Parameter | Default | Description |
|-----------|---------|-------------|
| `ENABLE_TRACKING` | `true` | Master switch for chat session mapping tracking |
## How It Works
This filter intercepts messages at the **inlet** stage (before processing) and:
1. **Extracts IDs**: Safely gets user_id from `__user__` and chat_id from `body`/`metadata`
2. **Validates**: Confirms both IDs are non-empty before proceeding
3. **Persists**: Writes or updates the mapping in a JSON file with atomic file operations
4. **Handles Errors**: Gracefully logs warnings if any step fails, without blocking the chat flow
### Storage Location
- **Container Environment** (`/app/backend/data` exists):
`/app/backend/data/copilot_workspace/api_key_chat_id_mapping.json`
- **Local Development** (no `/app/backend/data`):
`./copilot_workspace/api_key_chat_id_mapping.json`
### File Format
Stored as a JSON object with user IDs as keys and chat IDs as values:
```json
{
"user-1": "chat-abc-123",
"user-2": "chat-def-456",
"user-3": "chat-ghi-789"
}
```
## Support
If this plugin has been useful, a star on [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) is a big motivation for me. Thank you for the support.
## Technical Notes
- **No Response Modification**: The outlet hook returns the response unchanged
- **Atomic Writes**: Prevents partial writes using `.tmp` intermediate files
- **Context-Aware ID Extraction**: Handles `__user__` as dict/list/None and metadata from multiple sources
- **Logging**: All operations are logged for debugging; enable verbose logging with `SHOW_DEBUG_LOG` in dependent plugins

View File

@@ -0,0 +1,65 @@
# 🔗 聊天会话映射过滤器
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.1.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions)
自动追踪并持久化用户 ID 与聊天 ID 的映射关系,实现无缝的会话管理。
## 核心功能
🔄 **自动追踪** - 无需手动干预,在每条消息上自动捕获 user_id 和 chat_id
💾 **持久化存储** - 将映射关系保存到 JSON 文件,便于会话恢复和数据分析
🛡️ **原子性操作** - 使用临时文件写入防止数据损坏
⚙️ **灵活配置** - 通过 Valves 参数启用/禁用追踪功能
🔍 **智能上下文提取** - 从多个数据源body、metadata、__metadata__安全提取 ID
## 使用方法
1. **安装过滤器** - 将其添加到 OpenWebUI 插件
2. **全局启用** - 无需配置,追踪功能默认启用
3. **查看映射** - 检查 `copilot_workspace/api_key_chat_id_mapping.json` 中的存储映射
## 配置参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `ENABLE_TRACKING` | `true` | 聊天会话映射追踪的主开关 |
## 工作原理
该过滤器在 **inlet** 阶段(消息处理前)拦截消息并执行以下步骤:
1. **提取 ID**: 安全地从 `__user__` 获取 user_id`body`/`metadata` 获取 chat_id
2. **验证**: 确认两个 ID 都非空后再继续
3. **持久化**: 使用原子文件操作将映射写入或更新 JSON 文件
4. **错误处理**: 任何步骤失败时都会优雅地记录警告,不阻断聊天流程
### 存储位置
- **容器环境**(存在 `/app/backend/data`:
`/app/backend/data/copilot_workspace/api_key_chat_id_mapping.json`
- **本地开发**(无 `/app/backend/data`:
`./copilot_workspace/api_key_chat_id_mapping.json`
### 文件格式
存储为 JSON 对象,键是用户 ID值是聊天 ID
```json
{
"user-1": "chat-abc-123",
"user-2": "chat-def-456",
"user-3": "chat-ghi-789"
}
```
## 支持我们
如果这个插件对你有帮助,欢迎到 [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) 点个 Star这将是我持续改进的动力感谢支持。
## 技术细节
- **不修改响应**: outlet 钩子直接返回响应不做修改
- **原子写入**: 使用 `.tmp` 临时文件防止不完整的写入
- **上下文敏感的 ID 提取**: 处理 `__user__` 为 dict/list/None 的情况,以及来自多个源的 metadata
- **日志记录**: 所有操作都会被记录,便于调试;可通过启用依赖插件的 `SHOW_DEBUG_LOG` 查看详细日志

View File

@@ -0,0 +1,146 @@
"""
title: Chat Session Mapping Filter
author: Fu-Jie
author_url: https://github.com/Fu-Jie/openwebui-extensions
funding_url: https://github.com/open-webui
version: 0.1.0
description: Automatically tracks and persists the mapping between user IDs and chat IDs for session management.
"""
import os
import json
import logging
from pathlib import Path
from typing import Optional
from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
# Determine the chat mapping file location
if os.path.exists("/app/backend/data"):
CHAT_MAPPING_FILE = Path(
"/app/backend/data/copilot_workspace/api_key_chat_id_mapping.json"
)
else:
CHAT_MAPPING_FILE = Path(os.getcwd()) / "copilot_workspace" / "api_key_chat_id_mapping.json"
class Filter:
class Valves(BaseModel):
ENABLE_TRACKING: bool = Field(
default=True,
description="Enable chat session mapping tracking."
)
def __init__(self):
self.valves = self.Valves()
def inlet(
self,
body: dict,
__user__: Optional[dict] = None,
__metadata__: Optional[dict] = None,
**kwargs,
) -> dict:
"""
Inlet hook: Called before message processing.
Persists the mapping of user_id to chat_id.
"""
if not self.valves.ENABLE_TRACKING:
return body
user_id = self._get_user_id(__user__)
chat_id = self._get_chat_id(body, __metadata__)
if user_id and chat_id:
self._persist_mapping(user_id, chat_id)
return body
def outlet(
self,
body: dict,
response: str,
__user__: Optional[dict] = None,
__metadata__: Optional[dict] = None,
**kwargs,
) -> str:
"""
Outlet hook: No modification to response needed.
This filter only tracks mapping on inlet.
"""
return response
def _get_user_id(self, __user__: Optional[dict]) -> Optional[str]:
"""Safely extract user ID from __user__ parameter."""
if isinstance(__user__, (list, tuple)):
user_data = __user__[0] if __user__ else {}
elif isinstance(__user__, dict):
user_data = __user__
else:
user_data = {}
return str(user_data.get("id", "")).strip() or None
def _get_chat_id(
self, body: dict, __metadata__: Optional[dict] = None
) -> Optional[str]:
"""Safely extract chat ID from body or metadata."""
chat_id = ""
# Try to extract from body
if isinstance(body, dict):
chat_id = body.get("chat_id", "")
# Fallback: Check body.metadata
if not chat_id:
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
chat_id = body_metadata.get("chat_id", "")
# Fallback: Check __metadata__
if not chat_id and __metadata__ and isinstance(__metadata__, dict):
chat_id = __metadata__.get("chat_id", "")
return str(chat_id).strip() or None
def _persist_mapping(self, user_id: str, chat_id: str) -> None:
"""Persist the user_id to chat_id mapping to file."""
try:
# Create parent directory if needed
CHAT_MAPPING_FILE.parent.mkdir(parents=True, exist_ok=True)
# Load existing mapping
mapping = {}
if CHAT_MAPPING_FILE.exists():
try:
loaded = json.loads(
CHAT_MAPPING_FILE.read_text(encoding="utf-8")
)
if isinstance(loaded, dict):
mapping = {str(k): str(v) for k, v in loaded.items()}
except Exception as e:
logger.warning(
f"Failed to read mapping file {CHAT_MAPPING_FILE}: {e}"
)
# Update mapping with current user_id and chat_id
mapping[user_id] = chat_id
# Write to temporary file and atomically replace
temp_file = CHAT_MAPPING_FILE.with_suffix(
CHAT_MAPPING_FILE.suffix + ".tmp"
)
temp_file.write_text(
json.dumps(mapping, ensure_ascii=False, indent=2, sort_keys=True)
+ "\n",
encoding="utf-8",
)
temp_file.replace(CHAT_MAPPING_FILE)
logger.info(
f"Persisted mapping: user_id={user_id} -> chat_id={chat_id}"
)
except Exception as e:
logger.warning(f"Failed to persist chat session mapping: {e}")