Compare commits
162 Commits
v2026.01.1
...
v2026.02.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6369a1591 | ||
|
|
81634f57fa | ||
|
|
70dc62aaab | ||
|
|
8762c05e04 | ||
|
|
db52ac1041 | ||
|
|
795ac34cd9 | ||
|
|
89a12a4fe8 | ||
|
|
f882997337 | ||
|
|
8e2c1b467e | ||
|
|
db2433afa1 | ||
|
|
fd125c9dea | ||
|
|
0a726aacb6 | ||
|
|
bf82e093d4 | ||
|
|
53ae7ef003 | ||
|
|
c2c7e3b2d3 | ||
|
|
26df5f5144 | ||
|
|
aebeb3677c | ||
|
|
076c1d62c6 | ||
|
|
21cf6ecc1d | ||
|
|
e6bdab54c9 | ||
|
|
2232e5adb3 | ||
|
|
d17089972a | ||
|
|
a7991147be | ||
|
|
be6eb35567 | ||
|
|
90bc295871 | ||
|
|
310ad5d1d3 | ||
|
|
00ce724430 | ||
|
|
43b68b3ff0 | ||
|
|
b6c1335335 | ||
|
|
8b0b33015f | ||
|
|
11ee9086b2 | ||
|
|
3578ffc543 | ||
|
|
b8531f1979 | ||
|
|
8607afd4c3 | ||
|
|
41d9963d35 | ||
|
|
bd5f3d3f7c | ||
|
|
dda8262bc0 | ||
|
|
0271013ec2 | ||
|
|
e1f210d600 | ||
|
|
eac8f6f355 | ||
|
|
3998b93034 | ||
|
|
ae04e95e13 | ||
|
|
12d638e134 | ||
|
|
4a9eb8ed3d | ||
|
|
af127bbfd5 | ||
|
|
db7bb6250a | ||
|
|
24b029e617 | ||
|
|
ff63ab3118 | ||
|
|
07e7e74fe1 | ||
|
|
55456775c1 | ||
|
|
05bb2e4644 | ||
|
|
83fa20ed08 | ||
|
|
ec69524357 | ||
|
|
829361da63 | ||
|
|
af05ecec6a | ||
|
|
1e08ae7d10 | ||
|
|
b24233ee07 | ||
|
|
e5d1550986 | ||
|
|
7f5deb603e | ||
|
|
55b2a28f79 | ||
|
|
db9bcb2c31 | ||
|
|
ad2773e8f1 | ||
|
|
7d4da3be8a | ||
|
|
28166728a4 | ||
|
|
4dfca903c4 | ||
|
|
b619d3f402 | ||
|
|
1e68f985fb | ||
|
|
596b571887 | ||
|
|
c4d36c32a0 | ||
|
|
6adbcd8d42 | ||
|
|
89c039fe33 | ||
|
|
3a73ccfaa7 | ||
|
|
7eff265e1c | ||
|
|
989b45fc16 | ||
|
|
163d8ce8bd | ||
|
|
4e32e1a1da | ||
|
|
070e9f2456 | ||
|
|
219ba83df3 | ||
|
|
e412aeb93d | ||
|
|
38102ca0c4 | ||
|
|
6ab69fba1c | ||
|
|
e0c0f69dc8 | ||
|
|
7921b14dae | ||
|
|
30cde9e871 | ||
|
|
ac50cd249a | ||
|
|
927db6dbaa | ||
|
|
376c398ac7 | ||
|
|
a167a3cf83 | ||
|
|
c51e7dfdf7 | ||
|
|
1d4d13b34b | ||
|
|
18e8775f38 | ||
|
|
813b019653 | ||
|
|
b0b1542939 | ||
|
|
15f19d8b8d | ||
|
|
82253b114c | ||
|
|
e0bfbf6dd4 | ||
|
|
4689e80e7a | ||
|
|
556e6c1c67 | ||
|
|
3ab84a526d | ||
|
|
bdce96f912 | ||
|
|
4811b99a4b | ||
|
|
fb2a64c07a | ||
|
|
e023e4f2e2 | ||
|
|
0b16b1e0f4 | ||
|
|
59073ad7ac | ||
|
|
8248644c45 | ||
|
|
f38e6394c9 | ||
|
|
0aaa529c6b | ||
|
|
b81a6562a1 | ||
|
|
c5b10db23a | ||
|
|
d16e444643 | ||
|
|
8202468099 | ||
|
|
766e8bd20f | ||
|
|
1214ab5a8c | ||
|
|
ebddbb25f8 | ||
|
|
59545e1110 | ||
|
|
500e090b11 | ||
|
|
a75ee555fa | ||
|
|
6a8c2164cd | ||
|
|
7f7efa325a | ||
|
|
9ba6cb08fc | ||
|
|
1872271a2d | ||
|
|
813b50864a | ||
|
|
b18cefe320 | ||
|
|
a54c359fcf | ||
|
|
8d83221a4a | ||
|
|
1879000720 | ||
|
|
ba92649a98 | ||
|
|
d2276dcaae | ||
|
|
25c9d20f3d | ||
|
|
0d853577df | ||
|
|
f91f3d8692 | ||
|
|
0f7cad8dfa | ||
|
|
db1a1e7ef0 | ||
|
|
e7de80a059 | ||
|
|
0d8c4e048e | ||
|
|
014a5a9d1f | ||
|
|
a6dd970859 | ||
|
|
aac730f5b1 | ||
|
|
ff95d9328e | ||
|
|
afe1d8cf52 | ||
|
|
67b819f3de | ||
|
|
9b6acb6b95 | ||
|
|
a9a59e1e34 | ||
|
|
5b05397356 | ||
|
|
7a7dbc0cfa | ||
|
|
6ac0ba6efe | ||
|
|
d3d008efb4 | ||
|
|
4f1528128a | ||
|
|
93c4326206 | ||
|
|
0fca7fe524 | ||
|
|
afdcab10c6 | ||
|
|
f8cc5eabe6 | ||
|
|
f304eb7633 | ||
|
|
827204e082 | ||
|
|
641d7ee8c8 | ||
|
|
3b11537b5e | ||
|
|
e51d87ae80 | ||
|
|
f16e7c996c | ||
|
|
55eb295c12 | ||
|
|
4767351c5e | ||
|
|
1d2502eb3f |
@@ -11,6 +11,7 @@ This workflow outlines the standard process for developing, documenting, and rel
|
|||||||
Reference: `.github/copilot-instructions.md`
|
Reference: `.github/copilot-instructions.md`
|
||||||
|
|
||||||
### Bilingual Requirement
|
### Bilingual Requirement
|
||||||
|
|
||||||
Every plugin **MUST** have bilingual versions for both code and documentation:
|
Every plugin **MUST** have bilingual versions for both code and documentation:
|
||||||
|
|
||||||
- **Code**:
|
- **Code**:
|
||||||
@@ -21,6 +22,7 @@ Every plugin **MUST** have bilingual versions for both code and documentation:
|
|||||||
- Chinese: `plugins/{type}/{name}/README_CN.md`
|
- Chinese: `plugins/{type}/{name}/README_CN.md`
|
||||||
|
|
||||||
### Code Structure
|
### Code Structure
|
||||||
|
|
||||||
- **Docstring**: Must include `title`, `author`, `version`, `description`, etc.
|
- **Docstring**: Must include `title`, `author`, `version`, `description`, etc.
|
||||||
- **Valves**: Use `pydantic` for configuration.
|
- **Valves**: Use `pydantic` for configuration.
|
||||||
- **Database**: Re-use `open_webui.internal.db` shared connection.
|
- **Database**: Re-use `open_webui.internal.db` shared connection.
|
||||||
@@ -32,8 +34,9 @@ Every plugin **MUST** have bilingual versions for both code and documentation:
|
|||||||
- Use Chat Persistence API for database storage
|
- Use Chat Persistence API for database storage
|
||||||
- Always update both `messages[]` and `history.messages`
|
- Always update both `messages[]` and `history.messages`
|
||||||
|
|
||||||
### Commit Messages
|
### Commit Messages & Release Notes
|
||||||
- **Language**: **English ONLY**. Do not use Chinese in commit messages.
|
|
||||||
|
- **Language**: **English ONLY**. Do not use Chinese in commit messages or release notes.
|
||||||
- **Format**: Conventional Commits (e.g., `feat:`, `fix:`, `docs:`).
|
- **Format**: Conventional Commits (e.g., `feat:`, `fix:`, `docs:`).
|
||||||
|
|
||||||
## 2. Documentation Updates
|
## 2. Documentation Updates
|
||||||
@@ -41,10 +44,12 @@ Every plugin **MUST** have bilingual versions for both code and documentation:
|
|||||||
When adding or updating a plugin, you **MUST** update the following documentation files to maintain consistency:
|
When adding or updating a plugin, you **MUST** update the following documentation files to maintain consistency:
|
||||||
|
|
||||||
### Plugin Directory
|
### Plugin Directory
|
||||||
|
|
||||||
- `README.md`: Update version, description, and usage. **Explicitly describe new features in a prominent position at the beginning.**
|
- `README.md`: Update version, description, and usage. **Explicitly describe new features in a prominent position at the beginning.**
|
||||||
- `README_CN.md`: Update version, description, and usage. **Explicitly describe new features in a prominent position at the beginning.**
|
- `README_CN.md`: Update version, description, and usage. **Explicitly describe new features in a prominent position at the beginning.**
|
||||||
|
|
||||||
### Global Documentation (`docs/`)
|
### Global Documentation (`docs/`)
|
||||||
|
|
||||||
- **Index Pages**:
|
- **Index Pages**:
|
||||||
- `docs/plugins/{type}/index.md`: Add/Update list item with **correct version**.
|
- `docs/plugins/{type}/index.md`: Add/Update list item with **correct version**.
|
||||||
- `docs/plugins/{type}/index.zh.md`: Add/Update list item with **correct version**.
|
- `docs/plugins/{type}/index.zh.md`: Add/Update list item with **correct version**.
|
||||||
@@ -53,6 +58,7 @@ When adding or updating a plugin, you **MUST** update the following documentatio
|
|||||||
- `docs/plugins/{type}/{name}.zh.md`: Ensure content matches README_CN.
|
- `docs/plugins/{type}/{name}.zh.md`: Ensure content matches README_CN.
|
||||||
|
|
||||||
### Root README
|
### Root README
|
||||||
|
|
||||||
- `README.md`: Add to "Featured Plugins" if applicable.
|
- `README.md`: Add to "Featured Plugins" if applicable.
|
||||||
- `README_CN.md`: Add to "Featured Plugins" if applicable.
|
- `README_CN.md`: Add to "Featured Plugins" if applicable.
|
||||||
|
|
||||||
@@ -61,6 +67,7 @@ When adding or updating a plugin, you **MUST** update the following documentatio
|
|||||||
Reference: `.github/workflows/release.yml`
|
Reference: `.github/workflows/release.yml`
|
||||||
|
|
||||||
### Version Bumping
|
### Version Bumping
|
||||||
|
|
||||||
- **Rule**: Version bump is required **ONLY when the user explicitly requests a release**. Regular code changes do NOT require version bumps.
|
- **Rule**: Version bump is required **ONLY when the user explicitly requests a release**. Regular code changes do NOT require version bumps.
|
||||||
- **Format**: Semantic Versioning (e.g., `1.0.0` -> `1.0.1`).
|
- **Format**: Semantic Versioning (e.g., `1.0.0` -> `1.0.1`).
|
||||||
- **When to Bump**: Only update the version when:
|
- **When to Bump**: Only update the version when:
|
||||||
@@ -78,6 +85,7 @@ Reference: `.github/workflows/release.yml`
|
|||||||
8. Docs Detail CN (`docs/.../{name}.zh.md`)
|
8. Docs Detail CN (`docs/.../{name}.zh.md`)
|
||||||
|
|
||||||
### Automated Release Process
|
### Automated Release Process
|
||||||
|
|
||||||
1. **Trigger**: Push to `main` branch with changes in `plugins/**/*.py`.
|
1. **Trigger**: Push to `main` branch with changes in `plugins/**/*.py`.
|
||||||
2. **Detection**: `scripts/extract_plugin_versions.py` detects changed plugins and compares versions.
|
2. **Detection**: `scripts/extract_plugin_versions.py` detects changed plugins and compares versions.
|
||||||
3. **Release**:
|
3. **Release**:
|
||||||
@@ -90,8 +98,12 @@ Reference: `.github/workflows/release.yml`
|
|||||||
- Action: Automatically updates the plugin code and metadata on OpenWebUI.com using `scripts/publish_plugin.py`.
|
- Action: Automatically updates the plugin code and metadata on OpenWebUI.com using `scripts/publish_plugin.py`.
|
||||||
- **Auto-Sync**: If a local plugin has no ID but matches an existing published plugin by **Title**, the script will automatically fetch the ID, update the local file, and proceed with the update.
|
- **Auto-Sync**: If a local plugin has no ID but matches an existing published plugin by **Title**, the script will automatically fetch the ID, update the local file, and proceed with the update.
|
||||||
- Requirement: `OPENWEBUI_API_KEY` secret must be set.
|
- Requirement: `OPENWEBUI_API_KEY` secret must be set.
|
||||||
|
- **README Link**: When announcing a release, always include the GitHub README URL for the plugin:
|
||||||
|
- Format: `https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/{type}/{name}/README.md`
|
||||||
|
- Example: `https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/filters/folder-memory/README.md`
|
||||||
|
|
||||||
### Pull Request Check
|
### Pull Request Check
|
||||||
|
|
||||||
- Workflow: `.github/workflows/plugin-version-check.yml`
|
- Workflow: `.github/workflows/plugin-version-check.yml`
|
||||||
- Checks if plugin files are modified.
|
- Checks if plugin files are modified.
|
||||||
- **Fails** if version number is not updated.
|
- **Fails** if version number is not updated.
|
||||||
@@ -111,5 +123,3 @@ Before committing:
|
|||||||
## 5. Git Operations (Agent Rules)
|
## 5. Git Operations (Agent Rules)
|
||||||
|
|
||||||
Strictly follow the rules defined in `.github/copilot-instructions.md` → **Git Operations (Agent Rules)** section.
|
Strictly follow the rules defined in `.github/copilot-instructions.md` → **Git Operations (Agent Rules)** section.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,15 @@
|
|||||||
"bug",
|
"bug",
|
||||||
"ideas"
|
"ideas"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "nahoj",
|
||||||
|
"name": "Johan Grande",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/469017?v=4",
|
||||||
|
"profile": "https://perso.crans.org/grande/",
|
||||||
|
"contributions": [
|
||||||
|
"ideas"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
124
.github/copilot-instructions.md
vendored
124
.github/copilot-instructions.md
vendored
@@ -42,11 +42,15 @@ plugins/actions/export_to_docx/
|
|||||||
- 格式: `**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** x.x.x | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)`
|
- 格式: `**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** x.x.x | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)`
|
||||||
- **注意**: Author 和 Project 为固定值,仅需更新 Version 版本号
|
- **注意**: Author 和 Project 为固定值,仅需更新 Version 版本号
|
||||||
3. **描述 (Description)**: 一句话功能介绍
|
3. **描述 (Description)**: 一句话功能介绍
|
||||||
4. **最新更新 (What's New)**: **必须**放在描述之后,显著展示最新版本的变更点 (仅展示最近 3 次更新)
|
4. **最新更新 (What's New)**: **必须**放在描述之后,仅展示**最近 1 次**更新
|
||||||
5. **核心特性 (Key Features)**: 使用 Emoji + 粗体标题 + 描述格式
|
5. **核心特性 (Key Features)**: 使用 Emoji + 粗体标题 + 描述格式
|
||||||
6. **使用方法 (How to Use)**: 按步骤说明
|
6. **使用方法 (How to Use)**: 按步骤说明
|
||||||
7. **配置参数 (Configuration/Valves)**: 使用表格格式,包含参数名、默认值、描述
|
7. **配置参数 (Configuration/Valves)**: 使用表格格式,包含参数名、默认值、描述
|
||||||
8. **其他 (Others)**: 支持的模板类型、语法示例、故障排除等
|
8. **支持 (Support)**: **必须**包含,放在配置参数之后、故障排除之前
|
||||||
|
- English: `If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.`
|
||||||
|
- 中文: `如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。`
|
||||||
|
9. **其他 (Others)**: 支持的模板类型、语法示例、故障排除等
|
||||||
|
- **Changelog**: 统一指向 GitHub 项目历史,不在 README 中列出具体变更
|
||||||
|
|
||||||
### 2. 插件目录结构 (Plugin Directory Structure)
|
### 2. 插件目录结构 (Plugin Directory Structure)
|
||||||
|
|
||||||
@@ -62,18 +66,41 @@ plugins/
|
|||||||
│ ├── ACTION_PLUGIN_TEMPLATE_CN.py # Chinese template
|
│ ├── ACTION_PLUGIN_TEMPLATE_CN.py # Chinese template
|
||||||
│ └── README.md
|
│ └── README.md
|
||||||
├── filters/ # Filter 插件 (输入处理)
|
├── filters/ # Filter 插件 (输入处理)
|
||||||
│ ├── my_filter/
|
│ └── ...
|
||||||
│ │ ├── my_filter.py
|
|
||||||
│ │ ├── 我的过滤器.py
|
|
||||||
│ │ ├── README.md
|
|
||||||
│ │ └── README_CN.md
|
|
||||||
│ └── README.md
|
|
||||||
├── pipes/ # Pipe 插件 (输出处理)
|
├── pipes/ # Pipe 插件 (输出处理)
|
||||||
│ └── ...
|
│ └── ...
|
||||||
└── pipelines/ # Pipeline 插件
|
├── pipelines/ # Pipeline 插件
|
||||||
|
└── ...
|
||||||
|
├── debug/ # 调试与开发工具 (Debug & Development Tools)
|
||||||
|
│ ├── my_debug_tool/
|
||||||
|
│ │ ├── debug_script.py
|
||||||
|
│ │ └── notes.md
|
||||||
|
│ └── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 调试目录规范 (Debug Directory Standards)
|
||||||
|
|
||||||
|
`plugins/debug/` 目录用于存放调试用的脚本、临时验证代码或开发笔记。
|
||||||
|
|
||||||
|
**目录结构 (Directory Structure)**:
|
||||||
|
应根据调试工具所属的插件或功能模块进行子目录分类,而非将文件散落在根目录。
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins/debug/
|
||||||
|
├── my_plugin_name/ # 特定插件的调试文件 (Debug files for specific plugin)
|
||||||
|
│ ├── debug_script.py
|
||||||
|
│ └── guides/
|
||||||
|
├── common_tools/ # 通用调试工具 (General debug tools)
|
||||||
|
│ └── ...
|
||||||
└── ...
|
└── ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**规范说明 (Guidelines)**:
|
||||||
|
- **不强制要求 README**: 该目录下的子项目不需要包含 `README.md`。
|
||||||
|
- **发布豁免**: 该目录下的内容**绝不会**被发布脚本处理。
|
||||||
|
- **内容灵活性**: 可以包含 Python 脚本、Markdown 文档、JSON 数据等。
|
||||||
|
- **分类存放**: 任何调试产物(如 `test_*.py`, `inspect_*.py`)都不应直接存放在项目根目录,必须移动到此目录下相应的子文件夹中。
|
||||||
|
|
||||||
### 3. 文档字符串规范 (Docstring Standard)
|
### 3. 文档字符串规范 (Docstring Standard)
|
||||||
|
|
||||||
每个插件文件必须以标准化的文档字符串开头:
|
每个插件文件必须以标准化的文档字符串开头:
|
||||||
@@ -100,13 +127,14 @@ description: 插件功能的简短描述。Brief description of plugin functiona
|
|||||||
| `author_url` | 作者主页链接 | `https://github.com/Fu-Jie/awesome-openwebui` |
|
| `author_url` | 作者主页链接 | `https://github.com/Fu-Jie/awesome-openwebui` |
|
||||||
| `funding_url` | 赞助/项目链接 | `https://github.com/open-webui` |
|
| `funding_url` | 赞助/项目链接 | `https://github.com/open-webui` |
|
||||||
| `version` | 语义化版本号 | `0.1.0`, `1.2.3` |
|
| `version` | 语义化版本号 | `0.1.0`, `1.2.3` |
|
||||||
| `icon_url` | 图标 (Base64 编码的 SVG) | 见下方图标规范 |
|
| `icon_url` | 图标 (Base64 编码的 SVG) | 仅 Action 插件**必须**提供。其他类型可选。 |
|
||||||
| `requirements` | 额外依赖 (仅 OpenWebUI 环境未安装的) | `python-docx==1.1.2` |
|
| `requirements` | 额外依赖 (仅 OpenWebUI 环境未安装的) | `python-docx==1.1.2` |
|
||||||
| `description` | 功能描述 | `将对话导出为 Word 文档` |
|
| `description` | 功能描述 | `将对话导出为 Word 文档` |
|
||||||
|
|
||||||
#### 图标规范 (Icon Guidelines)
|
#### 图标规范 (Icon Guidelines)
|
||||||
|
|
||||||
- 图标来源:从 [Lucide Icons](https://lucide.dev/icons/) 获取符合插件功能的图标
|
- 图标来源:从 [Lucide Icons](https://lucide.dev/icons/) 获取符合插件功能的图标
|
||||||
|
- 适用范围:Action 插件**必须**提供,其他插件可选
|
||||||
- 格式:Base64 编码的 SVG
|
- 格式:Base64 编码的 SVG
|
||||||
- 获取方法:从 Lucide 下载 SVG,然后使用 Base64 编码
|
- 获取方法:从 Lucide 下载 SVG,然后使用 Base64 编码
|
||||||
- 示例格式:
|
- 示例格式:
|
||||||
@@ -408,6 +436,51 @@ async def long_running_task_with_notification(self, event_emitter, ...):
|
|||||||
return task_future.result()
|
return task_future.result()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 7. 前端数据获取与交互规范 (Frontend Data Access & Interaction)
|
||||||
|
|
||||||
|
#### 获取前端信息 (Retrieving Frontend Info)
|
||||||
|
|
||||||
|
当需要获取用户浏览器的上下文信息(如语言、时区、LocalStorage)时,**必须**使用 `__event_call__` 的 `execute` 类型,而不是通过文件上传或复杂的 API 请求。
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _get_frontend_value(self, js_code: str) -> str:
|
||||||
|
"""Helper to execute JS and get return value."""
|
||||||
|
try:
|
||||||
|
response = await __event_call__(
|
||||||
|
{
|
||||||
|
"type": "execute",
|
||||||
|
"data": {
|
||||||
|
"code": js_code,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return str(response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to execute JS: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# 示例:获取界面语言 (Get UI Language)
|
||||||
|
async def get_user_language(self):
|
||||||
|
js_code = """
|
||||||
|
return (
|
||||||
|
localStorage.getItem('locale') ||
|
||||||
|
localStorage.getItem('language') ||
|
||||||
|
navigator.language ||
|
||||||
|
'en-US'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
return await self._get_frontend_value(js_code)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 适用场景与引导 (Usage Guidelines)
|
||||||
|
|
||||||
|
- **语言适配**: 动态获取界面语言 (`ru-RU`, `zh-CN`) 自动切换输出语言。
|
||||||
|
- **时区处理**: 获取 `Intl.DateTimeFormat().resolvedOptions().timeZone` 处理时间。
|
||||||
|
- **客户端存储**: 读取 `localStorage` 中的用户偏好设置。
|
||||||
|
- **硬件能力**: 获取 `navigator.clipboard` 或 `navigator.geolocation` (需授权)。
|
||||||
|
|
||||||
|
**注意**: 即使插件有 `Valves` 配置,也应优先尝试自动探测,提升用户体验。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚡ Action 插件规范 (Action Plugin Standards)
|
## ⚡ Action 插件规范 (Action Plugin Standards)
|
||||||
@@ -788,6 +861,19 @@ Filter 实例是**单例 (Singleton)**。
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 🧪 测试规范 (Testing Standards)
|
||||||
|
|
||||||
|
### 1. Copilot SDK 测试模型 (Copilot SDK Test Models)
|
||||||
|
|
||||||
|
在编写 Copilot SDK 相关的测试脚本时 (如 `test_injection.py`, `test_capabilities.py` 等),**必须**优先使用以下免费/低成本模型之一,严禁使用高昂费用的模型进行常规测试,除非用户明确要求:
|
||||||
|
|
||||||
|
- `gpt-5-mini` (首选 / Preferred)
|
||||||
|
- `gpt-4.1`
|
||||||
|
|
||||||
|
此规则适用于所有自动化测试脚本和临时验证脚本。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 🔄 工作流与流程 (Workflow & Process)
|
## 🔄 工作流与流程 (Workflow & Process)
|
||||||
|
|
||||||
### 1. ✅ 开发检查清单 (Development Checklist)
|
### 1. ✅ 开发检查清单 (Development Checklist)
|
||||||
@@ -822,11 +908,27 @@ Filter 实例是**单例 (Singleton)**。
|
|||||||
|
|
||||||
#### Commit Message 规范
|
#### Commit Message 规范
|
||||||
使用 Conventional Commits 格式 (`feat`, `fix`, `docs`, etc.)。
|
使用 Conventional Commits 格式 (`feat`, `fix`, `docs`, etc.)。
|
||||||
|
**必须**在提交标题与正文中清晰描述变更内容,确保在 Release 页面可读且可追踪。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
- 标题必须包含“做了什么”与影响范围(避免含糊词)。
|
||||||
|
- 正文必须列出关键变更点(1-3 条),与实际改动一一对应。
|
||||||
|
- 若影响用户或插件行为,必须在正文标明影响与迁移说明。
|
||||||
|
|
||||||
|
推荐格式:
|
||||||
|
- `feat(actions): add export settings panel`
|
||||||
|
- `fix(filters): handle empty metadata to avoid crash`
|
||||||
|
- `docs(plugins): update bilingual README structure`
|
||||||
|
|
||||||
|
正文示例:
|
||||||
|
- Add valves for export format selection
|
||||||
|
- Update README/README_CN to include What's New section
|
||||||
|
- Migration: default TITLE_SOURCE changed to chat_title
|
||||||
|
|
||||||
### 4. 🤖 Git Operations (Agent Rules)
|
### 4. 🤖 Git Operations (Agent Rules)
|
||||||
|
|
||||||
|
- **允许**: 直接推送到 `main` 分支并发布。
|
||||||
- **允许**: 创建功能分支 (`feature/xxx`),推送到功能分支。
|
- **允许**: 创建功能分支 (`feature/xxx`),推送到功能分支。
|
||||||
- **禁止**: 直接推送到 `main`,自动合并到 `main`。
|
|
||||||
|
|
||||||
### 5. 🤝 贡献者认可规范 (Contributor Recognition)
|
### 5. 🤝 贡献者认可规范 (Contributor Recognition)
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/publish_plugin.yml
vendored
1
.github/workflows/publish_plugin.yml
vendored
@@ -6,6 +6,7 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'plugins/**/*.py'
|
- 'plugins/**/*.py'
|
||||||
|
- '!plugins/debug/**'
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
46
.github/workflows/release.yml
vendored
46
.github/workflows/release.yml
vendored
@@ -246,6 +246,52 @@ jobs:
|
|||||||
echo "=== Collected Files ==="
|
echo "=== Collected Files ==="
|
||||||
find release_plugins -name "*.py" -type f | head -20
|
find release_plugins -name "*.py" -type f | head -20
|
||||||
|
|
||||||
|
- name: Update plugin icon URLs
|
||||||
|
run: |
|
||||||
|
echo "Updating icon_url in plugins to use absolute GitHub URLs..."
|
||||||
|
# Base URL for raw content using the release tag
|
||||||
|
REPO_URL="https://raw.githubusercontent.com/${{ github.repository }}/${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
|
find release_plugins -name "*.py" | while read -r file; do
|
||||||
|
# $file is like release_plugins/plugins/actions/infographic/infographic.py
|
||||||
|
# Remove release_plugins/ prefix to get the path in the repo
|
||||||
|
src_file="${file#release_plugins/}"
|
||||||
|
src_dir=$(dirname "$src_file")
|
||||||
|
base_name=$(basename "$src_file" .py)
|
||||||
|
|
||||||
|
# Check if a corresponding png exists in the source repository
|
||||||
|
png_file="${src_dir}/${base_name}.png"
|
||||||
|
|
||||||
|
if [ -f "$png_file" ]; then
|
||||||
|
echo "Found icon for $src_file: $png_file"
|
||||||
|
TARGET_ICON_URL="${REPO_URL}/${png_file}"
|
||||||
|
|
||||||
|
# Use python for safe replacement
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
file_path = '$file'
|
||||||
|
icon_url = '$TARGET_ICON_URL'
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Replace icon_url: ... with new url
|
||||||
|
# Matches 'icon_url: ...' and replaces it
|
||||||
|
new_content = re.sub(r'^icon_url:.*$', f'icon_url: {icon_url}', content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(new_content)
|
||||||
|
print(f'Successfully updated icon_url in {file_path}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error updating {file_path}: {e}', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
- name: Debug Filenames
|
- name: Debug Filenames
|
||||||
run: |
|
run: |
|
||||||
python3 -c "import sys; print(f'Filesystem encoding: {sys.getfilesystemencoding()}')"
|
python3 -c "import sys; print(f'Filesystem encoding: {sys.getfilesystemencoding()}')"
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -1,6 +1,6 @@
|
|||||||
# OpenWebUI Extras
|
# OpenWebUI Extras
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
[](#contributors-)
|
[](#contributors-)
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
English | [中文](./README_CN.md)
|
English | [中文](./README_CN.md)
|
||||||
@@ -10,28 +10,28 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
|||||||
<!-- STATS_START -->
|
<!-- STATS_START -->
|
||||||
## 📊 Community Stats
|
## 📊 Community Stats
|
||||||
|
|
||||||
> 🕐 Auto-updated: 2026-01-17 12:15
|
> 🕐 Auto-updated: 2026-02-07 17:12
|
||||||
|
|
||||||
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
||||||
|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|
|
||||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **118** | **108** | **25** |
|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **204** | **257** | **41** |
|
||||||
|
|
||||||
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
||||||
|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **16** | **1609** | **19531** | **94** | **123** |
|
| **20** | **3711** | **43662** | **220** | **256** |
|
||||||
|
|
||||||
### 🔥 Top 6 Popular Plugins
|
### 🔥 Top 6 Popular Plugins
|
||||||
|
|
||||||
> 🕐 Auto-updated: 2026-01-17 12:15
|
> 🕐 Auto-updated: 2026-02-07 17:12
|
||||||
|
|
||||||
| Rank | Plugin | Version | Downloads | Views | Updated |
|
| Rank | Plugin | Version | Downloads | Views | Updated |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|
|
||||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 502 | 4566 | 2026-01-16 |
|
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.2 | 883 | 7810 | 2026-01-28 |
|
||||||
| 🥈 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.4.9 | 215 | 2215 | 2026-01-14 |
|
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.5.0 | 635 | 5880 | 2026-01-30 |
|
||||||
| 🥉 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 201 | 738 | 2026-01-07 |
|
| 🥉 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 347 | 2686 | 2026-01-28 |
|
||||||
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.1.3 | 171 | 1880 | 2026-01-14 |
|
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.2.2 | 330 | 3436 | 2026-01-28 |
|
||||||
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 128 | 1214 | 2026-01-14 |
|
| 5️⃣ | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 327 | 1520 | 2026-01-29 |
|
||||||
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 128 | 2241 | 2026-01-07 |
|
| 6️⃣ | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | 1.2.4 | 294 | 4270 | 2026-01-29 |
|
||||||
|
|
||||||
*See full stats in [Community Stats Report](./docs/community-stats.md)*
|
*See full stats in [Community Stats Report](./docs/community-stats.md)*
|
||||||
<!-- STATS_END -->
|
<!-- STATS_END -->
|
||||||
@@ -43,6 +43,7 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
|||||||
Located in the `plugins/` directory, containing Python-based enhancements:
|
Located in the `plugins/` directory, containing Python-based enhancements:
|
||||||
|
|
||||||
#### Actions
|
#### Actions
|
||||||
|
|
||||||
- **Smart Mind Map** (`smart-mind-map`): Generates interactive mind maps from text.
|
- **Smart Mind Map** (`smart-mind-map`): Generates interactive mind maps from text.
|
||||||
- **Smart Infographic** (`infographic`): Transforms text into professional infographics using AntV.
|
- **Smart Infographic** (`infographic`): Transforms text into professional infographics using AntV.
|
||||||
- **Flash Card** (`flash-card`): Quickly generates beautiful flashcards for learning.
|
- **Flash Card** (`flash-card`): Quickly generates beautiful flashcards for learning.
|
||||||
@@ -51,11 +52,18 @@ Located in the `plugins/` directory, containing Python-based enhancements:
|
|||||||
- **Export to Word** (`export_to_docx`): Exports chat history to Word documents.
|
- **Export to Word** (`export_to_docx`): Exports chat history to Word documents.
|
||||||
|
|
||||||
#### Filters
|
#### Filters
|
||||||
|
|
||||||
- **Async Context Compression** (`async-context-compression`): Optimizes token usage via context compression.
|
- **Async Context Compression** (`async-context-compression`): Optimizes token usage via context compression.
|
||||||
- **Context Enhancement** (`context_enhancement_filter`): Enhances chat context.
|
- **Context Enhancement** (`context_enhancement_filter`): Enhances chat context.
|
||||||
|
- **Folder Memory** (`folder-memory`): Automatically extracts project rules from conversations and injects them into the folder's system prompt.
|
||||||
- **Markdown Normalizer** (`markdown_normalizer`): Fixes common Markdown formatting issues in LLM outputs.
|
- **Markdown Normalizer** (`markdown_normalizer`): Fixes common Markdown formatting issues in LLM outputs.
|
||||||
|
|
||||||
|
#### Pipes
|
||||||
|
|
||||||
|
- **GitHub Copilot SDK** (`github-copilot-sdk`): Official GitHub Copilot SDK integration. Supports dynamic models, multi-turn conversation, streaming, multimodal input, and infinite sessions.
|
||||||
|
|
||||||
#### Pipelines
|
#### Pipelines
|
||||||
|
|
||||||
- **MoE Prompt Refiner** (`moe_prompt_refiner`): Refines prompts for Mixture of Experts (MoE) summary requests to generate high-quality comprehensive reports.
|
- **MoE Prompt Refiner** (`moe_prompt_refiner`): Refines prompts for Mixture of Experts (MoE) summary requests to generate high-quality comprehensive reports.
|
||||||
|
|
||||||
### 🎯 Prompts
|
### 🎯 Prompts
|
||||||
@@ -65,6 +73,12 @@ Located in the `prompts/` directory, containing fine-tuned System Prompts:
|
|||||||
- **Coding**: Programming assistance prompts.
|
- **Coding**: Programming assistance prompts.
|
||||||
- **Marketing**: Marketing and copywriting prompts.
|
- **Marketing**: Marketing and copywriting prompts.
|
||||||
|
|
||||||
|
## 🛠️ Extensions
|
||||||
|
|
||||||
|
Standalone frontend extensions to supercharge your Open WebUI:
|
||||||
|
|
||||||
|
- **[Open WebUI Prompt Plus](https://github.com/Fu-Jie/open-webui-prompt-plus)**: An all-in-one prompt management suite featuring AI-powered prompt generation, spotlight-style quick search, and advanced category organization.
|
||||||
|
|
||||||
## 📖 Documentation
|
## 📖 Documentation
|
||||||
|
|
||||||
Located in the `docs/en/` directory:
|
Located in the `docs/en/` directory:
|
||||||
@@ -100,6 +114,7 @@ This project is a collection of resources and does not require a Python environm
|
|||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
If you have great prompts or plugins to share:
|
If you have great prompts or plugins to share:
|
||||||
|
|
||||||
1. Fork this repository.
|
1. Fork this repository.
|
||||||
2. Add your files to the appropriate `prompts/` or `plugins/` directory.
|
2. Add your files to the appropriate `prompts/` or `plugins/` directory.
|
||||||
3. Submit a Pull Request.
|
3. Submit a Pull Request.
|
||||||
@@ -119,6 +134,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbb-dev"><img src="https://avatars.githubusercontent.com/u/37469229?v=4?s=100" width="100px;" alt="rbb-dev"/><br /><sub><b>rbb-dev</b></sub></a><br /><a href="#ideas-rbb-dev" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Fu-Jie/awesome-openwebui/commits?author=rbb-dev" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rbb-dev"><img src="https://avatars.githubusercontent.com/u/37469229?v=4?s=100" width="100px;" alt="rbb-dev"/><br /><sub><b>rbb-dev</b></sub></a><br /><a href="#ideas-rbb-dev" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/Fu-Jie/awesome-openwebui/commits?author=rbb-dev" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://trade.xyz/?ref=BZ1RJRXWO"><img src="https://avatars.githubusercontent.com/u/7317522?v=4?s=100" width="100px;" alt="Raxxoor"/><br /><sub><b>Raxxoor</b></sub></a><br /><a href="https://github.com/Fu-Jie/awesome-openwebui/issues?q=author%3Adhaern" title="Bug reports">🐛</a> <a href="#ideas-dhaern" title="Ideas, Planning, & Feedback">🤔</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://trade.xyz/?ref=BZ1RJRXWO"><img src="https://avatars.githubusercontent.com/u/7317522?v=4?s=100" width="100px;" alt="Raxxoor"/><br /><sub><b>Raxxoor</b></sub></a><br /><a href="https://github.com/Fu-Jie/awesome-openwebui/issues?q=author%3Adhaern" title="Bug reports">🐛</a> <a href="#ideas-dhaern" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/i-iooi-i"><img src="https://avatars.githubusercontent.com/u/1827701?v=4?s=100" width="100px;" alt="ZOLO"/><br /><sub><b>ZOLO</b></sub></a><br /><a href="https://github.com/Fu-Jie/awesome-openwebui/issues?q=author%3Ai-iooi-i" title="Bug reports">🐛</a> <a href="#ideas-i-iooi-i" title="Ideas, Planning, & Feedback">🤔</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/i-iooi-i"><img src="https://avatars.githubusercontent.com/u/1827701?v=4?s=100" width="100px;" alt="ZOLO"/><br /><sub><b>ZOLO</b></sub></a><br /><a href="https://github.com/Fu-Jie/awesome-openwebui/issues?q=author%3Ai-iooi-i" title="Bug reports">🐛</a> <a href="#ideas-i-iooi-i" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://perso.crans.org/grande/"><img src="https://avatars.githubusercontent.com/u/469017?v=4?s=100" width="100px;" alt="Johan Grande"/><br /><sub><b>Johan Grande</b></sub></a><br /><a href="#ideas-nahoj" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
33
README_CN.md
33
README_CN.md
@@ -7,28 +7,28 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
<!-- STATS_START -->
|
<!-- STATS_START -->
|
||||||
## 📊 社区统计
|
## 📊 社区统计
|
||||||
|
|
||||||
> 🕐 自动更新于 2026-01-17 12:15
|
> 🕐 自动更新于 2026-02-07 17:12
|
||||||
|
|
||||||
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
||||||
|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|
|
||||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **118** | **108** | **25** |
|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **204** | **257** | **41** |
|
||||||
|
|
||||||
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
||||||
|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **16** | **1609** | **19531** | **94** | **123** |
|
| **20** | **3711** | **43662** | **220** | **256** |
|
||||||
|
|
||||||
### 🔥 热门插件 Top 6
|
### 🔥 热门插件 Top 6
|
||||||
|
|
||||||
> 🕐 自动更新于 2026-01-17 12:15
|
> 🕐 自动更新于 2026-02-07 17:12
|
||||||
|
|
||||||
| 排名 | 插件 | 版本 | 下载 | 浏览 | 更新日期 |
|
| 排名 | 插件 | 版本 | 下载 | 浏览 | 更新日期 |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|
|
||||||
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.1 | 502 | 4566 | 2026-01-16 |
|
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 0.9.2 | 883 | 7810 | 2026-01-28 |
|
||||||
| 🥈 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.4.9 | 215 | 2215 | 2026-01-14 |
|
| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 1.5.0 | 635 | 5880 | 2026-01-30 |
|
||||||
| 🥉 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 201 | 738 | 2026-01-07 |
|
| 🥉 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 347 | 2686 | 2026-01-28 |
|
||||||
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.1.3 | 171 | 1880 | 2026-01-14 |
|
| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 1.2.2 | 330 | 3436 | 2026-01-28 |
|
||||||
| 5️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 0.4.3 | 128 | 1214 | 2026-01-14 |
|
| 5️⃣ | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 0.3.7 | 327 | 1520 | 2026-01-29 |
|
||||||
| 6️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 0.2.4 | 128 | 2241 | 2026-01-07 |
|
| 6️⃣ | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | 1.2.4 | 294 | 4270 | 2026-01-29 |
|
||||||
|
|
||||||
*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
||||||
<!-- STATS_END -->
|
<!-- STATS_END -->
|
||||||
@@ -40,6 +40,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
位于 `plugins/` 目录,包含各类 Python 编写的功能增强插件:
|
位于 `plugins/` 目录,包含各类 Python 编写的功能增强插件:
|
||||||
|
|
||||||
#### Actions (交互增强)
|
#### Actions (交互增强)
|
||||||
|
|
||||||
- **Smart Mind Map** (`smart-mind-map`): 智能分析文本并生成交互式思维导图。
|
- **Smart Mind Map** (`smart-mind-map`): 智能分析文本并生成交互式思维导图。
|
||||||
- **Smart Infographic** (`infographic`): 基于 AntV 的智能信息图生成工具。
|
- **Smart Infographic** (`infographic`): 基于 AntV 的智能信息图生成工具。
|
||||||
- **Flash Card** (`flash-card`): 快速生成精美的学习记忆卡片。
|
- **Flash Card** (`flash-card`): 快速生成精美的学习记忆卡片。
|
||||||
@@ -48,17 +49,22 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
- **Export to Word** (`export_to_docx`): 将对话内容导出为 Word 文档。
|
- **Export to Word** (`export_to_docx`): 将对话内容导出为 Word 文档。
|
||||||
|
|
||||||
#### Filters (消息处理)
|
#### Filters (消息处理)
|
||||||
|
|
||||||
- **Async Context Compression** (`async-context-compression`): 异步上下文压缩,优化 Token 使用。
|
- **Async Context Compression** (`async-context-compression`): 异步上下文压缩,优化 Token 使用。
|
||||||
- **Context Enhancement** (`context_enhancement_filter`): 上下文增强过滤器。
|
- **Context Enhancement** (`context_enhancement_filter`): 上下文增强过滤器。
|
||||||
|
- **Folder Memory** (`folder-memory`): 自动从对话中提取项目规则并注入到文件夹系统提示词中。
|
||||||
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Gemini Manifold 配套增强。
|
- **Gemini Manifold Companion** (`gemini_manifold_companion`): Gemini Manifold 配套增强。
|
||||||
- **Gemini Multimodal Filter** (`web_gemini_multimodel_filter`): 为任意模型提供多模态能力(PDF、Office、视频等),支持智能路由和字幕精修。
|
- **Gemini Multimodal Filter** (`web_gemini_multimodel_filter`): 为任意模型提供多模态能力(PDF、Office、视频等),支持智能路由和字幕精修。
|
||||||
- **Markdown Normalizer** (`markdown_normalizer`): 修复 LLM 输出中常见的 Markdown 格式问题。
|
- **Markdown Normalizer** (`markdown_normalizer`): 修复 LLM 输出中常见的 Markdown 格式问题。
|
||||||
- **Multi-Model Context Merger** (`multi_model_context_merger`): 自动合并并注入多模型回答的上下文。
|
- **Multi-Model Context Merger** (`multi_model_context_merger`): 自动合并并注入多模型回答的上下文。
|
||||||
|
|
||||||
#### Pipes (模型管道)
|
#### Pipes (模型管道)
|
||||||
|
|
||||||
|
- **GitHub Copilot SDK** (`github-copilot-sdk`): GitHub Copilot SDK 官方集成。支持动态模型、多轮对话、流式输出、图片输入及无限会话。
|
||||||
- **Gemini Manifold** (`gemini_mainfold`): 集成 Gemini 模型的管道。
|
- **Gemini Manifold** (`gemini_mainfold`): 集成 Gemini 模型的管道。
|
||||||
|
|
||||||
#### Pipelines (工作流管道)
|
#### Pipelines (工作流管道)
|
||||||
|
|
||||||
- **MoE Prompt Refiner** (`moe_prompt_refiner`): 优化多模型 (MoE) 汇总请求的提示词,生成高质量的综合报告。
|
- **MoE Prompt Refiner** (`moe_prompt_refiner`): 优化多模型 (MoE) 汇总请求的提示词,生成高质量的综合报告。
|
||||||
|
|
||||||
### 🎯 提示词 (Prompts)
|
### 🎯 提示词 (Prompts)
|
||||||
@@ -70,6 +76,12 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
|
|
||||||
每个提示词都独立保存为 Markdown 文件,可直接在 OpenWebUI 中使用。
|
每个提示词都独立保存为 Markdown 文件,可直接在 OpenWebUI 中使用。
|
||||||
|
|
||||||
|
## 🛠️ 扩展 (Extensions)
|
||||||
|
|
||||||
|
Open WebUI 的前端增强扩展:
|
||||||
|
|
||||||
|
- **[Open WebUI Prompt Plus](https://github.com/Fu-Jie/open-webui-prompt-plus)**: 一站式提示词管理套件,支持 AI 提示词生成、Spotlight 风格快速搜索及高级分类管理。
|
||||||
|
|
||||||
## 📖 开发文档
|
## 📖 开发文档
|
||||||
|
|
||||||
位于 `docs/zh/` 目录:
|
位于 `docs/zh/` 目录:
|
||||||
@@ -106,6 +118,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
### 贡献代码
|
### 贡献代码
|
||||||
|
|
||||||
如果你有优质的提示词或插件想要分享:
|
如果你有优质的提示词或插件想要分享:
|
||||||
|
|
||||||
1. Fork 本仓库。
|
1. Fork 本仓库。
|
||||||
2. 将你的文件添加到对应的 `prompts/` 或 `plugins/` 目录。
|
2. 将你的文件添加到对应的 `prompts/` 或 `plugins/` 目录。
|
||||||
3. 提交 Pull Request。
|
3. 提交 Pull Request。
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ The Chinese version (README_CN.md) MUST be translated based on this English vers
|
|||||||
|
|
||||||
## What's New
|
## What's New
|
||||||
|
|
||||||
<!-- Keep the changelog for the last 3 versions here. Remove this section for the initial release. -->
|
<!-- Keep only the latest update here. Remove this section for the initial release. -->
|
||||||
|
|
||||||
### v1.0.0
|
### v1.0.0
|
||||||
|
|
||||||
- **Initial Release**: Released the first version of the plugin.
|
- **Initial Release**: Released the first version of the plugin.
|
||||||
- **[Feature Name]**: [Brief description of the feature].
|
- **[Feature Name]**: [Brief description of the feature].
|
||||||
|
|
||||||
@@ -36,9 +37,17 @@ The Chinese version (README_CN.md) MUST be translated based on this English vers
|
|||||||
| `VALVE_NAME` | `Default Value` | Description of what this setting does. |
|
| `VALVE_NAME` | `Default Value` | Description of what this setting does. |
|
||||||
| `ANOTHER_VALVE` | `True` | Another setting description. |
|
| `ANOTHER_VALVE` | `True` | Another setting description. |
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
## Troubleshooting ❓
|
## Troubleshooting ❓
|
||||||
|
|
||||||
- **Plugin not working?**: Check if the filter/action is enabled in the model settings.
|
- **Plugin not working?**: Check if the filter/action is enabled in the model settings.
|
||||||
- **Debug Logs**: Enable `SHOW_DEBUG_LOG` in Valves and check the browser console (F12) for detailed logs.
|
- **Debug Logs**: Enable `SHOW_DEBUG_LOG` in Valves and check the browser console (F12) for detailed logs.
|
||||||
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
||||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"label": "downloads",
|
"label": "downloads",
|
||||||
"message": "1.6k",
|
"message": "3.7k",
|
||||||
"color": "blue",
|
"color": "blue",
|
||||||
"namedLogo": "openwebui"
|
"namedLogo": "openwebui"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"label": "followers",
|
"label": "followers",
|
||||||
"message": "118",
|
"message": "204",
|
||||||
"color": "blue"
|
"color": "blue"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"label": "plugins",
|
"label": "plugins",
|
||||||
"message": "16",
|
"message": "20",
|
||||||
"color": "green"
|
"color": "green"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"label": "points",
|
"label": "points",
|
||||||
"message": "108",
|
"message": "257",
|
||||||
"color": "orange"
|
"color": "orange"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"label": "upvotes",
|
"label": "upvotes",
|
||||||
"message": "94",
|
"message": "220",
|
||||||
"color": "brightgreen"
|
"color": "brightgreen"
|
||||||
}
|
}
|
||||||
@@ -1,80 +1,50 @@
|
|||||||
{
|
{
|
||||||
"total_posts": 16,
|
"total_posts": 20,
|
||||||
"total_downloads": 1609,
|
"total_downloads": 3711,
|
||||||
"total_views": 19531,
|
"total_views": 43662,
|
||||||
"total_upvotes": 94,
|
"total_upvotes": 220,
|
||||||
"total_downvotes": 2,
|
"total_downvotes": 2,
|
||||||
"total_saves": 123,
|
"total_saves": 256,
|
||||||
"total_comments": 23,
|
"total_comments": 47,
|
||||||
"by_type": {
|
"by_type": {
|
||||||
|
"pipe": 1,
|
||||||
"action": 14,
|
"action": 14,
|
||||||
"unknown": 2
|
"unknown": 4,
|
||||||
|
"filter": 1
|
||||||
},
|
},
|
||||||
"posts": [
|
"posts": [
|
||||||
{
|
{
|
||||||
"title": "Smart Mind Map",
|
"title": "Smart Mind Map",
|
||||||
"slug": "turn_any_text_into_beautiful_mind_maps_3094c59a",
|
"slug": "turn_any_text_into_beautiful_mind_maps_3094c59a",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.9.1",
|
"version": "0.9.2",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.",
|
"description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.",
|
||||||
"downloads": 502,
|
"downloads": 883,
|
||||||
"views": 4566,
|
"views": 7810,
|
||||||
"upvotes": 13,
|
"upvotes": 22,
|
||||||
"saves": 28,
|
"saves": 48,
|
||||||
"comments": 11,
|
"comments": 13,
|
||||||
"created_at": "2025-12-30",
|
"created_at": "2025-12-30",
|
||||||
"updated_at": "2026-01-16",
|
"updated_at": "2026-01-28",
|
||||||
"url": "https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a"
|
"url": "https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "📊 Smart Infographic (AntV)",
|
"title": "Smart Infographic",
|
||||||
"slug": "smart_infographic_ad6f0c7f",
|
"slug": "smart_infographic_ad6f0c7f",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "1.4.9",
|
"version": "1.5.0",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.",
|
"description": "AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.",
|
||||||
"downloads": 215,
|
"downloads": 635,
|
||||||
"views": 2215,
|
"views": 5880,
|
||||||
"upvotes": 10,
|
"upvotes": 24,
|
||||||
"saves": 15,
|
"saves": 33,
|
||||||
"comments": 2,
|
"comments": 10,
|
||||||
"created_at": "2025-12-28",
|
"created_at": "2025-12-28",
|
||||||
"updated_at": "2026-01-14",
|
"updated_at": "2026-01-30",
|
||||||
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
|
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"title": "Export to Excel",
|
|
||||||
"slug": "export_mulit_table_to_excel_244b8f9d",
|
|
||||||
"type": "action",
|
|
||||||
"version": "0.3.7",
|
|
||||||
"author": "Fu-Jie",
|
|
||||||
"description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.",
|
|
||||||
"downloads": 201,
|
|
||||||
"views": 738,
|
|
||||||
"upvotes": 3,
|
|
||||||
"saves": 5,
|
|
||||||
"comments": 0,
|
|
||||||
"created_at": "2025-05-30",
|
|
||||||
"updated_at": "2026-01-07",
|
|
||||||
"url": "https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Async Context Compression",
|
|
||||||
"slug": "async_context_compression_b1655bc8",
|
|
||||||
"type": "action",
|
|
||||||
"version": "1.1.3",
|
|
||||||
"author": "Fu-Jie",
|
|
||||||
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
|
|
||||||
"downloads": 171,
|
|
||||||
"views": 1880,
|
|
||||||
"upvotes": 7,
|
|
||||||
"saves": 18,
|
|
||||||
"comments": 0,
|
|
||||||
"created_at": "2025-11-08",
|
|
||||||
"updated_at": "2026-01-14",
|
|
||||||
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"title": "Export to Word (Enhanced)",
|
"title": "Export to Word (Enhanced)",
|
||||||
"slug": "export_to_word_enhanced_formatting_fca6a315",
|
"slug": "export_to_word_enhanced_formatting_fca6a315",
|
||||||
@@ -82,15 +52,63 @@
|
|||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support.",
|
"description": "Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support.",
|
||||||
"downloads": 128,
|
"downloads": 347,
|
||||||
"views": 1214,
|
"views": 2686,
|
||||||
"upvotes": 6,
|
"upvotes": 14,
|
||||||
"saves": 14,
|
"saves": 26,
|
||||||
"comments": 0,
|
"comments": 2,
|
||||||
"created_at": "2026-01-03",
|
"created_at": "2026-01-03",
|
||||||
"updated_at": "2026-01-14",
|
"updated_at": "2026-01-28",
|
||||||
"url": "https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315"
|
"url": "https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Async Context Compression",
|
||||||
|
"slug": "async_context_compression_b1655bc8",
|
||||||
|
"type": "action",
|
||||||
|
"version": "1.2.2",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
|
||||||
|
"downloads": 330,
|
||||||
|
"views": 3436,
|
||||||
|
"upvotes": 14,
|
||||||
|
"saves": 33,
|
||||||
|
"comments": 0,
|
||||||
|
"created_at": "2025-11-08",
|
||||||
|
"updated_at": "2026-01-28",
|
||||||
|
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Export to Excel",
|
||||||
|
"slug": "export_mulit_table_to_excel_244b8f9d",
|
||||||
|
"type": "action",
|
||||||
|
"version": "0.3.7",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.",
|
||||||
|
"downloads": 327,
|
||||||
|
"views": 1520,
|
||||||
|
"upvotes": 7,
|
||||||
|
"saves": 6,
|
||||||
|
"comments": 0,
|
||||||
|
"created_at": "2025-05-30",
|
||||||
|
"updated_at": "2026-01-29",
|
||||||
|
"url": "https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Markdown Normalizer",
|
||||||
|
"slug": "markdown_normalizer_baaa8732",
|
||||||
|
"type": "action",
|
||||||
|
"version": "1.2.4",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "A content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting.",
|
||||||
|
"downloads": 294,
|
||||||
|
"views": 4270,
|
||||||
|
"upvotes": 17,
|
||||||
|
"saves": 27,
|
||||||
|
"comments": 5,
|
||||||
|
"created_at": "2026-01-12",
|
||||||
|
"updated_at": "2026-01-29",
|
||||||
|
"url": "https://openwebui.com/posts/markdown_normalizer_baaa8732"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Flash Card",
|
"title": "Flash Card",
|
||||||
"slug": "flash_card_65a2ea8f",
|
"slug": "flash_card_65a2ea8f",
|
||||||
@@ -98,15 +116,31 @@
|
|||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
||||||
"downloads": 128,
|
"downloads": 213,
|
||||||
"views": 2241,
|
"views": 3247,
|
||||||
"upvotes": 8,
|
"upvotes": 13,
|
||||||
"saves": 10,
|
"saves": 14,
|
||||||
"comments": 2,
|
"comments": 2,
|
||||||
"created_at": "2025-12-30",
|
"created_at": "2025-12-30",
|
||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-28",
|
||||||
"url": "https://openwebui.com/posts/flash_card_65a2ea8f"
|
"url": "https://openwebui.com/posts/flash_card_65a2ea8f"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "AI Task Instruction Generator",
|
||||||
|
"slug": "ai_task_instruction_generator_9bab8b37",
|
||||||
|
"type": "unknown",
|
||||||
|
"version": "",
|
||||||
|
"author": "",
|
||||||
|
"description": "",
|
||||||
|
"downloads": 153,
|
||||||
|
"views": 2234,
|
||||||
|
"upvotes": 8,
|
||||||
|
"saves": 3,
|
||||||
|
"comments": 0,
|
||||||
|
"created_at": "2026-01-28",
|
||||||
|
"updated_at": "2026-01-28",
|
||||||
|
"url": "https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Deep Dive",
|
"title": "Deep Dive",
|
||||||
"slug": "deep_dive_c0b846e4",
|
"slug": "deep_dive_c0b846e4",
|
||||||
@@ -114,10 +148,10 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.",
|
"description": "A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.",
|
||||||
"downloads": 57,
|
"downloads": 134,
|
||||||
"views": 596,
|
"views": 1162,
|
||||||
"upvotes": 3,
|
"upvotes": 6,
|
||||||
"saves": 5,
|
"saves": 11,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2026-01-08",
|
"created_at": "2026-01-08",
|
||||||
"updated_at": "2026-01-08",
|
"updated_at": "2026-01-08",
|
||||||
@@ -130,77 +164,93 @@
|
|||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
||||||
"downloads": 56,
|
"downloads": 119,
|
||||||
"views": 1231,
|
"views": 2111,
|
||||||
"upvotes": 9,
|
"upvotes": 13,
|
||||||
"saves": 3,
|
"saves": 6,
|
||||||
"comments": 1,
|
"comments": 4,
|
||||||
"created_at": "2026-01-04",
|
"created_at": "2026-01-04",
|
||||||
"updated_at": "2026-01-14",
|
"updated_at": "2026-01-28",
|
||||||
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Markdown Normalizer",
|
"title": "智能信息图",
|
||||||
"slug": "markdown_normalizer_baaa8732",
|
|
||||||
"type": "action",
|
|
||||||
"version": "1.1.2",
|
|
||||||
"author": "Fu-Jie",
|
|
||||||
"description": "A content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting.",
|
|
||||||
"downloads": 55,
|
|
||||||
"views": 1642,
|
|
||||||
"upvotes": 8,
|
|
||||||
"saves": 14,
|
|
||||||
"comments": 5,
|
|
||||||
"created_at": "2026-01-12",
|
|
||||||
"updated_at": "2026-01-13",
|
|
||||||
"url": "https://openwebui.com/posts/markdown_normalizer_baaa8732"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "📊 智能信息图 (AntV Infographic)",
|
|
||||||
"slug": "智能信息图_e04a48ff",
|
"slug": "智能信息图_e04a48ff",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "1.4.9",
|
"version": "1.5.0",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
||||||
"downloads": 41,
|
"downloads": 56,
|
||||||
"views": 644,
|
"views": 1006,
|
||||||
"upvotes": 4,
|
"upvotes": 10,
|
||||||
"saves": 0,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-12-28",
|
"created_at": "2025-12-28",
|
||||||
"updated_at": "2026-01-14",
|
"updated_at": "2026-01-29",
|
||||||
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "📂 Folder Memory – Auto-Evolving Project Context",
|
||||||
|
"slug": "folder_memory_auto_evolving_project_context_4a9875b2",
|
||||||
|
"type": "filter",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "Automatically extracts project rules from conversations and injects them into the folder's system prompt.",
|
||||||
|
"downloads": 56,
|
||||||
|
"views": 1209,
|
||||||
|
"upvotes": 6,
|
||||||
|
"saves": 8,
|
||||||
|
"comments": 0,
|
||||||
|
"created_at": "2026-01-20",
|
||||||
|
"updated_at": "2026-01-20",
|
||||||
|
"url": "https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "GitHub Copilot Official SDK Pipe",
|
||||||
|
"slug": "github_copilot_official_sdk_pipe_ce96f7b4",
|
||||||
|
"type": "pipe",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "Integrate GitHub Copilot SDK. Supports dynamic models, multi-turn conversation, streaming, multimodal input, infinite sessions, and frontend debug logging.",
|
||||||
|
"downloads": 52,
|
||||||
|
"views": 1882,
|
||||||
|
"upvotes": 12,
|
||||||
|
"saves": 6,
|
||||||
|
"comments": 1,
|
||||||
|
"created_at": "2026-01-26",
|
||||||
|
"updated_at": "2026-02-07",
|
||||||
|
"url": "https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "思维导图",
|
"title": "思维导图",
|
||||||
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
|
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.9.1",
|
"version": "0.9.2",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
||||||
"downloads": 22,
|
"downloads": 37,
|
||||||
"views": 385,
|
"views": 564,
|
||||||
"upvotes": 2,
|
"upvotes": 6,
|
||||||
"saves": 1,
|
"saves": 2,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-12-31",
|
"created_at": "2025-12-31",
|
||||||
"updated_at": "2026-01-14",
|
"updated_at": "2026-01-28",
|
||||||
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "异步上下文压缩",
|
"title": "异步上下文压缩",
|
||||||
"slug": "异步上下文压缩_5c0617cb",
|
"slug": "异步上下文压缩_5c0617cb",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "1.1.3",
|
"version": "1.2.2",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
|
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
|
||||||
"downloads": 14,
|
"downloads": 31,
|
||||||
"views": 332,
|
"views": 628,
|
||||||
"upvotes": 4,
|
"upvotes": 7,
|
||||||
"saves": 1,
|
"saves": 4,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-11-08",
|
"created_at": "2025-11-08",
|
||||||
"updated_at": "2026-01-14",
|
"updated_at": "2026-01-28",
|
||||||
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
|
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -210,13 +260,13 @@
|
|||||||
"version": "0.2.4",
|
"version": "0.2.4",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
|
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
|
||||||
"downloads": 13,
|
"downloads": 27,
|
||||||
"views": 420,
|
"views": 661,
|
||||||
"upvotes": 4,
|
"upvotes": 8,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-12-30",
|
"created_at": "2025-12-30",
|
||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-28",
|
||||||
"url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3"
|
"url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -226,15 +276,31 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。",
|
"description": "全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。",
|
||||||
"downloads": 6,
|
"downloads": 17,
|
||||||
"views": 254,
|
"views": 400,
|
||||||
"upvotes": 2,
|
"upvotes": 5,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2026-01-08",
|
"created_at": "2026-01-08",
|
||||||
"updated_at": "2026-01-08",
|
"updated_at": "2026-01-08",
|
||||||
"url": "https://openwebui.com/posts/精读_99830b0f"
|
"url": "https://openwebui.com/posts/精读_99830b0f"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager",
|
||||||
|
"slug": "open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e",
|
||||||
|
"type": "unknown",
|
||||||
|
"version": "",
|
||||||
|
"author": "",
|
||||||
|
"description": "",
|
||||||
|
"downloads": 0,
|
||||||
|
"views": 1429,
|
||||||
|
"upvotes": 12,
|
||||||
|
"saves": 17,
|
||||||
|
"comments": 8,
|
||||||
|
"created_at": "2026-01-25",
|
||||||
|
"updated_at": "2026-01-28",
|
||||||
|
"url": "https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Review of Claude Haiku 4.5",
|
"title": "Review of Claude Haiku 4.5",
|
||||||
"slug": "review_of_claude_haiku_45_41b0db39",
|
"slug": "review_of_claude_haiku_45_41b0db39",
|
||||||
@@ -243,8 +309,8 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"views": 41,
|
"views": 135,
|
||||||
"upvotes": 0,
|
"upvotes": 2,
|
||||||
"saves": 0,
|
"saves": 0,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2026-01-14",
|
"created_at": "2026-01-14",
|
||||||
@@ -259,9 +325,9 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"downloads": 0,
|
"downloads": 0,
|
||||||
"views": 1132,
|
"views": 1392,
|
||||||
"upvotes": 11,
|
"upvotes": 14,
|
||||||
"saves": 7,
|
"saves": 9,
|
||||||
"comments": 2,
|
"comments": 2,
|
||||||
"created_at": "2026-01-10",
|
"created_at": "2026-01-10",
|
||||||
"updated_at": "2026-01-10",
|
"updated_at": "2026-01-10",
|
||||||
@@ -273,11 +339,11 @@
|
|||||||
"name": "Fu-Jie",
|
"name": "Fu-Jie",
|
||||||
"profile_url": "https://openwebui.com/u/Fu-Jie",
|
"profile_url": "https://openwebui.com/u/Fu-Jie",
|
||||||
"profile_image": "https://community.s3.openwebui.com/uploads/users/b15d1348-4347-42b4-b815-e053342d6cb0/profile_d9510745-4bd4-4f8f-a997-4a21847d9300.webp",
|
"profile_image": "https://community.s3.openwebui.com/uploads/users/b15d1348-4347-42b4-b815-e053342d6cb0/profile_d9510745-4bd4-4f8f-a997-4a21847d9300.webp",
|
||||||
"followers": 118,
|
"followers": 204,
|
||||||
"following": 2,
|
"following": 4,
|
||||||
"total_points": 108,
|
"total_points": 257,
|
||||||
"post_points": 92,
|
"post_points": 218,
|
||||||
"comment_points": 16,
|
"comment_points": 39,
|
||||||
"contributions": 25
|
"contributions": 41
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,46 @@
|
|||||||
# 📊 OpenWebUI Community Stats Report
|
# 📊 OpenWebUI Community Stats Report
|
||||||
|
|
||||||
> 📅 Updated: 2026-01-17 12:15
|
> 📅 Updated: 2026-02-07 17:12
|
||||||
|
|
||||||
## 📈 Overview
|
## 📈 Overview
|
||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📝 Total Posts | 16 |
|
| 📝 Total Posts | 20 |
|
||||||
| ⬇️ Total Downloads | 1609 |
|
| ⬇️ Total Downloads | 3711 |
|
||||||
| 👁️ Total Views | 19531 |
|
| 👁️ Total Views | 43662 |
|
||||||
| 👍 Total Upvotes | 94 |
|
| 👍 Total Upvotes | 220 |
|
||||||
| 💾 Total Saves | 123 |
|
| 💾 Total Saves | 256 |
|
||||||
| 💬 Total Comments | 23 |
|
| 💬 Total Comments | 47 |
|
||||||
|
|
||||||
## 📂 By Type
|
## 📂 By Type
|
||||||
|
|
||||||
|
- **pipe**: 1
|
||||||
- **action**: 14
|
- **action**: 14
|
||||||
- **unknown**: 2
|
- **unknown**: 4
|
||||||
|
- **filter**: 1
|
||||||
|
|
||||||
## 📋 Posts List
|
## 📋 Posts List
|
||||||
|
|
||||||
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 502 | 4566 | 13 | 28 | 2026-01-16 |
|
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.2 | 883 | 7810 | 22 | 48 | 2026-01-28 |
|
||||||
| 2 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.9 | 215 | 2215 | 10 | 15 | 2026-01-14 |
|
| 2 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.5.0 | 635 | 5880 | 24 | 33 | 2026-01-30 |
|
||||||
| 3 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 201 | 738 | 3 | 5 | 2026-01-07 |
|
| 3 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 347 | 2686 | 14 | 26 | 2026-01-28 |
|
||||||
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.1.3 | 171 | 1880 | 7 | 18 | 2026-01-14 |
|
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.2.2 | 330 | 3436 | 14 | 33 | 2026-01-28 |
|
||||||
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 128 | 1214 | 6 | 14 | 2026-01-14 |
|
| 5 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 327 | 1520 | 7 | 6 | 2026-01-29 |
|
||||||
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 128 | 2241 | 8 | 10 | 2026-01-07 |
|
| 6 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.2.4 | 294 | 4270 | 17 | 27 | 2026-01-29 |
|
||||||
| 7 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 57 | 596 | 3 | 5 | 2026-01-08 |
|
| 7 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 213 | 3247 | 13 | 14 | 2026-01-28 |
|
||||||
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 56 | 1231 | 9 | 3 | 2026-01-14 |
|
| 8 | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | unknown | | 153 | 2234 | 8 | 3 | 2026-01-28 |
|
||||||
| 9 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.1.2 | 55 | 1642 | 8 | 14 | 2026-01-13 |
|
| 9 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 134 | 1162 | 6 | 11 | 2026-01-08 |
|
||||||
| 10 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.9 | 41 | 644 | 4 | 0 | 2026-01-14 |
|
| 10 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 119 | 2111 | 13 | 6 | 2026-01-28 |
|
||||||
| 11 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 22 | 385 | 2 | 1 | 2026-01-14 |
|
| 11 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.5.0 | 56 | 1006 | 10 | 1 | 2026-01-29 |
|
||||||
| 12 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.1.3 | 14 | 332 | 4 | 1 | 2026-01-14 |
|
| 12 | [📂 Folder Memory – Auto-Evolving Project Context](https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2) | filter | 0.1.0 | 56 | 1209 | 6 | 8 | 2026-01-20 |
|
||||||
| 13 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 13 | 420 | 4 | 1 | 2026-01-07 |
|
| 13 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | pipe | 0.3.0 | 52 | 1882 | 12 | 6 | 2026-02-07 |
|
||||||
| 14 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 6 | 254 | 2 | 1 | 2026-01-08 |
|
| 14 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.2 | 37 | 564 | 6 | 2 | 2026-01-28 |
|
||||||
| 15 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 41 | 0 | 0 | 2026-01-14 |
|
| 15 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.2.2 | 31 | 628 | 7 | 4 | 2026-01-28 |
|
||||||
| 16 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 1132 | 11 | 7 | 2026-01-10 |
|
| 16 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 27 | 661 | 8 | 1 | 2026-01-28 |
|
||||||
|
| 17 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 17 | 400 | 5 | 1 | 2026-01-08 |
|
||||||
|
| 18 | [🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager](https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e) | unknown | | 0 | 1429 | 12 | 17 | 2026-01-28 |
|
||||||
|
| 19 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 135 | 2 | 0 | 2026-01-14 |
|
||||||
|
| 20 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 1392 | 14 | 9 | 2026-01-10 |
|
||||||
|
|||||||
@@ -1,40 +1,46 @@
|
|||||||
# 📊 OpenWebUI 社区统计报告
|
# 📊 OpenWebUI 社区统计报告
|
||||||
|
|
||||||
> 📅 更新时间: 2026-01-17 12:15
|
> 📅 更新时间: 2026-02-07 17:12
|
||||||
|
|
||||||
## 📈 总览
|
## 📈 总览
|
||||||
|
|
||||||
| 指标 | 数值 |
|
| 指标 | 数值 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📝 发布数量 | 16 |
|
| 📝 发布数量 | 20 |
|
||||||
| ⬇️ 总下载量 | 1609 |
|
| ⬇️ 总下载量 | 3711 |
|
||||||
| 👁️ 总浏览量 | 19531 |
|
| 👁️ 总浏览量 | 43662 |
|
||||||
| 👍 总点赞数 | 94 |
|
| 👍 总点赞数 | 220 |
|
||||||
| 💾 总收藏数 | 123 |
|
| 💾 总收藏数 | 256 |
|
||||||
| 💬 总评论数 | 23 |
|
| 💬 总评论数 | 47 |
|
||||||
|
|
||||||
## 📂 按类型分类
|
## 📂 按类型分类
|
||||||
|
|
||||||
|
- **pipe**: 1
|
||||||
- **action**: 14
|
- **action**: 14
|
||||||
- **unknown**: 2
|
- **unknown**: 4
|
||||||
|
- **filter**: 1
|
||||||
|
|
||||||
## 📋 发布列表
|
## 📋 发布列表
|
||||||
|
|
||||||
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 502 | 4566 | 13 | 28 | 2026-01-16 |
|
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.2 | 883 | 7810 | 22 | 48 | 2026-01-28 |
|
||||||
| 2 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.9 | 215 | 2215 | 10 | 15 | 2026-01-14 |
|
| 2 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.5.0 | 635 | 5880 | 24 | 33 | 2026-01-30 |
|
||||||
| 3 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 201 | 738 | 3 | 5 | 2026-01-07 |
|
| 3 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 347 | 2686 | 14 | 26 | 2026-01-28 |
|
||||||
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.1.3 | 171 | 1880 | 7 | 18 | 2026-01-14 |
|
| 4 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | action | 1.2.2 | 330 | 3436 | 14 | 33 | 2026-01-28 |
|
||||||
| 5 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 128 | 1214 | 6 | 14 | 2026-01-14 |
|
| 5 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 327 | 1520 | 7 | 6 | 2026-01-29 |
|
||||||
| 6 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 128 | 2241 | 8 | 10 | 2026-01-07 |
|
| 6 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.2.4 | 294 | 4270 | 17 | 27 | 2026-01-29 |
|
||||||
| 7 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 57 | 596 | 3 | 5 | 2026-01-08 |
|
| 7 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 213 | 3247 | 13 | 14 | 2026-01-28 |
|
||||||
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 56 | 1231 | 9 | 3 | 2026-01-14 |
|
| 8 | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | unknown | | 153 | 2234 | 8 | 3 | 2026-01-28 |
|
||||||
| 9 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | action | 1.1.2 | 55 | 1642 | 8 | 14 | 2026-01-13 |
|
| 9 | [Deep Dive](https://openwebui.com/posts/deep_dive_c0b846e4) | action | 1.0.0 | 134 | 1162 | 6 | 11 | 2026-01-08 |
|
||||||
| 10 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.9 | 41 | 644 | 4 | 0 | 2026-01-14 |
|
| 10 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 119 | 2111 | 13 | 6 | 2026-01-28 |
|
||||||
| 11 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 22 | 385 | 2 | 1 | 2026-01-14 |
|
| 11 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.5.0 | 56 | 1006 | 10 | 1 | 2026-01-29 |
|
||||||
| 12 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.1.3 | 14 | 332 | 4 | 1 | 2026-01-14 |
|
| 12 | [📂 Folder Memory – Auto-Evolving Project Context](https://openwebui.com/posts/folder_memory_auto_evolving_project_context_4a9875b2) | filter | 0.1.0 | 56 | 1209 | 6 | 8 | 2026-01-20 |
|
||||||
| 13 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 13 | 420 | 4 | 1 | 2026-01-07 |
|
| 13 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | pipe | 0.3.0 | 52 | 1882 | 12 | 6 | 2026-02-07 |
|
||||||
| 14 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 6 | 254 | 2 | 1 | 2026-01-08 |
|
| 14 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.2 | 37 | 564 | 6 | 2 | 2026-01-28 |
|
||||||
| 15 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 41 | 0 | 0 | 2026-01-14 |
|
| 15 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | action | 1.2.2 | 31 | 628 | 7 | 4 | 2026-01-28 |
|
||||||
| 16 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 1132 | 11 | 7 | 2026-01-10 |
|
| 16 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 27 | 661 | 8 | 1 | 2026-01-28 |
|
||||||
|
| 17 | [精读](https://openwebui.com/posts/精读_99830b0f) | action | 1.0.0 | 17 | 400 | 5 | 1 | 2026-01-08 |
|
||||||
|
| 18 | [🚀 Open WebUI Prompt Plus: AI-Powered Prompt Manager](https://openwebui.com/posts/open_webui_prompt_plus_ai_powered_prompt_manager_s_15fa060e) | unknown | | 0 | 1429 | 12 | 17 | 2026-01-28 |
|
||||||
|
| 19 | [Review of Claude Haiku 4.5](https://openwebui.com/posts/review_of_claude_haiku_45_41b0db39) | unknown | | 0 | 135 | 2 | 0 | 2026-01-14 |
|
||||||
|
| 20 | [ 🛠️ Debug Open WebUI Plugins in Your Browser](https://openwebui.com/posts/debug_open_webui_plugins_in_your_browser_81bf7960) | unknown | | 0 | 1392 | 14 | 9 | 2026-01-10 |
|
||||||
|
|||||||
22
docs/extensions/index.md
Normal file
22
docs/extensions/index.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: Extensions
|
||||||
|
---
|
||||||
|
|
||||||
|
# Extensions
|
||||||
|
|
||||||
|
Standalone frontend extensions to supercharge your Open WebUI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Open WebUI Prompt Plus
|
||||||
|
|
||||||
|
**An all-in-one prompt management suite for power users.**
|
||||||
|
|
||||||
|
[Open WebUI Prompt Plus](https://github.com/Fu-Jie/open-webui-prompt-plus) elevates your experience with:
|
||||||
|
|
||||||
|
- **🤖 AI-Powered Prompt Generator**: Turn natural language into structured prompts.
|
||||||
|
- **⚡ Quick Insert Panel**: Spotlight-style search (`Cmd/Ctrl + Shift + P`).
|
||||||
|
- **📂 Advanced Management**: Dynamic categories, favorites, and usage stats.
|
||||||
|
- **📝 Native Variable Support**: Visual form rendering for template variables.
|
||||||
|
|
||||||
|
[:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/open-webui-prompt-plus){ .md-button .md-button--primary }
|
||||||
22
docs/extensions/index.zh.md
Normal file
22
docs/extensions/index.zh.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: 扩展 (Extensions)
|
||||||
|
---
|
||||||
|
|
||||||
|
# 扩展 (Extensions)
|
||||||
|
|
||||||
|
Open WebUI 的独立前端增强扩展。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Open WebUI Prompt Plus
|
||||||
|
|
||||||
|
**专为高级用户打造的一站式提示词管理套件。**
|
||||||
|
|
||||||
|
[Open WebUI Prompt Plus](https://github.com/Fu-Jie/open-webui-prompt-plus) 通过以下功能提升您的体验:
|
||||||
|
|
||||||
|
- **🤖 AI 提示词生成器**: 将自然语言转化为结构化提示词。
|
||||||
|
- **⚡ 快速插入面板**: Spotlight 风格的快速搜索 (`Cmd/Ctrl + Shift + P`)。
|
||||||
|
- **📂 高级管理**: 动态分类、收藏夹及使用统计。
|
||||||
|
- **📝 原生变量支持**: 模板变量的可视化表单渲染。
|
||||||
|
|
||||||
|
[:fontawesome-brands-github: 查看 GitHub](https://github.com/Fu-Jie/open-webui-prompt-plus){ .md-button .md-button--primary }
|
||||||
@@ -349,6 +349,53 @@ await __event_emitter__(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Advanced Use Case: Retrieving Frontend Data
|
||||||
|
|
||||||
|
One of the most powerful capabilities of the `execute` event type is the ability to fetch data from the browser environment (JavaScript) and return it to your Python backend. This allows plugins to access information like:
|
||||||
|
|
||||||
|
- `localStorage` items (user preferences, tokens)
|
||||||
|
- `navigator` properties (language, geolocation, platform)
|
||||||
|
- `document` properties (cookies, URL parameters)
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
The JavaScript code you provide in the `"code"` field is executed in the browser. If your JS code includes a `return` statement, that value is sent back to Python as the result of `await __event_call__`.
|
||||||
|
|
||||||
|
**Example: Getting the User's UI Language**
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
# Execute JS on the frontend to get language settings
|
||||||
|
response = await __event_call__(
|
||||||
|
{
|
||||||
|
"type": "execute",
|
||||||
|
"data": {
|
||||||
|
# This JS code runs in the browser.
|
||||||
|
# The 'return' value is sent back to Python.
|
||||||
|
"code": """
|
||||||
|
return (
|
||||||
|
localStorage.getItem('locale') ||
|
||||||
|
localStorage.getItem('language') ||
|
||||||
|
navigator.language ||
|
||||||
|
'en-US'
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 'response' will contain the string returned by JS (e.g., "en-US", "zh-CN")
|
||||||
|
# Note: Wrap in try-except to handle potential timeouts or JS errors
|
||||||
|
logger.info(f"Frontend Language: {response}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get frontend data: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key capabilities unlocked:**
|
||||||
|
- **Context Awareness:** Adapt responses based on user time zone or language.
|
||||||
|
- **Client-Side Storage:** Use `localStorage` to persist simple plugin settings without a database.
|
||||||
|
- **Hardware Access:** Request geolocation or clipboard access (requires user permission).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🏗️ When & Where to Use Events
|
## 🏗️ When & Where to Use Events
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ hide:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: View Guides](enhancements/index.md)
|
[:octicons-arrow-right-24: View Guides](enhancements/index.md)
|
||||||
|
|
||||||
|
- :material-rocket-launch:{ .lg .middle } **Extensions**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Standalone frontend extensions to supercharge your Open WebUI interface.
|
||||||
|
|
||||||
|
[:octicons-arrow-right-24: Browse Extensions](extensions/index.md)
|
||||||
|
|
||||||
- :material-book-open-page-variant:{ .lg .middle } **Development**
|
- :material-book-open-page-variant:{ .lg .middle } **Development**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ hide:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: 查看指南](enhancements/index.md)
|
[:octicons-arrow-right-24: 查看指南](enhancements/index.md)
|
||||||
|
|
||||||
|
- :material-rocket-launch:{ .lg .middle } **扩展 (Extensions)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
独立的 OpenWebUI 前端增强扩展,全面提升交互体验。
|
||||||
|
|
||||||
|
[:octicons-arrow-right-24: 浏览扩展](extensions/index.md)
|
||||||
|
|
||||||
- :material-book-open-page-variant:{ .lg .middle } **开发指南**
|
- :material-book-open-page-variant:{ .lg .middle } **开发指南**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Export to Word
|
# Export to Word
|
||||||
|
|
||||||
<span class="category-badge action">Action</span>
|
<span class="category-badge action">Action</span>
|
||||||
<span class="version-badge">v0.4.3</span>
|
<span class="version-badge">v0.4.4</span>
|
||||||
|
|
||||||
Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
||||||
|
|
||||||
@@ -53,11 +53,17 @@ You can configure the following settings via the **Valves** button in the plugin
|
|||||||
| `MATH_ENABLE` | Enable LaTeX math block conversion. | `True` |
|
| `MATH_ENABLE` | Enable LaTeX math block conversion. | `True` |
|
||||||
| `MATH_INLINE_DOLLAR_ENABLE` | Enable inline `$ ... $` math conversion. | `True` |
|
| `MATH_INLINE_DOLLAR_ENABLE` | Enable inline `$ ... $` math conversion. | `True` |
|
||||||
|
|
||||||
## 🔥 What's New in v0.4.3
|
## 🔥 What's New in v0.4.4
|
||||||
|
|
||||||
|
- 🧹 **Content Cleanup**: Enhanced stripping of `<details>` blocks (often used for tool calls/thinking process) to ensure a clean final document.
|
||||||
|
- 📄 **Standard Document Formatting**: Applied professional document formatting standards for titles and headings (centered title, bold, optimized font sizes and spacing), including GB/T compliance for Chinese content.
|
||||||
|
- 🔠 **Font Rendering Fix**: Fixed an issue where CJK characters would fallback to MS Gothic in Word; now correctly uses the configured Asian font (e.g., SimSun).
|
||||||
|
- ⚙️ **Title Alignment**: Added `TITLE_ALIGNMENT` valve to configure document title alignment (left, center, right).
|
||||||
|
|
||||||
### User-Level Configuration (UserValves)
|
### User-Level Configuration (UserValves)
|
||||||
|
|
||||||
Users can override the following settings in their personal settings:
|
Users can override the following settings in their personal settings:
|
||||||
|
|
||||||
- `TITLE_SOURCE`
|
- `TITLE_SOURCE`
|
||||||
- `UI_LANGUAGE`
|
- `UI_LANGUAGE`
|
||||||
- `FONT_LATIN`, `FONT_ASIAN`, `FONT_CODE`
|
- `FONT_LATIN`, `FONT_ASIAN`, `FONT_CODE`
|
||||||
@@ -120,4 +126,4 @@ Users can override the following settings in their personal settings:
|
|||||||
## Source Code
|
## Source Code
|
||||||
|
|
||||||
[:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/export_to_docx){ .md-button }
|
[:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/export_to_docx){ .md-button }
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.4 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Export to Word(导出为 Word)
|
# Export to Word(导出为 Word)
|
||||||
|
|
||||||
<span class="category-badge action">Action</span>
|
<span class="category-badge action">Action</span>
|
||||||
<span class="version-badge">v0.4.3</span>
|
<span class="version-badge">v0.4.4</span>
|
||||||
|
|
||||||
将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。
|
将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。
|
||||||
|
|
||||||
@@ -53,9 +53,17 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
|
|||||||
| `启用数学公式` | 启用 LaTeX 数学公式块转换。 | `True` |
|
| `启用数学公式` | 启用 LaTeX 数学公式块转换。 | `True` |
|
||||||
| `启用行内公式` | 启用行内 `$ ... $` 数学公式转换。 | `True` |
|
| `启用行内公式` | 启用行内 `$ ... $` 数学公式转换。 | `True` |
|
||||||
|
|
||||||
|
## 🔥 v0.4.4 更新内容
|
||||||
|
|
||||||
|
- 🧹 **内容清理加强**: 增强了对 `<details>` 块(通常包含工具调用或思考过程)的清理,确保最终文档整洁。
|
||||||
|
- 📄 **文档格式标准化**: 采用了专业的文档排版标准(兼容中文 GB/T 规范),标题居中加粗,各级标题使用标准字号和间距。
|
||||||
|
- 🔠 **字体渲染修复**: 修复了 CJK 字符在 Word 中回退到 MS Gothic 的问题;现在正确使用配置的中文字体(例如宋体)。
|
||||||
|
- ⚙️ **标题对齐配置**: 新增 `标题对齐方式` Valve,支持配置文档标题的对齐方式(左对齐、居中、右对齐)。
|
||||||
|
|
||||||
### 用户级配置 (UserValves)
|
### 用户级配置 (UserValves)
|
||||||
|
|
||||||
用户可以在个人设置中覆盖以下配置:
|
用户可以在个人设置中覆盖以下配置:
|
||||||
|
|
||||||
- `文档标题来源`
|
- `文档标题来源`
|
||||||
- `界面语言`
|
- `界面语言`
|
||||||
- `英文字体`, `中文字体`, `代码字体`
|
- `英文字体`, `中文字体`, `代码字体`
|
||||||
@@ -117,4 +125,5 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档
|
|||||||
|
|
||||||
## 源码
|
## 源码
|
||||||
|
|
||||||
[:fontawes**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)/tree/main/plugins/actions/export_to_docx){ .md-button }
|
[:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/export_to_docx){ .md-button }
|
||||||
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.4 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Actions are interactive plugins that:
|
|||||||
|
|
||||||
Export the current conversation to a formatted Word doc with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
Export the current conversation to a formatted Word doc with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
||||||
|
|
||||||
**Version:** 0.4.2
|
**Version:** 0.4.4
|
||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](export-to-word.md)
|
[:octicons-arrow-right-24: Documentation](export-to-word.md)
|
||||||
|
|
||||||
@@ -77,8 +77,6 @@ Actions are interactive plugins that:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](deep-dive.md)
|
[:octicons-arrow-right-24: Documentation](deep-dive.md)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Actions 是交互式插件,能够:
|
|||||||
|
|
||||||
将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。
|
将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。
|
||||||
|
|
||||||
**版本:** 0.4.2
|
**版本:** 0.4.4
|
||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](export-to-word.md)
|
[:octicons-arrow-right-24: 查看文档](export-to-word.md)
|
||||||
|
|
||||||
@@ -77,8 +77,6 @@ Actions 是交互式插件,能够:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](deep-dive.zh.md)
|
[:octicons-arrow-right-24: 查看文档](deep-dive.zh.md)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Smart Mind Map
|
# Smart Mind Map
|
||||||
|
|
||||||
<span class="category-badge action">Action</span>
|
<span class="category-badge action">Action</span>
|
||||||
<span class="version-badge">v0.9.1</span>
|
<span class="version-badge">v0.9.2</span>
|
||||||
|
|
||||||
Intelligently analyzes text content and generates interactive mind maps for better visualization and understanding.
|
Intelligently analyzes text content and generates interactive mind maps for better visualization and understanding.
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ The Smart Mind Map plugin transforms text content into beautiful, interactive mi
|
|||||||
- :material-gesture-swipe: **Rich Controls**: Zoom, reset view, expand level selector (All/2/3) and fullscreen
|
- :material-gesture-swipe: **Rich Controls**: Zoom, reset view, expand level selector (All/2/3) and fullscreen
|
||||||
- :material-palette: **Theme Aware**: Auto-detects OpenWebUI light/dark theme with manual toggle
|
- :material-palette: **Theme Aware**: Auto-detects OpenWebUI light/dark theme with manual toggle
|
||||||
- :material-download: **One-Click Export**: Download high-res PNG, copy SVG, or copy Markdown source
|
- :material-download: **One-Click Export**: Download high-res PNG, copy SVG, or copy Markdown source
|
||||||
- :material-translate: **Multi-language**: Adapts output language to the user context
|
- :material-translate: **Multi-language**: Matches output language to the input text
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Smart Mind Map(智能思维导图)
|
# Smart Mind Map(智能思维导图)
|
||||||
|
|
||||||
<span class="category-badge action">Action</span>
|
<span class="category-badge action">Action</span>
|
||||||
<span class="version-badge">v0.9.1</span>
|
<span class="version-badge">v0.9.2</span>
|
||||||
|
|
||||||
智能分析文本内容,生成交互式思维导图,帮助你更直观地理解信息结构。
|
智能分析文本内容,生成交互式思维导图,帮助你更直观地理解信息结构。
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ Smart Mind Map 会将文本转换成漂亮的交互式思维导图。插件会
|
|||||||
- :material-gesture-swipe: **丰富控制**:缩放/重置、展开层级(全部/2/3 级)与全屏
|
- :material-gesture-swipe: **丰富控制**:缩放/重置、展开层级(全部/2/3 级)与全屏
|
||||||
- :material-palette: **主题感知**:自动检测 OpenWebUI 亮/暗色主题并支持手动切换
|
- :material-palette: **主题感知**:自动检测 OpenWebUI 亮/暗色主题并支持手动切换
|
||||||
- :material-download: **一键导出**:下载高分辨率 PNG、复制 SVG 或 Markdown
|
- :material-download: **一键导出**:下载高分辨率 PNG、复制 SVG 或 Markdown
|
||||||
- :material-translate: **多语言**:根据用户语言自动输出
|
- :material-translate: **多语言**:输出语言与输入文本一致
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Async Context Compression
|
# Async Context Compression
|
||||||
|
|
||||||
<span class="category-badge filter">Filter</span>
|
<span class="category-badge filter">Filter</span>
|
||||||
<span class="version-badge">v1.1.3</span>
|
<span class="version-badge">v1.2.2</span>
|
||||||
|
|
||||||
Reduces token consumption in long conversations through intelligent summarization while maintaining conversational coherence.
|
Reduces token consumption in long conversations through intelligent summarization while maintaining conversational coherence.
|
||||||
|
|
||||||
@@ -34,6 +34,12 @@ This is especially useful for:
|
|||||||
- :material-check-all: **Open WebUI v0.7.x Compatibility**: Dynamic DB session handling
|
- :material-check-all: **Open WebUI v0.7.x Compatibility**: Dynamic DB session handling
|
||||||
- :material-account-convert: **Improved Compatibility**: Summary role changed to `assistant`
|
- :material-account-convert: **Improved Compatibility**: Summary role changed to `assistant`
|
||||||
- :material-shield-check: **Enhanced Stability**: Resolved race conditions in state management
|
- :material-shield-check: **Enhanced Stability**: Resolved race conditions in state management
|
||||||
|
- :material-ruler: **Preflight Context Check**: Validates context fit before sending
|
||||||
|
- :material-format-align-justify: **Structure-Aware Trimming**: Preserves document structure
|
||||||
|
- :material-content-cut: **Native Tool Output Trimming**: Trims verbose tool outputs (Note: Non-native tool outputs are not fully injected into context)
|
||||||
|
- :material-chart-bar: **Detailed Token Logging**: Granular token breakdown
|
||||||
|
- :material-account-search: **Smart Model Matching**: Inherit config from base models
|
||||||
|
- :material-image-off: **Multimodal Support**: Images are preserved but tokens are **NOT** calculated
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,10 +70,14 @@ graph TD
|
|||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
|--------|------|---------|-------------|
|
|--------|------|---------|-------------|
|
||||||
| `token_threshold` | integer | `4000` | Trigger compression above this token count |
|
| `compression_threshold_tokens` | integer | `64000` | Trigger compression above this token count |
|
||||||
| `preserve_recent` | integer | `5` | Number of recent messages to keep uncompressed |
|
| `max_context_tokens` | integer | `128000` | Hard limit for context |
|
||||||
| `summary_model` | string | `"auto"` | Model to use for summarization |
|
| `keep_first` | integer | `1` | Always keep the first N messages |
|
||||||
| `compression_ratio` | float | `0.3` | Target compression ratio |
|
| `keep_last` | integer | `6` | Always keep the last N messages |
|
||||||
|
| `summary_model` | string | `None` | Model to use for summarization |
|
||||||
|
| `summary_model_max_context` | integer | `0` | Max context tokens for summary model |
|
||||||
|
| `max_summary_tokens` | integer | `16384` | Maximum tokens for the summary |
|
||||||
|
| `enable_tool_output_trimming` | boolean | `false` | Enable trimming of large tool outputs |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Async Context Compression(异步上下文压缩)
|
# Async Context Compression(异步上下文压缩)
|
||||||
|
|
||||||
<span class="category-badge filter">Filter</span>
|
<span class="category-badge filter">Filter</span>
|
||||||
<span class="version-badge">v1.1.3</span>
|
<span class="version-badge">v1.2.2</span>
|
||||||
|
|
||||||
通过智能摘要减少长对话的 token 消耗,同时保持对话连贯。
|
通过智能摘要减少长对话的 token 消耗,同时保持对话连贯。
|
||||||
|
|
||||||
@@ -34,6 +34,12 @@ Async Context Compression 过滤器通过以下方式帮助管理长对话的 to
|
|||||||
- :material-check-all: **Open WebUI v0.7.x 兼容性**:动态数据库会话处理
|
- :material-check-all: **Open WebUI v0.7.x 兼容性**:动态数据库会话处理
|
||||||
- :material-account-convert: **兼容性提升**:摘要角色改为 `assistant`
|
- :material-account-convert: **兼容性提升**:摘要角色改为 `assistant`
|
||||||
- :material-shield-check: **稳定性增强**:解决状态管理竞态条件
|
- :material-shield-check: **稳定性增强**:解决状态管理竞态条件
|
||||||
|
- :material-ruler: **预检上下文检查**:发送前验证上下文是否超限
|
||||||
|
- :material-format-align-justify: **结构感知裁剪**:保留文档结构的智能裁剪
|
||||||
|
- :material-content-cut: **原生工具输出裁剪**:自动裁剪冗长的工具输出(注意:非原生工具调用输出不会完整注入上下文)
|
||||||
|
- :material-chart-bar: **详细 Token 日志**:提供细粒度的 Token 统计
|
||||||
|
- :material-account-search: **智能模型匹配**:自定义模型自动继承基础模型配置
|
||||||
|
- :material-image-off: **多模态支持**:图片内容保留但 Token **不参与计算**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,10 +70,14 @@ graph TD
|
|||||||
|
|
||||||
| 选项 | 类型 | 默认值 | 说明 |
|
| 选项 | 类型 | 默认值 | 说明 |
|
||||||
|--------|------|---------|-------------|
|
|--------|------|---------|-------------|
|
||||||
| `token_threshold` | integer | `4000` | 超过该 token 数触发压缩 |
|
| `compression_threshold_tokens` | integer | `64000` | 超过该 token 数触发压缩 |
|
||||||
| `preserve_recent` | integer | `5` | 保留不压缩的最近消息数量 |
|
| `max_context_tokens` | integer | `128000` | 上下文硬性上限 |
|
||||||
| `summary_model` | string | `"auto"` | 用于摘要的模型 |
|
| `keep_first` | integer | `1` | 始终保留的前 N 条消息 |
|
||||||
| `compression_ratio` | float | `0.3` | 目标压缩比例 |
|
| `keep_last` | integer | `6` | 始终保留的后 N 条消息 |
|
||||||
|
| `summary_model` | string | `None` | 用于摘要的模型 |
|
||||||
|
| `summary_model_max_context` | integer | `0` | 摘要模型的最大上下文 Token 数 |
|
||||||
|
| `max_summary_tokens` | integer | `16384` | 摘要的最大 token 数 |
|
||||||
|
| `enable_tool_output_trimming` | boolean | `false` | 启用长工具输出裁剪 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
57
docs/plugins/filters/folder-memory.md
Normal file
57
docs/plugins/filters/folder-memory.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Folder Memory
|
||||||
|
|
||||||
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.1.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 What's new in 0.1.0
|
||||||
|
- **Initial Release**: Automated "Project Rules" management for OpenWebUI folders.
|
||||||
|
- **Folder-Level Persistence**: Automatically updates folder system prompts with extracted rules.
|
||||||
|
- **Optimized Performance**: Runs asynchronously and supports `PRIORITY` configuration for seamless integration with other filters.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Folder Memory** is an intelligent context filter plugin for OpenWebUI. It automatically extracts consistent "Project Rules" from ongoing conversations within a folder and injects them back into the folder's system prompt.
|
||||||
|
|
||||||
|
This ensures that all future conversations within that folder share the same evolved context and rules, without manual updates.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Extraction**: Analyzes chat history every N messages to extract project rules.
|
||||||
|
- **Non-destructive Injection**: Updates only the specific "Project Rules" block in the system prompt, preserving other instructions.
|
||||||
|
- **Async Processing**: Runs in the background without blocking the user's chat experience.
|
||||||
|
- **ORM Integration**: Directly updates folder data using OpenWebUI's internal models for reliability.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Conversations must occur inside a folder.** This plugin only triggers when a chat belongs to a folder (i.e., you need to create a folder in OpenWebUI and start a conversation within it).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Copy `folder_memory.py` to your OpenWebUI `plugins/filters/` directory (or upload via Admin UI).
|
||||||
|
2. Enable the filter in your **Settings** -> **Filters**.
|
||||||
|
3. (Optional) Configure the triggering threshold (default: every 10 messages).
|
||||||
|
|
||||||
|
## Configuration (Valves)
|
||||||
|
|
||||||
|
| Valve | Default | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `PRIORITY` | `20` | Priority level for the filter operations. |
|
||||||
|
| `MESSAGE_TRIGGER_COUNT` | `10` | The number of messages required to trigger a rule analysis. |
|
||||||
|
| `MODEL_ID` | `""` | The model used to generate rules. If empty, uses the current chat model. |
|
||||||
|
| `RULES_BLOCK_TITLE` | `## 📂 Project Rules` | The title displayed above the injected rules block. |
|
||||||
|
| `SHOW_DEBUG_LOG` | `False` | Show detailed debug logs in the browser console. |
|
||||||
|
| `UPDATE_ROOT_FOLDER` | `False` | If enabled, finds and updates the root folder rules instead of the current subfolder. |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **Trigger**: When a conversation reaches `MESSAGE_TRIGGER_COUNT` (e.g., 10, 20 messages).
|
||||||
|
2. **Analysis**: The plugin sends the recent conversation + existing rules to the LLM.
|
||||||
|
3. **Synthesis**: The LLM merges new insights with old rules, removing obsolete ones.
|
||||||
|
4. **Update**: The new rule set replaces the `<!-- OWUI_PROJECT_RULES_START -->` block in the folder's system prompt.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
See [ROADMAP](https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/filters/folder-memory/ROADMAP.md) for future plans, including "Project Knowledge" collection.
|
||||||
57
docs/plugins/filters/folder-memory.zh.md
Normal file
57
docs/plugins/filters/folder-memory.zh.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# 文件夹记忆 (Folder Memory)
|
||||||
|
|
||||||
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.1.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📌 0.1.0 版本特性
|
||||||
|
- **首个版本发布**:专注于自动化的“项目规则”管理。
|
||||||
|
- **文件夹级持久化**:自动将提取的规则回写到文件夹系统提示词中。
|
||||||
|
- **性能优化**:采用异步处理机制,并支持 `PRIORITY` 配置,确保与其他过滤器(如上下文压缩)完美协作。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文件夹记忆 (Folder Memory)** 是一个 OpenWebUI 的智能上下文过滤器插件。它能自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||||
|
|
||||||
|
这确保了该文件夹内的所有未来对话都能共享相同的进化上下文和规则,无需手动更新。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **自动提取**:每隔 N 条消息分析一次聊天记录,提取项目规则。
|
||||||
|
- **无损注入**:仅更新系统提示词中的特定“项目规则”块,保留其他指令。
|
||||||
|
- **异步处理**:在后台运行,不阻塞用户的聊天体验。
|
||||||
|
- **ORM 集成**:直接使用 OpenWebUI 的内部模型更新文件夹数据,确保可靠性。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
- **对话必须在文件夹内进行。** 此插件仅在聊天属于某个文件夹时触发(即您需要先在 OpenWebUI 中创建一个文件夹,并在其内部开始对话)。
|
||||||
|
|
||||||
|
## 安装指南
|
||||||
|
|
||||||
|
1. 将 `folder_memory.py` (或中文版 `folder_memory_cn.py`) 复制到 OpenWebUI 的 `plugins/filters/` 目录(或通过管理员 UI 上传)。
|
||||||
|
2. 在 **设置** -> **过滤器** 中启用该插件。
|
||||||
|
3. (可选)配置触发阈值(默认:每 10 条消息)。
|
||||||
|
|
||||||
|
## 配置 (Valves)
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `PRIORITY` | `20` | 过滤器操作的优先级。 |
|
||||||
|
| `MESSAGE_TRIGGER_COUNT` | `10` | 触发规则分析的消息数量阈值。 |
|
||||||
|
| `MODEL_ID` | `""` | 用于生成规则的模型 ID。若为空,则使用当前对话模型。 |
|
||||||
|
| `RULES_BLOCK_TITLE` | `## 📂 项目规则` | 显示在注入规则块上方的标题。 |
|
||||||
|
| `SHOW_DEBUG_LOG` | `False` | 在浏览器控制台显示详细调试日志。 |
|
||||||
|
| `UPDATE_ROOT_FOLDER` | `False` | 如果启用,将向上查找并更新根文件夹的规则,而不是当前子文件夹。 |
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **触发**:当对话达到 `MESSAGE_TRIGGER_COUNT`(例如 10、20 条消息)时。
|
||||||
|
2. **分析**:插件将最近的对话 + 现有规则发送给 LLM。
|
||||||
|
3. **综合**:LLM 将新见解与旧规则合并,移除过时的规则。
|
||||||
|
4. **更新**:新的规则集替换文件夹系统提示词中的 `<!-- OWUI_PROJECT_RULES_START -->` 块。
|
||||||
|
|
||||||
|
## 路线图
|
||||||
|
|
||||||
|
查看 [ROADMAP](https://github.com/Fu-Jie/awesome-openwebui/blob/main/plugins/filters/folder-memory/ROADMAP.md) 了解未来计划,包括“项目知识”收集功能。
|
||||||
@@ -22,7 +22,7 @@ Filters act as middleware in the message pipeline:
|
|||||||
|
|
||||||
Reduces token consumption in long conversations through intelligent summarization while maintaining coherence.
|
Reduces token consumption in long conversations through intelligent summarization while maintaining coherence.
|
||||||
|
|
||||||
**Version:** 1.1.3
|
**Version:** 1.2.2
|
||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](async-context-compression.md)
|
[:octicons-arrow-right-24: Documentation](async-context-compression.md)
|
||||||
|
|
||||||
@@ -36,7 +36,15 @@ Filters act as middleware in the message pipeline:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](context-enhancement.md)
|
[:octicons-arrow-right-24: Documentation](context-enhancement.md)
|
||||||
|
|
||||||
|
- :material-folder-refresh:{ .lg .middle } **Folder Memory**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Automatically extracts consistent "Project Rules" from ongoing conversations within a folder and injects them back into the folder's system prompt.
|
||||||
|
|
||||||
|
**Version:** 0.1.0
|
||||||
|
|
||||||
|
[:octicons-arrow-right-24: Documentation](folder-memory.md)
|
||||||
|
|
||||||
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
|
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
|
||||||
|
|
||||||
@@ -44,7 +52,7 @@ Filters act as middleware in the message pipeline:
|
|||||||
|
|
||||||
Fixes common Markdown formatting issues in LLM outputs, including Mermaid syntax, code blocks, and LaTeX formulas.
|
Fixes common Markdown formatting issues in LLM outputs, including Mermaid syntax, code blocks, and LaTeX formulas.
|
||||||
|
|
||||||
**Version:** 1.1.2
|
**Version:** 1.2.4
|
||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](markdown_normalizer.md)
|
[:octicons-arrow-right-24: Documentation](markdown_normalizer.md)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Filter 充当消息管线中的中间件:
|
|||||||
|
|
||||||
通过智能总结减少长对话的 token 消耗,同时保持连贯性。
|
通过智能总结减少长对话的 token 消耗,同时保持连贯性。
|
||||||
|
|
||||||
**版本:** 1.1.3
|
**版本:** 1.2.2
|
||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](async-context-compression.md)
|
[:octicons-arrow-right-24: 查看文档](async-context-compression.md)
|
||||||
|
|
||||||
@@ -36,7 +36,15 @@ Filter 充当消息管线中的中间件:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](context-enhancement.md)
|
[:octicons-arrow-right-24: 查看文档](context-enhancement.md)
|
||||||
|
|
||||||
|
- :material-folder-refresh:{ .lg .middle } **Folder Memory**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||||
|
|
||||||
|
**版本:** 0.1.0
|
||||||
|
|
||||||
|
[:octicons-arrow-right-24: 查看文档](folder-memory.zh.md)
|
||||||
|
|
||||||
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
|
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**
|
||||||
|
|
||||||
@@ -44,7 +52,7 @@ Filter 充当消息管线中的中间件:
|
|||||||
|
|
||||||
修复 LLM 输出中常见的 Markdown 格式问题,包括 Mermaid 语法、代码块和 LaTeX 公式。
|
修复 LLM 输出中常见的 Markdown 格式问题,包括 Mermaid 语法、代码块和 LaTeX 公式。
|
||||||
|
|
||||||
**版本:** 1.0.1
|
**版本:** 1.2.4
|
||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](markdown_normalizer.zh.md)
|
[:octicons-arrow-right-24: 查看文档](markdown_normalizer.zh.md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# Markdown Normalizer Filter
|
# Markdown Normalizer Filter
|
||||||
|
|
||||||
A production-grade content normalizer filter for Open WebUI that fixes common Markdown formatting issues in LLM outputs. It ensures that code blocks, LaTeX formulas, Mermaid diagrams, and other Markdown elements are rendered correctly.
|
A content normalizer filter for Open WebUI that fixes common Markdown formatting issues in LLM outputs. It ensures that code blocks, LaTeX formulas, Mermaid diagrams, and other Markdown elements are rendered correctly.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* **Details Tag Normalization**: Ensures proper spacing for `<details>` tags (used for thought chains). Adds a blank line after `</details>` and ensures a newline after self-closing `<details />` tags to prevent rendering issues.
|
* **Details Tag Normalization**: Ensures proper spacing for `<details>` tags (used for thought chains). Adds a blank line after `</details>` and ensures a newline after self-closing `<details />` tags to prevent rendering issues.
|
||||||
* **Mermaid Syntax Fix**: Automatically fixes common Mermaid syntax errors, such as unquoted node labels (including multi-line labels and citations) and unclosed subgraphs, ensuring diagrams render correctly.
|
* **Emphasis Spacing Fix**: Fixes extra spaces inside emphasis markers (e.g., `** text **` -> `**text**`) which can cause rendering failures. Includes safeguards to protect math expressions (e.g., `2 * 3 * 4`) and list variables.
|
||||||
|
* **Mermaid Syntax Fix**: Automatically fixes common Mermaid syntax errors, such as unquoted node labels (including multi-line labels and citations) and unclosed subgraphs. **New in v1.1.2**: Comprehensive protection for edge labels (text on connecting lines) across all link types (solid, dotted, thick).
|
||||||
* **Frontend Console Debugging**: Supports printing structured debug logs directly to the browser console (F12) for easier troubleshooting.
|
* **Frontend Console Debugging**: Supports printing structured debug logs directly to the browser console (F12) for easier troubleshooting.
|
||||||
* **Code Block Formatting**: Fixes broken code block prefixes, suffixes, and indentation.
|
* **Code Block Formatting**: Fixes broken code block prefixes, suffixes, and indentation.
|
||||||
* **LaTeX Normalization**: Standardizes LaTeX formula delimiters (`\[` -> `$$`, `\(` -> `$`).
|
* **LaTeX Normalization**: Standardizes LaTeX formula delimiters (`\[` -> `$$`, `\(` -> `$`).
|
||||||
@@ -30,11 +31,8 @@ A production-grade content normalizer filter for Open WebUI that fixes common Ma
|
|||||||
* `priority`: Filter priority (default: 50).
|
* `priority`: Filter priority (default: 50).
|
||||||
* `enable_escape_fix`: Fix excessive escape characters.
|
* `enable_escape_fix`: Fix excessive escape characters.
|
||||||
* `enable_thought_tag_fix`: Normalize thought tags.
|
* `enable_thought_tag_fix`: Normalize thought tags.
|
||||||
* `enable_thought_tag_fix`: 规范化思维标签。
|
|
||||||
* `enable_details_tag_fix`: Normalize details tags (default: True).
|
* `enable_details_tag_fix`: Normalize details tags (default: True).
|
||||||
* `enable_details_tag_fix`: 规范化 Details 标签 (默认: True)。
|
|
||||||
* `enable_code_block_fix`: Fix code block formatting.
|
* `enable_code_block_fix`: Fix code block formatting.
|
||||||
* `enable_code_block_fix`: 修复代码块格式。
|
|
||||||
* `enable_latex_fix`: Normalize LaTeX formulas.
|
* `enable_latex_fix`: Normalize LaTeX formulas.
|
||||||
* `enable_list_fix`: Fix list item newlines (Experimental).
|
* `enable_list_fix`: Fix list item newlines (Experimental).
|
||||||
* `enable_unclosed_block_fix`: Auto-close unclosed code blocks.
|
* `enable_unclosed_block_fix`: Auto-close unclosed code blocks.
|
||||||
@@ -43,9 +41,58 @@ A production-grade content normalizer filter for Open WebUI that fixes common Ma
|
|||||||
* `enable_heading_fix`: Fix missing space in headings.
|
* `enable_heading_fix`: Fix missing space in headings.
|
||||||
* `enable_table_fix`: Fix missing closing pipe in tables.
|
* `enable_table_fix`: Fix missing closing pipe in tables.
|
||||||
* `enable_xml_tag_cleanup`: Cleanup leftover XML tags.
|
* `enable_xml_tag_cleanup`: Cleanup leftover XML tags.
|
||||||
|
* `enable_emphasis_spacing_fix`: Fix extra spaces in emphasis (default: True).
|
||||||
* `show_status`: Show status notification when fixes are applied.
|
* `show_status`: Show status notification when fixes are applied.
|
||||||
* `show_debug_log`: Print debug logs to browser console.
|
* `show_debug_log`: Print debug logs to browser console.
|
||||||
|
|
||||||
|
## Troubleshooting ❓
|
||||||
|
|
||||||
|
* **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### v1.2.4
|
||||||
|
|
||||||
|
* **Documentation Updates**: Synchronized version numbers across all documentation and code files.
|
||||||
|
|
||||||
|
### v1.2.3
|
||||||
|
|
||||||
|
* **List Marker Protection Enhancement**: Fixed a bug where list markers (`*`) followed by plain text and emphasis were having their spaces incorrectly stripped (e.g., `* U16 forward` became `*U16 forward`).
|
||||||
|
* **Placeholder Support**: Confirmed that 4 or more underscores (e.g., `____`) are correctly treated as placeholders and not modified by the emphasis fix.
|
||||||
|
|
||||||
|
### v1.2.2
|
||||||
|
|
||||||
|
* **Code Block Indentation Fix**: Fixed an issue where code blocks nested inside lists were having their indentation incorrectly stripped. Now preserves proper indentation for nested code blocks.
|
||||||
|
* **Underscore Emphasis Support**: Extended emphasis spacing fix to support `__` (double underscore for bold) and `___` (triple underscore for bold+italic) syntax.
|
||||||
|
* **List Marker Protection**: Fixed a bug where list markers (`*`) followed by emphasis markers (`**`) were incorrectly merged (e.g., `* **Yes**` became `***Yes**`). Added safeguard to prevent this.
|
||||||
|
* **Test Suite**: Added comprehensive pytest test suite with 56 test cases covering all major features.
|
||||||
|
|
||||||
|
### v1.2.1
|
||||||
|
|
||||||
|
* **Emphasis Spacing Fix**: Added a new fix for extra spaces inside emphasis markers (e.g., `** text **` -> `**text**`).
|
||||||
|
* Uses a recursive approach to handle nested emphasis (e.g., `**bold _italic _**`).
|
||||||
|
* Includes safeguards to prevent modifying math expressions (e.g., `2 * 3 * 4`) or list variables.
|
||||||
|
* Controlled by the `enable_emphasis_spacing_fix` valve (default: True).
|
||||||
|
|
||||||
|
### v1.2.0
|
||||||
|
|
||||||
|
* **Details Tag Support**: Added normalization for `<details>` tags.
|
||||||
|
* Ensures a blank line is added after `</details>` closing tags to separate thought content from the main response.
|
||||||
|
* Ensures a newline is added after self-closing `<details ... />` tags to prevent them from interfering with subsequent Markdown headings (e.g., fixing `<details/>#Heading`).
|
||||||
|
* Includes safeguard to prevent modification of `<details>` tags inside code blocks.
|
||||||
|
|
||||||
|
### v1.1.2
|
||||||
|
|
||||||
|
* **Mermaid Edge Label Protection**: Implemented comprehensive protection for edge labels (text on connecting lines) to prevent them from being incorrectly modified. Now supports all Mermaid link types including solid (`--`), dotted (`-.`), and thick (`==`) lines with or without arrows.
|
||||||
|
* **Bug Fixes**: Fixed an issue where lines without arrows (e.g., `A -- text --- B`) were not correctly protected.
|
||||||
|
|
||||||
|
### v1.1.0
|
||||||
|
|
||||||
|
* **Mermaid Fix Refinement**: Improved regex to handle nested parentheses in node labels (e.g., `ID("Label (text)")`) and avoided matching connection labels.
|
||||||
|
* **HTML Safeguard Optimization**: Refined `_contains_html` to allow common tags like `<br/>`, `<b>`, `<i>`, etc., ensuring Mermaid diagrams with these tags are still normalized.
|
||||||
|
* **Full-width Symbol Cleanup**: Fixed duplicate keys and incorrect quote mapping in `FULLWIDTH_MAP`.
|
||||||
|
* **Bug Fixes**: Fixed missing `Dict` import in Python files.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
# Markdown 格式化过滤器 (Markdown Normalizer)
|
# Markdown 格式化过滤器 (Markdown Normalizer)
|
||||||
|
|
||||||
这是一个用于 Open WebUI 的生产级内容格式化过滤器,旨在修复 LLM 输出中常见的 Markdown 格式问题。它能确保代码块、LaTeX 公式、Mermaid 图表和其他 Markdown 元素被正确渲染。
|
这是一个用于 Open WebUI 的内容格式化过滤器,旨在修复 LLM 输出中常见的 Markdown 格式问题。它能确保代码块、LaTeX 公式、Mermaid 图表和其他 Markdown 元素被正确渲染。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
* **Mermaid 语法修复**: 自动修复常见的 Mermaid 语法错误,如未加引号的节点标签(支持多行标签和引用标记)和未闭合的子图 (Subgraph),确保图表能正确渲染。
|
* **Details 标签规范化**: 确保 `<details>` 标签(常用于思维链)有正确的间距。在 `</details>` 后添加空行,并在自闭合 `<details />` 标签后添加换行,防止渲染问题。
|
||||||
|
* **强调空格修复**: 修复强调标记内部的多余空格(例如 `** 文本 **` -> `**文本**`),这会导致 Markdown 渲染失败。包含保护机制,防止误修改数学表达式(如 `2 * 3 * 4`)或列表变量。
|
||||||
|
* **Mermaid 语法修复**: 自动修复常见的 Mermaid 语法错误,如未加引号的节点标签(支持多行标签和引用标记)和未闭合的子图 (Subgraph)。**v1.1.2 新增**: 全面保护各种类型的连线标签(实线、虚线、粗线),防止被误修改。
|
||||||
* **前端控制台调试**: 支持将结构化的调试日志直接打印到浏览器控制台 (F12),方便排查问题。
|
* **前端控制台调试**: 支持将结构化的调试日志直接打印到浏览器控制台 (F12),方便排查问题。
|
||||||
* **代码块格式化**: 修复破损的代码块前缀、后缀和缩进问题。
|
* **代码块格式化**: 修复破损的代码块前缀、后缀和缩进问题。
|
||||||
* **LaTeX 规范化**: 标准化 LaTeX 公式定界符 (`\[` -> `$$`, `\(` -> `$`)。
|
* **LaTeX 规范化**: 标准化 LaTeX 公式定界符 (`\[` -> `$$`, `\(` -> `$`)。
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
* `priority`: 过滤器优先级 (默认: 50)。
|
* `priority`: 过滤器优先级 (默认: 50)。
|
||||||
* `enable_escape_fix`: 修复过度的转义字符。
|
* `enable_escape_fix`: 修复过度的转义字符。
|
||||||
* `enable_thought_tag_fix`: 规范化思维标签。
|
* `enable_thought_tag_fix`: 规范化思维标签。
|
||||||
|
* `enable_details_tag_fix`: 规范化 Details 标签 (默认: True)。
|
||||||
* `enable_code_block_fix`: 修复代码块格式。
|
* `enable_code_block_fix`: 修复代码块格式。
|
||||||
* `enable_latex_fix`: 规范化 LaTeX 公式。
|
* `enable_latex_fix`: 规范化 LaTeX 公式。
|
||||||
* `enable_list_fix`: 修复列表项换行 (实验性)。
|
* `enable_list_fix`: 修复列表项换行 (实验性)。
|
||||||
@@ -38,9 +41,58 @@
|
|||||||
* `enable_heading_fix`: 修复标题中缺失的空格。
|
* `enable_heading_fix`: 修复标题中缺失的空格。
|
||||||
* `enable_table_fix`: 修复表格中缺失的闭合管道符。
|
* `enable_table_fix`: 修复表格中缺失的闭合管道符。
|
||||||
* `enable_xml_tag_cleanup`: 清理残留的 XML 标签。
|
* `enable_xml_tag_cleanup`: 清理残留的 XML 标签。
|
||||||
|
* `enable_emphasis_spacing_fix`: 修复强调语法中的多余空格 (默认: True)。
|
||||||
* `show_status`: 应用修复时显示状态通知。
|
* `show_status`: 应用修复时显示状态通知。
|
||||||
* `show_debug_log`: 在浏览器控制台打印调试日志。
|
* `show_debug_log`: 在浏览器控制台打印调试日志。
|
||||||
|
|
||||||
|
## 故障排除 (Troubleshooting) ❓
|
||||||
|
|
||||||
|
* **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### v1.2.4
|
||||||
|
|
||||||
|
* **文档更新**: 同步了所有文档和代码文件的版本号。
|
||||||
|
|
||||||
|
### v1.2.3
|
||||||
|
|
||||||
|
* **列表标记保护增强**: 修复了列表标记 (`*`) 后跟普通文本和强调标记时,空格被错误剥离的问题(例如 `* U16 前锋` 变成 `*U16 前锋`)。
|
||||||
|
* **占位符支持**: 确认 4 个或更多下划线(如 `____`)会被正确视为占位符,不会被强调修复逻辑修改。
|
||||||
|
|
||||||
|
### v1.2.2
|
||||||
|
|
||||||
|
* **代码块缩进修复**: 修复了列表中嵌套代码块的缩进被错误剥离的问题。现在会正确保留嵌套代码块的缩进。
|
||||||
|
* **下划线强调语法支持**: 扩展强调空格修复以支持 `__` (双下划线加粗) 和 `___` (三下划线加粗斜体) 语法。
|
||||||
|
* **列表标记保护**: 修复了列表标记 (`*`) 后跟强调标记 (`**`) 被错误合并的 Bug(例如 `* **是**` 变成 `***是**`)。添加了保护逻辑防止此问题。
|
||||||
|
* **测试套件**: 新增完整的 pytest 测试套件,包含 56 个测试用例,覆盖所有主要功能。
|
||||||
|
|
||||||
|
### v1.2.1
|
||||||
|
|
||||||
|
* **强调空格修复**: 新增了对强调标记内部多余空格的修复(例如 `** 文本 **` -> `**文本**`)。
|
||||||
|
* 采用递归方法处理嵌套强调(例如 `**加粗 _斜体 _**`)。
|
||||||
|
* 包含保护机制,防止误修改数学表达式(如 `2 * 3 * 4`)或列表变量。
|
||||||
|
* 通过 `enable_emphasis_spacing_fix` 开关控制(默认:开启)。
|
||||||
|
|
||||||
|
### v1.2.0
|
||||||
|
|
||||||
|
* **Details 标签支持**: 新增了对 `<details>` 标签的规范化支持。
|
||||||
|
* 确保在 `</details>` 闭合标签后添加空行,将思维内容与正文分隔开。
|
||||||
|
* 确保在自闭合 `<details ... />` 标签后添加换行,防止其干扰后续的 Markdown 标题(例如修复 `<details/>#标题`)。
|
||||||
|
* 包含保护机制,防止修改代码块内部的 `<details>` 标签。
|
||||||
|
|
||||||
|
### v1.1.2
|
||||||
|
|
||||||
|
* **Mermaid 连线标签保护**: 实现了全面的连线标签保护机制,防止连接线上的文字被误修改。现在支持所有 Mermaid 连线类型,包括实线 (`--`)、虚线 (`-.`) 和粗线 (`==`),无论是否带有箭头。
|
||||||
|
* **Bug 修复**: 修复了无箭头连线(如 `A -- text --- B`)未被正确保护的问题。
|
||||||
|
|
||||||
|
### v1.1.0
|
||||||
|
|
||||||
|
* **Mermaid 修复优化**: 改进了正则表达式以处理节点标签中的嵌套括号(如 `ID("标签 (文本)")`),并避免误匹配连接线上的文字。
|
||||||
|
* **HTML 保护机制优化**: 优化了 `_contains_html` 检测,允许 `<br/>`, `<b>`, `<i>` 等常见标签,确保包含这些标签的 Mermaid 图表能被正常规范化。
|
||||||
|
* **全角符号清理**: 修复了 `FULLWIDTH_MAP` 中的重复键名和错误的引号映射。
|
||||||
|
* **Bug 修复**: 修复了 Python 文件中缺失的 `Dict` 类型导入。
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ OpenWebUI supports four types of plugins, each serving a different purpose:
|
|||||||
|
|
||||||
| Plugin | Type | Description | Version |
|
| Plugin | Type | Description | Version |
|
||||||
|--------|------|-------------|---------|
|
|--------|------|-------------|---------|
|
||||||
| [Smart Mind Map](actions/smart-mind-map.md) | Action | Generate interactive mind maps from text | 0.9.1 |
|
| [Smart Mind Map](actions/smart-mind-map.md) | Action | Generate interactive mind maps from text | 0.9.2 |
|
||||||
| [Smart Infographic](actions/smart-infographic.md) | Action | Transform text into professional infographics | 1.4.9 |
|
| [Smart Infographic](actions/smart-infographic.md) | Action | Transform text into professional infographics | 1.4.9 |
|
||||||
| [Flash Card](actions/flash-card.md) | Action | Create beautiful learning flashcards | 0.2.4 |
|
| [Flash Card](actions/flash-card.md) | Action | Create beautiful learning flashcards | 0.2.4 |
|
||||||
| [Export to Excel](actions/export-to-excel.md) | Action | Export chat history to Excel files | 0.3.7 |
|
| [Export to Excel](actions/export-to-excel.md) | Action | Export chat history to Excel files | 0.3.7 |
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ OpenWebUI 支持四种类型的插件,每种都有不同的用途:
|
|||||||
|
|
||||||
| 插件 | 类型 | 描述 | 版本 |
|
| 插件 | 类型 | 描述 | 版本 |
|
||||||
|--------|------|-------------|---------|
|
|--------|------|-------------|---------|
|
||||||
| [Smart Mind Map(智能思维导图)](actions/smart-mind-map.md) | Action | 从文本生成交互式思维导图 | 0.9.1 |
|
| [Smart Mind Map(智能思维导图)](actions/smart-mind-map.md) | Action | 从文本生成交互式思维导图 | 0.9.2 |
|
||||||
| [Smart Infographic(智能信息图)](actions/smart-infographic.md) | Action | 将文本转成专业信息图 | 1.4.9 |
|
| [Smart Infographic(智能信息图)](actions/smart-infographic.md) | Action | 将文本转成专业信息图 | 1.4.9 |
|
||||||
| [Flash Card(闪记卡)](actions/flash-card.md) | Action | 生成精美学习卡片 | 0.2.4 |
|
| [Flash Card(闪记卡)](actions/flash-card.md) | Action | 生成精美学习卡片 | 0.2.4 |
|
||||||
| [Export to Excel(导出到 Excel)](actions/export-to-excel.md) | Action | 导出聊天记录为 Excel | 0.3.7 |
|
| [Export to Excel(导出到 Excel)](actions/export-to-excel.md) | Action | 导出聊天记录为 Excel | 0.3.7 |
|
||||||
|
|||||||
104
docs/plugins/pipes/github-copilot-sdk.md
Normal file
104
docs/plugins/pipes/github-copilot-sdk.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# GitHub Copilot SDK Pipe for OpenWebUI
|
||||||
|
|
||||||
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.3.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
|
This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/open-webui) that allows you to use GitHub Copilot models (such as `gpt-5`, `gpt-5-mini`, `claude-sonnet-4.5`) directly within OpenWebUI. It is built upon the official [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk), providing a native integration experience.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Active GitHub Copilot Subscription Required**
|
||||||
|
> This plugin requires a valid GitHub Copilot subscription (Individual, Business, or Enterprise). It will verify your subscription status during authentication.
|
||||||
|
|
||||||
|
## 🚀 What's New (v0.3.0) - The Power of "Unified Ecosystem"
|
||||||
|
|
||||||
|
* **🔌 Zero-Config Tool Bridge**: Automatically transforms your existing OpenWebUI Functions (Tools) into Copilot-compatible tools. **Copilot now has total access to your entire WebUI toolset!**
|
||||||
|
* **🔗 Dynamic MCP Discovery**: Seamlessly connects to MCP servers defined in **Admin Settings -> Connections**. No configuration files required—it just works.
|
||||||
|
* **⚡ High-Performance Async Engine**: Background CLI updates and optimized event-driven streaming ensure lightning-fast responses without UI lag.
|
||||||
|
* **🛡️ Robust Interoperability**: Advanced sanitization and dynamic Pydantic model generation ensure smooth integration even with complex third-party tools.
|
||||||
|
|
||||||
|
## ✨ Key Capabilities
|
||||||
|
|
||||||
|
* **🌉 The Ultimate Bridge**: The first and only plugin that creates a seamless bridge between **OpenWebUI Tools** and **GitHub Copilot SDK**.
|
||||||
|
* **🚀 Official & Native**: Built directly on the official Python SDK, providing the most stable and authentic Copilot experience.
|
||||||
|
* **🌊 Advanced Streaming (Thought Process)**: Supports full model reasoning/thinking display with typewriter effects.
|
||||||
|
* **🖼️ Intelligent Multimodal**: Full support for images and attachments, enabling Copilot to "see" your workspace.
|
||||||
|
* **🛠️ Effortless Setup**: Automatic CLI detection, version enforcement, and dependency management.
|
||||||
|
* **🔑 Dual-Layer Security**: Supports secure OAuth flow for Chat and standard PAT for extended MCP capabilities.
|
||||||
|
|
||||||
|
## 📦 Installation & Usage
|
||||||
|
|
||||||
|
### 1. Import Function
|
||||||
|
|
||||||
|
1. Open OpenWebUI.
|
||||||
|
2. Go to **Workspace** -> **Functions**.
|
||||||
|
3. Click **+** (Create Function).
|
||||||
|
4. Paste the content of `github_copilot_sdk.py` (or `github_copilot_sdk_cn.py` for Chinese) completely.
|
||||||
|
5. Save.
|
||||||
|
|
||||||
|
### 2. Configure Valves (Settings)
|
||||||
|
|
||||||
|
Find "GitHub Copilot" in the function list and click the **⚙️ (Valves)** icon to configure:
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **GH_TOKEN** | **(Required)** GitHub Access Token (PAT or OAuth Token). Access to Chat. | - |
|
||||||
|
| **DEBUG** | Whether to enable debug logs (output to browser console). | `False` |
|
||||||
|
| **LOG_LEVEL** | Copilot CLI log level: none, error, warning, info, debug, all. | `error` |
|
||||||
|
| **SHOW_THINKING** | Show model reasoning/thinking process (requires streaming + model support). | `True` |
|
||||||
|
| **COPILOT_CLI_VERSION** | Specific Copilot CLI version to install/enforce. | `0.0.405` |
|
||||||
|
| **EXCLUDE_KEYWORDS** | Exclude models containing these keywords (comma separated). | - |
|
||||||
|
| **WORKSPACE_DIR** | Restricted workspace directory for file operations. | - |
|
||||||
|
| **INFINITE_SESSION** | Enable Infinite Sessions (automatic context compaction). | `True` |
|
||||||
|
| **COMPACTION_THRESHOLD** | Background compaction threshold (0.0-1.0). | `0.8` |
|
||||||
|
| **BUFFER_THRESHOLD** | Buffer exhaustion threshold (0.0-1.0). | `0.95` |
|
||||||
|
| **TIMEOUT** | Timeout for each stream chunk (seconds). | `300` |
|
||||||
|
| **CUSTOM_ENV_VARS** | Custom environment variables (JSON format). | - |
|
||||||
|
| **REASONING_EFFORT** | Reasoning effort level: low, medium, high. `xhigh` is supported for some models. | `medium` |
|
||||||
|
| **ENFORCE_FORMATTING** | Add formatting instructions to system prompt for better readability. | `True` |
|
||||||
|
| **ENABLE_MCP_SERVER** | Enable Direct MCP Client connection (Recommended). | `True` |
|
||||||
|
| **ENABLE_OPENWEBUI_TOOLS** | Enable OpenWebUI Tools (includes defined and server tools). | `True` |
|
||||||
|
|
||||||
|
#### User Valves (per-user overrides)
|
||||||
|
|
||||||
|
These optional settings can be set per user (overrides global Valves):
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **GH_TOKEN** | Personal GitHub Token (overrides global setting). | - |
|
||||||
|
| **REASONING_EFFORT** | Reasoning effort level (low/medium/high/xhigh). | - |
|
||||||
|
| **DEBUG** | Enable technical debug logs. | `False` |
|
||||||
|
| **SHOW_THINKING** | Show model reasoning/thinking process. | `True` |
|
||||||
|
| **ENABLE_OPENWEBUI_TOOLS** | Enable OpenWebUI Tools (overrides global). | `True` |
|
||||||
|
| **ENABLE_MCP_SERVER** | Enable MCP server loading (overrides global). | `True` |
|
||||||
|
| **ENFORCE_FORMATTING** | Enforce formatting guidelines (overrides global). | `True` |
|
||||||
|
|
||||||
|
### 3. Get Token
|
||||||
|
|
||||||
|
To use GitHub Copilot, you need a GitHub Personal Access Token (PAT) with appropriate permissions.
|
||||||
|
|
||||||
|
**Steps to generate your token:**
|
||||||
|
|
||||||
|
1. Visit [GitHub Token Settings](https://github.com/settings/tokens?type=beta).
|
||||||
|
2. Click **Generate new token (fine-grained)**.
|
||||||
|
3. **Repository access**: Select **Public Repositories** (simplest) or **All repositories**.
|
||||||
|
4. **Permissions**:
|
||||||
|
* If you chose **All repositories**, you must click **Account permissions**.
|
||||||
|
* Find **Copilot Requests**, and select **Access**.
|
||||||
|
5. Generate and copy the Token.
|
||||||
|
|
||||||
|
## 📋 Dependencies
|
||||||
|
|
||||||
|
This Pipe will automatically attempt to install the following dependencies:
|
||||||
|
|
||||||
|
* `github-copilot-sdk` (Python package)
|
||||||
|
* `github-copilot-cli` (Binary file, installed via official script)
|
||||||
|
|
||||||
|
## Troubleshooting ❓
|
||||||
|
|
||||||
|
* **Images and Multimodal Usage**:
|
||||||
|
* Ensure `MODEL_ID` is a model that supports multimodal input.
|
||||||
|
* **Thinking not shown**:
|
||||||
|
* Ensure **streaming is enabled** and the selected model supports reasoning output.
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT
|
||||||
104
docs/plugins/pipes/github-copilot-sdk.zh.md
Normal file
104
docs/plugins/pipes/github-copilot-sdk.zh.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# GitHub Copilot SDK 官方管道
|
||||||
|
|
||||||
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.3.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
|
这是一个用于 [OpenWebUI](https://github.com/open-webui/open-webui) 的高级 Pipe 函数,允许你直接在 OpenWebUI 中使用 GitHub Copilot 模型(如 `gpt-5`, `gpt-5-mini`, `claude-sonnet-4.5`)。它基于官方 [GitHub Copilot SDK for Python](https://github.com/github/copilot-sdk) 构建,提供了原生级的集成体验。
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **需 GitHub Copilot 订阅**
|
||||||
|
> 本插件需要有效的 GitHub Copilot 订阅(个人版、商业版或企业版)。插件将在认证阶段验证您的订阅状态。
|
||||||
|
|
||||||
|
## 🚀 最新特性 (v0.3.0) - “统一生态”的力量
|
||||||
|
|
||||||
|
* **🔌 零配置工具桥接 (Unified Tool Bridge)**: 自动将您现有的 OpenWebUI Functions (工具) 转换为 Copilot 兼容工具。**Copilot 现在可以无缝调用您手头所有的 WebUI 工具!**
|
||||||
|
* **🔗 动态 MCP 自动发现**: 直接联动 OpenWebUI **管理面板 -> 连接**。无需编写任何配置文件,即插即用,瞬间扩展 Copilot 能力边界。
|
||||||
|
* **⚡ 高性能异步引擎**: 异步 CLI 更新检查与高度优化的事件驱动流式处理,确保对话毫秒级响应。
|
||||||
|
* **🛡️ 卓越的兼容性**: 独创的动态 Pydantic 模型生成技术,确保复杂工具参数在 Copilot 端也能得到精准验证。
|
||||||
|
|
||||||
|
## ✨ 核心能力
|
||||||
|
|
||||||
|
* **🌉 强大的生态桥接**: 首个且唯一完美打通 **OpenWebUI Tools** 与 **GitHub Copilot SDK** 的插件。
|
||||||
|
* **🚀 官方原生产体验**: 基于官方 Python SDK 构建,提供最稳定、最纯正的 Copilot 交互体验。
|
||||||
|
* **🌊 深度推理展示**: 完整支持模型思考过程 (Thinking Process) 的流式渲染。
|
||||||
|
* **🖼️ 智能多模态**: 支持图像识别与附件上传,让 Copilot 拥有视觉能力。
|
||||||
|
* **🛠️ 极简部署流程**: 自动检测环境、自动下载 CLI、自动管理依赖,全自动化开箱即用。
|
||||||
|
* **🔑 安全认证体系**: 完美支持 OAuth 授权与 PAT 模式,兼顾便捷与安全性。
|
||||||
|
|
||||||
|
## 📦 安装与使用
|
||||||
|
|
||||||
|
### 1. 导入函数
|
||||||
|
|
||||||
|
1. 打开 OpenWebUI。
|
||||||
|
2. 进入 **Workspace** -> **Functions**。
|
||||||
|
3. 点击 **+** (创建函数)。
|
||||||
|
4. 将 `github_copilot_sdk_cn.py` 的内容完整粘贴进去。
|
||||||
|
5. 保存。
|
||||||
|
|
||||||
|
### 2. 配置 Valves (设置)
|
||||||
|
|
||||||
|
在函数列表中找到 "GitHub Copilot",点击 **⚙️ (Valves)** 图标进行配置:
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **GH_TOKEN** | **(必填)** GitHub 访问令牌 (PAT 或 OAuth Token)。用于聊天。 | - |
|
||||||
|
| **DEBUG** | 是否开启调试日志(输出到浏览器控制台)。 | `False` |
|
||||||
|
| **LOG_LEVEL** | Copilot CLI 日志级别: none, error, warning, info, debug, all。 | `error` |
|
||||||
|
| **SHOW_THINKING** | 是否显示模型推理/思考过程(需开启流式 + 模型支持)。 | `True` |
|
||||||
|
| **COPILOT_CLI_VERSION** | 指定安装/强制使用的 Copilot CLI 版本。 | `0.0.405` |
|
||||||
|
| **EXCLUDE_KEYWORDS** | 排除包含这些关键词的模型(逗号分隔)。 | - |
|
||||||
|
| **WORKSPACE_DIR** | 文件操作的受限工作区目录。 | - |
|
||||||
|
| **INFINITE_SESSION** | 启用无限会话(自动上下文压缩)。 | `True` |
|
||||||
|
| **COMPACTION_THRESHOLD** | 后台压缩阈值 (0.0-1.0)。 | `0.8` |
|
||||||
|
| **BUFFER_THRESHOLD** | 缓冲区耗尽阈值 (0.0-1.0)。 | `0.95` |
|
||||||
|
| **TIMEOUT** | 每个流式分块超时(秒)。 | `300` |
|
||||||
|
| **CUSTOM_ENV_VARS** | 自定义环境变量 (JSON 格式)。 | - |
|
||||||
|
| **REASONING_EFFORT** | 推理强度级别: low, medium, high. `xhigh` 仅部分模型支持。 | `medium` |
|
||||||
|
| **ENFORCE_FORMATTING** | 在系统提示词中添加格式化指导。 | `True` |
|
||||||
|
| **ENABLE_MCP_SERVER** | 启用直接 MCP 客户端连接 (建议)。 | `True` |
|
||||||
|
| **ENABLE_OPENWEBUI_TOOLS** | 启用 OpenWebUI 工具 (包括自定义和服务器工具)。 | `True` |
|
||||||
|
|
||||||
|
#### 用户 Valves(按用户覆盖)
|
||||||
|
|
||||||
|
以下设置可按用户单独配置(覆盖全局 Valves):
|
||||||
|
|
||||||
|
| 参数 | 说明 | 默认值 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **GH_TOKEN** | 个人 GitHub Token(覆盖全局设置)。 | - |
|
||||||
|
| **REASONING_EFFORT** | 推理强度级别(low/medium/high/xhigh)。 | - |
|
||||||
|
| **DEBUG** | 是否启用技术调试日志。 | `False` |
|
||||||
|
| **SHOW_THINKING** | 是否显示思考过程。 | `True` |
|
||||||
|
| **ENABLE_OPENWEBUI_TOOLS** | 启用 OpenWebUI 工具(覆盖全局设置)。 | `True` |
|
||||||
|
| **ENABLE_MCP_SERVER** | 启用动态 MCP 服务器加载(覆盖全局设置)。 | `True` |
|
||||||
|
| **ENFORCE_FORMATTING** | 强制启用格式化指导(覆盖全局设置)。 | `True` |
|
||||||
|
|
||||||
|
### 3. 获取 Token
|
||||||
|
|
||||||
|
要使用 GitHub Copilot,您需要一个具有适当权限的 GitHub 个人访问令牌 (PAT)。
|
||||||
|
|
||||||
|
**获取步骤:**
|
||||||
|
|
||||||
|
1. 访问 [GitHub 令牌设置](https://github.com/settings/tokens?type=beta)。
|
||||||
|
2. 点击 **Generate new token (fine-grained)**。
|
||||||
|
3. **Repository access**: 选择 **Public Repositories** (最简单) 或 **All repositories**。
|
||||||
|
4. **Permissions**:
|
||||||
|
* 如果您选择了 **All repositories**,则必须点击 **Account permissions**。
|
||||||
|
* 找到 **Copilot Requests**,选择 **Access**。
|
||||||
|
5. 生成并复制令牌。
|
||||||
|
|
||||||
|
## 📋 依赖说明
|
||||||
|
|
||||||
|
该 Pipe 会自动尝试安装以下依赖(如果环境中缺失):
|
||||||
|
|
||||||
|
* `github-copilot-sdk` (Python 包)
|
||||||
|
* `github-copilot-cli` (二进制文件,通过官方脚本安装)
|
||||||
|
|
||||||
|
## 故障排除 (Troubleshooting) ❓
|
||||||
|
|
||||||
|
* **图片及多模态使用说明**:
|
||||||
|
* 确保 `MODEL_ID` 是支持多模态的模型。
|
||||||
|
* **看不到思考过程**:
|
||||||
|
* 确认已开启**流式输出**,且所选模型支持推理输出。
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -15,7 +15,7 @@ Pipes allow you to:
|
|||||||
|
|
||||||
## Available Pipe Plugins
|
## Available Pipe Plugins
|
||||||
|
|
||||||
|
- [GitHub Copilot SDK](github-copilot-sdk.md) (v0.3.0) - Official GitHub Copilot SDK integration. Features **zero-config OpenWebUI Tool Bridge** and **dynamic MCP discovery**. Supports streaming, multimodal, and infinite sessions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Pipes 可以用于:
|
|||||||
|
|
||||||
## 可用的 Pipe 插件
|
## 可用的 Pipe 插件
|
||||||
|
|
||||||
|
- [GitHub Copilot SDK](github-copilot-sdk.zh.md) (v0.3.0) - GitHub Copilot SDK 官方集成。**零配置工具桥接**与**动态 MCP 发现**。支持流式输出、多模态及无限会话。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ A comprehensive thinking lens that dives deep into any content - from context to
|
|||||||
| **Clear Previous HTML (CLEAR_PREVIOUS_HTML)** | `True` | Whether to clear previous plugin results. |
|
| **Clear Previous HTML (CLEAR_PREVIOUS_HTML)** | `True` | Whether to clear previous plugin results. |
|
||||||
| **Message Count (MESSAGE_COUNT)** | `1` | Number of recent messages to analyze. |
|
| **Message Count (MESSAGE_COUNT)** | `1` | Number of recent messages to analyze. |
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
## 🌗 Theme Support
|
## 🌗 Theme Support
|
||||||
|
|
||||||
The plugin automatically detects and adapts to OpenWebUI's theme settings:
|
The plugin automatically detects and adapts to OpenWebUI's theme settings:
|
||||||
@@ -88,3 +92,7 @@ The plugin generates a structured thinking timeline:
|
|||||||
- **Debug Logs**: Enable `SHOW_STATUS` in Valves to see progress updates.
|
- **Debug Logs**: Enable `SHOW_STATUS` in Valves to see progress updates.
|
||||||
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
||||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -39,6 +39,10 @@
|
|||||||
| **清除旧 HTML (CLEAR_PREVIOUS_HTML)** | `True` | 是否清除之前的插件结果。 |
|
| **清除旧 HTML (CLEAR_PREVIOUS_HTML)** | `True` | 是否清除之前的插件结果。 |
|
||||||
| **消息数量 (MESSAGE_COUNT)** | `1` | 要分析的最近消息数量。 |
|
| **消息数量 (MESSAGE_COUNT)** | `1` | 要分析的最近消息数量。 |
|
||||||
|
|
||||||
|
## ⭐ 支持
|
||||||
|
|
||||||
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
## 🌗 主题支持
|
## 🌗 主题支持
|
||||||
|
|
||||||
插件会自动检测并适配 OpenWebUI 的主题设置:
|
插件会自动检测并适配 OpenWebUI 的主题设置:
|
||||||
@@ -88,3 +92,7 @@
|
|||||||
- **调试日志**: 在 Valves 中启用 `SHOW_STATUS` 以查看进度更新。
|
- **调试日志**: 在 Valves 中启用 `SHOW_STATUS` 以查看进度更新。
|
||||||
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
||||||
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
# 📝 Export to Word (Enhanced)
|
# 📝 Export to Word (Enhanced)
|
||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.4.4 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**.
|
||||||
|
|
||||||
## 🔥 What's New in v0.4.3
|
## 🔥 What's New in v0.4.4
|
||||||
|
|
||||||
- ✨ **S3 Object Storage Support**: Direct access to images stored in S3/MinIO via boto3, bypassing API layer for faster exports.
|
- 🧹 **Content Cleanup**: Enhanced stripping of `<details>` blocks (often used for tool calls/thinking process) to ensure a clean final document.
|
||||||
- 🔧 **Multi-level File Fallback**: 6-level fallback mechanism for file retrieval (DB → S3 → Local → URL → API → Attributes).
|
- 📄 **Standard Document Formatting**: Applied professional document formatting standards for titles and headings (centered title, bold, optimized font sizes and spacing), including GB/T compliance for Chinese content.
|
||||||
- 🛡️ **Improved Error Handling**: Better logging and error messages for file retrieval failures.
|
- 🔠 **Font Rendering Fix**: Fixed an issue where CJK characters would fallback to MS Gothic in Word; now correctly uses the configured Asian font (e.g., SimSun).
|
||||||
|
- ⚙️ **Title Alignment**: Added `TITLE_ALIGNMENT` valve to configure document title alignment (left, center, right).
|
||||||
|
|
||||||
## ✨ Key Features
|
## ✨ Key Features
|
||||||
|
|
||||||
@@ -44,6 +45,10 @@ Export conversation to Word (.docx) with **syntax highlighting**, **native math
|
|||||||
| **Mermaid PNG Scale** | `3.0` | Resolution multiplier for Mermaid images |
|
| **Mermaid PNG Scale** | `3.0` | Resolution multiplier for Mermaid images |
|
||||||
| **Math Enable** | `True` | Enable LaTeX math conversion |
|
| **Math Enable** | `True` | Enable LaTeX math conversion |
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
## 🛠️ Supported Markdown Syntax
|
## 🛠️ Supported Markdown Syntax
|
||||||
|
|
||||||
| Syntax | Word Result |
|
| Syntax | Word Result |
|
||||||
@@ -71,25 +76,13 @@ Export conversation to Word (.docx) with **syntax highlighting**, **native math
|
|||||||
- `latex2mathml` - LaTeX to MathML conversion
|
- `latex2mathml` - LaTeX to MathML conversion
|
||||||
- `mathml2omml` - MathML to Office Math (OMML) conversion
|
- `mathml2omml` - MathML to Office Math (OMML) conversion
|
||||||
|
|
||||||
## 📝 Changelog
|
|
||||||
|
|
||||||
### v0.4.3
|
|
||||||
- **S3 Object Storage**: Direct S3/MinIO access via boto3 for faster image retrieval.
|
|
||||||
- **6-Level Fallback**: Robust file retrieval: DB → S3 → Local → URL → API → Attributes.
|
|
||||||
- **Better Logging**: Improved error messages for debugging file access issues.
|
|
||||||
|
|
||||||
### v0.4.1
|
|
||||||
- **Chinese Parameter Names**: Localized configuration names for Chinese version.
|
|
||||||
|
|
||||||
### v0.4.0
|
|
||||||
- **Multi-language Support**: UI language switching (English/Chinese).
|
|
||||||
- **Font & Style Configuration**: Customizable fonts and table colors.
|
|
||||||
- **Mermaid Enhancements**: Hybrid SVG+PNG rendering, background color config.
|
|
||||||
- **Performance**: Real-time progress updates for large exports.
|
|
||||||
|
|
||||||
## Troubleshooting ❓
|
## Troubleshooting ❓
|
||||||
|
|
||||||
- **Plugin not working?**: Check if the filter/action is enabled in the model settings.
|
- **Plugin not working?**: Check if the filter/action is enabled in the model settings.
|
||||||
- **Debug Logs**: Check the browser console (F12) for detailed logs if available.
|
- **Debug Logs**: Check the browser console (F12) for detailed logs if available.
|
||||||
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
||||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## 📝 Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
# 📝 导出为 Word (增强版)
|
# 📝 导出为 Word (增强版)
|
||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.4.4 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
将对话导出为 Word (.docx),支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用参考**和**增强表格格式**。
|
将对话导出为 Word (.docx),支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用参考**和**增强表格格式**。
|
||||||
|
|
||||||
## 🔥 v0.4.3 更新内容
|
## 🔥 v0.4.4 更新内容
|
||||||
|
|
||||||
- ✨ **S3 对象存储支持**: 通过 boto3 直连 S3/MinIO,绕过 API 层,导出速度更快。
|
- 🧹 **内容清理加强**: 增强了对 `<details>` 块(通常包含工具调用或思考过程)的清理,确保最终文档整洁。
|
||||||
- 🔧 **多级文件回退**: 6 级文件获取机制(数据库 → S3 → 本地 → URL → API → 属性)。
|
- 📄 **文档格式标准化**: 采用了专业的文档排版标准(兼容中文 GB/T 规范),标题居中加粗,各级标题使用标准字号和间距。
|
||||||
- 🛡️ **错误处理优化**: 更完善的日志记录和错误提示,便于调试文件访问问题。
|
- 🔠 **字体渲染修复**: 修复了 CJK 字符在 Word 中回退到 MS Gothic 的问题;现在正确使用配置的中文字体(例如宋体)。
|
||||||
|
- ⚙️ **标题对齐配置**: 新增 `标题对齐方式` Valve,支持配置文档标题的对齐方式(左对齐、居中、右对齐)。
|
||||||
|
|
||||||
## ✨ 核心特性
|
## ✨ 核心特性
|
||||||
|
|
||||||
@@ -44,6 +45,10 @@
|
|||||||
| **Mermaid_PNG缩放比例** | `3.0` | Mermaid 图片分辨率倍数 |
|
| **Mermaid_PNG缩放比例** | `3.0` | Mermaid 图片分辨率倍数 |
|
||||||
| **启用数学公式** | `True` | 启用 LaTeX 公式转换 |
|
| **启用数学公式** | `True` | 启用 LaTeX 公式转换 |
|
||||||
|
|
||||||
|
## ⭐ 支持
|
||||||
|
|
||||||
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
## 🛠️ 支持的 Markdown 语法
|
## 🛠️ 支持的 Markdown 语法
|
||||||
|
|
||||||
| 语法 | Word 效果 |
|
| 语法 | Word 效果 |
|
||||||
@@ -71,25 +76,13 @@
|
|||||||
- `latex2mathml` - LaTeX 转 MathML
|
- `latex2mathml` - LaTeX 转 MathML
|
||||||
- `mathml2omml` - MathML 转 Office Math (OMML)
|
- `mathml2omml` - MathML 转 Office Math (OMML)
|
||||||
|
|
||||||
## 📝 更新日志
|
|
||||||
|
|
||||||
### v0.4.3
|
|
||||||
- **S3 对象存储**: 通过 boto3 直连 S3/MinIO,图片获取速度更快。
|
|
||||||
- **6 级回退机制**: 稳健的文件获取:数据库 → S3 → 本地 → URL → API → 属性。
|
|
||||||
- **日志优化**: 改进错误提示,便于调试文件访问问题。
|
|
||||||
|
|
||||||
### v0.4.1
|
|
||||||
- **中文参数名**: 配置项名称和描述全部汉化。
|
|
||||||
|
|
||||||
### v0.4.0
|
|
||||||
- **多语言支持**: 界面语言切换(中文/英文)。
|
|
||||||
- **字体与样式配置**: 支持自定义中英文字体、代码字体以及表格颜色。
|
|
||||||
- **Mermaid 增强**: 混合 SVG+PNG 渲染,支持背景色配置。
|
|
||||||
- **性能优化**: 导出大型文档时提供实时进度反馈。
|
|
||||||
|
|
||||||
## 故障排除 (Troubleshooting) ❓
|
## 故障排除 (Troubleshooting) ❓
|
||||||
|
|
||||||
- **插件不工作?**: 请检查是否在模型设置中启用了该过滤器/动作。
|
- **插件不工作?**: 请检查是否在模型设置中启用了该过滤器/动作。
|
||||||
- **调试日志**: 请查看浏览器控制台 (F12) 获取详细日志(如果可用)。
|
- **调试日志**: 请查看浏览器控制台 (F12) 获取详细日志(如果可用)。
|
||||||
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
||||||
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## 📝 更新日志
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
title: Export to Word (Enhanced)
|
title: Export to Word Enhanced
|
||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/open-webui
|
funding_url: https://github.com/open-webui
|
||||||
version: 0.4.3
|
version: 0.4.4
|
||||||
openwebui_id: fca6a315-2a45-42cc-8c96-55cbc85f87f2
|
openwebui_id: fca6a315-2a45-42cc-8c96-55cbc85f87f2
|
||||||
icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K
|
icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K
|
||||||
requirements: python-docx, Pygments, latex2mathml, mathml2omml
|
requirements: python-docx, Pygments, latex2mathml, mathml2omml
|
||||||
@@ -101,9 +101,8 @@ _TRANSPARENT_1PX_PNG = base64.b64decode(
|
|||||||
_ASVG_NS = "http://schemas.microsoft.com/office/drawing/2016/SVG/main"
|
_ASVG_NS = "http://schemas.microsoft.com/office/drawing/2016/SVG/main"
|
||||||
nsmap.setdefault("asvg", _ASVG_NS)
|
nsmap.setdefault("asvg", _ASVG_NS)
|
||||||
|
|
||||||
_REASONING_DETAILS_RE = re.compile(
|
_ALL_DETAILS_RE = re.compile(
|
||||||
r"<details\b[^>]*\btype\s*=\s*(?:\"reasoning\"|'reasoning'|reasoning)[^>]*>.*?</details\s*>",
|
r"<details\b[^>]*>.*?</details\s*>", re.IGNORECASE | re.DOTALL
|
||||||
re.IGNORECASE | re.DOTALL,
|
|
||||||
)
|
)
|
||||||
_THINK_RE = re.compile(r"<think\b[^>]*>.*?</think\s*>", re.IGNORECASE | re.DOTALL)
|
_THINK_RE = re.compile(r"<think\b[^>]*>.*?</think\s*>", re.IGNORECASE | re.DOTALL)
|
||||||
_ANALYSIS_RE = re.compile(
|
_ANALYSIS_RE = re.compile(
|
||||||
@@ -178,6 +177,12 @@ class Action:
|
|||||||
description="Font for code blocks and inline code (e.g., 'Consolas', 'Courier New', 'Monaco')",
|
description="Font for code blocks and inline code (e.g., 'Consolas', 'Courier New', 'Monaco')",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Title alignment
|
||||||
|
TITLE_ALIGNMENT: str = Field(
|
||||||
|
default="center",
|
||||||
|
description="Title alignment: 'left', 'center', or 'right'",
|
||||||
|
)
|
||||||
|
|
||||||
# Table styling
|
# Table styling
|
||||||
TABLE_HEADER_COLOR: str = Field(
|
TABLE_HEADER_COLOR: str = Field(
|
||||||
default="F2F2F2",
|
default="F2F2F2",
|
||||||
@@ -242,60 +247,60 @@ class Action:
|
|||||||
)
|
)
|
||||||
|
|
||||||
class UserValves(BaseModel):
|
class UserValves(BaseModel):
|
||||||
TITLE_SOURCE: str = Field(
|
TITLE_SOURCE: Optional[str] = Field(
|
||||||
default="chat_title",
|
default=None,
|
||||||
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
|
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
|
||||||
)
|
)
|
||||||
UI_LANGUAGE: str = Field(
|
UI_LANGUAGE: Optional[str] = Field(
|
||||||
default="en",
|
default=None,
|
||||||
description="UI language for export messages. Options: 'en' (English), 'zh' (Chinese)",
|
description="UI language for export messages. Options: 'en' (English), 'zh' (Chinese)",
|
||||||
)
|
)
|
||||||
FONT_LATIN: str = Field(
|
FONT_LATIN: Optional[str] = Field(
|
||||||
default="Times New Roman",
|
default=None,
|
||||||
description="Font for Latin characters (e.g., 'Times New Roman', 'Calibri', 'Arial')",
|
description="Font for Latin characters (e.g., 'Times New Roman', 'Calibri', 'Arial')",
|
||||||
)
|
)
|
||||||
FONT_ASIAN: str = Field(
|
FONT_ASIAN: Optional[str] = Field(
|
||||||
default="SimSun",
|
default=None,
|
||||||
description="Font for Asian characters (e.g., 'SimSun', 'Microsoft YaHei', 'PingFang SC')",
|
description="Font for Asian characters (e.g., 'SimSun', 'Microsoft YaHei', 'PingFang SC')",
|
||||||
)
|
)
|
||||||
FONT_CODE: str = Field(
|
FONT_CODE: Optional[str] = Field(
|
||||||
default="Consolas",
|
default=None,
|
||||||
description="Font for code blocks and inline code (e.g., 'Consolas', 'Courier New', 'Monaco')",
|
description="Font for code blocks and inline code (e.g., 'Consolas', 'Courier New', 'Monaco')",
|
||||||
)
|
)
|
||||||
TABLE_HEADER_COLOR: str = Field(
|
TABLE_HEADER_COLOR: Optional[str] = Field(
|
||||||
default="F2F2F2",
|
default=None,
|
||||||
description="Table header background color (hex, without #)",
|
description="Table header background color (hex, without #)",
|
||||||
)
|
)
|
||||||
TABLE_ZEBRA_COLOR: str = Field(
|
TABLE_ZEBRA_COLOR: Optional[str] = Field(
|
||||||
default="FBFBFB",
|
default=None,
|
||||||
description="Table zebra stripe background color for alternate rows (hex, without #)",
|
description="Table zebra stripe background color for alternate rows (hex, without #)",
|
||||||
)
|
)
|
||||||
MERMAID_PNG_SCALE: float = Field(
|
MERMAID_PNG_SCALE: Optional[float] = Field(
|
||||||
default=3.0,
|
default=None,
|
||||||
description="PNG render resolution multiplier (higher = clearer, larger file)",
|
description="PNG render resolution multiplier (higher = clearer, larger file)",
|
||||||
)
|
)
|
||||||
MERMAID_DISPLAY_SCALE: float = Field(
|
MERMAID_DISPLAY_SCALE: Optional[float] = Field(
|
||||||
default=1.0,
|
default=None,
|
||||||
description="Diagram width relative to available page width (<=1 recommended)",
|
description="Diagram width relative to available page width (<=1 recommended)",
|
||||||
)
|
)
|
||||||
MERMAID_OPTIMIZE_LAYOUT: bool = Field(
|
MERMAID_OPTIMIZE_LAYOUT: Optional[bool] = Field(
|
||||||
default=False,
|
default=None,
|
||||||
description="Optimize Mermaid layout: convert LR to TD for graph/flowchart",
|
description="Optimize Mermaid layout: convert LR to TD for graph/flowchart",
|
||||||
)
|
)
|
||||||
MERMAID_BACKGROUND: str = Field(
|
MERMAID_BACKGROUND: Optional[str] = Field(
|
||||||
default="",
|
default=None,
|
||||||
description="Mermaid background color. Empty = transparent (recommended for Word dark mode). Used only for optional PNG fill.",
|
description="Mermaid background color. Empty = transparent (recommended for Word dark mode). Used only for optional PNG fill.",
|
||||||
)
|
)
|
||||||
MERMAID_CAPTIONS_ENABLE: bool = Field(
|
MERMAID_CAPTIONS_ENABLE: Optional[bool] = Field(
|
||||||
default=True,
|
default=None,
|
||||||
description="Add figure captions under Mermaid images/charts",
|
description="Add figure captions under Mermaid images/charts",
|
||||||
)
|
)
|
||||||
MATH_ENABLE: bool = Field(
|
MATH_ENABLE: Optional[bool] = Field(
|
||||||
default=True,
|
default=None,
|
||||||
description="Enable LaTeX math block conversion (\\\\[...\\\\] and $$...$$) into Word equations",
|
description="Enable LaTeX math block conversion (\\\\[...\\\\] and $$...$$) into Word equations",
|
||||||
)
|
)
|
||||||
MATH_INLINE_DOLLAR_ENABLE: bool = Field(
|
MATH_INLINE_DOLLAR_ENABLE: Optional[bool] = Field(
|
||||||
default=True,
|
default=None,
|
||||||
description="Enable inline $...$ math conversion into Word equations (conservative parsing to reduce false positives)",
|
description="Enable inline $...$ math conversion into Word equations (conservative parsing to reduce false positives)",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -449,12 +454,20 @@ class Action:
|
|||||||
user_id = __user__.get("id", "unknown_user")
|
user_id = __user__.get("id", "unknown_user")
|
||||||
|
|
||||||
# Apply UserValves if present
|
# Apply UserValves if present
|
||||||
if __user__ and "valves" in __user__:
|
if __user__:
|
||||||
# Update self.valves with user-specific values
|
# Robustly parse UserValves whether it's a dict or Pydantic model
|
||||||
# Note: This assumes per-request instantiation or that we are okay with modifying the singleton.
|
raw_valves = __user__.get("valves", {})
|
||||||
# Given the plugin architecture, we'll update it for this execution.
|
if isinstance(raw_valves, self.UserValves):
|
||||||
for key, value in __user__["valves"].model_dump().items():
|
user_valves = raw_valves
|
||||||
if hasattr(self.valves, key):
|
elif isinstance(raw_valves, dict):
|
||||||
|
user_valves = self.UserValves(**raw_valves)
|
||||||
|
else:
|
||||||
|
user_valves = None
|
||||||
|
|
||||||
|
if user_valves:
|
||||||
|
for key, value in user_valves.model_dump(exclude_unset=True).items():
|
||||||
|
# Only override if the value is not None (and explicitly set)
|
||||||
|
if hasattr(self.valves, key) and value is not None:
|
||||||
setattr(self.valves, key, value)
|
setattr(self.valves, key, value)
|
||||||
|
|
||||||
# Get user language from Valves configuration
|
# Get user language from Valves configuration
|
||||||
@@ -492,6 +505,37 @@ class Action:
|
|||||||
try:
|
try:
|
||||||
message_content = last_assistant_message["content"]
|
message_content = last_assistant_message["content"]
|
||||||
if isinstance(message_content, str):
|
if isinstance(message_content, str):
|
||||||
|
if __event_emitter__ and self.valves.SHOW_DEBUG_LOG:
|
||||||
|
debug_data = {}
|
||||||
|
for name, regex in [
|
||||||
|
("Details Block", _ALL_DETAILS_RE),
|
||||||
|
("Think Block", _THINK_RE),
|
||||||
|
("Analysis Block", _ANALYSIS_RE),
|
||||||
|
]:
|
||||||
|
matches = regex.findall(message_content)
|
||||||
|
if matches:
|
||||||
|
debug_data[name] = [
|
||||||
|
(m[:200] + "...") if len(m) > 200 else m
|
||||||
|
for m in matches
|
||||||
|
]
|
||||||
|
if debug_data:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Context Stripping Analysis",
|
||||||
|
debug_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log font configuration
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Font Configuration",
|
||||||
|
{
|
||||||
|
"Latin Font": self.valves.FONT_LATIN,
|
||||||
|
"Asian Font": self.valves.FONT_ASIAN,
|
||||||
|
"Code Font": self.valves.FONT_CODE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
message_content = self._strip_reasoning_blocks(message_content)
|
message_content = self._strip_reasoning_blocks(message_content)
|
||||||
|
|
||||||
if not message_content or not message_content.strip():
|
if not message_content or not message_content.strip():
|
||||||
@@ -1107,30 +1151,7 @@ class Action:
|
|||||||
if not isinstance(name, str):
|
if not isinstance(name, str):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _is_emoji_codepoint(codepoint: int) -> bool:
|
without_emoji = self._remove_emojis(name)
|
||||||
# Common emoji ranges + flag regional indicators.
|
|
||||||
return (
|
|
||||||
0x1F000 <= codepoint <= 0x1FAFF
|
|
||||||
or 0x1F1E6 <= codepoint <= 0x1F1FF
|
|
||||||
or 0x2600 <= codepoint <= 0x26FF
|
|
||||||
or 0x2700 <= codepoint <= 0x27BF
|
|
||||||
or 0x2300 <= codepoint <= 0x23FF
|
|
||||||
or 0x2B00 <= codepoint <= 0x2BFF
|
|
||||||
)
|
|
||||||
|
|
||||||
def _is_emoji_modifier(codepoint: int) -> bool:
|
|
||||||
# VS15/VS16, ZWJ, keycap, skin tones, and tag characters used in some emoji sequences.
|
|
||||||
return (
|
|
||||||
codepoint in (0x200D, 0xFE0E, 0xFE0F, 0x20E3)
|
|
||||||
or 0x1F3FB <= codepoint <= 0x1F3FF
|
|
||||||
or 0xE0020 <= codepoint <= 0xE007F
|
|
||||||
)
|
|
||||||
|
|
||||||
without_emoji = "".join(
|
|
||||||
ch
|
|
||||||
for ch in name
|
|
||||||
if not (_is_emoji_codepoint(ord(ch)) or _is_emoji_modifier(ord(ch)))
|
|
||||||
)
|
|
||||||
cleaned = re.sub(r'[\\/*?:"<>|]', "", without_emoji)
|
cleaned = re.sub(r'[\\/*?:"<>|]', "", without_emoji)
|
||||||
cleaned = re.sub(r"\s+", " ", cleaned).strip().strip(".")
|
cleaned = re.sub(r"\s+", " ", cleaned).strip().strip(".")
|
||||||
return cleaned[:50].strip()
|
return cleaned[:50].strip()
|
||||||
@@ -1498,7 +1519,10 @@ class Action:
|
|||||||
|
|
||||||
# If there is no h1 in content, prepend chat title as h1 when provided
|
# If there is no h1 in content, prepend chat title as h1 when provided
|
||||||
if top_heading and not has_h1:
|
if top_heading and not has_h1:
|
||||||
self.add_heading(doc, top_heading, 1)
|
# Remove emojis from title for a professional look
|
||||||
|
clean_title = self._remove_emojis(top_heading)
|
||||||
|
# Use Title style (level 0) for the main document title
|
||||||
|
self.add_heading(doc, clean_title, 0)
|
||||||
|
|
||||||
lines = markdown_text.split("\n")
|
lines = markdown_text.split("\n")
|
||||||
i = 0
|
i = 0
|
||||||
@@ -1758,7 +1782,7 @@ class Action:
|
|||||||
cur = text
|
cur = text
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
prev = cur
|
prev = cur
|
||||||
cur = _REASONING_DETAILS_RE.sub("", cur)
|
cur = _ALL_DETAILS_RE.sub("", cur)
|
||||||
cur = _THINK_RE.sub("", cur)
|
cur = _THINK_RE.sub("", cur)
|
||||||
cur = _ANALYSIS_RE.sub("", cur)
|
cur = _ANALYSIS_RE.sub("", cur)
|
||||||
if cur == prev:
|
if cur == prev:
|
||||||
@@ -2242,14 +2266,155 @@ class Action:
|
|||||||
font = style.font
|
font = style.font
|
||||||
font.name = self.valves.FONT_LATIN
|
font.name = self.valves.FONT_LATIN
|
||||||
font.size = Pt(11)
|
font.size = Pt(11)
|
||||||
# Set Asian font
|
|
||||||
style._element.rPr.rFonts.set(qn("w:eastAsia"), self.valves.FONT_ASIAN)
|
# Ensure rPr element exists
|
||||||
|
rPr = style._element.get_or_add_rPr()
|
||||||
|
rFonts = rPr.get_or_add_rFonts()
|
||||||
|
|
||||||
|
# Set Latin and Asian fonts explicitly
|
||||||
|
rFonts.set(qn("w:ascii"), self.valves.FONT_LATIN)
|
||||||
|
rFonts.set(qn("w:hAnsi"), self.valves.FONT_LATIN)
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.FONT_ASIAN)
|
||||||
|
|
||||||
|
# Set language to zh-CN to prevent MS Gothic fallback (Japanese font)
|
||||||
|
# Even for English interface, we want to prioritize Chinese glyphs over Japanese for CJK
|
||||||
|
lang = rPr.find(qn("w:lang"))
|
||||||
|
if lang is None:
|
||||||
|
lang = OxmlElement("w:lang")
|
||||||
|
rPr.append(lang)
|
||||||
|
lang.set(qn("w:val"), "en-US")
|
||||||
|
lang.set(qn("w:eastAsia"), "zh-CN")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[Font Config] Latin: {self.valves.FONT_LATIN}, Asian: {self.valves.FONT_ASIAN}"
|
||||||
|
)
|
||||||
|
|
||||||
# Set paragraph format
|
# Set paragraph format
|
||||||
paragraph_format = style.paragraph_format
|
paragraph_format = style.paragraph_format
|
||||||
paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||||
paragraph_format.space_after = Pt(6)
|
paragraph_format.space_after = Pt(6)
|
||||||
|
|
||||||
|
# Configure Title style (used for document title)
|
||||||
|
# Standard format: 22pt (二号), bold, centered, 24pt space after
|
||||||
|
if "Title" in doc.styles:
|
||||||
|
title_style = doc.styles["Title"]
|
||||||
|
title_font = title_style.font
|
||||||
|
title_font.name = self.valves.FONT_LATIN
|
||||||
|
title_font.size = Pt(22) # 二号字体
|
||||||
|
title_font.bold = True
|
||||||
|
title_font.color.rgb = RGBColor(0, 0, 0)
|
||||||
|
|
||||||
|
# Set paragraph format: alignment based on configuration
|
||||||
|
title_pf = title_style.paragraph_format
|
||||||
|
alignment_map = {
|
||||||
|
"left": WD_ALIGN_PARAGRAPH.LEFT,
|
||||||
|
"center": WD_ALIGN_PARAGRAPH.CENTER,
|
||||||
|
"right": WD_ALIGN_PARAGRAPH.RIGHT,
|
||||||
|
}
|
||||||
|
title_pf.alignment = alignment_map.get(
|
||||||
|
self.valves.TITLE_ALIGNMENT.lower(), WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
)
|
||||||
|
title_pf.space_before = Pt(0)
|
||||||
|
title_pf.space_after = Pt(24)
|
||||||
|
|
||||||
|
t_rPr = title_style._element.get_or_add_rPr()
|
||||||
|
t_rFonts = t_rPr.get_or_add_rFonts()
|
||||||
|
t_rFonts.set(qn("w:ascii"), self.valves.FONT_LATIN)
|
||||||
|
t_rFonts.set(qn("w:hAnsi"), self.valves.FONT_LATIN)
|
||||||
|
t_rFonts.set(qn("w:eastAsia"), self.valves.FONT_ASIAN)
|
||||||
|
|
||||||
|
# Set language to zh-CN
|
||||||
|
t_lang = t_rPr.find(qn("w:lang"))
|
||||||
|
if t_lang is None:
|
||||||
|
t_lang = OxmlElement("w:lang")
|
||||||
|
t_rPr.append(t_lang)
|
||||||
|
t_lang.set(qn("w:val"), "en-US")
|
||||||
|
t_lang.set(qn("w:eastAsia"), "zh-CN")
|
||||||
|
|
||||||
|
# Standard heading sizes based on Chinese document standards:
|
||||||
|
# Heading 1: 16pt (三号), bold, space before 24pt, space after 12pt
|
||||||
|
# Heading 2: 15pt (小三), bold, space before 18pt, space after 6pt
|
||||||
|
# Heading 3: 14pt (四号), bold, space before 12pt, space after 6pt
|
||||||
|
# Heading 4-9: 12pt (小四), bold, gradually reduced spacing
|
||||||
|
heading_formats = {
|
||||||
|
1: {"size": 16, "space_before": 24, "space_after": 12},
|
||||||
|
2: {"size": 15, "space_before": 18, "space_after": 6},
|
||||||
|
3: {"size": 14, "space_before": 12, "space_after": 6},
|
||||||
|
4: {"size": 12, "space_before": 12, "space_after": 6},
|
||||||
|
5: {"size": 12, "space_before": 6, "space_after": 6},
|
||||||
|
6: {"size": 11, "space_before": 6, "space_after": 3},
|
||||||
|
7: {"size": 11, "space_before": 6, "space_after": 3},
|
||||||
|
8: {"size": 10.5, "space_before": 6, "space_after": 3},
|
||||||
|
9: {"size": 10.5, "space_before": 6, "space_after": 3},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply font settings to Heading 1-9
|
||||||
|
for i in range(1, 10):
|
||||||
|
style_id = f"Heading {i}"
|
||||||
|
if style_id in doc.styles:
|
||||||
|
heading_style = doc.styles[style_id]
|
||||||
|
heading_font = heading_style.font
|
||||||
|
heading_font.name = self.valves.FONT_LATIN
|
||||||
|
heading_font.color.rgb = RGBColor(0, 0, 0)
|
||||||
|
|
||||||
|
# Apply standard formatting
|
||||||
|
fmt = heading_formats.get(
|
||||||
|
i, {"size": 11, "space_before": 6, "space_after": 3}
|
||||||
|
)
|
||||||
|
heading_font.size = Pt(fmt["size"])
|
||||||
|
heading_font.bold = True
|
||||||
|
|
||||||
|
heading_pf = heading_style.paragraph_format
|
||||||
|
heading_pf.space_before = Pt(fmt["space_before"])
|
||||||
|
heading_pf.space_after = Pt(fmt["space_after"])
|
||||||
|
|
||||||
|
# Ensure rPr exists
|
||||||
|
h_rPr = heading_style._element.get_or_add_rPr()
|
||||||
|
h_rFonts = h_rPr.get_or_add_rFonts()
|
||||||
|
|
||||||
|
# Set fonts
|
||||||
|
h_rFonts.set(qn("w:ascii"), self.valves.FONT_LATIN)
|
||||||
|
h_rFonts.set(qn("w:hAnsi"), self.valves.FONT_LATIN)
|
||||||
|
h_rFonts.set(qn("w:eastAsia"), self.valves.FONT_ASIAN)
|
||||||
|
|
||||||
|
# Set language to zh-CN
|
||||||
|
h_lang = h_rPr.find(qn("w:lang"))
|
||||||
|
if h_lang is None:
|
||||||
|
h_lang = OxmlElement("w:lang")
|
||||||
|
h_rPr.append(h_lang)
|
||||||
|
h_lang.set(qn("w:val"), "en-US")
|
||||||
|
h_lang.set(qn("w:eastAsia"), "zh-CN")
|
||||||
|
|
||||||
|
def _remove_emojis(self, text: str) -> str:
|
||||||
|
"""Remove emojis from text based on unicode ranges."""
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _is_emoji_codepoint(codepoint: int) -> bool:
|
||||||
|
# Common emoji ranges + flag regional indicators.
|
||||||
|
return (
|
||||||
|
0x1F000 <= codepoint <= 0x1FAFF
|
||||||
|
or 0x1F1E6 <= codepoint <= 0x1F1FF
|
||||||
|
or 0x2600 <= codepoint <= 0x26FF
|
||||||
|
or 0x2700 <= codepoint <= 0x27BF
|
||||||
|
or 0x2300 <= codepoint <= 0x23FF
|
||||||
|
or 0x2B00 <= codepoint <= 0x2BFF
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_emoji_modifier(codepoint: int) -> bool:
|
||||||
|
# VS15/VS16, ZWJ, keycap, skin tones, and tag characters used in some emoji sequences.
|
||||||
|
return (
|
||||||
|
codepoint in (0x200D, 0xFE0E, 0xFE0F, 0x20E3)
|
||||||
|
or 0x1F3FB <= codepoint <= 0x1F3FF
|
||||||
|
or 0xE0020 <= codepoint <= 0xE007F
|
||||||
|
)
|
||||||
|
|
||||||
|
return "".join(
|
||||||
|
ch
|
||||||
|
for ch in text
|
||||||
|
if not (_is_emoji_codepoint(ord(ch)) or _is_emoji_modifier(ord(ch)))
|
||||||
|
)
|
||||||
|
|
||||||
def add_heading(self, doc: Document, text: str, level: int):
|
def add_heading(self, doc: Document, text: str, level: int):
|
||||||
"""Add heading"""
|
"""Add heading"""
|
||||||
# Word heading levels start from 0, Markdown from 1
|
# Word heading levels start from 0, Markdown from 1
|
||||||
@@ -2285,6 +2450,12 @@ class Action:
|
|||||||
if strike:
|
if strike:
|
||||||
run.font.strike = True
|
run.font.strike = True
|
||||||
|
|
||||||
|
# Explicitly set East Asian font to prevent MS Gothic fallback
|
||||||
|
# Word may not inherit w:eastAsia from style, causing Japanese font fallback for CJK
|
||||||
|
rPr = run._element.get_or_add_rPr()
|
||||||
|
rFonts = rPr.get_or_add_rFonts()
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.FONT_ASIAN)
|
||||||
|
|
||||||
def _add_inline_code(self, paragraph, s: str):
|
def _add_inline_code(self, paragraph, s: str):
|
||||||
if s == "":
|
if s == "":
|
||||||
return
|
return
|
||||||
@@ -2684,7 +2855,11 @@ class Action:
|
|||||||
):
|
):
|
||||||
u = self._normalize_url(url)
|
u = self._normalize_url(url)
|
||||||
if not u:
|
if not u:
|
||||||
paragraph.add_run(display_text or text)
|
run = paragraph.add_run(display_text or text)
|
||||||
|
# Set East Asian font to prevent MS Gothic fallback
|
||||||
|
rPr = run._element.get_or_add_rPr()
|
||||||
|
rFonts = rPr.get_or_add_rFonts()
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.FONT_ASIAN)
|
||||||
return
|
return
|
||||||
|
|
||||||
part = getattr(paragraph, "part", None)
|
part = getattr(paragraph, "part", None)
|
||||||
@@ -2693,6 +2868,10 @@ class Action:
|
|||||||
run = paragraph.add_run(display_text or text)
|
run = paragraph.add_run(display_text or text)
|
||||||
run.font.color.rgb = RGBColor(0, 0, 255)
|
run.font.color.rgb = RGBColor(0, 0, 255)
|
||||||
run.font.underline = True
|
run.font.underline = True
|
||||||
|
# Set East Asian font to prevent MS Gothic fallback
|
||||||
|
rPr = run._element.get_or_add_rPr()
|
||||||
|
rFonts = rPr.get_or_add_rFonts()
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.FONT_ASIAN)
|
||||||
return
|
return
|
||||||
|
|
||||||
r_id = part.relate_to(u, RT.HYPERLINK, is_external=True)
|
r_id = part.relate_to(u, RT.HYPERLINK, is_external=True)
|
||||||
@@ -2706,6 +2885,11 @@ class Action:
|
|||||||
rStyle.set(qn("w:val"), "Hyperlink")
|
rStyle.set(qn("w:val"), "Hyperlink")
|
||||||
rPr.append(rStyle)
|
rPr.append(rStyle)
|
||||||
|
|
||||||
|
# Set East Asian font to prevent MS Gothic fallback
|
||||||
|
rFonts = OxmlElement("w:rFonts")
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.FONT_ASIAN)
|
||||||
|
rPr.append(rFonts)
|
||||||
|
|
||||||
color = OxmlElement("w:color")
|
color = OxmlElement("w:color")
|
||||||
color.set(qn("w:val"), "0000FF")
|
color.set(qn("w:val"), "0000FF")
|
||||||
rPr.append(color)
|
rPr.append(color)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
title: 导出为 Word (增强版)
|
title: 导出为Word增强版
|
||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/open-webui
|
funding_url: https://github.com/open-webui
|
||||||
version: 0.4.3
|
version: 0.4.4
|
||||||
openwebui_id: 8a6306c0-d005-4e46-aaae-8db3532c9ed5
|
openwebui_id: 8a6306c0-d005-4e46-aaae-8db3532c9ed5
|
||||||
icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K
|
icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K
|
||||||
requirements: python-docx, Pygments, latex2mathml, mathml2omml
|
requirements: python-docx, Pygments, latex2mathml, mathml2omml
|
||||||
@@ -101,9 +101,8 @@ _TRANSPARENT_1PX_PNG = base64.b64decode(
|
|||||||
_ASVG_NS = "http://schemas.microsoft.com/office/drawing/2016/SVG/main"
|
_ASVG_NS = "http://schemas.microsoft.com/office/drawing/2016/SVG/main"
|
||||||
nsmap.setdefault("asvg", _ASVG_NS)
|
nsmap.setdefault("asvg", _ASVG_NS)
|
||||||
|
|
||||||
_REASONING_DETAILS_RE = re.compile(
|
_ALL_DETAILS_RE = re.compile(
|
||||||
r"<details\b[^>]*\btype\s*=\s*(?:\"reasoning\"|'reasoning'|reasoning)[^>]*>.*?</details\s*>",
|
r"<details\b[^>]*>.*?</details\s*>", re.IGNORECASE | re.DOTALL
|
||||||
re.IGNORECASE | re.DOTALL,
|
|
||||||
)
|
)
|
||||||
_THINK_RE = re.compile(r"<think\b[^>]*>.*?</think\s*>", re.IGNORECASE | re.DOTALL)
|
_THINK_RE = re.compile(r"<think\b[^>]*>.*?</think\s*>", re.IGNORECASE | re.DOTALL)
|
||||||
_ANALYSIS_RE = re.compile(
|
_ANALYSIS_RE = re.compile(
|
||||||
@@ -178,6 +177,12 @@ class Action:
|
|||||||
description="Font for code blocks and inline code (e.g., 'Consolas', 'Courier New', 'Monaco')",
|
description="Font for code blocks and inline code (e.g., 'Consolas', 'Courier New', 'Monaco')",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Title alignment
|
||||||
|
标题对齐方式: str = Field(
|
||||||
|
default="center",
|
||||||
|
description="标题对齐方式: 'left' (左对齐), 'center' (居中), 或 'right' (右对齐)",
|
||||||
|
)
|
||||||
|
|
||||||
# Table styling
|
# Table styling
|
||||||
表头背景色: str = Field(
|
表头背景色: str = Field(
|
||||||
default="F2F2F2",
|
default="F2F2F2",
|
||||||
@@ -242,60 +247,60 @@ class Action:
|
|||||||
)
|
)
|
||||||
|
|
||||||
class UserValves(BaseModel):
|
class UserValves(BaseModel):
|
||||||
文档标题来源: str = Field(
|
文档标题来源: Optional[str] = Field(
|
||||||
default="chat_title",
|
default=None,
|
||||||
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
|
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
|
||||||
)
|
)
|
||||||
界面语言: str = Field(
|
界面语言: Optional[str] = Field(
|
||||||
default="zh",
|
default=None,
|
||||||
description="UI language for export messages. Options: 'en' (English), 'zh' (Chinese)",
|
description="UI language for export messages. Options: 'en' (English), 'zh' (Chinese)",
|
||||||
)
|
)
|
||||||
英文字体: str = Field(
|
英文字体: Optional[str] = Field(
|
||||||
default="Calibri",
|
default=None,
|
||||||
description="Font for Latin characters (e.g., 'Times New Roman', '', 'Arial')",
|
description="Font for Latin characters (e.g., 'Times New Roman', '', 'Arial')",
|
||||||
)
|
)
|
||||||
中文字体: str = Field(
|
中文字体: Optional[str] = Field(
|
||||||
default="SimSun",
|
default=None,
|
||||||
description="Font for Asian characters (e.g., 'SimSun', 'Microsoft YaHei', 'PingFang SC')",
|
description="Font for Asian characters (e.g., 'SimSun', 'Microsoft YaHei', 'PingFang SC')",
|
||||||
)
|
)
|
||||||
代码字体: str = Field(
|
代码字体: Optional[str] = Field(
|
||||||
default="Consolas",
|
default=None,
|
||||||
description="Font for code blocks and inline code (e.g., 'Consolas', 'Courier New', 'Monaco')",
|
description="Font for code blocks and inline code (e.g., 'Consolas', 'Courier New', 'Monaco')",
|
||||||
)
|
)
|
||||||
表头背景色: str = Field(
|
表头背景色: Optional[str] = Field(
|
||||||
default="F2F2F2",
|
default=None,
|
||||||
description="Table header background color (hex, without #)",
|
description="Table header background color (hex, without #)",
|
||||||
)
|
)
|
||||||
表格隔行背景色: str = Field(
|
表格隔行背景色: Optional[str] = Field(
|
||||||
default="FBFBFB",
|
default=None,
|
||||||
description="Table zebra stripe background color for alternate rows (hex, without #)",
|
description="Table zebra stripe background color for alternate rows (hex, without #)",
|
||||||
)
|
)
|
||||||
Mermaid_PNG缩放比例: float = Field(
|
Mermaid_PNG缩放比例: Optional[float] = Field(
|
||||||
default=3.0,
|
default=None,
|
||||||
description="PNG render resolution multiplier (higher = clearer, larger file)",
|
description="PNG render resolution multiplier (higher = clearer, larger file)",
|
||||||
)
|
)
|
||||||
Mermaid显示比例: float = Field(
|
Mermaid显示比例: Optional[float] = Field(
|
||||||
default=1.0,
|
default=None,
|
||||||
description="Diagram width relative to available page width (<=1 recommended)",
|
description="Diagram width relative to available page width (<=1 recommended)",
|
||||||
)
|
)
|
||||||
Mermaid布局优化: bool = Field(
|
Mermaid布局优化: Optional[bool] = Field(
|
||||||
default=False,
|
default=None,
|
||||||
description="Optimize Mermaid layout: convert LR to TD for graph/flowchart",
|
description="Optimize Mermaid layout: convert LR to TD for graph/flowchart",
|
||||||
)
|
)
|
||||||
Mermaid背景色: str = Field(
|
Mermaid背景色: Optional[str] = Field(
|
||||||
default="",
|
default=None,
|
||||||
description="Mermaid background color. Empty = transparent (recommended for Word dark mode). Used only for optional PNG fill.",
|
description="Mermaid background color. Empty = transparent (recommended for Word dark mode). Used only for optional PNG fill.",
|
||||||
)
|
)
|
||||||
启用Mermaid图注: bool = Field(
|
启用Mermaid图注: Optional[bool] = Field(
|
||||||
default=True,
|
default=None,
|
||||||
description="Add figure captions under Mermaid images/charts",
|
description="Add figure captions under Mermaid images/charts",
|
||||||
)
|
)
|
||||||
启用数学公式: bool = Field(
|
启用数学公式: Optional[bool] = Field(
|
||||||
default=True,
|
default=None,
|
||||||
description="Enable LaTeX math block conversion (\\\\[...\\\\] and $$...$$) into Word equations",
|
description="Enable LaTeX math block conversion (\\\\[...\\\\] and $$...$$) into Word equations",
|
||||||
)
|
)
|
||||||
启用行内公式: bool = Field(
|
启用行内公式: Optional[bool] = Field(
|
||||||
default=True,
|
default=None,
|
||||||
description="Enable inline $...$ math conversion into Word equations (conservative parsing to reduce false positives)",
|
description="Enable inline $...$ math conversion into Word equations (conservative parsing to reduce false positives)",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -449,10 +454,20 @@ class Action:
|
|||||||
user_id = __user__.get("id", "unknown_user")
|
user_id = __user__.get("id", "unknown_user")
|
||||||
|
|
||||||
# Apply UserValves if present
|
# Apply UserValves if present
|
||||||
if __user__ and "valves" in __user__:
|
if __user__:
|
||||||
# Update self.valves with user-specific values
|
# Robustly parse UserValves whether it's a dict or Pydantic model
|
||||||
for key, value in __user__["valves"].model_dump().items():
|
raw_valves = __user__.get("valves", {})
|
||||||
if hasattr(self.valves, key):
|
if isinstance(raw_valves, self.UserValves):
|
||||||
|
user_valves = raw_valves
|
||||||
|
elif isinstance(raw_valves, dict):
|
||||||
|
user_valves = self.UserValves(**raw_valves)
|
||||||
|
else:
|
||||||
|
user_valves = None
|
||||||
|
|
||||||
|
if user_valves:
|
||||||
|
for key, value in user_valves.model_dump(exclude_unset=True).items():
|
||||||
|
# Only override if the value is not None (and explicitly set)
|
||||||
|
if hasattr(self.valves, key) and value is not None:
|
||||||
setattr(self.valves, key, value)
|
setattr(self.valves, key, value)
|
||||||
|
|
||||||
# Get user language from Valves configuration
|
# Get user language from Valves configuration
|
||||||
@@ -490,6 +505,37 @@ class Action:
|
|||||||
try:
|
try:
|
||||||
message_content = last_assistant_message["content"]
|
message_content = last_assistant_message["content"]
|
||||||
if isinstance(message_content, str):
|
if isinstance(message_content, str):
|
||||||
|
if __event_emitter__ and self.valves.SHOW_DEBUG_LOG:
|
||||||
|
debug_data = {}
|
||||||
|
for name, regex in [
|
||||||
|
("Details Block (详情块)", _ALL_DETAILS_RE),
|
||||||
|
("Think Block (思考块)", _THINK_RE),
|
||||||
|
("Analysis Block (分析块)", _ANALYSIS_RE),
|
||||||
|
]:
|
||||||
|
matches = regex.findall(message_content)
|
||||||
|
if matches:
|
||||||
|
debug_data[name] = [
|
||||||
|
(m[:200] + "...") if len(m) > 200 else m
|
||||||
|
for m in matches
|
||||||
|
]
|
||||||
|
if debug_data:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"上下文内容清理分析 (Context Stripping Analysis)",
|
||||||
|
debug_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log font configuration
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"字体配置 (Font Configuration)",
|
||||||
|
{
|
||||||
|
"英文字体 (Latin Font)": self.valves.英文字体,
|
||||||
|
"中文字体 (Asian Font)": self.valves.中文字体,
|
||||||
|
"代码字体 (Code Font)": self.valves.代码字体,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
message_content = self._strip_reasoning_blocks(message_content)
|
message_content = self._strip_reasoning_blocks(message_content)
|
||||||
|
|
||||||
if not message_content or not message_content.strip():
|
if not message_content or not message_content.strip():
|
||||||
@@ -1101,34 +1147,11 @@ class Action:
|
|||||||
return title.strip() if isinstance(title, str) else ""
|
return title.strip() if isinstance(title, str) else ""
|
||||||
|
|
||||||
def clean_filename(self, name: str) -> str:
|
def clean_filename(self, name: str) -> str:
|
||||||
"""Clean illegal characters from filename and strip emoji."""
|
"""清理文件名中的非法字符并移除 Emoji"""
|
||||||
if not isinstance(name, str):
|
if not isinstance(name, str):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def _is_emoji_codepoint(codepoint: int) -> bool:
|
without_emoji = self._remove_emojis(name)
|
||||||
# Common emoji ranges + flag regional indicators.
|
|
||||||
return (
|
|
||||||
0x1F000 <= codepoint <= 0x1FAFF
|
|
||||||
or 0x1F1E6 <= codepoint <= 0x1F1FF
|
|
||||||
or 0x2600 <= codepoint <= 0x26FF
|
|
||||||
or 0x2700 <= codepoint <= 0x27BF
|
|
||||||
or 0x2300 <= codepoint <= 0x23FF
|
|
||||||
or 0x2B00 <= codepoint <= 0x2BFF
|
|
||||||
)
|
|
||||||
|
|
||||||
def _is_emoji_modifier(codepoint: int) -> bool:
|
|
||||||
# VS15/VS16, ZWJ, keycap, skin tones, and tag characters used in some emoji sequences.
|
|
||||||
return (
|
|
||||||
codepoint in (0x200D, 0xFE0E, 0xFE0F, 0x20E3)
|
|
||||||
or 0x1F3FB <= codepoint <= 0x1F3FF
|
|
||||||
or 0xE0020 <= codepoint <= 0xE007F
|
|
||||||
)
|
|
||||||
|
|
||||||
without_emoji = "".join(
|
|
||||||
ch
|
|
||||||
for ch in name
|
|
||||||
if not (_is_emoji_codepoint(ord(ch)) or _is_emoji_modifier(ord(ch)))
|
|
||||||
)
|
|
||||||
cleaned = re.sub(r'[\\/*?:"<>|]', "", without_emoji)
|
cleaned = re.sub(r'[\\/*?:"<>|]', "", without_emoji)
|
||||||
cleaned = re.sub(r"\s+", " ", cleaned).strip().strip(".")
|
cleaned = re.sub(r"\s+", " ", cleaned).strip().strip(".")
|
||||||
return cleaned[:50].strip()
|
return cleaned[:50].strip()
|
||||||
@@ -1496,7 +1519,10 @@ class Action:
|
|||||||
|
|
||||||
# If there is no h1 in content, prepend chat title as h1 when provided
|
# If there is no h1 in content, prepend chat title as h1 when provided
|
||||||
if top_heading and not has_h1:
|
if top_heading and not has_h1:
|
||||||
self.add_heading(doc, top_heading, 1)
|
# Remove emojis from title for a professional look
|
||||||
|
clean_title = self._remove_emojis(top_heading)
|
||||||
|
# Use Title style (level 0) for the main document title
|
||||||
|
self.add_heading(doc, clean_title, 0)
|
||||||
|
|
||||||
lines = markdown_text.split("\n")
|
lines = markdown_text.split("\n")
|
||||||
i = 0
|
i = 0
|
||||||
@@ -1756,7 +1782,7 @@ class Action:
|
|||||||
cur = text
|
cur = text
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
prev = cur
|
prev = cur
|
||||||
cur = _REASONING_DETAILS_RE.sub("", cur)
|
cur = _ALL_DETAILS_RE.sub("", cur)
|
||||||
cur = _THINK_RE.sub("", cur)
|
cur = _THINK_RE.sub("", cur)
|
||||||
cur = _ANALYSIS_RE.sub("", cur)
|
cur = _ANALYSIS_RE.sub("", cur)
|
||||||
if cur == prev:
|
if cur == prev:
|
||||||
@@ -2240,14 +2266,154 @@ class Action:
|
|||||||
font = style.font
|
font = style.font
|
||||||
font.name = self.valves.英文字体
|
font.name = self.valves.英文字体
|
||||||
font.size = Pt(11)
|
font.size = Pt(11)
|
||||||
# Set Asian font
|
|
||||||
style._element.rPr.rFonts.set(qn("w:eastAsia"), self.valves.中文字体)
|
# Ensure rPr element exists
|
||||||
|
rPr = style._element.get_or_add_rPr()
|
||||||
|
rFonts = rPr.get_or_add_rFonts()
|
||||||
|
|
||||||
|
# Set Latin and Asian fonts explicitly
|
||||||
|
rFonts.set(qn("w:ascii"), self.valves.英文字体)
|
||||||
|
rFonts.set(qn("w:hAnsi"), self.valves.英文字体)
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.中文字体)
|
||||||
|
|
||||||
|
# Set language to zh-CN to prevent MS Gothic fallback (Japanese font)
|
||||||
|
lang = rPr.find(qn("w:lang"))
|
||||||
|
if lang is None:
|
||||||
|
lang = OxmlElement("w:lang")
|
||||||
|
rPr.append(lang)
|
||||||
|
lang.set(qn("w:val"), "en-US")
|
||||||
|
lang.set(qn("w:eastAsia"), "zh-CN")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[Font Config] Latin: {self.valves.英文字体}, Asian: {self.valves.中文字体}"
|
||||||
|
)
|
||||||
|
|
||||||
# Set paragraph format
|
# Set paragraph format
|
||||||
paragraph_format = style.paragraph_format
|
paragraph_format = style.paragraph_format
|
||||||
paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE
|
||||||
paragraph_format.space_after = Pt(6)
|
paragraph_format.space_after = Pt(6)
|
||||||
|
|
||||||
|
# 配置 Title 样式 (用于文档标题)
|
||||||
|
# 标准格式: 22pt (二号), 加粗, 居中, 段后 24pt
|
||||||
|
if "Title" in doc.styles:
|
||||||
|
title_style = doc.styles["Title"]
|
||||||
|
title_font = title_style.font
|
||||||
|
title_font.name = self.valves.英文字体
|
||||||
|
title_font.size = Pt(22) # 二号字体
|
||||||
|
title_font.bold = True
|
||||||
|
title_font.color.rgb = RGBColor(0, 0, 0)
|
||||||
|
|
||||||
|
# 段落格式: 根据配置设置对齐方式和间距
|
||||||
|
title_pf = title_style.paragraph_format
|
||||||
|
alignment_map = {
|
||||||
|
"left": WD_ALIGN_PARAGRAPH.LEFT,
|
||||||
|
"center": WD_ALIGN_PARAGRAPH.CENTER,
|
||||||
|
"right": WD_ALIGN_PARAGRAPH.RIGHT,
|
||||||
|
}
|
||||||
|
title_pf.alignment = alignment_map.get(
|
||||||
|
self.valves.标题对齐方式.lower(), WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
)
|
||||||
|
title_pf.space_before = Pt(0)
|
||||||
|
title_pf.space_after = Pt(24)
|
||||||
|
|
||||||
|
t_rPr = title_style._element.get_or_add_rPr()
|
||||||
|
t_rFonts = t_rPr.get_or_add_rFonts()
|
||||||
|
t_rFonts.set(qn("w:ascii"), self.valves.英文字体)
|
||||||
|
t_rFonts.set(qn("w:hAnsi"), self.valves.英文字体)
|
||||||
|
t_rFonts.set(qn("w:eastAsia"), self.valves.中文字体)
|
||||||
|
|
||||||
|
# Set language for Title
|
||||||
|
t_lang = t_rPr.find(qn("w:lang"))
|
||||||
|
if t_lang is None:
|
||||||
|
t_lang = OxmlElement("w:lang")
|
||||||
|
t_rPr.append(t_lang)
|
||||||
|
t_lang.set(qn("w:val"), "en-US")
|
||||||
|
t_lang.set(qn("w:eastAsia"), "zh-CN")
|
||||||
|
|
||||||
|
# 标准标题字号 (基于中文文档规范):
|
||||||
|
# Heading 1: 16pt (三号), 加粗, 段前 24pt, 段后 12pt
|
||||||
|
# Heading 2: 15pt (小三), 加粗, 段前 18pt, 段后 6pt
|
||||||
|
# Heading 3: 14pt (四号), 加粗, 段前 12pt, 段后 6pt
|
||||||
|
# Heading 4-9: 12pt (小四), 加粗, 逐级减小间距
|
||||||
|
heading_formats = {
|
||||||
|
1: {"size": 16, "space_before": 24, "space_after": 12},
|
||||||
|
2: {"size": 15, "space_before": 18, "space_after": 6},
|
||||||
|
3: {"size": 14, "space_before": 12, "space_after": 6},
|
||||||
|
4: {"size": 12, "space_before": 12, "space_after": 6},
|
||||||
|
5: {"size": 12, "space_before": 6, "space_after": 6},
|
||||||
|
6: {"size": 11, "space_before": 6, "space_after": 3},
|
||||||
|
7: {"size": 11, "space_before": 6, "space_after": 3},
|
||||||
|
8: {"size": 10.5, "space_before": 6, "space_after": 3},
|
||||||
|
9: {"size": 10.5, "space_before": 6, "space_after": 3},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply font settings to Heading 1-9
|
||||||
|
for i in range(1, 10):
|
||||||
|
style_id = f"Heading {i}"
|
||||||
|
if style_id in doc.styles:
|
||||||
|
heading_style = doc.styles[style_id]
|
||||||
|
heading_font = heading_style.font
|
||||||
|
heading_font.name = self.valves.英文字体
|
||||||
|
heading_font.color.rgb = RGBColor(0, 0, 0)
|
||||||
|
|
||||||
|
# 应用标准格式
|
||||||
|
fmt = heading_formats.get(
|
||||||
|
i, {"size": 11, "space_before": 6, "space_after": 3}
|
||||||
|
)
|
||||||
|
heading_font.size = Pt(fmt["size"])
|
||||||
|
heading_font.bold = True
|
||||||
|
|
||||||
|
heading_pf = heading_style.paragraph_format
|
||||||
|
heading_pf.space_before = Pt(fmt["space_before"])
|
||||||
|
heading_pf.space_after = Pt(fmt["space_after"])
|
||||||
|
|
||||||
|
# Ensure rPr exists
|
||||||
|
h_rPr = heading_style._element.get_or_add_rPr()
|
||||||
|
h_rFonts = h_rPr.get_or_add_rFonts()
|
||||||
|
|
||||||
|
# Set fonts
|
||||||
|
h_rFonts.set(qn("w:ascii"), self.valves.英文字体)
|
||||||
|
h_rFonts.set(qn("w:hAnsi"), self.valves.英文字体)
|
||||||
|
h_rFonts.set(qn("w:eastAsia"), self.valves.中文字体)
|
||||||
|
|
||||||
|
# Set language for Heading
|
||||||
|
h_lang = h_rPr.find(qn("w:lang"))
|
||||||
|
if h_lang is None:
|
||||||
|
h_lang = OxmlElement("w:lang")
|
||||||
|
h_rPr.append(h_lang)
|
||||||
|
h_lang.set(qn("w:val"), "en-US")
|
||||||
|
h_lang.set(qn("w:eastAsia"), "zh-CN")
|
||||||
|
|
||||||
|
def _remove_emojis(self, text: str) -> str:
|
||||||
|
"""从文本中移除 Emoji (基于 Unicode 范围)"""
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _is_emoji_codepoint(codepoint: int) -> bool:
|
||||||
|
# Common emoji ranges + flag regional indicators.
|
||||||
|
return (
|
||||||
|
0x1F000 <= codepoint <= 0x1FAFF
|
||||||
|
or 0x1F1E6 <= codepoint <= 0x1F1FF
|
||||||
|
or 0x2600 <= codepoint <= 0x26FF
|
||||||
|
or 0x2700 <= codepoint <= 0x27BF
|
||||||
|
or 0x2300 <= codepoint <= 0x23FF
|
||||||
|
or 0x2B00 <= codepoint <= 0x2BFF
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_emoji_modifier(codepoint: int) -> bool:
|
||||||
|
# VS15/VS16, ZWJ, keycap, skin tones, and tag characters used in some emoji sequences.
|
||||||
|
return (
|
||||||
|
codepoint in (0x200D, 0xFE0E, 0xFE0F, 0x20E3)
|
||||||
|
or 0x1F3FB <= codepoint <= 0x1F3FF
|
||||||
|
or 0xE0020 <= codepoint <= 0xE007F
|
||||||
|
)
|
||||||
|
|
||||||
|
return "".join(
|
||||||
|
ch
|
||||||
|
for ch in text
|
||||||
|
if not (_is_emoji_codepoint(ord(ch)) or _is_emoji_modifier(ord(ch)))
|
||||||
|
)
|
||||||
|
|
||||||
def add_heading(self, doc: Document, text: str, level: int):
|
def add_heading(self, doc: Document, text: str, level: int):
|
||||||
"""Add heading"""
|
"""Add heading"""
|
||||||
# Word heading levels start from 0, Markdown from 1
|
# Word heading levels start from 0, Markdown from 1
|
||||||
@@ -2283,6 +2449,12 @@ class Action:
|
|||||||
if strike:
|
if strike:
|
||||||
run.font.strike = True
|
run.font.strike = True
|
||||||
|
|
||||||
|
# Explicitly set East Asian font to prevent MS Gothic fallback
|
||||||
|
# Word may not inherit w:eastAsia from style, causing Japanese font fallback for CJK
|
||||||
|
rPr = run._element.get_or_add_rPr()
|
||||||
|
rFonts = rPr.get_or_add_rFonts()
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.中文字体)
|
||||||
|
|
||||||
def _add_inline_code(self, paragraph, s: str):
|
def _add_inline_code(self, paragraph, s: str):
|
||||||
if s == "":
|
if s == "":
|
||||||
return
|
return
|
||||||
@@ -2678,7 +2850,11 @@ class Action:
|
|||||||
):
|
):
|
||||||
u = self._normalize_url(url)
|
u = self._normalize_url(url)
|
||||||
if not u:
|
if not u:
|
||||||
paragraph.add_run(display_text or text)
|
run = paragraph.add_run(display_text or text)
|
||||||
|
# Set East Asian font to prevent MS Gothic fallback
|
||||||
|
rPr = run._element.get_or_add_rPr()
|
||||||
|
rFonts = rPr.get_or_add_rFonts()
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.中文字体)
|
||||||
return
|
return
|
||||||
|
|
||||||
part = getattr(paragraph, "part", None)
|
part = getattr(paragraph, "part", None)
|
||||||
@@ -2687,6 +2863,10 @@ class Action:
|
|||||||
run = paragraph.add_run(display_text or text)
|
run = paragraph.add_run(display_text or text)
|
||||||
run.font.color.rgb = RGBColor(0, 0, 255)
|
run.font.color.rgb = RGBColor(0, 0, 255)
|
||||||
run.font.underline = True
|
run.font.underline = True
|
||||||
|
# Set East Asian font to prevent MS Gothic fallback
|
||||||
|
rPr = run._element.get_or_add_rPr()
|
||||||
|
rFonts = rPr.get_or_add_rFonts()
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.中文字体)
|
||||||
return
|
return
|
||||||
|
|
||||||
r_id = part.relate_to(u, RT.HYPERLINK, is_external=True)
|
r_id = part.relate_to(u, RT.HYPERLINK, is_external=True)
|
||||||
@@ -2700,6 +2880,11 @@ class Action:
|
|||||||
rStyle.set(qn("w:val"), "Hyperlink")
|
rStyle.set(qn("w:val"), "Hyperlink")
|
||||||
rPr.append(rStyle)
|
rPr.append(rStyle)
|
||||||
|
|
||||||
|
# Set East Asian font to prevent MS Gothic fallback
|
||||||
|
rFonts = OxmlElement("w:rFonts")
|
||||||
|
rFonts.set(qn("w:eastAsia"), self.valves.中文字体)
|
||||||
|
rPr.append(rFonts)
|
||||||
|
|
||||||
color = OxmlElement("w:color")
|
color = OxmlElement("w:color")
|
||||||
color.set(qn("w:val"), "0000FF")
|
color.set(qn("w:val"), "0000FF")
|
||||||
rPr.append(color)
|
rPr.append(color)
|
||||||
|
|||||||
@@ -1,4 +1,59 @@
|
|||||||
# Export to Excel
|
# 📊 Export to Excel
|
||||||
|
|
||||||
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.3.6 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
|
Export chat history to an Excel (.xlsx) file directly from the chat interface.
|
||||||
|
|
||||||
|
## 🔥 What's New in v0.3.6
|
||||||
|
|
||||||
|
- **OpenWebUI-Style Theme**: Modern dark header (#1f2937) with light gray zebra striping for better readability.
|
||||||
|
- **Zebra Striping**: Alternating row colors (#ffffff / #f3f4f6) for improved visual scanning.
|
||||||
|
- **Smart Data Type Conversion**: Automatically converts columns to numeric or datetime types with fallback to string.
|
||||||
|
- **Full Cell Bold/Italic**: Supports full cell Markdown bold (`**text**`) and italic (`*text*`) formatting in Excel.
|
||||||
|
- **Partial Markdown Cleanup**: Removes partial Markdown formatting symbols for cleaner Excel output.
|
||||||
|
- **Export Scope**: Added `EXPORT_SCOPE` to choose between the last message or all messages.
|
||||||
|
- **Smart Sheet Naming**: Names sheets based on Markdown headers, AI titles, or message index.
|
||||||
|
- **Multiple Tables Support**: Improved handling of multiple tables across messages.
|
||||||
|
- **Smart Filename Generation**: Supports filenames based on chat title, AI summary, or Markdown headers.
|
||||||
|
- **Configuration Options**: Added `TITLE_SOURCE` to control filename strategy.
|
||||||
|
- **AI Title Generation**: Added `MODEL_ID` to use AI for filename generation with progress notifications.
|
||||||
|
|
||||||
|
## ✨ Core Features
|
||||||
|
|
||||||
|
- 🚀 **One-Click Export**: Adds an “Export to Excel” action button to the chat.
|
||||||
|
- 🧠 **Automatic Header Extraction**: Intelligently identifies table headers from chat content.
|
||||||
|
- 📊 **Multi-Table Support**: Handles multiple tables within a single chat session.
|
||||||
|
|
||||||
|
## 🚀 How to Use
|
||||||
|
|
||||||
|
1. **Install**: Search for “Export to Excel” in the Open WebUI Community and install.
|
||||||
|
2. **Trigger**: In any chat, click the “Export to Excel” action button.
|
||||||
|
3. **Download**: The .xlsx file will be automatically downloaded.
|
||||||
|
|
||||||
|
## ⚙️ Configuration (Valves)
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `TITLE_SOURCE` | `chat_title` | Filename source: `chat_title`, `ai_generated`, or `markdown_title`. |
|
||||||
|
| `EXPORT_SCOPE` | `last_message` | Export scope: `last_message` or `all_messages`. |
|
||||||
|
| `MODEL_ID` | `""` | Model ID for AI title generation. Empty uses current chat model. |
|
||||||
|
| `SHOW_STATUS` | `True` | Show operation status updates. |
|
||||||
|
| `SHOW_DEBUG_LOG` | `False` | Print debug logs in the browser console (F12). |
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
|
## Troubleshooting ❓
|
||||||
|
|
||||||
|
- **Plugin not working?**: Check if the filter/action is enabled in the model settings.
|
||||||
|
- **Debug Logs**: Enable `SHOW_STATUS` and check the browser console (F12) if needed.
|
||||||
|
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
||||||
|
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)# Export to Excel
|
||||||
|
|
||||||
This plugin allows you to export your chat history to an Excel (.xlsx) file directly from the chat interface.
|
This plugin allows you to export your chat history to an Excel (.xlsx) file directly from the chat interface.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,59 @@
|
|||||||
# 导出为 Excel
|
# 📊 导出为 Excel
|
||||||
|
|
||||||
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.3.6 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
|
将对话历史直接导出为 Excel (.xlsx) 文件。
|
||||||
|
|
||||||
|
## 🔥 最新更新 v0.3.6
|
||||||
|
|
||||||
|
- **OpenWebUI 风格主题**:现代深灰表头(#1f2937)与浅灰斑马纹,提升可读性。
|
||||||
|
- **斑马纹效果**:隔行变色(#ffffff / #f3f4f6),方便视觉扫描。
|
||||||
|
- **智能数据类型转换**:自动将列转换为数字或日期类型,无法转换时保持字符串。
|
||||||
|
- **全单元格粗体/斜体**:支持全单元格 Markdown 粗体与斜体格式。
|
||||||
|
- **部分 Markdown 清理**:移除部分 Markdown 格式符号,输出更整洁。
|
||||||
|
- **导出范围**:新增 `EXPORT_SCOPE`,可选择导出最后一条或所有消息。
|
||||||
|
- **智能 Sheet 命名**:按 Markdown 标题、AI 标题或消息索引命名。
|
||||||
|
- **多表格支持**:优化了多表格处理能力。
|
||||||
|
- **智能文件名生成**:支持对话标题 / AI 总结 / Markdown 标题命名。
|
||||||
|
- **配置选项**:新增 `TITLE_SOURCE` 控制文件名策略。
|
||||||
|
- **AI 标题生成**:新增 `MODEL_ID`,支持 AI 标题生成与进度提示。
|
||||||
|
|
||||||
|
## ✨ 核心特性
|
||||||
|
|
||||||
|
- 🚀 **一键导出**:在聊天界面添加“导出为 Excel”按钮。
|
||||||
|
- 🧠 **自动表头提取**:智能识别聊天内容中的表格标题。
|
||||||
|
- 📊 **多表支持**:支持单次对话中的多个表格。
|
||||||
|
|
||||||
|
## 🚀 使用方法
|
||||||
|
|
||||||
|
1. **安装**:在 Open WebUI 社区搜索“导出为 Excel”并安装。
|
||||||
|
2. **触发**:在任意对话中,点击“导出为 Excel”动作按钮。
|
||||||
|
3. **下载**:.xlsx 文件将自动下载到你的设备。
|
||||||
|
|
||||||
|
## ⚙️ 配置参数 (Valves)
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 描述 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `TITLE_SOURCE` | `chat_title` | 文件名来源:`chat_title`、`ai_generated`、`markdown_title`。 |
|
||||||
|
| `EXPORT_SCOPE` | `last_message` | 导出范围:`last_message` 或 `all_messages`。 |
|
||||||
|
| `MODEL_ID` | `""` | AI 标题生成的模型 ID。为空则使用当前对话模型。 |
|
||||||
|
| `SHOW_STATUS` | `True` | 是否显示操作状态更新。 |
|
||||||
|
| `SHOW_DEBUG_LOG` | `False` | 是否在浏览器控制台输出调试日志 (F12)。 |
|
||||||
|
|
||||||
|
## ⭐ 支持
|
||||||
|
|
||||||
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
|
## 故障排除 (Troubleshooting) ❓
|
||||||
|
|
||||||
|
- **插件不工作?**: 请检查是否在模型设置中启用了该过滤器/动作。
|
||||||
|
- **调试日志**: 如需排查,启用 `SHOW_STATUS` 并查看浏览器控制台 (F12)。
|
||||||
|
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
||||||
|
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)# 导出为 Excel
|
||||||
|
|
||||||
此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。
|
此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,10 @@ Generate polished learning flashcards from any text—title, summary, key points
|
|||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.2.4 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.2.4 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
## Preview 📸
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## What's New
|
## What's New
|
||||||
|
|
||||||
### v0.2.4
|
### v0.2.4
|
||||||
|
|
||||||
- **Clean Output**: Removed debug messages from output.
|
- **Clean Output**: Removed debug messages from output.
|
||||||
|
|
||||||
## Key Features 🔑
|
## Key Features 🔑
|
||||||
@@ -39,9 +36,21 @@ Generate polished learning flashcards from any text—title, summary, key points
|
|||||||
| CLEAR_PREVIOUS_HTML | Whether to clear previous card HTML (otherwise append/merge) | false |
|
| CLEAR_PREVIOUS_HTML | Whether to clear previous card HTML (otherwise append/merge) | false |
|
||||||
| MESSAGE_COUNT | Use the latest N messages to build the card | 1 |
|
| MESSAGE_COUNT | Use the latest N messages to build the card | 1 |
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
## Troubleshooting ❓
|
## Troubleshooting ❓
|
||||||
|
|
||||||
- **Plugin not working?**: Check if the filter/action is enabled in the model settings.
|
- **Plugin not working?**: Check if the filter/action is enabled in the model settings.
|
||||||
- **Debug Logs**: Enable `SHOW_STATUS` in Valves to see progress updates.
|
- **Debug Logs**: Enable `SHOW_STATUS` in Valves to see progress updates.
|
||||||
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
||||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## Preview 📸
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -4,22 +4,17 @@
|
|||||||
|
|
||||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.2.4 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.2.4 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
## 预览 📸
|
## 🔥 最新更新 v0.2.4
|
||||||
|
|
||||||

|
* **输出优化**: 移除输出中的调试信息。
|
||||||
|
|
||||||
## 更新日志
|
|
||||||
|
|
||||||
### v0.2.4
|
|
||||||
- **输出优化**: 移除输出中的调试信息。
|
|
||||||
|
|
||||||
## 核心特性 🔑
|
## 核心特性 🔑
|
||||||
|
|
||||||
- **一键生成**:输入任意文本,直接产出结构化卡片。
|
* **一键生成**:输入任意文本,直接产出结构化卡片。
|
||||||
- **要点聚合**:自动提取 3-5 个记忆要点与 2-4 个标签。
|
* **要点聚合**:自动提取 3-5 个记忆要点与 2-4 个标签。
|
||||||
- **多语言支持**:可设定目标语言(默认中文)。
|
* **多语言支持**:可设定目标语言(默认中文)。
|
||||||
- **渐进合并**:多次调用会将新卡片合并到同一 HTML 容器中;如需重置可启用清空选项。
|
* **渐进合并**:多次调用会将新卡片合并到同一 HTML 容器中;如需重置可启用清空选项。
|
||||||
- **状态提示**:实时推送“生成中/完成/错误”等状态与通知。
|
* **状态提示**:实时推送“生成中/完成/错误”等状态与通知。
|
||||||
|
|
||||||
## 使用方法 🛠️
|
## 使用方法 🛠️
|
||||||
|
|
||||||
@@ -39,9 +34,21 @@
|
|||||||
| CLEAR_PREVIOUS_HTML | 是否清空旧的卡片 HTML(否则合并追加) | false |
|
| CLEAR_PREVIOUS_HTML | 是否清空旧的卡片 HTML(否则合并追加) | false |
|
||||||
| MESSAGE_COUNT | 取最近 N 条消息生成卡片 | 1 |
|
| MESSAGE_COUNT | 取最近 N 条消息生成卡片 | 1 |
|
||||||
|
|
||||||
|
## ⭐ 支持
|
||||||
|
|
||||||
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
## 故障排除 (Troubleshooting) ❓
|
## 故障排除 (Troubleshooting) ❓
|
||||||
|
|
||||||
- **插件不工作?**: 请检查是否在模型设置中启用了该过滤器/动作。
|
* **插件不工作?**: 请检查是否在模型设置中启用了该过滤器/动作。
|
||||||
- **调试日志**: 在 Valves 中启用 `SHOW_STATUS` 以查看进度更新。
|
* **调试日志**: 在 Valves 中启用 `SHOW_STATUS` 以查看进度更新。
|
||||||
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
* **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
||||||
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
* **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## 预览 📸
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
# 📊 Smart Infographic (AntV)
|
# Smart Infographic
|
||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.4.9 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.5.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
An Open WebUI plugin powered by the AntV Infographic engine. It transforms long text into professional, beautiful infographics with a single click.
|
An Open WebUI plugin powered by the AntV Infographic engine. It transforms long text into professional, beautiful infographics with a single click.
|
||||||
|
|
||||||
## 🔥 What's New in v1.4.9
|
## 🔥 What's New in v1.5.0
|
||||||
|
|
||||||
- 🎨 **70+ Official Templates**: Integrated comprehensive AntV infographic template library.
|
- 🌐 **Smart Language Detection**: Automatically detects the accurate UI language from your browser.
|
||||||
- 🖼️ **Iconify & unDraw Support**: Richer visuals with official icons and illustrations.
|
- 🗣️ **Context-Aware Generation**: Generated infographics now strictly follow the language of your input content (e.g., input Japanese -> output Japanese infographic).
|
||||||
- 📏 **Visual Optimization**: Improved text wrapping, adaptive sizing, and layout refinement.
|
- 🐛 **Bug Fixes**: Fixed issues with language synchronization between the UI and generated content.
|
||||||
- ✨ **PNG Upload**: Infographics now upload as PNG format for better Word export compatibility.
|
|
||||||
- 🔧 **Canvas Conversion**: Uses browser canvas for high-quality SVG to PNG conversion (2x scale).
|
|
||||||
|
|
||||||
### Previous: v1.4.0
|
|
||||||
|
|
||||||
- ✨ **Default Mode Change**: Default output mode is now `image` (static image) for better compatibility.
|
|
||||||
- 📱 **Responsive Sizing**: Images now auto-adapt to the chat container width.
|
|
||||||
|
|
||||||
## ✨ Key Features
|
## ✨ Key Features
|
||||||
|
|
||||||
@@ -46,6 +39,10 @@ You can adjust the following parameters in the plugin settings to optimize the g
|
|||||||
| **Message Count (MESSAGE_COUNT)** | `1` | Number of recent messages to use for analysis. Increase this for more context. |
|
| **Message Count (MESSAGE_COUNT)** | `1` | Number of recent messages to use for analysis. Increase this for more context. |
|
||||||
| **Output Mode (OUTPUT_MODE)** | `image` | `image` for static image embedding (default, better compatibility), `html` for interactive chart. |
|
| **Output Mode (OUTPUT_MODE)** | `image` | `image` for static image embedding (default, better compatibility), `html` for interactive chart. |
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
## 🛠️ Supported Template Types
|
## 🛠️ Supported Template Types
|
||||||
|
|
||||||
| Category | Template Name | Use Case |
|
| Category | Template Name | Use Case |
|
||||||
@@ -63,6 +60,9 @@ You can adjust the following parameters in the plugin settings to optimize the g
|
|||||||
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
- **Error Messages**: If you see an error, please copy the full error message and report it.
|
||||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|
||||||
## 📝 Syntax Example (For Advanced Users)
|
## 📝 Syntax Example (For Advanced Users)
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
# 📊 智能信息图 (AntV Infographic)
|
# 智能信息图
|
||||||
|
|
||||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.4.9 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.5.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
基于 AntV Infographic 引擎的 Open WebUI 插件,能够将长文本内容一键转换为专业、美观的信息图表。
|
基于 AntV Infographic 引擎的 Open WebUI 插件,能够将长文本内容一键转换为专业、美观的信息图表。
|
||||||
|
|
||||||
## 🔥 v1.4.9 更新日志
|
## 🔥 最新更新 v1.5.0
|
||||||
|
|
||||||
- 🎨 **70+ 官方模板**:全面集成 AntV 官方信息图模板库。
|
- 🌐 **智能语言检测**:自动从浏览器准确识别当前界面语言设置。
|
||||||
- 🖼️ **图标与插图支持**:支持 Iconify 图标库与 unDraw 插图库,视觉效果更丰富。
|
- 🗣️ **上下文感知生成**:生成的信息图内容现在严格跟随用户输入内容的语言(例如:输入日语 -> 生成日语信息图)。
|
||||||
- 📏 **视觉优化**:改进文本换行逻辑,优化自适应尺寸,提升卡片布局精细度。
|
- 🐛 **问题修复**:修复了界面语言与生成内容语言不同步的问题。
|
||||||
- ✨ **PNG 上传**:信息图现在以 PNG 格式上传,与 Word 导出完美兼容。
|
|
||||||
- 🔧 **Canvas 转换**:使用浏览器 Canvas 高质量转换 SVG 为 PNG(2倍缩放)。
|
|
||||||
|
|
||||||
### 此前: v1.4.0
|
|
||||||
|
|
||||||
- ✨ **默认模式变更**:默认输出模式调整为 `image`(静态图片)。
|
|
||||||
- 📱 **响应式尺寸**:图片模式下自动适应聊天容器宽度。
|
|
||||||
|
|
||||||
## ✨ 核心特性
|
## ✨ 核心特性
|
||||||
|
|
||||||
@@ -46,6 +39,10 @@
|
|||||||
| **上下文消息数 (MESSAGE_COUNT)** | `1` | 用于分析的最近消息条数。增加此值可让 AI 参考更多对话背景。 |
|
| **上下文消息数 (MESSAGE_COUNT)** | `1` | 用于分析的最近消息条数。增加此值可让 AI 参考更多对话背景。 |
|
||||||
| **输出模式 (OUTPUT_MODE)** | `image` | `image` 为静态图片嵌入(默认,兼容性好),`html` 为交互式图表。 |
|
| **输出模式 (OUTPUT_MODE)** | `image` | `image` 为静态图片嵌入(默认,兼容性好),`html` 为交互式图表。 |
|
||||||
|
|
||||||
|
## ⭐ 支持
|
||||||
|
|
||||||
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
## 🛠️ 支持的模板类型
|
## 🛠️ 支持的模板类型
|
||||||
|
|
||||||
| 分类 | 模板名称 | 适用场景 |
|
| 分类 | 模板名称 | 适用场景 |
|
||||||
@@ -63,6 +60,9 @@
|
|||||||
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
- **错误信息**: 如果看到错误,请复制完整的错误信息并报告。
|
||||||
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|
||||||
## 📝 语法示例 (高级用户)
|
## 📝 语法示例 (高级用户)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 234 KiB |
@@ -1,10 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
title: 📊 Smart Infographic (AntV)
|
title: Smart Infographic
|
||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/open-webui
|
funding_url: https://github.com/open-webui
|
||||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
||||||
version: 1.4.9
|
version: 1.5.0
|
||||||
openwebui_id: ad6f0c7f-c571-4dea-821d-8e71697274cf
|
openwebui_id: ad6f0c7f-c571-4dea-821d-8e71697274cf
|
||||||
description: AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.
|
description: AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.
|
||||||
"""
|
"""
|
||||||
@@ -32,6 +32,10 @@ logger = logging.getLogger(__name__)
|
|||||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||||
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
||||||
|
|
||||||
|
## Important Language Rule
|
||||||
|
- **GENERATE CONTENT IN INPUT LANGUAGE**: You must generate the text content of the infographic in the **exact same language** as the user's input content (the text you are analyzing).
|
||||||
|
- **Format Consistency**: Even if this system prompt is in English, if the user input is in Chinese, the infographic content must be in Chinese. If input is Japanese, output Japanese.
|
||||||
|
|
||||||
## Infographic Syntax Specification
|
## Infographic Syntax Specification
|
||||||
|
|
||||||
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
||||||
@@ -958,7 +962,11 @@ class Action:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.valves = self.Valves()
|
self.valves = self.Valves()
|
||||||
|
|
||||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
async def _get_user_context(
|
||||||
|
self,
|
||||||
|
__user__: Optional[Dict[str, Any]],
|
||||||
|
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
) -> Dict[str, str]:
|
||||||
"""Safely extracts user context information."""
|
"""Safely extracts user context information."""
|
||||||
if isinstance(__user__, (list, tuple)):
|
if isinstance(__user__, (list, tuple)):
|
||||||
user_data = __user__[0] if __user__ else {}
|
user_data = __user__[0] if __user__ else {}
|
||||||
@@ -967,10 +975,32 @@ class Action:
|
|||||||
else:
|
else:
|
||||||
user_data = {}
|
user_data = {}
|
||||||
|
|
||||||
|
user_id = user_data.get("id", "unknown_user")
|
||||||
|
user_name = user_data.get("name", "User")
|
||||||
|
user_language = user_data.get("language", "en-US")
|
||||||
|
|
||||||
|
if __event_call__:
|
||||||
|
try:
|
||||||
|
js_code = """
|
||||||
|
return (
|
||||||
|
localStorage.getItem('locale') ||
|
||||||
|
localStorage.getItem('language') ||
|
||||||
|
navigator.language ||
|
||||||
|
'en-US'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
frontend_lang = await __event_call__(
|
||||||
|
{"type": "execute", "data": {"code": js_code}}
|
||||||
|
)
|
||||||
|
if frontend_lang and isinstance(frontend_lang, str):
|
||||||
|
user_language = frontend_lang
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to retrieve frontend language: {e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user_id": user_data.get("id", "unknown_user"),
|
"user_id": user_id,
|
||||||
"user_name": user_data.get("name", "User"),
|
"user_name": user_name,
|
||||||
"user_language": user_data.get("language", "en-US"),
|
"user_language": user_language,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_chat_context(
|
def _get_chat_context(
|
||||||
@@ -1469,18 +1499,10 @@ class Action:
|
|||||||
logger.info("Action: Infographic started (v1.4.0)")
|
logger.info("Action: Infographic started (v1.4.0)")
|
||||||
|
|
||||||
# Get user information
|
# Get user information
|
||||||
if isinstance(__user__, (list, tuple)):
|
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||||
user_language = __user__[0].get("language", "en") if __user__ else "en"
|
user_name = user_ctx["user_name"]
|
||||||
user_name = __user__[0].get("name", "User") if __user__[0] else "User"
|
user_id = user_ctx["user_id"]
|
||||||
user_id = (
|
user_language = user_ctx["user_language"]
|
||||||
__user__[0]["id"]
|
|
||||||
if __user__ and "id" in __user__[0]
|
|
||||||
else "unknown_user"
|
|
||||||
)
|
|
||||||
elif isinstance(__user__, dict):
|
|
||||||
user_language = __user__.get("language", "en")
|
|
||||||
user_name = __user__.get("name", "User")
|
|
||||||
user_id = __user__.get("id", "unknown_user")
|
|
||||||
|
|
||||||
# Get current time
|
# Get current time
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
title: 📊 智能信息图 (AntV Infographic)
|
title: 智能信息图
|
||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/open-webui
|
funding_url: https://github.com/open-webui
|
||||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxsaW5lIHgxPSIxMiIgeTE9IjIwIiB4Mj0iMTIiIHkyPSIxMCIgLz4KICA8bGluZSB4MT0iMTgiIHkxPSIyMCIgeDI9IjE4IiB5Mj0iNCIgLz4KICA8bGluZSB4MT0iNiIgeTE9IjIwIiB4Mj0iNiIgeTI9IjE2IiAvPgo8L3N2Zz4=
|
||||||
version: 1.4.9
|
version: 1.5.0
|
||||||
openwebui_id: e04a48ff-23ee-4a41-8ea7-66c19524e7c8
|
openwebui_id: e04a48ff-23ee-4a41-8ea7-66c19524e7c8
|
||||||
description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。
|
description: 基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。
|
||||||
"""
|
"""
|
||||||
@@ -32,6 +32,10 @@ logger = logging.getLogger(__name__)
|
|||||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||||
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
||||||
|
|
||||||
|
## Important Language Rule (语言规则)
|
||||||
|
- **Priority Input Language (优先使用输入语言)**: You must generate the text content of the infographic in the **exact same language** as the user's input content.
|
||||||
|
- **Example**: If the user provides a summary in Chinese, the labels and descriptions in the infographic must be in Chinese.
|
||||||
|
|
||||||
## Infographic Syntax Specification
|
## Infographic Syntax Specification
|
||||||
|
|
||||||
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
||||||
@@ -974,7 +978,11 @@ class Action:
|
|||||||
"Sunday": "星期日",
|
"Sunday": "星期日",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
async def _get_user_context(
|
||||||
|
self,
|
||||||
|
__user__: Optional[Dict[str, Any]],
|
||||||
|
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
) -> Dict[str, str]:
|
||||||
"""安全提取用户上下文信息。"""
|
"""安全提取用户上下文信息。"""
|
||||||
if isinstance(__user__, (list, tuple)):
|
if isinstance(__user__, (list, tuple)):
|
||||||
user_data = __user__[0] if __user__ else {}
|
user_data = __user__[0] if __user__ else {}
|
||||||
@@ -983,10 +991,32 @@ class Action:
|
|||||||
else:
|
else:
|
||||||
user_data = {}
|
user_data = {}
|
||||||
|
|
||||||
|
user_id = user_data.get("id", "unknown_user")
|
||||||
|
user_name = user_data.get("name", "用户")
|
||||||
|
user_language = user_data.get("language", "zh-CN")
|
||||||
|
|
||||||
|
if __event_call__:
|
||||||
|
try:
|
||||||
|
js_code = """
|
||||||
|
return (
|
||||||
|
localStorage.getItem('locale') ||
|
||||||
|
localStorage.getItem('language') ||
|
||||||
|
navigator.language ||
|
||||||
|
'zh-CN'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
frontend_lang = await __event_call__(
|
||||||
|
{"type": "execute", "data": {"code": js_code}}
|
||||||
|
)
|
||||||
|
if frontend_lang and isinstance(frontend_lang, str):
|
||||||
|
user_language = frontend_lang
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user_id": user_data.get("id", "unknown_user"),
|
"user_id": user_id,
|
||||||
"user_name": user_data.get("name", "用户"),
|
"user_name": user_name,
|
||||||
"user_language": user_data.get("language", "zh-CN"),
|
"user_language": user_language,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_chat_context(
|
def _get_chat_context(
|
||||||
@@ -1509,20 +1539,10 @@ class Action:
|
|||||||
logger.info("Action: 信息图启动 (v1.4.0)")
|
logger.info("Action: 信息图启动 (v1.4.0)")
|
||||||
|
|
||||||
# 获取用户信息
|
# 获取用户信息
|
||||||
if isinstance(__user__, (list, tuple)):
|
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||||
user_language = (
|
user_name = user_ctx["user_name"]
|
||||||
__user__[0].get("language", "zh-CN") if __user__ else "zh-CN"
|
user_id = user_ctx["user_id"]
|
||||||
)
|
user_language = user_ctx["user_language"]
|
||||||
user_name = __user__[0].get("name", "用户") if __user__[0] else "用户"
|
|
||||||
user_id = (
|
|
||||||
__user__[0]["id"]
|
|
||||||
if __user__ and "id" in __user__[0]
|
|
||||||
else "unknown_user"
|
|
||||||
)
|
|
||||||
elif isinstance(__user__, dict):
|
|
||||||
user_language = __user__.get("language", "zh-CN")
|
|
||||||
user_name = __user__.get("name", "用户")
|
|
||||||
user_id = __user__.get("id", "unknown_user")
|
|
||||||
|
|
||||||
# 获取当前时间
|
# 获取当前时间
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|||||||
@@ -2,17 +2,14 @@
|
|||||||
|
|
||||||
Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes long-form text content and automatically generates interactive mind maps, helping users structure and visualize knowledge.
|
Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes long-form text content and automatically generates interactive mind maps, helping users structure and visualize knowledge.
|
||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.9.1 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.9.2 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
## What's New in v0.9.1
|
## What's New in v0.9.2
|
||||||
|
|
||||||
**New Feature: Image Output Mode**
|
**Language Rule Alignment**
|
||||||
|
|
||||||
- **Static Image Support**: Added `OUTPUT_MODE` configuration parameter.
|
- **Input Language First**: Mind map output now strictly matches the input text language.
|
||||||
- `html` (default): Interactive HTML mind map.
|
- **Consistent Behavior**: Matches the infographic language rule for predictable multilingual output.
|
||||||
- `image`: Static SVG image embedded directly in Markdown (**No HTML code output**, cleaner chat history).
|
|
||||||
- **Efficient Storage**: Image mode uploads SVG to `/api/v1/files`, avoiding huge base64 strings in chat history.
|
|
||||||
- **Smart Features**: Auto-responsive width and automatic theme detection (light/dark) for generated images.
|
|
||||||
|
|
||||||
## Key Features 🔑
|
## Key Features 🔑
|
||||||
|
|
||||||
@@ -41,6 +38,10 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
|
|||||||
| `MESSAGE_COUNT` | `1` | Number of recent messages to use for generation (1-5). |
|
| `MESSAGE_COUNT` | `1` | Number of recent messages to use for generation (1-5). |
|
||||||
| `OUTPUT_MODE` | `html` | Output mode: `html` (interactive) or `image` (static). |
|
| `OUTPUT_MODE` | `html` | Output mode: `html` (interactive) or `image` (static). |
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
## Troubleshooting ❓
|
## Troubleshooting ❓
|
||||||
|
|
||||||
- **Plugin not working?**: Check if the action is enabled in the chat settings.
|
- **Plugin not working?**: Check if the action is enabled in the chat settings.
|
||||||
@@ -62,3 +63,7 @@ Smart Mind Map is a powerful OpenWebUI action plugin that intelligently analyzes
|
|||||||
1. **Text Preparation**: Provide text with clear structure and distinct hierarchies.
|
1. **Text Preparation**: Provide text with clear structure and distinct hierarchies.
|
||||||
2. **Model Selection**: Use fast models like `gemini-2.5-flash` for daily use.
|
2. **Model Selection**: Use fast models like `gemini-2.5-flash` for daily use.
|
||||||
3. **Export Quality**: Use PNG for presentations and SVG for further editing.
|
3. **Export Quality**: Use PNG for presentations and SVG for further editing.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -2,17 +2,14 @@
|
|||||||
|
|
||||||
思维导图是一个强大的 OpenWebUI 动作插件,能够智能分析长篇文本内容,自动生成交互式思维导图,帮助用户结构化和可视化知识。
|
思维导图是一个强大的 OpenWebUI 动作插件,能够智能分析长篇文本内容,自动生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||||
|
|
||||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.9.1 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.9.2 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
## v0.9.1 更新亮点
|
## v0.9.2 更新亮点
|
||||||
|
|
||||||
**新功能:图片输出模式**
|
**语言规则对齐**
|
||||||
|
|
||||||
- **静态图片支持**:新增 `OUTPUT_MODE` 配置参数。
|
- **输入语言优先**:导图输出严格与输入文本语言一致。
|
||||||
- `html`(默认):交互式 HTML 思维导图。
|
- **一致性提升**:与信息图语言规则保持一致,多语言输出更可预期。
|
||||||
- `image`:静态 SVG 图片直接嵌入 Markdown(**不输出 HTML 代码**,聊天记录更简洁)。
|
|
||||||
- **高效存储**:图片模式将 SVG 上传至 `/api/v1/files`,避免聊天记录中出现超长 Base64 字符串。
|
|
||||||
- **智能特性**:生成的图片支持自动响应式宽度和自动主题检测(亮色/暗色)。
|
|
||||||
|
|
||||||
## 核心特性 🔑
|
## 核心特性 🔑
|
||||||
|
|
||||||
@@ -41,6 +38,10 @@
|
|||||||
| `MESSAGE_COUNT` | `1` | 用于生成思维导图的最近消息数量(1-5)。 |
|
| `MESSAGE_COUNT` | `1` | 用于生成思维导图的最近消息数量(1-5)。 |
|
||||||
| `OUTPUT_MODE` | `html` | 输出模式:`html`(交互式)或 `image`(静态图片)。 |
|
| `OUTPUT_MODE` | `html` | 输出模式:`html`(交互式)或 `image`(静态图片)。 |
|
||||||
|
|
||||||
|
## ⭐ 支持
|
||||||
|
|
||||||
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
## 故障排除 (Troubleshooting) ❓
|
## 故障排除 (Troubleshooting) ❓
|
||||||
|
|
||||||
- **插件无法启动**:检查 OpenWebUI 日志,确认插件已正确上传并启用。
|
- **插件无法启动**:检查 OpenWebUI 日志,确认插件已正确上传并启用。
|
||||||
@@ -62,3 +63,7 @@
|
|||||||
1. **文本准备**:提供结构清晰、层次分明的文本内容。
|
1. **文本准备**:提供结构清晰、层次分明的文本内容。
|
||||||
2. **模型选择**:日常使用推荐 `gemini-2.5-flash` 等快速模型。
|
2. **模型选择**:日常使用推荐 `gemini-2.5-flash` 等快速模型。
|
||||||
3. **导出质量**:PNG 适合演示分享,SVG 适合进一步矢量编辑。
|
3. **导出质量**:PNG 适合演示分享,SVG 适合进一步矢量编辑。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ author: Fu-Jie
|
|||||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/open-webui
|
funding_url: https://github.com/open-webui
|
||||||
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
version: 0.9.1
|
version: 0.9.2
|
||||||
openwebui_id: 3094c59a-b4dd-4e0c-9449-15e2dd547dc4
|
openwebui_id: 3094c59a-b4dd-4e0c-9449-15e2dd547dc4
|
||||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
||||||
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
|
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
|
||||||
@@ -33,7 +33,8 @@ SYSTEM_PROMPT_MINDMAP_ASSISTANT = """
|
|||||||
You are a professional mind map generation assistant, capable of efficiently analyzing long-form text provided by users and structuring its core themes, key concepts, branches, and sub-branches into standard Markdown list syntax for rendering by Markmap.js.
|
You are a professional mind map generation assistant, capable of efficiently analyzing long-form text provided by users and structuring its core themes, key concepts, branches, and sub-branches into standard Markdown list syntax for rendering by Markmap.js.
|
||||||
|
|
||||||
Please strictly follow these guidelines:
|
Please strictly follow these guidelines:
|
||||||
- **Language**: All output must be in the language specified by the user.
|
- **Language**: All output must be in the exact same language as the input text (the text you are analyzing).
|
||||||
|
- **Format Consistency**: Even if this system prompt is in English, if the user input is in Chinese, the mind map content must be in Chinese. If input is Japanese, output Japanese.
|
||||||
- **Format**: Your output must strictly be in Markdown list format, wrapped with ```markdown and ```.
|
- **Format**: Your output must strictly be in Markdown list format, wrapped with ```markdown and ```.
|
||||||
- Use `#` to define the central theme (root node).
|
- Use `#` to define the central theme (root node).
|
||||||
- Use `-` with two-space indentation to represent branches and sub-branches.
|
- Use `-` with two-space indentation to represent branches and sub-branches.
|
||||||
@@ -811,7 +812,11 @@ class Action:
|
|||||||
"Sunday": "Sunday",
|
"Sunday": "Sunday",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
async def _get_user_context(
|
||||||
|
self,
|
||||||
|
__user__: Optional[Dict[str, Any]],
|
||||||
|
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
) -> Dict[str, str]:
|
||||||
"""Extract basic user context with safe fallbacks."""
|
"""Extract basic user context with safe fallbacks."""
|
||||||
if isinstance(__user__, (list, tuple)):
|
if isinstance(__user__, (list, tuple)):
|
||||||
user_data = __user__[0] if __user__ else {}
|
user_data = __user__[0] if __user__ else {}
|
||||||
@@ -820,10 +825,32 @@ class Action:
|
|||||||
else:
|
else:
|
||||||
user_data = {}
|
user_data = {}
|
||||||
|
|
||||||
|
user_id = user_data.get("id", "unknown_user")
|
||||||
|
user_name = user_data.get("name", "User")
|
||||||
|
user_language = user_data.get("language", "en-US")
|
||||||
|
|
||||||
|
if __event_call__:
|
||||||
|
try:
|
||||||
|
js_code = """
|
||||||
|
return (
|
||||||
|
localStorage.getItem('locale') ||
|
||||||
|
localStorage.getItem('language') ||
|
||||||
|
navigator.language ||
|
||||||
|
'en-US'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
frontend_lang = await __event_call__(
|
||||||
|
{"type": "execute", "data": {"code": js_code}}
|
||||||
|
)
|
||||||
|
if frontend_lang and isinstance(frontend_lang, str):
|
||||||
|
user_language = frontend_lang
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to retrieve frontend language: {e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user_id": user_data.get("id", "unknown_user"),
|
"user_id": user_id,
|
||||||
"user_name": user_data.get("name", "User"),
|
"user_name": user_name,
|
||||||
"user_language": user_data.get("language", "en-US"),
|
"user_language": user_language,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_chat_context(
|
def _get_chat_context(
|
||||||
@@ -1369,8 +1396,8 @@ class Action:
|
|||||||
__metadata__: Optional[dict] = None,
|
__metadata__: Optional[dict] = None,
|
||||||
__request__: Optional[Request] = None,
|
__request__: Optional[Request] = None,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
logger.info("Action: Smart Mind Map (v0.9.1) started")
|
logger.info("Action: Smart Mind Map (v0.9.2) started")
|
||||||
user_ctx = self._get_user_context(__user__)
|
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||||
user_language = user_ctx["user_language"]
|
user_language = user_ctx["user_language"]
|
||||||
user_name = user_ctx["user_name"]
|
user_name = user_ctx["user_name"]
|
||||||
user_id = user_ctx["user_id"]
|
user_id = user_ctx["user_id"]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: 思维导图
|
|||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/open-webui
|
funding_url: https://github.com/open-webui
|
||||||
version: 0.9.1
|
version: 0.9.2
|
||||||
openwebui_id: 8d4b097b-219b-4dd2-b509-05fbe6388335
|
openwebui_id: 8d4b097b-219b-4dd2-b509-05fbe6388335
|
||||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSIyIiB5PSIxNiIgd2lkdGg9IjYiIGhlaWdodD0iNiIgcng9IjEiLz48cmVjdCB4PSI5IiB5PSIyIiB3aWR0aD0iNiIgaGVpZ2h0PSI2IiByeD0iMSIvPjxwYXRoIGQ9Ik01IDE2di0zYTEgMSAwIDAgMSAxLTFoMTJhMSAxIDAgMCAxIDEgMXYzIi8+PHBhdGggZD0iTTEyIDEyVjgiLz48L3N2Zz4=
|
||||||
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||||
@@ -32,7 +32,8 @@ SYSTEM_PROMPT_MINDMAP_ASSISTANT = """
|
|||||||
你是一个专业的思维导图生成助手,能够高效地分析用户提供的长篇文本,并将其核心主题、关键概念、分支和子分支结构化为标准的Markdown列表语法,以便Markmap.js进行渲染。
|
你是一个专业的思维导图生成助手,能够高效地分析用户提供的长篇文本,并将其核心主题、关键概念、分支和子分支结构化为标准的Markdown列表语法,以便Markmap.js进行渲染。
|
||||||
|
|
||||||
请严格遵循以下指导原则:
|
请严格遵循以下指导原则:
|
||||||
- **语言**: 所有输出必须使用用户指定的语言。
|
- **语言**: 所有输出必须与输入文本(正在分析的文本)保持完全一致的语言。
|
||||||
|
- **格式一致性**: 即使系统提示词是中文,只要用户输入是英文,导图内容必须是英文;若输入为日文,则输出日文。
|
||||||
- **格式**: 你的输出必须严格为Markdown列表格式,并用```markdown 和 ``` 包裹。
|
- **格式**: 你的输出必须严格为Markdown列表格式,并用```markdown 和 ``` 包裹。
|
||||||
- 使用 `#` 定义中心主题(根节点)。
|
- 使用 `#` 定义中心主题(根节点)。
|
||||||
- 使用 `-` 和两个空格的缩进表示分支和子分支。
|
- 使用 `-` 和两个空格的缩进表示分支和子分支。
|
||||||
@@ -809,7 +810,11 @@ class Action:
|
|||||||
"Sunday": "星期日",
|
"Sunday": "星期日",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
async def _get_user_context(
|
||||||
|
self,
|
||||||
|
__user__: Optional[Dict[str, Any]],
|
||||||
|
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
) -> Dict[str, str]:
|
||||||
"""Extract basic user context with safe fallbacks."""
|
"""Extract basic user context with safe fallbacks."""
|
||||||
if isinstance(__user__, (list, tuple)):
|
if isinstance(__user__, (list, tuple)):
|
||||||
user_data = __user__[0] if __user__ else {}
|
user_data = __user__[0] if __user__ else {}
|
||||||
@@ -818,10 +823,32 @@ class Action:
|
|||||||
else:
|
else:
|
||||||
user_data = {}
|
user_data = {}
|
||||||
|
|
||||||
|
user_id = user_data.get("id", "unknown_user")
|
||||||
|
user_name = user_data.get("name", "User")
|
||||||
|
user_language = user_data.get("language", "en-US")
|
||||||
|
|
||||||
|
if __event_call__:
|
||||||
|
try:
|
||||||
|
js_code = """
|
||||||
|
return (
|
||||||
|
localStorage.getItem('locale') ||
|
||||||
|
localStorage.getItem('language') ||
|
||||||
|
navigator.language ||
|
||||||
|
'en-US'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
frontend_lang = await __event_call__(
|
||||||
|
{"type": "execute", "data": {"code": js_code}}
|
||||||
|
)
|
||||||
|
if frontend_lang and isinstance(frontend_lang, str):
|
||||||
|
user_language = frontend_lang
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to retrieve frontend language: {e}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"user_id": user_data.get("id", "unknown_user"),
|
"user_id": user_id,
|
||||||
"user_name": user_data.get("name", "用户"),
|
"user_name": user_name,
|
||||||
"user_language": user_data.get("language", "zh-CN"),
|
"user_language": user_language,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_chat_context(
|
def _get_chat_context(
|
||||||
@@ -1348,8 +1375,8 @@ class Action:
|
|||||||
__metadata__: Optional[dict] = None,
|
__metadata__: Optional[dict] = None,
|
||||||
__request__: Optional[Request] = None,
|
__request__: Optional[Request] = None,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
logger.info("Action: 思维导图 (v0.9.1) started")
|
logger.info("Action: 思维导图 (v0.9.2) started")
|
||||||
user_ctx = self._get_user_context(__user__)
|
user_ctx = await self._get_user_context(__user__, __event_call__)
|
||||||
user_language = user_ctx["user_language"]
|
user_language = user_ctx["user_language"]
|
||||||
user_name = user_ctx["user_name"]
|
user_name = user_ctx["user_name"]
|
||||||
user_id = user_ctx["user_id"]
|
user_id = user_ctx["user_id"]
|
||||||
|
|||||||
359
plugins/debug/common_tools/update_readmes_to_market.py
Normal file
359
plugins/debug/common_tools/update_readmes_to_market.py
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
======================================================================
|
||||||
|
Staged README Synchronizer to OpenWebUI Community
|
||||||
|
暂存 README 文件同步到 OpenWebUI 社区工具
|
||||||
|
======================================================================
|
||||||
|
|
||||||
|
PURPOSE / 用途:
|
||||||
|
--------------
|
||||||
|
This script synchronizes staged README.md/README_CN.md files to their
|
||||||
|
corresponding OpenWebUI Community posts automatically. It's designed for
|
||||||
|
batch updating documentation content without modifying plugin versions
|
||||||
|
or media attachments.
|
||||||
|
|
||||||
|
本脚本自动将暂存的 README.md/README_CN.md 文件同步到对应的 OpenWebUI
|
||||||
|
社区帖子。专为批量更新文档内容设计,不修改插件版本或媒体附件。
|
||||||
|
|
||||||
|
USAGE / 使用方法:
|
||||||
|
----------------
|
||||||
|
1. Set up environment:
|
||||||
|
配置环境:
|
||||||
|
|
||||||
|
Create a .env file in the repository root with:
|
||||||
|
在仓库根目录创建 .env 文件,包含:
|
||||||
|
|
||||||
|
OPENWEBUI_API_KEY=your_api_key_here
|
||||||
|
|
||||||
|
2. Stage README files to sync:
|
||||||
|
暂存需要同步的 README 文件:
|
||||||
|
|
||||||
|
git add plugins/actions/my_plugin/README.md
|
||||||
|
git add plugins/actions/my_plugin/README_CN.md
|
||||||
|
|
||||||
|
3. Run the script:
|
||||||
|
运行脚本:
|
||||||
|
|
||||||
|
python plugins/debug/common_tools/update_readmes_to_market.py
|
||||||
|
|
||||||
|
WORKFLOW / 工作流程:
|
||||||
|
-------------------
|
||||||
|
1. Load OPENWEBUI_API_KEY from .env file
|
||||||
|
从 .env 文件加载 OPENWEBUI_API_KEY
|
||||||
|
|
||||||
|
2. Get list of staged README.md/README_CN.md files via git
|
||||||
|
通过 git 获取暂存的 README.md/README_CN.md 文件列表
|
||||||
|
|
||||||
|
3. For each staged README:
|
||||||
|
对于每个暂存的 README:
|
||||||
|
|
||||||
|
a. Locate the corresponding plugin .py file
|
||||||
|
定位对应的插件 .py 文件
|
||||||
|
|
||||||
|
b. Extract openwebui_id/post_id from plugin frontmatter
|
||||||
|
从插件前置信息中提取 openwebui_id/post_id
|
||||||
|
|
||||||
|
c. Fetch existing post data from OpenWebUI Community API
|
||||||
|
从 OpenWebUI 社区 API 获取现有帖子数据
|
||||||
|
|
||||||
|
d. Update post content with new README content
|
||||||
|
用新的 README 内容更新帖子内容
|
||||||
|
|
||||||
|
e. Push changes via API (preserves version & media)
|
||||||
|
通过 API 推送更改(保留版本和媒体)
|
||||||
|
|
||||||
|
REQUIREMENTS / 依赖要求:
|
||||||
|
-----------------------
|
||||||
|
- python-dotenv: For loading .env configuration
|
||||||
|
用于加载 .env 配置文件
|
||||||
|
- Git repository: Must be run from a git-tracked workspace
|
||||||
|
必须在 git 跟踪的工作区中运行
|
||||||
|
|
||||||
|
KEY FEATURES / 关键特性:
|
||||||
|
-----------------------
|
||||||
|
✅ Only updates content field (不仅更新内容字段)
|
||||||
|
✅ Skips files without openwebui_id (跳过没有 openwebui_id 的文件)
|
||||||
|
✅ Automatically matches CN/EN plugin files (自动匹配中英文插件文件)
|
||||||
|
✅ Supports staged plugin source code updates (支持暂存插件源码更新)
|
||||||
|
✅ Safe: Won't modify version or media fields (安全:不会修改版本或媒体字段)
|
||||||
|
|
||||||
|
NOTES / 注意事项:
|
||||||
|
---------------
|
||||||
|
- This is a DEBUG/DEVELOPMENT tool, not for production workflows
|
||||||
|
这是一个调试/开发工具,不用于生产工作流
|
||||||
|
|
||||||
|
- Always verify changes in OpenWebUI Community after sync
|
||||||
|
同步后务必在 OpenWebUI 社区中验证更改
|
||||||
|
|
||||||
|
- Requires valid API key with update permissions
|
||||||
|
需要具有更新权限的有效 API 密钥
|
||||||
|
|
||||||
|
AUTHOR / 作者:
|
||||||
|
-------------
|
||||||
|
Fu-Jie
|
||||||
|
GitHub: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
|
|
||||||
|
======================================================================
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
|
|
||||||
|
def _load_dotenv(repo_root: Path) -> None:
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv # type: ignore
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
print("Missing dependency: python-dotenv. Please install it and retry.")
|
||||||
|
raise SystemExit(1) from exc
|
||||||
|
|
||||||
|
env_path = repo_root / ".env"
|
||||||
|
load_dotenv(env_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[3]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_staged_readmes(repo_root: Path) -> List[Path]:
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"-C",
|
||||||
|
str(repo_root),
|
||||||
|
"diff",
|
||||||
|
"--cached",
|
||||||
|
"--name-only",
|
||||||
|
"--",
|
||||||
|
"*.md",
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
print(f"Failed to read staged files: {exc}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line.endswith("README.md") or line.endswith("README_CN.md"):
|
||||||
|
paths.append(repo_root / line)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def _get_staged_plugin_files(repo_root: Path) -> List[Path]:
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"-C",
|
||||||
|
str(repo_root),
|
||||||
|
"diff",
|
||||||
|
"--cached",
|
||||||
|
"--name-only",
|
||||||
|
"--",
|
||||||
|
"*.py",
|
||||||
|
],
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
print(f"Failed to read staged files: {exc}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
paths = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if "/plugins/" not in line:
|
||||||
|
continue
|
||||||
|
if line.endswith("__init__.py") or os.path.basename(line).startswith("test_"):
|
||||||
|
continue
|
||||||
|
paths.append(repo_root / line)
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_frontmatter(content: str) -> Dict[str, str]:
|
||||||
|
match = re.search(r'^\s*"""\n(.*?)\n"""', content, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
match = re.search(r'"""\n(.*?)\n"""', content, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
frontmatter = match.group(1)
|
||||||
|
meta: Dict[str, str] = {}
|
||||||
|
for line in frontmatter.split("\n"):
|
||||||
|
if ":" in line:
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
meta[key.strip()] = value.strip()
|
||||||
|
return meta
|
||||||
|
|
||||||
|
|
||||||
|
def _find_plugin_file(readme_path: Path) -> Optional[Path]:
|
||||||
|
plugin_dir = readme_path.parent
|
||||||
|
is_cn = readme_path.name.lower().endswith("readme_cn.md")
|
||||||
|
|
||||||
|
py_files = [
|
||||||
|
p
|
||||||
|
for p in plugin_dir.glob("*.py")
|
||||||
|
if p.name != "__init__.py" and not p.name.startswith("test_")
|
||||||
|
]
|
||||||
|
if not py_files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cn_files = [p for p in py_files if p.stem.endswith("_cn")]
|
||||||
|
en_files = [p for p in py_files if not p.stem.endswith("_cn")]
|
||||||
|
|
||||||
|
candidates = cn_files + en_files if is_cn else en_files + cn_files
|
||||||
|
|
||||||
|
# Prefer files that contain openwebui_id/post_id in frontmatter
|
||||||
|
for candidate in candidates:
|
||||||
|
post_id = _get_post_id(candidate)
|
||||||
|
if post_id:
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return candidates[0] if candidates else None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_post_id(plugin_file: Path) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
content = plugin_file.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
meta = _parse_frontmatter(content)
|
||||||
|
return meta.get("openwebui_id") or meta.get("post_id")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_plugin_metadata(plugin_file: Path) -> Dict[str, str]:
|
||||||
|
try:
|
||||||
|
content = plugin_file.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return _parse_frontmatter(content)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_readme_for_plugin(plugin_file: Path) -> Optional[str]:
|
||||||
|
plugin_dir = plugin_file.parent
|
||||||
|
is_cn = plugin_file.stem.endswith("_cn")
|
||||||
|
readme_candidates = ["README_CN.md", "README.md"] if is_cn else ["README.md", "README_CN.md"]
|
||||||
|
for name in readme_candidates:
|
||||||
|
readme_path = plugin_dir / name
|
||||||
|
if readme_path.exists():
|
||||||
|
return readme_path.read_text(encoding="utf-8")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
repo_root = _get_repo_root()
|
||||||
|
_load_dotenv(repo_root)
|
||||||
|
|
||||||
|
api_key = os.environ.get("OPENWEBUI_API_KEY")
|
||||||
|
if not api_key:
|
||||||
|
print("OPENWEBUI_API_KEY is not set in environment.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
client_module_path = repo_root / "scripts" / "openwebui_community_client.py"
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"openwebui_community_client", client_module_path
|
||||||
|
)
|
||||||
|
if not spec or not spec.loader:
|
||||||
|
print("Failed to load openwebui_community_client module.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
client = module.get_client(api_key)
|
||||||
|
|
||||||
|
staged_plugins = _get_staged_plugin_files(repo_root)
|
||||||
|
staged_readmes = _get_staged_readmes(repo_root)
|
||||||
|
if not staged_plugins and not staged_readmes:
|
||||||
|
print("No staged README or plugin files found.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
updated_post_ids: set[str] = set()
|
||||||
|
|
||||||
|
for plugin_file in staged_plugins:
|
||||||
|
if not plugin_file.exists():
|
||||||
|
print(f"Skipped (missing): {plugin_file}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
post_id = _get_post_id(plugin_file)
|
||||||
|
if not post_id:
|
||||||
|
print(f"Skipped (no openwebui_id): {plugin_file}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
post_data = client.get_post(post_id)
|
||||||
|
if not post_data:
|
||||||
|
print(f"Skipped (post not found): {plugin_file}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
source_code = plugin_file.read_text(encoding="utf-8")
|
||||||
|
metadata = _get_plugin_metadata(plugin_file)
|
||||||
|
readme_content = _find_readme_for_plugin(plugin_file)
|
||||||
|
|
||||||
|
ok = client.update_plugin(
|
||||||
|
post_id=post_id,
|
||||||
|
source_code=source_code,
|
||||||
|
readme_content=readme_content or metadata.get("description", ""),
|
||||||
|
metadata=metadata,
|
||||||
|
media_urls=None,
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
updated_post_ids.add(post_id)
|
||||||
|
print(f"Updated plugin -> {plugin_file} (post_id: {post_id})")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Failed: {plugin_file} ({exc})")
|
||||||
|
|
||||||
|
for readme_path in staged_readmes:
|
||||||
|
if not readme_path.exists():
|
||||||
|
print(f"Skipped (missing): {readme_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
plugin_file = _find_plugin_file(readme_path)
|
||||||
|
if not plugin_file:
|
||||||
|
print(f"Skipped (no plugin file): {readme_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
post_id = _get_post_id(plugin_file)
|
||||||
|
if not post_id:
|
||||||
|
print(f"Skipped (no openwebui_id): {readme_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if post_id in updated_post_ids:
|
||||||
|
print(f"Skipped (already updated via plugin): {readme_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
post_data = client.get_post(post_id)
|
||||||
|
if not post_data:
|
||||||
|
print(f"Skipped (post not found): {readme_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
readme_content = readme_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Update README content only, keep other fields unchanged.
|
||||||
|
post_data["content"] = readme_content
|
||||||
|
|
||||||
|
ok = client.update_post(post_id, post_data)
|
||||||
|
if ok:
|
||||||
|
print(f"Updated README -> {readme_path} (post_id: {post_id})")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Failed: {readme_path} ({exc})")
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
98
plugins/debug/copilot_sdk_research/GUIDE_ERRATA.md
Normal file
98
plugins/debug/copilot_sdk_research/GUIDE_ERRATA.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# 开发指南勘误与更新
|
||||||
|
|
||||||
|
## 权限控制章节修正(第 2.2 节)
|
||||||
|
|
||||||
|
### ⚠️ 关键勘误
|
||||||
|
|
||||||
|
在实际测试中发现,Shell 权限请求使用的是 **`fullCommandText`** 字段,而非文档中提到的 `command` 字段。
|
||||||
|
|
||||||
|
### 需要修改的代码行
|
||||||
|
|
||||||
|
**第 89 行(错误):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
command = request.get("command", "")
|
||||||
|
```
|
||||||
|
|
||||||
|
**应改为(正确):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完整的正确实现
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def on_user_permission_request(request, context):
|
||||||
|
"""
|
||||||
|
统一权限审批网关
|
||||||
|
"""
|
||||||
|
kind = request.get("kind") # shell, write, mcp, read, url
|
||||||
|
# ✅ 正确:使用 fullCommandText(shell)或 command(其他)
|
||||||
|
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||||
|
|
||||||
|
# 1. 超级模式:全部允许
|
||||||
|
if self.valves.PERMISSIONS_ALLOW_ALL:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 2. 默认安全:始终允许 "读" 和 "Web浏览"
|
||||||
|
if kind in ["read", "url"]:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 3. 细粒度控制
|
||||||
|
if kind == "shell":
|
||||||
|
if self.valves.PERMISSIONS_ALLOW_SHELL:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN
|
||||||
|
if pattern and command:
|
||||||
|
try:
|
||||||
|
if re.match(pattern, command):
|
||||||
|
return {"kind": "approved"}
|
||||||
|
except re.error:
|
||||||
|
print(f"[Config Error] Invalid Regex: {pattern}")
|
||||||
|
|
||||||
|
if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 4. 默认拒绝
|
||||||
|
print(f"[Permission Denied] Blocked: {kind} {command}")
|
||||||
|
return {
|
||||||
|
"kind": "denied-by-rules",
|
||||||
|
"rules": [{"kind": "check-openwebui-valves"}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell 权限请求的完整结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": "shell",
|
||||||
|
"toolCallId": "call_xxx",
|
||||||
|
"fullCommandText": "ls -la", // ← 关键字段
|
||||||
|
"intention": "List all files and directories",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"identifier": "ls -la",
|
||||||
|
"readOnly": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possiblePaths": [],
|
||||||
|
"possibleUrls": [],
|
||||||
|
"hasWriteFileRedirection": false,
|
||||||
|
"canOfferSessionApproval": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
已通过完整测试套件验证(8/8 通过),详见 [PERMISSION_TEST_REPORT.md](./PERMISSION_TEST_REPORT.md)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**更新日期**: 2026-01-30
|
||||||
|
**验证状态**: ✅ 已测试
|
||||||
|
**影响范围**: 2.2 权限与确认章节
|
||||||
173
plugins/debug/copilot_sdk_research/PERMISSION_TEST_REPORT.md
Normal file
173
plugins/debug/copilot_sdk_research/PERMISSION_TEST_REPORT.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# GitHub Copilot SDK 权限控制测试报告
|
||||||
|
|
||||||
|
## 测试日期
|
||||||
|
|
||||||
|
2026-01-30
|
||||||
|
|
||||||
|
## 测试环境
|
||||||
|
|
||||||
|
- **Model**: gpt-4.1
|
||||||
|
- **Python**: 3.12
|
||||||
|
- **Copilot SDK**: Latest
|
||||||
|
|
||||||
|
## 关键发现
|
||||||
|
|
||||||
|
### 1. Shell 权限请求结构
|
||||||
|
|
||||||
|
Shell 类型的权限请求使用 **`fullCommandText`** 字段,而非 `command` 字段。
|
||||||
|
|
||||||
|
**完整请求示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": "shell",
|
||||||
|
"toolCallId": "call_JKLi7tz3uSDQWE3LgzCpvSVy",
|
||||||
|
"fullCommandText": "ls -la",
|
||||||
|
"intention": "List all files and directories with details in the current directory",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"identifier": "ls -la",
|
||||||
|
"readOnly": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possiblePaths": [],
|
||||||
|
"possibleUrls": [],
|
||||||
|
"hasWriteFileRedirection": false,
|
||||||
|
"canOfferSessionApproval": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 正则匹配模式验证
|
||||||
|
|
||||||
|
正则白名单模式已验证有效,必须使用 `fullCommandText` 字段:
|
||||||
|
|
||||||
|
```python
|
||||||
|
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||||
|
pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN
|
||||||
|
if pattern and command:
|
||||||
|
if re.match(pattern, command):
|
||||||
|
return {"kind": "approved"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试结果
|
||||||
|
|
||||||
|
### 完整测试套件(8/8 通过 ✅)
|
||||||
|
|
||||||
|
| # | 测试用例 | 配置 | 提示词 | 预期 | 结果 | 状态 |
|
||||||
|
|---|---------|------|--------|------|------|------|
|
||||||
|
| 1 | Default Deny Shell | 默认 | 请执行: ls -la | ❌ Denied | ❌ Denied | ✅ |
|
||||||
|
| 2 | Allow All | allow_all=True | 请执行: ls -la | ✅ Approved | ✅ Approved | ✅ |
|
||||||
|
| 3 | Allow Shell | allow_shell=True | 请执行: pwd | ✅ Approved | ✅ Approved | ✅ |
|
||||||
|
| 4 | Regex Match: ^ls | pattern='^ls' | 请执行: ls -la | ✅ Approved | ✅ Approved | ✅ |
|
||||||
|
| 5 | Regex No Match | pattern='^ls' | 请执行: pwd | ❌ Denied | ❌ Denied | ✅ |
|
||||||
|
| 6 | Regex Complex | pattern='^(ls\|pwd\|echo)' | 请执行: pwd | ✅ Approved | ✅ Approved | ✅ |
|
||||||
|
| 7 | Regex No Match: git | pattern='^(ls\|pwd\|echo)' | 请执行: git status | ❌ Denied | ❌ Denied | ✅ |
|
||||||
|
| 8 | Read Permission | 默认 | Read: README.md | ✅ Approved | ✅ Approved | ✅ |
|
||||||
|
|
||||||
|
**总体通过率: 100%** 🎉
|
||||||
|
|
||||||
|
## 推荐配置示例
|
||||||
|
|
||||||
|
### 1. 安全模式(推荐生产环境)
|
||||||
|
|
||||||
|
```python
|
||||||
|
PERMISSIONS_ALLOW_ALL: bool = False
|
||||||
|
PERMISSIONS_ALLOW_SHELL: bool = False
|
||||||
|
PERMISSIONS_SHELL_ALLOW_PATTERN: str = "^(ls|pwd|echo|cat).*"
|
||||||
|
PERMISSIONS_ALLOW_WRITE: bool = False
|
||||||
|
PERMISSIONS_ALLOW_MCP: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 开发模式
|
||||||
|
|
||||||
|
```python
|
||||||
|
PERMISSIONS_ALLOW_ALL: bool = False
|
||||||
|
PERMISSIONS_ALLOW_SHELL: bool = False
|
||||||
|
PERMISSIONS_SHELL_ALLOW_PATTERN: str = "^(ls|pwd|echo|cat|grep|git status|npm test).*"
|
||||||
|
PERMISSIONS_ALLOW_WRITE: bool = False
|
||||||
|
PERMISSIONS_ALLOW_MCP: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 完全信任模式(仅限受控环境)
|
||||||
|
|
||||||
|
```python
|
||||||
|
PERMISSIONS_ALLOW_ALL: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现建议
|
||||||
|
|
||||||
|
### 正确的权限处理代码
|
||||||
|
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
async def on_user_permission_request(request: Dict[str, Any], context: Dict[str, str]):
|
||||||
|
"""
|
||||||
|
统一权限审批网关
|
||||||
|
"""
|
||||||
|
kind = request.get("kind")
|
||||||
|
# 关键:使用 fullCommandText 而非 command
|
||||||
|
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||||
|
|
||||||
|
# 1. 超级模式
|
||||||
|
if self.valves.PERMISSIONS_ALLOW_ALL:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 2. 默认安全(read、url)
|
||||||
|
if kind in ["read", "url"]:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 3. Shell 细粒度控制
|
||||||
|
if kind == "shell":
|
||||||
|
if self.valves.PERMISSIONS_ALLOW_SHELL:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN
|
||||||
|
if pattern and command:
|
||||||
|
try:
|
||||||
|
if re.match(pattern, command):
|
||||||
|
return {"kind": "approved"}
|
||||||
|
except re.error as e:
|
||||||
|
logger.error(f"Invalid regex: {pattern} - {e}")
|
||||||
|
|
||||||
|
# 4. Write 权限
|
||||||
|
if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 5. MCP 权限
|
||||||
|
if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 6. 默认拒绝
|
||||||
|
logger.warning(f"Permission Denied: {kind} {command}")
|
||||||
|
return {
|
||||||
|
"kind": "denied-by-rules",
|
||||||
|
"rules": [{"kind": "security-policy"}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见正则模式示例
|
||||||
|
|
||||||
|
| 用途 | 正则表达式 | 说明 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| 只读命令 | `^(ls|pwd|cat|echo|grep).*` | 允许常见只读命令 |
|
||||||
|
| Git 只读 | `^git (status\|log\|diff\|show).*` | 允许 Git 只读操作 |
|
||||||
|
| npm/yarn 测试 | `^(npm\|yarn) (test\|run).*` | 允许测试脚本 |
|
||||||
|
| 完全 shell | `.*` | ⚠️ 危险:允许所有命令 |
|
||||||
|
|
||||||
|
## 测试脚本位置
|
||||||
|
|
||||||
|
- 基础测试: [test_shell_permission_pattern.py](./test_shell_permission_pattern.py)
|
||||||
|
- 完整测试套件: [test_permission_comprehensive.py](./test_permission_comprehensive.py)
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
✅ **GitHub Copilot SDK 的权限控制机制完全有效**
|
||||||
|
✅ **正则白名单模式已验证可用**
|
||||||
|
⚠️ **必须使用 `fullCommandText` 字段获取命令内容**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**测试执行者**: GitHub Copilot
|
||||||
|
**审核状态**: ✅ 已验证
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
# OpenWebUI GitHub Copilot Pipe Enhancement Guide
|
||||||
|
|
||||||
|
基于 Copilot SDK 源码级研究的深度技术总结,旨在指导 OpenWebUI Pipe 的功能增强开发。
|
||||||
|
|
||||||
|
## 1. 认证机制 (Authentication)
|
||||||
|
|
||||||
|
官方支持通过环境变量传递 Token。在 Pipe 中,只要确保 `GH_TOKEN` 或 `GITHUB_TOKEN` 存在于环境变量中,Copilot CLI 即可自动识别,无需在 `CopilotClient` 构造函数中重复注入。
|
||||||
|
|
||||||
|
### 核心实现
|
||||||
|
|
||||||
|
Pipe 应确保将 Token(来自 Valve 或 Env)正确设置到当前进程的环境变量中。
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from copilot import CopilotClient
|
||||||
|
|
||||||
|
# 1. 设置环境变量 (如果从 Valve 获取)
|
||||||
|
if self.valves.GH_TOKEN:
|
||||||
|
os.environ["GH_TOKEN"] = self.valves.GH_TOKEN
|
||||||
|
|
||||||
|
# 2. 初始化客户端
|
||||||
|
# CopilotClient 启动的 CLI 子进程会自动继承当前环境中的 GH_TOKEN
|
||||||
|
client = CopilotClient({
|
||||||
|
# "cli_path": ...,
|
||||||
|
# 注意:无需在此处重复传入 github_token,CLI 会自动读取环境变量
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. 启动前检查 (建议)
|
||||||
|
# status = await client.get_auth_status()
|
||||||
|
# if not status.isAuthenticated: ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 权限与确认 (Permissions & Tools) - 核心控制点
|
||||||
|
|
||||||
|
这是用户最关心的部分:如何知道有哪些工具,以及如何控制它们的执行。
|
||||||
|
|
||||||
|
### 2.1 内置工具 (Built-in Tools)
|
||||||
|
|
||||||
|
Copilot CLI 内部管理了一组标准工具,**Python SDK 目前没有直接的 API (`client.list_tools()`) 来列出这些工具**。
|
||||||
|
|
||||||
|
但是,根据 SDK 的 `PermissionRequest` 类型定义 (`copilot/types.py`),我们可以反推其能力类别:
|
||||||
|
|
||||||
|
* **`shell`**: 执行终端命令 (对应 `run_terminal_command` 等)
|
||||||
|
* **`filesystem`** (对应 `read/write`): 文件读写 (对应 `read_file`, `edit_file`, `delete_file` 等)
|
||||||
|
* **`url`**: 网络访问 (对应 `fetch_url` 等)
|
||||||
|
* **`mcp`**: 连接的 MCP 服务器工具
|
||||||
|
|
||||||
|
> **提示**: `available_tools` 参数可以用来“隐藏”工具,让 Agent 根本不知道它有一把锤子。而 `on_permission_request` 是用来拦截 Agent 挥舞锤子的动作。通常我们建议**能力全开 (不设置 available_tools 限制)**,而在**权限层 (on_permission_request) 做拦截**。
|
||||||
|
|
||||||
|
### 2.2 实现“全部允许”与“按需允许”
|
||||||
|
|
||||||
|
建议在 Valves 中增加权限控制字段,并在 `on_permission_request` 中实现逻辑。
|
||||||
|
|
||||||
|
```python
|
||||||
|
import re
|
||||||
|
|
||||||
|
class Valves(BaseModel):
|
||||||
|
# ... 其他 Valve ...
|
||||||
|
# 权限控制开关
|
||||||
|
PERMISSIONS_ALLOW_ALL: bool = Field(default=False, description="DANGER: Auto-approve ALL actions (shell, write, etc).")
|
||||||
|
PERMISSIONS_ALLOW_SHELL: bool = Field(default=False, description="Auto-approve shell commands.")
|
||||||
|
PERMISSIONS_SHELL_ALLOW_PATTERN: str = Field(default="", description="Regex for approved shell commands (e.g., '^ls|^grep').")
|
||||||
|
PERMISSIONS_ALLOW_WRITE: bool = Field(default=False, description="Auto-approve file write/edit/delete.")
|
||||||
|
PERMISSIONS_ALLOW_MCP: bool = Field(default=True, description="Auto-approve MCP tool execution.")
|
||||||
|
|
||||||
|
# 权限处理 Hook 实现
|
||||||
|
async def on_user_permission_request(request, context):
|
||||||
|
"""
|
||||||
|
统一权限审批网关
|
||||||
|
request keys: kind, toolCallId, ... (shell requests have 'command')
|
||||||
|
"""
|
||||||
|
kind = request.get("kind") # shell, write, mcp, read, url
|
||||||
|
|
||||||
|
# 1. 超级模式:全部允许
|
||||||
|
if self.valves.PERMISSIONS_ALLOW_ALL:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 2. 默认安全:始终允许 "读" 和 "Web浏览" (根据需求调整)
|
||||||
|
if kind in ["read", "url"]:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 3. 细粒度控制
|
||||||
|
if kind == "shell":
|
||||||
|
# 3.1 完全允许 Shell
|
||||||
|
if self.valves.PERMISSIONS_ALLOW_SHELL:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 3.2 基于正则允许特定命令
|
||||||
|
command = request.get("command", "")
|
||||||
|
pattern = self.valves.PERMISSIONS_SHELL_ALLOW_PATTERN
|
||||||
|
if pattern and command:
|
||||||
|
try:
|
||||||
|
if re.match(pattern, command):
|
||||||
|
return {"kind": "approved"}
|
||||||
|
except re.error:
|
||||||
|
print(f"[Config Error] Invalid Regex: {pattern}")
|
||||||
|
|
||||||
|
if kind == "write" and self.valves.PERMISSIONS_ALLOW_WRITE:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
if kind == "mcp" and self.valves.PERMISSIONS_ALLOW_MCP:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# 4. 默认拒绝
|
||||||
|
print(f"[Permission Denied] Blocked request for: {kind} {request.get('command', '')}")
|
||||||
|
return {
|
||||||
|
"kind": "denied-by-rules",
|
||||||
|
"rules": [{"kind": "check-openwebui-valves"}]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 注册 Hook
|
||||||
|
session = await client.create_session({
|
||||||
|
# ...
|
||||||
|
"on_permission_request": on_user_permission_request
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Agent 与 MCP 集成 (Agents & MCP)
|
||||||
|
|
||||||
|
SDK 中的 Agent 和 MCP 并非独立文件,而是会话配置 (`SessionConfig`) 的一部分。Pipe 可以通过 Valves 动态构建这些配置。
|
||||||
|
|
||||||
|
### 关键映射关系
|
||||||
|
|
||||||
|
| SDK 概念 | OpenWebUI 对应 | 实现位置 | 关键参数 |
|
||||||
|
| :--- | :--- | :--- | :--- |
|
||||||
|
| **Custom Agent** | 自定义模型 / Persona | `create_session(custom_agents=[...])` | `name`, `prompt`, `tools` (仅名称) |
|
||||||
|
| **Agent Tools** | Valve 开关 / 预置工具 | `create_session(tools=[func1, func2])` | 必须先在 `tools` 注册函数,Agent 才能引用 |
|
||||||
|
| **MCP Server** | Valve 配置 (JSON) | `create_session(mcp_servers={...})` | `command`, `args`, `env` (本地) |
|
||||||
|
|
||||||
|
### 代码范式:动态构建 Agent
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def create_agent_session(client, user_prompt, model_name):
|
||||||
|
# 1. 定义工具 (必须是函数引用)
|
||||||
|
# 假设已从 OpenWebUI Tools 转换或内置
|
||||||
|
available_tools = [tool_web_search, tool_run_script]
|
||||||
|
|
||||||
|
# 2. 构建 Agent Manifest (针对当前请求的虚拟 Agent)
|
||||||
|
agent_manifest = {
|
||||||
|
"name": "openwebui_agent",
|
||||||
|
"description": "Dynamic agent from OpenWebUI",
|
||||||
|
"prompt": "You are a helpful assistant...", # 这里注入 System Prompt
|
||||||
|
"tools": ["web_search", "run_script"], # 引用上方工具的 name
|
||||||
|
"mcp_servers": {
|
||||||
|
# 可以在这里为特定 Agent 绑定 MCP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. 创建会话
|
||||||
|
session = await client.create_session({
|
||||||
|
"model": "gpt-4", # 底层模型
|
||||||
|
"custom_agents": [agent_manifest],
|
||||||
|
"tools": available_tools, # 注册实际代码
|
||||||
|
"available_tools": ["web_search"], # 白名单控制当前可用工具
|
||||||
|
# ... 权限配置
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. MCP 服务器配置 (Native MCP Support)
|
||||||
|
|
||||||
|
Pipe 可以直接支持标准 MCP 协议(Stdio)。不需要额外的 MCP 客户端代理,SDK 原生支持。
|
||||||
|
|
||||||
|
### Valve 配置结构建议
|
||||||
|
|
||||||
|
建议在 Pipe 的 Valves 中增加一个 `MCP_CONFIG` 字段(JSON 字符串),解析后直接传给 SDK。
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Valve 输入示例 (JSON)
|
||||||
|
# {
|
||||||
|
# "brave_search": {
|
||||||
|
# "type": "local",
|
||||||
|
# "command": "npx",
|
||||||
|
# "args": ["-y", "@modelcontextprotocol/server-brave-search"],
|
||||||
|
# "env": {"BRAVE_API_KEY": "..."}
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
|
||||||
|
# 代码实现
|
||||||
|
mcp_config = json.loads(self.valves.MCP_CONFIG)
|
||||||
|
session = await client.create_session({
|
||||||
|
# ...
|
||||||
|
"mcp_servers": mcp_config,
|
||||||
|
# 注意:必须配合权限自动审批,否则 MCP 工具无法调用
|
||||||
|
"on_permission_request": auto_approve_policy
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 会话管理:持久化 vs 重放 (Persistence)
|
||||||
|
|
||||||
|
OpenWebUI 是无状态的,但 Copilot SDK 是有状态的(保留上下文窗口优化)。
|
||||||
|
|
||||||
|
### 最佳实践:以 `chat_id` 为锚点
|
||||||
|
|
||||||
|
利用 OpenWebUI 提供的 `chat_id` 来决定是 `resume` 还是 `start`。
|
||||||
|
|
||||||
|
1. **Map**: 维护 `Dict[chat_id, session_id]` (内存或数据库)。
|
||||||
|
2. **Flow**:
|
||||||
|
* 请求进来 -> 检查 `chat_id` 是否有对应的 `session_id`。
|
||||||
|
* **有**: 尝试 `client.resume_session(session_id)`。
|
||||||
|
* *注意*:Resume 时必须重新传入 `tools`, `hooks`, `on_permission_request`,因为这些 Python 对象不会被序列化保存。
|
||||||
|
* **无/失败**: 调用 `client.create_session()`,并将新 `session_id` 存入 Map。
|
||||||
|
3. **Fallback**: 如果 Resume 失败(例如后端重启 SDK 进程丢失),回退到 Create 新会话,并可选地将 OpenWebUI 传来的 `messages` 历史以 System Message 或历史插入的方式“重放”进去(虽然 SDK 不直接支持 insert history,但可以通过连续的 `send` 模拟,但这很慢)。
|
||||||
|
* *简易方案*:Resume 失败就作为新对话开始,只带入 System Prompt。
|
||||||
|
|
||||||
|
## 6. 高级 Hook:提示词增强
|
||||||
|
|
||||||
|
利用 `on_user_prompt_submitted` 钩子,可以在不修改用户可见内容的情况下,向 Copilot 注入隐式上下文(例如当前文件内容、Pipe 的元指令)。
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def inject_context_hook(input_data, ctx):
|
||||||
|
user_prompt = input_data["prompt"]
|
||||||
|
|
||||||
|
# 比如:检测到用户在问代码,自动附加上下文
|
||||||
|
additional_context = "Current Language: Python. Framework: OpenWebUI."
|
||||||
|
|
||||||
|
return {
|
||||||
|
"modifiedPrompt": user_prompt, # 可以在这里改写提示词
|
||||||
|
"additionalContext": additional_context # 注入隐藏上下文
|
||||||
|
}
|
||||||
|
|
||||||
|
session = await client.create_session({
|
||||||
|
# ...
|
||||||
|
"hooks": {
|
||||||
|
"on_user_prompt_submitted": inject_context_hook
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结开发清单:**
|
||||||
|
|
||||||
|
1. [ ] **Env Auth**: 读取环境变量 -> `CopilotClient`。
|
||||||
|
2. [ ] **Permission Valve**: 实现 `PERMISSIONS_ALLOW_ALL/SHELL` 等 Valves。
|
||||||
|
3. [ ] **Auto-Approve Hook**: 实现 `on_permission_request` 逻辑。
|
||||||
|
4. [ ] **MCP Valve**: 添加 JSON Valve -> `session.mcp_servers`。
|
||||||
|
5. [ ] **Session Map**: 实现 `chat_id` <-> `session_id` 的简单的内存映射。
|
||||||
|
6. [ ] **Resume Logic**: 优先 `resume_session`,并记得并在 resume 时重传 Hook 和 Tools。
|
||||||
620
plugins/debug/copilot_sdk_research/copilot_sdk_guide.py
Normal file
620
plugins/debug/copilot_sdk_research/copilot_sdk_guide.py
Normal file
@@ -0,0 +1,620 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
from typing import Iterable, List, Optional
|
||||||
|
|
||||||
|
from copilot import CopilotClient
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("copilot_sdk_guide")
|
||||||
|
|
||||||
|
DEFAULT_CONTEXT_URLS = [
|
||||||
|
"https://raw.githubusercontent.com/github/copilot-sdk/main/README.md",
|
||||||
|
"https://raw.githubusercontent.com/github/copilot-sdk/main/python/README.md",
|
||||||
|
"https://raw.githubusercontent.com/github/copilot-sdk/main/docs/getting-started.md",
|
||||||
|
"https://raw.githubusercontent.com/github/copilot-cli/main/README.md",
|
||||||
|
"https://raw.githubusercontent.com/github/copilot-cli/main/changelog.md",
|
||||||
|
"https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli",
|
||||||
|
"https://docs.github.com/en/copilot/concepts/agents/about-agent-skills",
|
||||||
|
"https://raw.githubusercontent.com/github/awesome-copilot/main/README.md",
|
||||||
|
"https://raw.githubusercontent.com/github/awesome-copilot/main/skills/copilot-sdk/SKILL.md",
|
||||||
|
"https://raw.githubusercontent.com/github/awesome-copilot/main/instructions/agent-skills.instructions.md",
|
||||||
|
]
|
||||||
|
|
||||||
|
AWESOME_COPILOT_REPO = "github/awesome-copilot"
|
||||||
|
AWESOME_COPILOT_BRANCH = "main"
|
||||||
|
AWESOME_COPILOT_DOC_DIRS = ["docs/", "instructions/"]
|
||||||
|
|
||||||
|
TOPICS = [
|
||||||
|
"MCP Server Integration: JSON-RPC config and SDK hooks",
|
||||||
|
"Agent Manifests: Defining capabilities and permissions programmatically",
|
||||||
|
"Headless Auth: Device Code Flow and credential persistence",
|
||||||
|
"Session Replay vs Resume: Handling stateless frontend history",
|
||||||
|
"Advanced Session Hooks: Intercepting and modifying user prompts",
|
||||||
|
"Workspace Virtualization: Handling CWD for remote/virtual files",
|
||||||
|
"Error Recovery: Handling session disconnects and re-auth",
|
||||||
|
"Confirmation Events: programmatic handling of 'confirmation_required'",
|
||||||
|
"Skills: Conflict resolution and precedence defaults",
|
||||||
|
"Debugging: Tracing JSON-RPC traffic in the SDK",
|
||||||
|
"Billing & Policies: How seat management affects SDK features",
|
||||||
|
]
|
||||||
|
|
||||||
|
QUESTION_TEMPLATES = [
|
||||||
|
"Give a concise overview of {topic}.",
|
||||||
|
"Provide best practices and common pitfalls for {topic}.",
|
||||||
|
"Show a minimal example snippet for {topic}.",
|
||||||
|
"List recommended configuration defaults for {topic}.",
|
||||||
|
"How does {topic} relate to building a custom Agent?",
|
||||||
|
]
|
||||||
|
|
||||||
|
CLI_FOCUS_QUESTIONS = [
|
||||||
|
"How to configure MCP servers in ~/.copilot/config.json for SDK usage?",
|
||||||
|
"What CLI environment variables force 'Agent' mode vs 'Generic' mode?",
|
||||||
|
"Explain the 'confirmation' flow in CLI and how it maps to SDK events.",
|
||||||
|
"Does the CLI support 'dry-run' permission checks for tools?",
|
||||||
|
"What are the undocumented requirements for 'workspace' context updates?",
|
||||||
|
"How does the CLI handle 'device code' re-authentication automatically?",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_questions(max_questions: int) -> List[str]:
|
||||||
|
questions: List[str] = []
|
||||||
|
|
||||||
|
for topic in TOPICS:
|
||||||
|
for template in QUESTION_TEMPLATES:
|
||||||
|
questions.append(template.format(topic=topic))
|
||||||
|
|
||||||
|
questions.extend(CLI_FOCUS_QUESTIONS)
|
||||||
|
|
||||||
|
# De-duplicate while preserving order
|
||||||
|
seen = set()
|
||||||
|
uniq: List[str] = []
|
||||||
|
for q in questions:
|
||||||
|
if q in seen:
|
||||||
|
continue
|
||||||
|
seen.add(q)
|
||||||
|
uniq.append(q)
|
||||||
|
|
||||||
|
return uniq[:max_questions]
|
||||||
|
|
||||||
|
|
||||||
|
def build_deep_dive_prompts() -> List[str]:
|
||||||
|
return [
|
||||||
|
"Provide a python code example for configuring `CopilotClient` to connect to a local MCP server (e.g. Brave Search) via `CopilotClient` config.",
|
||||||
|
"Explain how to programmatically handle `tool.confirmation_required` events in a non-interactive stream using `session.on()`.",
|
||||||
|
"Show how to implement a 'Device Flow' login helper using SDK primitives (if available) or raw HTTP showing how to persist credentials.",
|
||||||
|
"Compare the pros and cons of 'Session Replay' (fast-forwarding history) vs 'Session Resume' (stateful ID) for a stateless web backend like OpenWebUI.",
|
||||||
|
"Detail the exact protocol for 'Virtual Workspace': how to implement a file system provider that feeds content to Copilot without physical files.",
|
||||||
|
"Create an 'Agent Manifest' example: how to define an Agent capable of specific high-privileged tools via SDK.",
|
||||||
|
"List all 'hidden' `SessionConfig` parameters relevant to Agent behavior and personality.",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_questions(path: str) -> List[str]:
|
||||||
|
if path.lower().endswith(".json"):
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if isinstance(data, list):
|
||||||
|
return [str(x).strip() for x in data if str(x).strip()]
|
||||||
|
raise ValueError("JSON must be an array of strings")
|
||||||
|
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
lines = [line.strip() for line in f.readlines()]
|
||||||
|
return [line for line in lines if line]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_url(url: str, headers: Optional[dict] = None) -> str:
|
||||||
|
import urllib.request
|
||||||
|
import time
|
||||||
|
|
||||||
|
retries = 3
|
||||||
|
if headers is None:
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
|
||||||
|
for i in range(retries):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=20) as response:
|
||||||
|
return response.read().decode("utf-8", errors="replace")
|
||||||
|
except Exception as exc:
|
||||||
|
if i == retries - 1:
|
||||||
|
logger.warning(
|
||||||
|
"Failed to fetch %s after %d attempts: %s", url, retries, exc
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
time.sleep(1 * (i + 1))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def list_repo_markdown_urls(
|
||||||
|
repo: str,
|
||||||
|
branch: str,
|
||||||
|
dir_prefixes: List[str],
|
||||||
|
) -> List[str]:
|
||||||
|
api_url = f"https://api.github.com/repos/{repo}/git/trees/{branch}?recursive=1"
|
||||||
|
headers = {}
|
||||||
|
if os.environ.get("GITHUB_TOKEN"):
|
||||||
|
headers["Authorization"] = f"token {os.environ.get('GITHUB_TOKEN')}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = fetch_url(api_url, headers=headers)
|
||||||
|
if not content:
|
||||||
|
return []
|
||||||
|
data = json.loads(content)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to list repo tree: %s", exc)
|
||||||
|
return []
|
||||||
|
|
||||||
|
tree = data.get("tree", []) if isinstance(data, dict) else []
|
||||||
|
urls: List[str] = []
|
||||||
|
for item in tree:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
path = item.get("path", "")
|
||||||
|
if not path or not path.endswith(".md"):
|
||||||
|
continue
|
||||||
|
if any(path.startswith(prefix) for prefix in dir_prefixes):
|
||||||
|
raw = f"https://raw.githubusercontent.com/{repo}/{branch}/{path}"
|
||||||
|
urls.append(raw)
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
|
def read_local_sdk_source(max_chars: int = 300000) -> str:
|
||||||
|
"""
|
||||||
|
Locates the installed 'copilot' package and reads its source code.
|
||||||
|
This ensures analysis is based on the actual installed version, not just docs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import copilot
|
||||||
|
except ImportError:
|
||||||
|
logger.error("Could not import 'copilot' SDK. Is it installed?")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
package_dir = os.path.dirname(copilot.__file__)
|
||||||
|
logger.info(f"Reading SDK source from: {package_dir}")
|
||||||
|
|
||||||
|
source_chunks = []
|
||||||
|
total_chars = 0
|
||||||
|
|
||||||
|
# Prioritize key files that define core logic
|
||||||
|
priority_files = ["client.py", "session.py", "types.py", "events.py", "__init__.py"]
|
||||||
|
|
||||||
|
# First pass: Recursively find all .py files
|
||||||
|
all_py_files = []
|
||||||
|
for root, dirs, files in os.walk(package_dir):
|
||||||
|
if "__pycache__" in root:
|
||||||
|
continue
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(".py"):
|
||||||
|
all_py_files.append(os.path.join(root, file))
|
||||||
|
|
||||||
|
# Sort files: priority files first, then alphabetical
|
||||||
|
def sort_key(path):
|
||||||
|
fname = os.path.basename(path)
|
||||||
|
if fname in priority_files:
|
||||||
|
return (0, priority_files.index(fname))
|
||||||
|
return (1, path)
|
||||||
|
|
||||||
|
all_py_files.sort(key=sort_key)
|
||||||
|
|
||||||
|
for path in all_py_files:
|
||||||
|
rel_path = os.path.relpath(path, os.path.dirname(package_dir))
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Add file delimiter for the model
|
||||||
|
header = f"\n\n# ==================================================\n# SOURCE CODE FILE: {rel_path}\n# ==================================================\n"
|
||||||
|
chunk = header + content
|
||||||
|
|
||||||
|
if total_chars + len(chunk) > max_chars:
|
||||||
|
remaining = max_chars - total_chars
|
||||||
|
if remaining > len(header) + 100:
|
||||||
|
source_chunks.append(
|
||||||
|
chunk[:remaining] + "\n# [TRUNCATED DUE TO LENGTH LIMIT]"
|
||||||
|
)
|
||||||
|
logger.warning(f"Context limit reached. Stopping at {rel_path}")
|
||||||
|
break
|
||||||
|
|
||||||
|
source_chunks.append(chunk)
|
||||||
|
total_chars += len(chunk)
|
||||||
|
logger.info(f"Loaded source file: {rel_path} ({len(content)} chars)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to read source file {path}: {e}")
|
||||||
|
|
||||||
|
return "".join(source_chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def build_context(urls: Iterable[str], max_chars: int) -> str:
|
||||||
|
chunks: List[str] = []
|
||||||
|
remaining = max_chars
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
content = fetch_url(url)
|
||||||
|
header = f"[Source: {url}]\n"
|
||||||
|
if len(header) >= remaining:
|
||||||
|
break
|
||||||
|
remaining -= len(header)
|
||||||
|
|
||||||
|
if len(content) > remaining:
|
||||||
|
content = content[:remaining] + "\n[TRUNCATED]\n"
|
||||||
|
remaining = 0
|
||||||
|
else:
|
||||||
|
remaining -= len(content)
|
||||||
|
|
||||||
|
chunks.append(header + content)
|
||||||
|
logger.info("Fetched context: %s", url)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Failed to fetch %s: %s", url, exc)
|
||||||
|
|
||||||
|
return "\n\n".join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def write_jsonl(path: str, item: dict) -> None:
|
||||||
|
with open(path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(item, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def write_markdown_header(path: str, title: str, meta: dict) -> None:
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"# {title}\n\n")
|
||||||
|
for k, v in meta.items():
|
||||||
|
f.write(f"- **{k}**: {v}\n")
|
||||||
|
f.write("\n---\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def append_markdown_qa(path: str, question: str, answer: str) -> None:
|
||||||
|
with open(path, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"## Q: {question}\n\n")
|
||||||
|
f.write(f"{answer}\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def clamp_questions(questions: List[str], max_questions: int) -> List[str]:
|
||||||
|
return questions[: max(1, min(max_questions, 400))]
|
||||||
|
|
||||||
|
|
||||||
|
def print_progress_bar(
|
||||||
|
iteration,
|
||||||
|
total,
|
||||||
|
prefix="",
|
||||||
|
suffix="",
|
||||||
|
decimals=1,
|
||||||
|
length=50,
|
||||||
|
fill="█",
|
||||||
|
printEnd="\r",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Call in a loop to create terminal progress bar
|
||||||
|
"""
|
||||||
|
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||||
|
filledLength = int(length * iteration // total)
|
||||||
|
bar = fill * filledLength + "-" * (length - filledLength)
|
||||||
|
# Clear line extension to handle shrinking suffixes
|
||||||
|
print(f"\r{prefix} |{bar}| {percent}% {suffix}\033[K", end=printEnd)
|
||||||
|
# Print New Line on Complete
|
||||||
|
if iteration == total:
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_session(
|
||||||
|
model: str,
|
||||||
|
questions: List[str],
|
||||||
|
output_dir: str,
|
||||||
|
context: str,
|
||||||
|
session_id: Optional[str],
|
||||||
|
delay: float,
|
||||||
|
output_lang: str,
|
||||||
|
enable_infinite_sessions: bool,
|
||||||
|
timeout: int,
|
||||||
|
) -> None:
|
||||||
|
client = CopilotClient()
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
session_config = {"model": model}
|
||||||
|
if session_id:
|
||||||
|
session_config["session_id"] = session_id
|
||||||
|
if enable_infinite_sessions:
|
||||||
|
session_config["infinite_sessions"] = {
|
||||||
|
"enabled": True,
|
||||||
|
"background_compaction_threshold": 0.8,
|
||||||
|
"buffer_exhaustion_threshold": 0.95,
|
||||||
|
}
|
||||||
|
|
||||||
|
session = await client.create_session(session_config)
|
||||||
|
|
||||||
|
timestamp = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
jsonl_path = os.path.join(output_dir, f"copilot_sdk_guide_{timestamp}.jsonl")
|
||||||
|
md_path = os.path.join(output_dir, f"copilot_sdk_guide_{timestamp}.md")
|
||||||
|
|
||||||
|
write_markdown_header(
|
||||||
|
md_path,
|
||||||
|
"GitHub Copilot SDK & CLI 研究报告",
|
||||||
|
{
|
||||||
|
"model": model,
|
||||||
|
"questions": len(questions),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"language": output_lang,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
lang_instruction = "Chinese" if "zh" in output_lang.lower() else "English"
|
||||||
|
|
||||||
|
system_prompt = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
You are an expert assistant. Focus on GitHub Copilot SDK and GitHub Copilot CLI.
|
||||||
|
|
||||||
|
CRITICAL INSTRUCTION: SOURCE CODE FIRST.
|
||||||
|
You have been provided with the ACTUAL PYTHON SOURCE CODE of the `copilot` SDK in the context.
|
||||||
|
When answering questions:
|
||||||
|
1. FIRST, analyze the provided source code (look for class definitions, type hints, methods).
|
||||||
|
2. THEN, refer to documentation if source code is ambiguous.
|
||||||
|
3. Do NOT hallucinate methods that do not exist in the source code.
|
||||||
|
4. If a feature (like MCP) is not explicitly in the code, explain how to implement it using the available primitives (low-level hooks/events).
|
||||||
|
|
||||||
|
Provide accurate, concise answers in {lang_instruction}. When relevant, include command names,
|
||||||
|
configuration keys, and pitfalls. Use bullet points where useful.
|
||||||
|
|
||||||
|
Output requirements:
|
||||||
|
- Write in {lang_instruction}.
|
||||||
|
- Provide practical code snippets (Python/TypeScript/CLI) when helpful.
|
||||||
|
- Include a short "建议/落地" section for integration into a pipe.
|
||||||
|
- If citing facts from provided context, briefly mention the source URL.
|
||||||
|
"""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
if context:
|
||||||
|
system_prompt += "\n\nAdditional context:\n" + context
|
||||||
|
|
||||||
|
await session.send_and_wait({"prompt": system_prompt}, timeout=timeout)
|
||||||
|
|
||||||
|
total_q = len(questions)
|
||||||
|
print_progress_bar(0, total_q, prefix="Progress:", suffix="Starting...", length=30)
|
||||||
|
|
||||||
|
for idx, question in enumerate(questions, start=1):
|
||||||
|
# Update progress bar (Asking...)
|
||||||
|
q_short = (question[:40] + "...") if len(question) > 40 else question.ljust(43)
|
||||||
|
print_progress_bar(
|
||||||
|
idx - 1, total_q, prefix="Progress:", suffix=f"Asking: {q_short}", length=30
|
||||||
|
)
|
||||||
|
|
||||||
|
# Log to file/debug only
|
||||||
|
logger.debug("[%s/%s] Asking: %s", idx, total_q, question)
|
||||||
|
|
||||||
|
answer = ""
|
||||||
|
max_retries = 3
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
response = await session.send_and_wait(
|
||||||
|
{"prompt": question}, timeout=timeout
|
||||||
|
)
|
||||||
|
answer = response.data.content if response and response.data else ""
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error asking question (Attempt {attempt+1}/{max_retries}): {e}"
|
||||||
|
)
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
else:
|
||||||
|
answer = f"Error retrieving answer: {e}"
|
||||||
|
|
||||||
|
write_jsonl(
|
||||||
|
jsonl_path,
|
||||||
|
{
|
||||||
|
"index": idx,
|
||||||
|
"question": question,
|
||||||
|
"answer": answer,
|
||||||
|
"model": model,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
append_markdown_qa(md_path, question, answer)
|
||||||
|
|
||||||
|
# Update progress bar (Done...)
|
||||||
|
print_progress_bar(
|
||||||
|
idx, total_q, prefix="Progress:", suffix=f"Done: {q_short}", length=30
|
||||||
|
)
|
||||||
|
|
||||||
|
if delay > 0:
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
|
await session.destroy()
|
||||||
|
await client.stop()
|
||||||
|
|
||||||
|
logger.info("Saved output to %s and %s", jsonl_path, md_path)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Ask up to 100 Copilot SDK questions via GitHub Copilot SDK",
|
||||||
|
)
|
||||||
|
parser.add_argument("--model", default="gpt-5.2-codex", help="Model to use")
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-questions",
|
||||||
|
type=int,
|
||||||
|
default=100,
|
||||||
|
help="Max number of questions (1-400)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--questions-file",
|
||||||
|
default="",
|
||||||
|
help="Path to .txt or .json list of questions",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--context-url",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Additional context URL (repeatable)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-default-context",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable default Copilot SDK context URLs",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-awesome-copilot-docs",
|
||||||
|
action="store_true",
|
||||||
|
help="Include all markdown files from awesome-copilot/docs",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include-awesome-copilot-instructions",
|
||||||
|
action="store_true",
|
||||||
|
help="Include all markdown files from awesome-copilot/instructions",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-sdk-source",
|
||||||
|
action="store_true",
|
||||||
|
help="Do NOT read local SDK source code (default: reads source)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--session-id",
|
||||||
|
default="",
|
||||||
|
help="Optional custom session ID",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-dir",
|
||||||
|
default="",
|
||||||
|
help="Directory to save outputs",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--delay",
|
||||||
|
type=float,
|
||||||
|
default=0.5,
|
||||||
|
help="Delay between questions (seconds)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-context-chars",
|
||||||
|
type=int,
|
||||||
|
default=400000,
|
||||||
|
help="Max characters of aggregated context (default: 400000)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable-infinite-sessions",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable infinite sessions (default: enabled)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-lang",
|
||||||
|
default="zh-CN",
|
||||||
|
help="Output language (default: zh-CN)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--deep-dive",
|
||||||
|
action="store_true",
|
||||||
|
help="Append deep-dive prompts for more detailed research",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=int,
|
||||||
|
default=3600,
|
||||||
|
help="Session request timeout in seconds (default: 3600)",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
if args.questions_file:
|
||||||
|
questions = load_questions(args.questions_file)
|
||||||
|
else:
|
||||||
|
# Generate enough questions to cover everything
|
||||||
|
questions = build_questions(9999)
|
||||||
|
|
||||||
|
if args.deep_dive:
|
||||||
|
# Prepend deep dive questions to ensure they are prioritized
|
||||||
|
questions = build_deep_dive_prompts() + questions
|
||||||
|
|
||||||
|
questions = clamp_questions(questions, args.max_questions)
|
||||||
|
if not questions:
|
||||||
|
logger.error("No questions to ask")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
context_urls = [] if args.no_default_context else list(DEFAULT_CONTEXT_URLS)
|
||||||
|
|
||||||
|
if args.include_awesome_copilot_docs:
|
||||||
|
context_urls.extend(
|
||||||
|
list_repo_markdown_urls(
|
||||||
|
AWESOME_COPILOT_REPO,
|
||||||
|
AWESOME_COPILOT_BRANCH,
|
||||||
|
["docs/"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.include_awesome_copilot_instructions:
|
||||||
|
context_urls.extend(
|
||||||
|
list_repo_markdown_urls(
|
||||||
|
AWESOME_COPILOT_REPO,
|
||||||
|
AWESOME_COPILOT_BRANCH,
|
||||||
|
["instructions/"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
context_urls.extend(args.context_url or [])
|
||||||
|
|
||||||
|
# 1. Read local source code first (Priority: High)
|
||||||
|
# We allocate up to max_context_chars to source code initially.
|
||||||
|
# The actual usage will likely be less for a typical SDK.
|
||||||
|
source_context = ""
|
||||||
|
source_chars_count = 0
|
||||||
|
if not args.no_sdk_source:
|
||||||
|
source_context = read_local_sdk_source(args.max_context_chars)
|
||||||
|
source_chars_count = len(source_context)
|
||||||
|
logger.info(f"Source context usage: {source_chars_count} chars")
|
||||||
|
|
||||||
|
# 2. Calculate remaining budget for Web Docs (Priority: Secondary)
|
||||||
|
# We ensure we don't exceed the global limit.
|
||||||
|
remaining_chars = max(10000, args.max_context_chars - source_chars_count)
|
||||||
|
logger.info(f"Remaining budget for web docs: {remaining_chars} chars")
|
||||||
|
|
||||||
|
# 3. Fetch remote docs
|
||||||
|
web_context = build_context(context_urls, remaining_chars)
|
||||||
|
|
||||||
|
combined_context = ""
|
||||||
|
# Assemble context in order of authority (Source > Docs)
|
||||||
|
if source_context:
|
||||||
|
combined_context += (
|
||||||
|
"# PRIMARY SOURCE: LOCAL SDK CODE (AUTHORITATIVE)\n"
|
||||||
|
+ source_context
|
||||||
|
+ "\n\n"
|
||||||
|
)
|
||||||
|
if web_context:
|
||||||
|
combined_context += (
|
||||||
|
"# SECONDARY SOURCE: WEB DOCUMENTATION & AWESOME-COPILOT\n" + web_context
|
||||||
|
)
|
||||||
|
|
||||||
|
output_dir = args.output_dir or os.path.join(
|
||||||
|
os.getcwd(), "plugins", "debug", "copilot_sdk_research", "outputs"
|
||||||
|
)
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
run_session(
|
||||||
|
model=args.model,
|
||||||
|
questions=questions,
|
||||||
|
output_dir=output_dir,
|
||||||
|
context=combined_context,
|
||||||
|
session_id=args.session_id or None,
|
||||||
|
delay=args.delay,
|
||||||
|
output_lang=args.output_lang,
|
||||||
|
enable_infinite_sessions=not args.disable_infinite_sessions,
|
||||||
|
timeout=args.timeout,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
2
plugins/debug/copilot_sdk_research/dummy_test_file.txt
Normal file
2
plugins/debug/copilot_sdk_research/dummy_test_file.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
This is a dummy test file for verifying permission controls.
|
||||||
|
You can delete this file safely.
|
||||||
26
plugins/debug/copilot_sdk_research/inspect_permissions.py
Normal file
26
plugins/debug/copilot_sdk_research/inspect_permissions.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
import copilot
|
||||||
|
from copilot import types
|
||||||
|
|
||||||
|
print(f"Copilot location: {copilot.__file__}")
|
||||||
|
|
||||||
|
print("\n=== Inspecting PermissionRequest types ===")
|
||||||
|
# Look for definition of PermissionRequest or similar
|
||||||
|
|
||||||
|
# In the absence of direct access to the CLI output structure documentation,
|
||||||
|
# we can check if there are type hints or typed dicts in copilot.types
|
||||||
|
|
||||||
|
for name, obj in inspect.getmembers(types):
|
||||||
|
if "Permission" in name or "Request" in name:
|
||||||
|
print(f"\nType: {name}")
|
||||||
|
try:
|
||||||
|
if hasattr(obj, "__annotations__"):
|
||||||
|
print(obj.__annotations__)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("copilot module not installed")
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{"index": 1, "question": "Provide a python code example for configuring `CopilotClient` to connect to a local MCP server (e.g. Brave Search) via `CopilotClient` config.", "answer": "SDK 源码显示 MCP 服务器通过 `create_session`/`resume_session` 的 `mcp_servers` 配置传入并映射为 `mcpServers`,不是 CopilotClient 构造参数(源:本地源码 copilot/types.py / copilot/client.py)。下面是本地 stdio MCP(以 Brave Search 为例,命令/密钥请替换)的 Python 示例。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave_search\": {\n \"type\": \"local\", # 或 \"stdio\"\n \"command\": \"/path/to/brave-search-mcp\", # 替换为你的 MCP server 启动命令\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n \"timeout\": 30000, # 毫秒\n }\n },\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 查询 OpenAI 最新公告\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n要点/坑:\n- `tools`: `[\"*\"]` 表示全量,`[]` 表示不启用\n- `timeout`: 毫秒\n- `command/args/env`: 按你的 MCP server 实际启动方式替换\n\n建议/落地:\n- pipe 内复用同一 session,结束时统一 `client.stop()`", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 2, "question": "Explain how to programmatically handle `tool.confirmation_required` events in a non-interactive stream using `session.on()`.", "answer": "SDK 源码未定义 `tool.confirmation_required` 事件,权限确认由 JSON‑RPC `permission.request` 触发并交给 `on_permission_request` 回调处理,`session.on()` 只能订阅 `tool.user_requested`/`tool.execution_start` 等事件做记录关联(来源:本地源码 copilot/client.py、copilot/session.py、copilot/types.py)。 \n在非交互流里必须提供 `on_permission_request` 自动审批/拒绝,否则默认拒绝且回调异常也会被拒绝。\n\n要点/坑 \n- 配置键: `on_permission_request`(SessionConfig) \n- 权限类型: `shell` / `write` / `mcp` / `read` / `url` \n- 回调异常或缺失 -> `denied-no-approval-rule-and-could-not-request-from-user`\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n def on_event(ev):\n if ev.type == SessionEventType.TOOL_USER_REQUESTED:\n print(f\"tool requested: {ev.data.tool_name} {ev.data.tool_call_id}\")\n elif ev.type == SessionEventType.TOOL_EXECUTION_START:\n print(f\"tool start: {ev.data.tool_name}\")\n\n async def on_permission_request(req, ctx):\n kind = req.get(\"kind\")\n if kind in (\"read\", \"mcp\"):\n return {\"kind\": \"approved\"}\n return {\"kind\": \"denied-by-rules\", \"rules\": [{\"kind\": \"deny-non-read\"}]}\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": on_permission_request,\n })\n session.on(on_event)\n\n await session.send_and_wait({\"prompt\": \"请调用需要权限的工具\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地 \n- pipe 启动时注册审批策略 + `session.on` 记录 `tool_call_id` 做审计", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 3, "question": "Show how to implement a 'Device Flow' login helper using SDK primitives (if available) or raw HTTP showing how to persist credentials.", "answer": "SDK 本身没有 device‑flow 登录 API;可用鉴权原语只有 CopilotClient 的 `github_token`/`use_logged_in_user` 与 `get_auth_status`(源:本地源码 copilot/client.py、copilot/types.py)。 \n下面用 GitHub OAuth Device Flow 原始 HTTP 获取 token、持久化本地并注入 `github_token`(端点说明:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)。\n\n```python\nimport asyncio\nimport json\nimport os\nimport time\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\n\nfrom copilot import CopilotClient\n\nCLIENT_ID = \"YOUR_OAUTH_APP_CLIENT_ID\"\nSCOPE = \"read:user\" # 按你的 OAuth App 需求调整\nTOKEN_PATH = Path.home() / \".config\" / \"myapp\" / \"copilot_token.json\"\n\ndef http_post(url, data):\n body = urllib.parse.urlencode(data).encode()\n req = urllib.request.Request(url, data=body, headers={\"Accept\": \"application/json\"})\n with urllib.request.urlopen(req) as resp:\n return json.loads(resp.read().decode())\n\ndef load_token():\n if TOKEN_PATH.exists():\n return json.loads(TOKEN_PATH.read_text()).get(\"access_token\")\n return None\n\ndef save_token(token):\n TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)\n TOKEN_PATH.write_text(json.dumps(token))\n os.chmod(TOKEN_PATH, 0o600)\n\ndef device_flow():\n code = http_post(\n \"https://github.com/login/device/code\",\n {\"client_id\": CLIENT_ID, \"scope\": SCOPE},\n )\n print(f\"Open {code['verification_uri']} and enter {code['user_code']}\")\n interval = int(code.get(\"interval\", 5))\n\n while True:\n token = http_post(\n \"https://github.com/login/oauth/access_token\",\n {\n \"client_id\": CLIENT_ID,\n \"device_code\": code[\"device_code\"],\n \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n },\n )\n if \"access_token\" in token:\n return token\n if token.get(\"error\") == \"slow_down\":\n interval += 5\n elif token.get(\"error\") not in (\"authorization_pending\",):\n raise RuntimeError(token)\n time.sleep(interval)\n\nasync def main():\n access_token = load_token()\n if not access_token:\n token = device_flow()\n save_token(token)\n access_token = token[\"access_token\"]\n\n client = CopilotClient({\"github_token\": access_token})\n await client.start()\n # ... create_session / send ...\n await client.stop()\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n```\n\n要点/坑 \n- `cli_url` 与 `github_token` 互斥 \n- SDK 不保存凭据 需自行持久化 \n- 轮询需处理 `authorization_pending` / `slow_down`\n\n建议/落地 \n- pipe 启动时读取 token 文件 不存在则走 device flow \n- 用 `github_token` 注入 保持非交互运行", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 4, "question": "Compare the pros and cons of 'Session Replay' (fast-forwarding history) vs 'Session Resume' (stateful ID) for a stateless web backend like OpenWebUI.", "answer": "对无状态后端(OpenWebUI)而言,SDK 只支持通过 `resume_session(session_id)` 的状态续接;所谓“Session Replay”需要你自行重放历史 prompt(来源:本地 SDK 源码 copilot/client.py、copilot/session.py)。 \n对比要点(短语): \n- Replay:无状态/易扩展;缺点是耗时耗 tokens、易截断/丢工具状态、可能重复副作用 \n- Resume:省 tokens、保留工具/compaction/`workspace_path` 状态;缺点是需持久化 session_id、依赖会话存活/跨实例一致性。 \n建议/落地:优先持久化 chat_id→session_id 并 resume_session,失效时回退为重放最近 N 轮并重建工具上下文。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 5, "question": "Detail the exact protocol for 'Virtual Workspace': how to implement a file system provider that feeds content to Copilot without physical files.", "answer": "SDK 源码里没有“Virtual Workspace”协议或文件系统 provider 接口;会话只识别真实路径(`SessionConfig.working_directory`、`MessageOptions.attachments`)。要在无物理文件情况下供给内容,只能用自定义工具或 MCP 服务器把“读写文件”变成 tool 调用(来源:本地 SDK 源码 copilot/types.py / copilot/client.py)。\n\n可用原语(源码)\n- `SessionConfig.tools` / `available_tools`:自定义工具与白名单\n- `MessageOptions.attachments`:仅支持真实路径文件/目录\n\n方案 A:自定义工具实现 VFS(推荐)\n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nVFS = {\"README.md\": \"# Virtual README\\n内容来自内存\\n\"}\n\nclass VfsReadParams(BaseModel):\n path: str = Field(description=\"virtual path\")\n\n@define_tool(name=\"vfs_read\", description=\"Read a virtual file\")\ndef vfs_read(params: VfsReadParams) -> str:\n return VFS.get(params.path, \"\")\n\n@define_tool(name=\"vfs_list\", description=\"List virtual files\")\ndef vfs_list() -> list[str]:\n return sorted(VFS.keys())\n\nasync def main():\n client = CopilotClient()\n await client.start()\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [vfs_read, vfs_list],\n \"available_tools\": [\"vfs_read\", \"vfs_list\"],\n \"system_message\": {\"content\": \"Use vfs_* tools for all file access.\"},\n })\n await session.send_and_wait({\"prompt\": \"打开 README.md\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n方案 B:MCP 服务器(本地 stdio,VFS 由你实现)\n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"vfs\": {\n \"type\": \"local\",\n \"command\": \"python\",\n \"args\": [\"-m\", \"my_vfs_mcp_server\"],\n \"tools\": [\"*\"],\n }\n },\n})\n```\n\n注意/坑 \n- `available_tools` 只保留你的 VFS 工具以强制使用 \n- 内置文件工具只读真实文件;要兼容只能落地临时文件 \n- 大文件需分块返回,避免上下文爆炸 \n\n建议/落地 \n- pipe 内维护 VFS 存储(DB/Redis/内存) \n- 启动 session 时注入 `system_message` + `available_tools` 强制 VFS 入口", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 6, "question": "Create an 'Agent Manifest' example: how to define an Agent capable of specific high-privileged tools via SDK.", "answer": "SDK 没有独立“Agent Manifest”文件格式,等价于 `create_session` 的 `custom_agents`(CustomAgentConfig),并用 `tools`/`available_tools` 约束高权限工具,权限由 `on_permission_request` 处理(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \n示例如下(manifest 为 Python dict,可序列化为 JSON 配置)。\n\n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nclass AdminShellParams(BaseModel):\n cmd: str = Field(description=\"approved shell command\")\n\n@define_tool(name=\"admin_shell\", description=\"Run an approved shell command\")\ndef admin_shell(params: AdminShellParams) -> str:\n # TODO: 执行受控命令(白名单/沙箱)\n return f\"approved: {params.cmd}\"\n\nclass AdminWriteParams(BaseModel):\n path: str = Field(description=\"target path\")\n content: str = Field(description=\"content\")\n\n@define_tool(name=\"admin_write\", description=\"Write to privileged store\")\ndef admin_write(params: AdminWriteParams) -> str:\n # TODO: 受控写入\n return \"ok\"\n\nasync def on_permission_request(req, _ctx):\n if req.get(\"kind\") in (\"shell\", \"write\"):\n return {\"kind\": \"approved\"}\n return {\"kind\": \"denied-by-rules\", \"rules\": [{\"kind\": \"deny\"}]}\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n agent_manifest = {\n \"name\": \"ops-admin\",\n \"display_name\": \"Ops Admin\",\n \"description\": \"Privileged ops agent\",\n \"prompt\": \"Use only admin_* tools for privileged ops.\",\n \"tools\": [\"admin_shell\", \"admin_write\"],\n \"infer\": True,\n }\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [admin_shell, admin_write],\n \"custom_agents\": [agent_manifest],\n \"available_tools\": [\"admin_shell\", \"admin_write\"],\n \"on_permission_request\": on_permission_request,\n })\n\n await session.send_and_wait({\"prompt\": \"使用 ops-admin 执行受控操作\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n要点/坑 \n- session.tools 未注册 -> agent.tools 不可用 \n- custom_agents.tools 仅写名称 \n- 非交互缺少 on_permission_request -> 默认拒绝 \n- SDK 无显式 agent 选择参数 -> 需在 prompt 指定 \n\n建议/落地 \n- pipe 启动时加载 manifest 注入 custom_agents + available_tools + on_permission_request", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 7, "question": "List all 'hidden' `SessionConfig` parameters relevant to Agent behavior and personality.", "answer": "基于本地 SDK 源码 `copilot/types.py` 的 SessionConfig,影响 Agent 行为/人格的相关参数如下(来源:本地 SDK 源码)。 \n- `system_message`: `append`/`replace`;`replace` 会移除 SDK guardrails(风险) \n- `custom_agents`: `prompt`/`description`/`display_name`/`tools`/`mcp_servers`/`infer`;`tools` 仅名称且必须在 `tools` 注册 \n- `tools` / `available_tools` / `excluded_tools`: 能力与行为边界;`available_tools` 优先于 `excluded_tools` \n- `hooks`: `on_user_prompt_submitted`/`on_pre_tool_use`/`on_post_tool_use`/`on_session_start`/`on_session_end`/`on_error_occurred`,可改写 prompt/工具参数/结果 \n- `on_permission_request`: 非交互审批策略;缺失或异常 => 拒绝 \n- `on_user_input_request`: 启用/处理 `ask_user` \n- `mcp_servers`: 注入外部工具集 \n- `skill_directories` / `disabled_skills`: 技能启用/禁用 \n- `model` / `provider`: 模型与 BYOK 影响输出风格与能力 \n- `working_directory`: 上下文范围 \n- `infinite_sessions`: compaction 阈值影响记忆保留 \n- `config_dir`: CLI 配置/状态目录,行为取决于 CLI 侧配置 \n\n建议/落地:在 pipe 启动时固定 `system_message` + `available_tools/excluded_tools` + `hooks`,并持久化 session_id 以便 `resume_session`。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 8, "question": "Give a concise overview of MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "SDK 中 MCP 通过 create_session/resume_session 的 mcp_servers 传入(映射到 JSON‑RPC 的 mcpServers),支持 local/stdio 与 http/sse,两类配置字段分别为 type/command/args/env/cwd/timeout/tools 或 type/url/headers/timeout/tools(来源:本地 SDK 源码)。 \nSDK hooks 使用 hooks(on_pre_tool_use/on_post_tool_use/on_user_prompt_submitted/on_session_start/on_session_end/on_error_occurred) 进行拦截与附加上下文,权限需 on_permission_request 对 kind='mcp' 明确批准,且 session.on 可审计 tool.execution_* 事件中的 mcp_server_name/mcp_tool_name。 \n示例见下,建议/落地:pipe 中启用 on_permission_request、限制 tools 白名单并记录 MCP 事件。\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def on_permission_request(req, _):\n return {\"kind\": \"approved\"} if req.get(\"kind\") == \"mcp\" else {\n \"kind\": \"denied-by-rules\",\n \"rules\": [{\"kind\": \"deny\"}],\n }\n\nasync def on_pre_tool_use(input, _):\n if input.get(\"toolName\", \"\").startswith(\"mcp.\"):\n return {\"additionalContext\": \"MCP tool call observed\"}\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n \"timeout\": 30000,\n }\n },\n \"hooks\": {\"on_pre_tool_use\": on_pre_tool_use},\n \"on_permission_request\": on_permission_request,\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 查询 Copilot SDK 新闻\"})\n await client.stop()\n\nasyncio.run(main())\n```", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 9, "question": "Provide best practices and common pitfalls for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "基于本地 SDK 源码,MCP 配置通过 `create_session`/`resume_session` 的 `mcp_servers` 进入 JSON‑RPC `mcpServers`,并通过 `on_permission_request` 与 `hooks` 控制工具审批与行为(来源:本地 SDK 源码) \n- 最佳实践:local/stdio 用 `type/command/args/env/cwd/timeout/tools`;remote http/sse 用 `type/url/headers/timeout/tools`;`tools` 尽量白名单、必要时 `[\"*\"]`;`timeout` 毫秒;`hooks.on_pre_tool_use` 做改参/拒绝,`session.on` 记录 `tool.execution_*` 与 `mcp_server_name`/`mcp_tool_name` \n- 常见坑:把 MCP 配置放在 CopilotClient;缺少 `on_permission_request`(kind=\"mcp\")导致默认拒绝;`tools: []` 等于禁用全部;设置 `available_tools` 却未包含 MCP 工具名导致被过滤 \n\n建议/落地:pipe 中固定审批策略 + 统一 MCP 配置模板 + 审计日志", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 10, "question": "Show a minimal example snippet for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "基于本地 SDK 源码,MCP 通过 `mcp_servers` 传入并用 `hooks` 处理工具调用(来源:本地 SDK 源码)。最小示例: \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n }\n },\n \"hooks\": {\n \"on_pre_tool_use\": lambda input, _: {\"additionalContext\": \"MCP tool call\"},\n },\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n })\n\n await session.send_and_wait({\"prompt\": \"用 Brave Search 搜索 Copilot SDK\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地:固定 `on_permission_request`(kind=\"mcp\")并限制 `tools` 白名单。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 11, "question": "List recommended configuration defaults for MCP Server Integration: JSON-RPC config and SDK hooks.", "answer": "推荐默认值(MCP JSON‑RPC 配置,来源:本地 SDK 源码):local/stdio 服务器用 `type=\"local\"/\"stdio\"`(远程用 `http`/`sse`)、`tools` 白名单(无则 `\"*\"`)、`timeout=30000` 毫秒、`env` 仅放密钥、`cwd` 指向受控目录。 \nHooks 默认:`on_pre_tool_use` 追加上下文并可设 `permissionDecision`(ask/allow)、必要时改参,`on_post_tool_use` 记录结果,`on_error_occurred` 设置 retry/skip。 \n建议/落地:非交互场景配置 `on_permission_request` 仅放行 `kind=\"mcp\"`,否则拒绝;工具名不确定时先不设 `available_tools`,避免误过滤。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 12, "question": "How does MCP Server Integration: JSON-RPC config and SDK hooks relate to building a custom Agent?", "answer": "SDK 源码中,自定义 Agent 通过 `SessionConfig.custom_agents` 定义,`CustomAgentConfig` 支持 `mcp_servers`,因此“Agent 绑定 MCP”就是在 agent config 里声明 `mcp_servers`,底层在 `create_session` 时转成 JSON‑RPC 的 `mcpServers`(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \nHooks 是会话级拦截器(`on_pre_tool_use`/`on_post_tool_use` 等),对 MCP 工具同样生效;权限确认需 `on_permission_request` 审批 `kind=\"mcp\"`。 \n建议/落地:pipe 中为目标 Agent 绑定 MCP + 审批钩子,示例: \n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"custom_agents\": [{\n \"name\": \"search\",\n \"prompt\": \"Use MCP for search.\",\n \"mcp_servers\": {\n \"brave\": {\n \"type\": \"local\",\n \"command\": \"/path/to/brave-mcp\",\n \"args\": [],\n \"env\": {\"BRAVE_API_KEY\": \"YOUR_KEY\"},\n \"tools\": [\"*\"],\n }\n },\n }],\n \"hooks\": {\"on_pre_tool_use\": lambda i, _: {\"additionalContext\": \"mcp\"}},\n \"on_permission_request\": lambda r, _: {\"kind\": \"approved\"},\n})\n```", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 13, "question": "Give a concise overview of Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "SDK 没有独立的 Agent Manifest 文件,等价做法是用 `SessionConfig.custom_agents` 描述 agent,并用 `tools`/`available_tools`/`excluded_tools` 限定能力。 \n权限通过 `on_permission_request` 与 `hooks`(如 `on_pre_tool_use`)在会话级控制,且 `custom_agents.tools` 只能写工具名并需在 `SessionConfig.tools` 注册。 \n建议/落地:pipe 启动时加载 manifest→注入 `custom_agents` + `available_tools` + `on_permission_request` 固化权限边界。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 14, "question": "Provide best practices and common pitfalls for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "SDK 没有独立 Manifest 文件,等价于 `SessionConfig.custom_agents` + `tools/available_tools/excluded_tools` + `on_permission_request` + `hooks` 来定义能力与权限(来源:本地 SDK 源码)。 \n最佳实践:`custom_agents.tools` 只写已在 `SessionConfig.tools` 注册的名称,配合 `available_tools` 白名单与 `on_permission_request` 精准放行,必要时用 `hooks.on_pre_tool_use` 做参数校验/审计,`system_message` 选 append 以保留 guardrails。 \n常见坑:工具未注册或名称不一致、缺少 `on_permission_request` 导致默认拒绝、误用 `system_message` replace 去掉安全限制、期待 agent 自动被选中或跨实例不持久 `session_id`;建议/落地:pipe 启动时加载 manifest→注入 `custom_agents/available_tools/on_permission_request` 并持久化 `session_id`。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 15, "question": "Show a minimal example snippet for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "最小示例(本地 SDK 源码:`custom_agents` + `tools` + `available_tools` + `on_permission_request`): \n```python\nimport asyncio\nfrom pydantic import BaseModel, Field\nfrom copilot import CopilotClient, define_tool\n\nclass PingParams(BaseModel):\n text: str = Field(description=\"text to echo\")\n\n@define_tool(name=\"priv_ping\", description=\"Privileged echo\")\ndef priv_ping(params: PingParams) -> str:\n return f\"ok: {params.text}\"\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n agent_manifest = {\n \"name\": \"priv-agent\",\n \"prompt\": \"Only use priv_ping for actions.\",\n \"tools\": [\"priv_ping\"],\n \"infer\": True,\n }\n\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"tools\": [priv_ping],\n \"custom_agents\": [agent_manifest],\n \"available_tools\": [\"priv_ping\"],\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n })\n\n await session.send_and_wait({\"prompt\": \"调用 priv_ping\"})\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地:保持工具名一致并白名单化,非交互务必提供 `on_permission_request`。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 16, "question": "List recommended configuration defaults for Agent Manifests: Defining capabilities and permissions programmatically.", "answer": "基于本地 SDK 源码(copilot/types.py / copilot/client.py),推荐默认值如下: \n- custom_agents:name 必填、prompt 必填、tools 仅名称、infer=True,display_name/description 可选 \n- available_tools=白名单、excluded_tools=空、system_message=append \n- on_permission_request:除明确允许的 kind 其余拒绝;hooks.on_pre_tool_use 做审计/参数校验(其余 hooks 按需) \n建议/落地:pipe 启动时加载 manifest→注册 tools + available_tools + on_permission_request,并持久化 session_id。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 17, "question": "How does Agent Manifests: Defining capabilities and permissions programmatically relate to building a custom Agent?", "answer": "“Agent Manifest”在 SDK 里就是 `SessionConfig.custom_agents` 的配置对象,用它定义自定义 Agent 的 `name/prompt/tools/mcp_servers/infer`,并在同一 session 里用 `tools` 注册实际工具、用 `available_tools/excluded_tools` 限定能力(来源:本地 SDK 源码)。 \n权限不在 manifest 内,而是由会话级 `on_permission_request` 和 hooks(如 `on_pre_tool_use`)统一控制,且对所有 Agent 生效。 \n建议/落地:pipe 启动时把 manifest 转成 `custom_agents` 并同步注册工具 + 白名单,再配置 `on_permission_request` 作为权限策略。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 18, "question": "Give a concise overview of Headless Auth: Device Code Flow and credential persistence.", "answer": "SDK 仅提供 `CopilotClient` 的 `github_token/use_logged_in_user` 与 `get_auth_status()`,没有内置 device‑flow API(源码:copilot/client.py、copilot/types.py)。 \nHeadless Device Flow 需走 GitHub OAuth Device Flow HTTP 端点获取 token(https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow),将 token 持久化(权限 600)并通过 `CopilotClient({\"github_token\": token})` 注入,且与 `cli_url` 互斥。 \n建议/落地:启动时读 token 文件,不存在则执行 device flow 并保存,再启动客户端。\n\n```python\ntoken = load_token() or device_flow() # 调用 device code + access_token 端点\nsave_token(token) # chmod 600\nclient = CopilotClient({\"github_token\": token[\"access_token\"]})\nawait client.start()\n```", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 19, "question": "Provide best practices and common pitfalls for Headless Auth: Device Code Flow and credential persistence.", "answer": "SDK 不内置 device flow,只能用 GitHub OAuth Device Flow HTTP 换取 token 并通过 CopilotClient({github_token}) 注入,且与 cli_url 互斥(源码:copilot/client.py、copilot/types.py)。 \n最佳实践/坑:- token 文件 chmod 600 或 OS keychain;- 记录到期与刷新策略,启动时用 get_auth_status 验证;- 轮询需处理 authorization_pending/slow_down;- 未持久化、use_logged_in_user 与 github_token 同时使用或与 cli_url 混用会失败(端点说明:https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)。 \n建议/落地:pipe 启动时读 token 文件,不存在则走 device flow 并保存,失败时回退重新授权。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 20, "question": "Show a minimal example snippet for Headless Auth: Device Code Flow and credential persistence.", "answer": "最小示例:用 GitHub Device Flow(端点说明见 https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow)拿 token,持久化后注入 CopilotClient({github_token}). \n```python\nimport asyncio\nimport json\nimport os\nimport time\nimport urllib.parse\nimport urllib.request\nfrom pathlib import Path\nfrom copilot import CopilotClient\n\nCLIENT_ID = \"YOUR_OAUTH_APP_CLIENT_ID\"\nTOKEN_PATH = Path.home() / \".config\" / \"myapp\" / \"copilot_token.json\"\n\ndef post(url, data):\n body = urllib.parse.urlencode(data).encode()\n req = urllib.request.Request(url, data=body, headers={\"Accept\": \"application/json\"})\n with urllib.request.urlopen(req) as resp:\n return json.loads(resp.read().decode())\n\ndef device_flow():\n code = post(\"https://github.com/login/device/code\", {\"client_id\": CLIENT_ID, \"scope\": \"read:user\"})\n print(f\"Open {code['verification_uri']} and enter {code['user_code']}\")\n interval = int(code.get(\"interval\", 5))\n while True:\n tok = post(\n \"https://github.com/login/oauth/access_token\",\n {\n \"client_id\": CLIENT_ID,\n \"device_code\": code[\"device_code\"],\n \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",\n },\n )\n if \"access_token\" in tok:\n return tok\n if tok.get(\"error\") == \"slow_down\":\n interval += 5\n elif tok.get(\"error\") != \"authorization_pending\":\n raise RuntimeError(tok)\n time.sleep(interval)\n\ndef load_token():\n return json.loads(TOKEN_PATH.read_text()).get(\"access_token\") if TOKEN_PATH.exists() else None\n\ndef save_token(tok):\n TOKEN_PATH.parent.mkdir(parents=True, exist_ok=True)\n TOKEN_PATH.write_text(json.dumps(tok))\n os.chmod(TOKEN_PATH, 0o600)\n\nasync def main():\n access_token = load_token()\n if not access_token:\n tok = device_flow()\n save_token(tok)\n access_token = tok[\"access_token\"]\n\n client = CopilotClient({\"github_token\": access_token})\n await client.start()\n # ... create_session / send ...\n await client.stop()\n\nasyncio.run(main())\n```\n\n建议/落地 \n- 启动先读 token 文件;缺失再 device flow \n- token 文件权限 600 \n- 失效则重新授权", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 21, "question": "List recommended configuration defaults for Headless Auth: Device Code Flow and credential persistence.", "answer": "基于本地 SDK 源码(`github_token` 与 `cli_url` 互斥)与 GitHub Device Flow 文档(https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow),推荐默认值: \n- Token 持久化:`~/.config/<app>/copilot_token.json` 或系统密钥链,文件权限 600 \n- Client 选项:仅 `github_token`,`use_logged_in_user` 留空/False,禁用 `cli_url` \n- 轮询:使用 device code 返回的 `interval`(缺省 5s),处理 `authorization_pending`/`slow_down` \n- 启动校验:`get_auth_status()` 失败即重新授权 \n\n建议/落地:启动先读 token 文件 → 无则 device flow → 保存 → `client.start()`", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 22, "question": "How does Headless Auth: Device Code Flow and credential persistence relate to building a custom Agent?", "answer": "Headless Auth 只负责拿到并持久化 `github_token`,让 `CopilotClient` 能在无交互环境启动会话并加载 `custom_agents`;因此它是自定义 Agent 能稳定运行的前置条件,而不是 Agent 定义本身(来源:本地 SDK 源码)。 \n建议/落地:在无状态后端启动时读取 token 文件(不存在就走 device flow)→ `CopilotClient({\"github_token\": token})` → `create_session(custom_agents=...)`。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 23, "question": "Give a concise overview of Session Replay vs Resume: Handling stateless frontend history.", "answer": "基于本地 SDK 源码(copilot/client.py、copilot/session.py),Resume 是 `resume_session(session_id)` 保留会话状态,Replay 只能由应用重放历史 prompt。优缺点:Resume 省 tokens/保工具与 compaction 状态但依赖持久化 session_id;Replay 无状态易扩展但成本高且易丢副作用控制。建议/落地:无状态前端请保存 session_id 并优先 Resume,失效时回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 24, "question": "Provide best practices and common pitfalls for Session Replay vs Resume: Handling stateless frontend history.", "answer": "最佳实践:在无状态前端每次请求携带 session_id 并优先 `resume_session`,同时恢复 tools/hooks/on_permission_request/system_message 以保持行为一致(来源:本地 SDK 源码)。 \n常见坑:Replay 会重复执行工具且耗 tokens;Resume 未重新注册工具或权限处理器会出现 tool not supported/默认拒绝;会话失效未回退。 \n建议/落地:持久化 chat_id→session_id,resume 失败时仅回放最近 N 轮并禁用/幂等化有副作用的工具。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 25, "question": "Show a minimal example snippet for Session Replay vs Resume: Handling stateless frontend history.", "answer": "来源:本地 SDK 源码(copilot/client.py, copilot/session.py),最小示例如下(优先 Resume,失败则 Replay)。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def handle_request(session_id: str | None, history: list[str], latest: str):\n client = CopilotClient()\n await client.start()\n\n try:\n # Resume: 复用会话状态\n if not session_id:\n raise RuntimeError(\"no session_id\")\n session = await client.resume_session(session_id)\n except Exception:\n # Replay: 逐条重放历史\n session = await client.create_session({\"model\": \"gpt-5\"})\n session_id = session.session_id\n for prompt in history:\n await session.send_and_wait({\"prompt\": prompt})\n\n response = await session.send_and_wait({\"prompt\": latest})\n await client.stop()\n return response.data.content, session_id\n\n# asyncio.run(handle_request(session_id, history, latest_prompt))\n```\n建议/落地:持久化 session_id,resume 失败时仅回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 26, "question": "List recommended configuration defaults for Session Replay vs Resume: Handling stateless frontend history.", "answer": "基于本地 SDK 源码,推荐默认配置如下(Resume / Replay): \n- Resume:持久化 session_id;`resume_session({...})` 传回 tools、hooks、on_permission_request、working_directory、streaming、provider、mcp_servers、custom_agents、skill_directories、disabled_skills;`disable_resume` 保持默认 False \n- Replay:`create_session({...})` 传回 model、system_message、tools、available_tools/excluded_tools、hooks、on_permission_request、working_directory、provider、mcp_servers、custom_agents、infinite_sessions(默认启用);仅回放最近 N 轮;默认拒绝 write/shell 或用只读白名单", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 27, "question": "How does Session Replay vs Resume: Handling stateless frontend history relate to building a custom Agent?", "answer": "Resume 会保留 session_id 关联的 custom_agents/工具状态,Replay 需要每次 create_session 时重新注入 custom_agents + tools,否则 persona/能力会丢失(来源:本地 SDK 源码)。对于无状态前端,Resume 更可靠且省 token,但要持久化 session_id 并在 resume 时重新注册 tools/hooks;Replay 则需回放历史并保证相同 custom_agents 配置。建议/落地:pipe 优先 resume_session,失败再 replay 最近 N 轮并重建 custom_agents/tools/权限策略。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 28, "question": "Give a concise overview of Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "拦截/修改用户提示要用 `SessionHooks.on_user_prompt_submitted`(不是 `session.on` 事件订阅),`input.prompt` 可改为 `output.modifiedPrompt` 并附加 `additionalContext`/`suppressOutput`,且需在 `create_session`/`resume_session` 传入 `hooks` 才会触发 `hooks.invoke`(来源:本地 SDK 源码 copilot/types.py、copilot/session.py、copilot/client.py)。 \n```python\nfrom copilot import CopilotClient\n\nasync def on_user_prompt_submitted(input, _ctx):\n return {\"modifiedPrompt\": input[\"prompt\"].strip(), \"additionalContext\": \"normalize whitespace\"}\n\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"hooks\": {\"on_user_prompt_submitted\": on_user_prompt_submitted},\n})\n```\n建议/落地:在 pipe 入口统一做 prompt 清洗/拼接上下文,并记录修改前后用于审计。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 29, "question": "Provide best practices and common pitfalls for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "最佳实践:在 create_session/resume_session 传入 hooks.on_user_prompt_submitted,返回 {modifiedPrompt, additionalContext, suppressOutput},修改保持幂等、最小化、可审计(来源:本地 SDK 源码 copilot/types.py、copilot/client.py、copilot/session.py)。 \n常见坑:忘记传 hooks 或 resume 时未重新传、handler 抛异常会被吞并返回 None、返回非 dict 或过度改写提示导致无效/偏航。 \n建议/落地:在 pipe 入口统一注册该 hook 并记录修改前后,保持 system_message 为 append。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 30, "question": "Show a minimal example snippet for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "最小示例(来源:本地 SDK 源码): \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def on_user_prompt_submitted(hook_input, _ctx):\n return {\n \"modifiedPrompt\": hook_input[\"prompt\"].strip(),\n \"additionalContext\": \"normalized\",\n }\n\nasync def main():\n client = CopilotClient()\n await client.start()\n session = await client.create_session({\n \"model\": \"gpt-5\",\n \"hooks\": {\"on_user_prompt_submitted\": on_user_prompt_submitted},\n })\n await session.send_and_wait({\"prompt\": \" hello \"})\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:在 pipe 入口统一注册该 hook,并记录修改前后以便审计。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 31, "question": "List recommended configuration defaults for Advanced Session Hooks: Intercepting and modifying user prompts.", "answer": "- 推荐默认值(来源:本地 SDK 源码 copilot/types.py):`hooks={\"on_user_prompt_submitted\": handler}`,handler 默认返回 `{\"modifiedPrompt\": input[\"prompt\"], \"additionalContext\": \"\", \"suppressOutput\": False}` 或 `None`(不改写)。 \n- `create_session`/`resume_session` 都要传同一 hooks;`system_message` 保持 append 以保留 guardrails。 \n- 建议/落地:pipe 入口统一注册该 hook 并记录原/改提示用于审计。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 32, "question": "How does Advanced Session Hooks: Intercepting and modifying user prompts relate to building a custom Agent?", "answer": "Hooks 是会话级拦截器,对所有自定义 Agent 都生效;因此它是定义 Agent 行为边界和输入规范化的“外层控制”,而不是 Agent Manifest 本身(来源:本地 SDK 源码 copilot/types.py、copilot/session.py)。 \n在自定义 Agent 场景下,`on_user_prompt_submitted` 可统一清洗/增强用户指令,再由 custom_agents 的 prompt 决定具体风格。 \n建议/落地:pipe 启动时同时注入 custom_agents + hooks,确保 resume 时同样注册 hooks。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 33, "question": "Give a concise overview of Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 只有 `SessionConfig.working_directory` 来约束 CWD,`attachments` 也要求真实路径,没有“虚拟工作区/FS provider”协议(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。要处理远程/虚拟文件,需用自定义工具或 MCP 服务器实现 VFS 读写,或把内容落地到 working_directory 下的临时文件再交给模型。建议/落地:pipe 固定 working_directory 映射到会话工作区,统一走 vfs_* 工具/MCP,且在 on_permission_request 限制 write/shell。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 34, "question": "Provide best practices and common pitfalls for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 没有虚拟工作区/FS provider,仅支持 `SessionConfig.working_directory` 与真实路径 `attachments`(来源:本地 SDK 源码),因此远程/虚拟文件需落地或用自定义工具/MCP 提供内容。 \n最佳实践:固定 working_directory 到可控目录/会话 workspace,VFS 工具分块返回,配合 `available_tools`/`on_permission_request` 限权;常见坑:直接传虚拟路径/URL、working_directory 不存在、resume 未重注册工具/权限、一次性返回过大内容导致上下文截断。 \n建议/落地:pipe 入口做 remote→临时文件或 vfs_* 映射,并持久化路径索引。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 35, "question": "Show a minimal example snippet for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "SDK 仅提供 `SessionConfig.working_directory` 与真实路径附件,没有虚拟 FS/provider(来源:本地 SDK 源码 copilot/types.py)。最小做法是把远程内容落地到 CWD 下再作为附件传入: \n```python\nimport asyncio\nimport pathlib\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n workdir = \"/tmp/owui\"\n pathlib.Path(workdir).mkdir(parents=True, exist_ok=True)\n remote_content = \"...\"\n path = f\"{workdir}/remote.txt\"\n pathlib.Path(path).write_text(remote_content)\n\n session = await client.create_session({\"model\": \"gpt-5\", \"working_directory\": workdir})\n await session.send_and_wait({\n \"prompt\": \"Summarize remote.txt\",\n \"attachments\": [{\"type\": \"file\", \"path\": path}],\n })\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:pipe 入口固定 working_directory,落地临时文件后立即处理,并用 `on_permission_request` 限制 write/shell。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 36, "question": "List recommended configuration defaults for Workspace Virtualization: Handling CWD for remote/virtual files.", "answer": "推荐默认值(来源:本地 SDK 源码 copilot/types.py): \n- `working_directory`: 指向可控的会话目录(如 session.workspace_path 或 /tmp/<app>/<session_id>) \n- `available_tools`: 只允许 vfs_* 或只读工具,避免任意 shell/write \n- `on_permission_request`: 默认拒绝 write/shell,仅放行 read/mcp \n- `attachments`: 仅真实路径文件;远程内容先落地 \n\n建议/落地:pipe 入口统一映射 remote→临时文件或 VFS 工具,并持久化映射表。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 37, "question": "How does Workspace Virtualization: Handling CWD for remote/virtual files relate to building a custom Agent?", "answer": "SDK 没有虚拟文件系统协议,custom agent 只能依赖 `working_directory` 和真实路径附件,因此处理远程/虚拟文件需通过自定义工具/MCP 并在 agent prompt/available_tools 中强制使用(来源:本地 SDK 源码 copilot/types.py、copilot/client.py)。 \n这使“工作区虚拟化”成为 agent 能力与权限设计的一部分,需要 `available_tools`/`on_permission_request`/hooks 来限定访问范围。 \n建议/落地:pipe 中固定 working_directory 到会话目录,注入 vfs_* 工具白名单并在 prompt 明确使用 VFS。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 38, "question": "Give a concise overview of Error Recovery: Handling session disconnects and re-auth.", "answer": "本地 SDK 源码中没有显式“重连”API,断线通常表现为请求异常或 `SessionEventType.SESSION_ERROR`,需要 `stop()/force_stop()` 后重新 `start()` 并 `resume_session`(失败则 replay)(来源:本地 SDK 源码)。 \n认证可用 `get_auth_status()` 检测,未认证时重新创建 `CopilotClient({\"github_token\": token})`;若 `cli_url` 连接外部服务器则认证由外部处理(来源:本地 SDK 源码)。\n\n```python\nimport asyncio\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nasync def safe_send(session_id, prompt, token):\n client = CopilotClient({\"github_token\": token})\n await client.start()\n try:\n session = await (client.resume_session(session_id) if session_id else client.create_session({\"model\": \"gpt-5\"}))\n session.on(lambda ev: ev.type == SessionEventType.SESSION_ERROR and print(ev.data.message))\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp, session.session_id\n except Exception:\n await client.stop()\n client = CopilotClient({\"github_token\": token})\n await client.start()\n session = await client.create_session({\"model\": \"gpt-5\"})\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp, session.session_id\n finally:\n await client.stop()\n```\n\n建议/落地:持久化 session_id 与 token,优先 resume,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 39, "question": "Provide best practices and common pitfalls for Error Recovery: Handling session disconnects and re-auth.", "answer": "最佳实践(基于本地 SDK 源码):监听 `SESSION_ERROR`/捕获请求异常后 `stop()/force_stop()`→重新 `start()`→`resume_session`(失败再 replay),启动时用 `get_auth_status()` 校验并在失效时重建 `CopilotClient({\"github_token\": ...})`,同时在 resume 时重新注册 tools/hooks/权限处理器。 \n常见坑:把 `cli_url` 与 `github_token` 混用、忘记重注册工具导致 `tool not supported`、缺少 `on_permission_request` 导致默认拒绝、以及不回退到 replay 造成会话中断。 \n建议/落地:无状态后端持久化 `session_id+token`、实现幂等的重试/回放策略并记录错误上下文用于告警。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 40, "question": "Show a minimal example snippet for Error Recovery: Handling session disconnects and re-auth.", "answer": "最小示例(本地 SDK 源码:copilot/client.py、copilot/session.py): \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def send_with_recovery(session_id, prompt, token):\n client = CopilotClient({\"github_token\": token})\n await client.start()\n try:\n session = await (client.resume_session(session_id) if session_id else client.create_session({\"model\": \"gpt-5\"}))\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp.data.content, session.session_id\n except Exception:\n await client.stop()\n client = CopilotClient({\"github_token\": token})\n await client.start()\n session = await client.create_session({\"model\": \"gpt-5\"})\n resp = await session.send_and_wait({\"prompt\": prompt})\n return resp.data.content, session.session_id\n finally:\n await client.stop()\n```\n建议/落地:持久化 token+session_id,resume 失败再重建并回放必要历史。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 41, "question": "List recommended configuration defaults for Error Recovery: Handling session disconnects and re-auth.", "answer": "推荐默认值(字段来源:本地 SDK 源码):CopilotClientOptions—auto_start=True、log_level='info'、use_stdio=True(非 cli_url)、github_token=持久化 token;SessionConfig/ResumeSessionConfig—hooks.on_error_occurred=handler、on_permission_request=非交互审批、working_directory 固定、mcp_servers/custom_agents/streaming 与首次一致、可固定 config_dir 并持久化 session_id。 \n常见坑:cli_url 与 github_token 互斥、resume 时未重传 tools/hooks 导致 tool not supported 或默认拒绝。 \n建议/落地:pipe 保存 token+session_id,断线后先 resume_session,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 42, "question": "How does Error Recovery: Handling session disconnects and re-auth relate to building a custom Agent?", "answer": "错误恢复与自定义 Agent 强相关:Agent 定义与工具/权限处理器都是 session 级配置,断线或重认证后必须重建客户端并重新注入 `custom_agents`、`tools`、`hooks`、`on_permission_request`,否则能力/人格会丢失(来源:本地 SDK 源码)。 \n建议/落地:持久化 session_id+token,重连优先 `resume_session` 并重新注册上述配置,失败再回放最近 N 轮。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 43, "question": "Give a concise overview of Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 源码里没有独立的 `confirmation_required` 事件;权限确认是 JSON‑RPC `permission.request`,通过 `SessionConfig.on_permission_request` 回调处理(缺失或异常会默认拒绝)。 \n`session.on()` 只能用于审计/关联工具事件(如 `tool.user_requested`、`tool.execution_start`),不能替代确认逻辑。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 44, "question": "Provide best practices and common pitfalls for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 源码未定义 `confirmation_required` 事件,确认流程必须用 `SessionConfig.on_permission_request` 处理,缺失或异常会默认拒绝(来源:本地 SDK 源码)。 \n最佳实践/坑: \n- ✅ 对 `kind` 建立白名单策略(如仅允许 `read/mcp`),记录决策并用 `session.on` 仅做审计关联 `tool.user_requested` \n- ✅ `resume_session` 时重新注册 `on_permission_request`/hooks \n- ❌ 误以为 `session.on()` 可完成确认、返回非规范 dict 或抛异常导致全部拒绝", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 45, "question": "Show a minimal example snippet for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "SDK 里没有 `confirmation_required` 事件,需用 `SessionConfig.on_permission_request` 处理权限确认。 \n最小示例: \n```python\nfrom copilot import CopilotClient\n\nclient = CopilotClient()\nawait client.start()\n\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n})\n```\n建议/落地:在 pipe 中统一注册 on_permission_request,并在 resume_session 时重复注册。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 46, "question": "List recommended configuration defaults for Confirmation Events: programmatic handling of 'confirmation_required'.", "answer": "推荐默认值(SDK 无 confirmation_required 事件,改用 on_permission_request,来源:本地 SDK 源码):- 对 kind 建白名单(常见只放行 read/mcp)- 未命中默认返回 denied-by-rules 并给 rules - resume_session 时重注册 on_permission_request/hooks。 \n建议/落地:pipe 启动即注入 on_permission_request 并记录审批日志。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 47, "question": "How does Confirmation Events: programmatic handling of 'confirmation_required' relate to building a custom Agent?", "answer": "SDK 没有 `confirmation_required` 事件,权限确认由 `create_session`/`resume_session` 的 `on_permission_request` 处理,且对同一 session 的所有 custom agent 生效(来源:本地 SDK 源码)。 \n建议/落地:在 pipe 启动时注入 `on_permission_request` 白名单策略并在 resume 时重注册,以约束高权限工具。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 48, "question": "Give a concise overview of Skills: Conflict resolution and precedence defaults.", "answer": "在本地 SDK 源码中,Skills 只通过 SessionConfig 的 `skill_directories` 与 `disabled_skills` 暴露,未定义冲突解析/优先级规则,因此行为取决于 CLI 运行时实现(来源:本地 SDK 源码 copilot/types.py)。 \n为可控性,建议确保技能名唯一、仅配置单一目录或由你自行按顺序拼接目录,并用 `disabled_skills` 明确屏蔽同名/不应启用的技能。 \n建议/落地:在无状态后端每次 `create_session`/`resume_session` 时显式传入同一份 `skill_directories` 与 `disabled_skills`,避免因默认加载顺序差异导致行为漂移。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 49, "question": "Provide best practices and common pitfalls for Skills: Conflict resolution and precedence defaults.", "answer": "SDK 源码仅暴露 SessionConfig/ResumeSessionConfig 的 `skill_directories` 和 `disabled_skills`,未定义冲突解析或优先级,行为由 CLI 侧决定(来源:本地 SDK 源码 copilot/types.py)。 \n最佳实践:技能名全局唯一、只配置单一目录或显式排序,并用 `disabled_skills` 禁用冲突项;常见坑是依赖默认加载顺序或 resume 未重传这两个字段导致行为漂移。 \n建议/落地:在 pipe 启动与 resume 时统一注入同一份 `skill_directories`/`disabled_skills` 并记录变更。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 50, "question": "Show a minimal example snippet for Skills: Conflict resolution and precedence defaults.", "answer": "SDK 里技能只通过 `skill_directories` 与 `disabled_skills` 配置,冲突/优先级未在源码定义,需自行避免同名。 \n最小示例: \n```python\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"skill_directories\": [\"/opt/skills\"],\n \"disabled_skills\": [\"legacy_skill\"],\n})\n``` \n建议:技能名唯一并在 `resume_session` 时重传这两项。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 51, "question": "List recommended configuration defaults for Skills: Conflict resolution and precedence defaults.", "answer": "推荐默认值(基于 SDK 仅暴露 `skill_directories`/`disabled_skills`): \n- `skill_directories`: 明确设置为单一目录(或你自己控制顺序的列表),不要依赖 CLI 默认搜索顺序 \n- `disabled_skills`: 默认空列表;如存在同名/旧技能则显式禁用 \n- Resume/Replay:始终复用同一份配置以避免“隐式优先级”漂移", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 52, "question": "How does Skills: Conflict resolution and precedence defaults relate to building a custom Agent?", "answer": "Skills 是会话级配置(`skill_directories`/`disabled_skills`),会影响所有自定义 Agent 的可用能力与提示行为;SDK 不定义冲突优先级,因此需要你在构建自定义 Agent 时确保技能命名唯一并显式禁用冲突项(来源:本地 SDK 源码)。 \n建议/落地:为 custom agent 固定同一份 skills 配置,并在 resume 时重复传入,避免行为漂移。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 53, "question": "Give a concise overview of Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "本地 SDK 源码里 JsonRpcClient 没有内置 JSON‑RPC trace,只能通过 `CopilotClient({\"log_level\":\"debug\"})` 打开 CLI 侧日志(非原始包)(来源:本地 SDK 源码)。 \n要抓实际 JSON‑RPC 流量,可 monkey‑patch `JsonRpcClient._send_message`/`_read_message` 或包装 stdio 进行 tee,最小示例如下。 \n建议/落地:在 pipe 中开启 patch 并对日志做脱敏与采样。 \n```python\nfrom copilot.jsonrpc import JsonRpcClient\n\norig_send = JsonRpcClient._send_message\nasync def _send(self, message):\n print(\">>\", message)\n return await orig_send(self, message)\nJsonRpcClient._send_message = _send\n\norig_read = JsonRpcClient._read_message\ndef _read(self):\n msg = orig_read(self)\n if msg:\n print(\"<<\", msg)\n return msg\nJsonRpcClient._read_message = _read\n```", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 54, "question": "Provide best practices and common pitfalls for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "源码里 JsonRpcClient 没有内置 trace,只能用 `CopilotClient({\"log_level\":\"debug\"})` 查看 CLI 侧日志或 monkey‑patch `JsonRpcClient._send_message/_read_message` 做 tee(来源:本地 SDK 源码)。 \n最佳实践:仅在调试启用、对 github_token/API key/内容脱敏、采样/限流、只读不改消息并注意线程安全;常见坑:记录完整 prompt/工具参数导致泄露,或同步打印阻塞 IO 造成超时/断连。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 55, "question": "Show a minimal example snippet for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "最小示例(monkey‑patch `JsonRpcClient` 打印收发): \n```python\nfrom copilot.jsonrpc import JsonRpcClient\n\norig_send = JsonRpcClient._send_message\nasync def _send(self, msg):\n print(\">>\", msg)\n return await orig_send(self, msg)\nJsonRpcClient._send_message = _send\n\norig_read = JsonRpcClient._read_message\ndef _read(self):\n msg = orig_read(self)\n if msg:\n print(\"<<\", msg)\n return msg\nJsonRpcClient._read_message = _read\n```\n建议:仅在调试启用并脱敏日志。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 56, "question": "List recommended configuration defaults for Debugging: Tracing JSON-RPC traffic in the SDK.", "answer": "推荐默认值(基于 SDK 源码):- 生产环境关闭 trace;- 调试时 `CopilotClient({\"log_level\":\"debug\"})`;- 如需 JSON‑RPC 报文,采用临时 monkey‑patch 并启用脱敏/采样;- 不修改消息内容,避免线程阻塞。 \n建议/落地:pipe 中通过环境变量开关调试与采样率。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 57, "question": "How does Debugging: Tracing JSON-RPC traffic in the SDK relate to building a custom Agent?", "answer": "JSON‑RPC trace 能看到 custom_agents 在 `session.create/session.resume` 的实际 payload,以及后续 `tool.call`/`permission.request`/`hooks.invoke` 是否按预期触发,从而验证自定义 Agent 的能力与权限边界(来源:本地 SDK 源码)。 \n建议/落地:仅在开发环境开启并脱敏日志;用它核对 customAgents/tools/权限决策与 MCP 配置。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 58, "question": "Give a concise overview of Billing & Policies: How seat management affects SDK features.", "answer": "- 访问与政策:SDK 依赖 Copilot CLI,需有效 Copilot 订阅且组织/企业未禁用 CLI,否则无法使用(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md) \n- 计费与认证:每次 prompt 计入 Copilot premium requests 配额,PAT 需 Copilot Requests 权限(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)\n\n建议/落地:启动先 `get_auth_status()`,失败时提示分配席位/启用 CLI 或更新 token", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 59, "question": "Provide best practices and common pitfalls for Billing & Policies: How seat management affects SDK features.", "answer": "最佳实践:确认用户/组织席位允许 Copilot CLI/SDK 使用,启动前用 `get_auth_status()` 预检,使用 PAT 时确保具备 Copilot Requests 权限(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n常见坑:未分配席位或组织/企业策略禁用 CLI 导致 SDK 失效、PAT 权限不足或 premium requests 用尽引发认证/配额错误(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:在 pipe 启动时做席位与鉴权检测并给出明确提示/自助链接,记录每次请求计费与配额告警阈值以便限流。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 60, "question": "Show a minimal example snippet for Billing & Policies: How seat management affects SDK features.", "answer": "基于 SDK README,最小示例是在启动时用 `get_auth_status()` 检查是否已分配席位/可用认证(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md)。 \n```python\nimport asyncio\nfrom copilot import CopilotClient\n\nasync def main():\n client = CopilotClient()\n await client.start()\n\n auth = await client.get_auth_status()\n if not auth.isAuthenticated:\n raise RuntimeError(\"Copilot 未认证/未分配席位,请先登录或分配权限\")\n\n # ...正常创建 session...\n await client.stop()\n\nasyncio.run(main())\n```\n建议/落地:pipe 启动先做该检查,失败时提示分配席位或更新 PAT 权限。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 61, "question": "List recommended configuration defaults for Billing & Policies: How seat management affects SDK features.", "answer": "推荐默认值:CopilotClient 用 `use_logged_in_user=True`、`auto_start=True`、`log_level=\"info\"`;如用 PAT 则用 `github_token` 注入并确保具备 Copilot Requests 权限,且启动前 `get_auth_status()` 失败即按席位/策略限制处理(来源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md ,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:pipe 启动先做 auth 预检并给出续订/分配席位提示,避免会话中途才触发权限或配额错误。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 62, "question": "How does Billing & Policies: How seat management affects SDK features relate to building a custom Agent?", "answer": "席位与组织策略决定 Copilot CLI/SDK 是否可用,因此自定义 Agent 只是同一会话上的配置,前提仍是有有效订阅与允许的策略,否则 Agent 无法运行(源:https://raw.githubusercontent.com/github/copilot-sdk/main/README.md ,https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。 \n建议/落地:在创建自定义 Agent 前先 `get_auth_status()` 预检并对配额/政策限制做降级提示。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 63, "question": "How to configure MCP servers in ~/.copilot/config.json for SDK usage?", "answer": "我在 CLI 文档里未看到 `~/.copilot/config.json` 的 MCP 配置结构;CLI 仅提供 `/mcp add|edit|delete|show` 命令管理配置(来源:Copilot CLI 文档)。 \n因此建议用 CLI 写入配置,并在 SDK 中用 `config_dir` 指向该配置目录;或直接在 SDK 的 `create_session(mcp_servers=...)` 传入。 \n如果你确认存在 `~/.copilot/config.json` 的具体 schema,请提供示例或路径,我可按你的文件格式给出精确配置。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 64, "question": "What CLI environment variables force 'Agent' mode vs 'Generic' mode?", "answer": "CLI 文档未提到任何“通过环境变量强制 Agent 模式/Generic 模式”的开关(源:Copilot CLI 文档)。 \n已知与模式相关的是命令/参数,例如 `--agent` 选择自定义 agent,或交互内 `/agent` 切换;但不是环境变量。 \n如果你指的是内部实验开关,请提供具体变量名或版本线索,我可继续追索。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 65, "question": "Explain the 'confirmation' flow in CLI and how it maps to SDK events.", "answer": "- CLI:工具权限弹窗确认,可用 `/allow-all` 或 `/yolo` 自动批准。(源: https://raw.githubusercontent.com/github/copilot-cli/main/README.md) \n- SDK:对应 JSON‑RPC `permission.request`→`on_permission_request` 返回 `approved/denied-*`,`session.on()` 只用于审计 tool.execution_*,无独立 confirmation 事件。(源: 本地 SDK 源码 copilot/client.py、copilot/session.py、copilot/types.py)\n\n示例:\n```python\nfrom copilot import CopilotClient\nfrom copilot.generated.session_events import SessionEventType\n\nclient = CopilotClient()\nawait client.start()\nsession = await client.create_session({\n \"model\": \"gpt-5\",\n \"on_permission_request\": lambda req, _: {\"kind\": \"approved\"},\n})\nsession.on(lambda ev: ev.type == SessionEventType.TOOL_EXECUTION_START and print(ev.data.tool_name))\n```\n\n建议/落地:非交互模式实现 `on_permission_request` 并记录 `tool.execution_*` 事件。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 66, "question": "Does the CLI support 'dry-run' permission checks for tools?", "answer": "CLI 文档未提到“dry-run”权限检查,只有正常确认与 `/allow-all`、`/yolo` 自动批准(源:https://raw.githubusercontent.com/github/copilot-cli/main/README.md)。SDK 侧仅有 `permission.request`→`on_permission_request` 与 `hooks.on_pre_tool_use` 的拦截/拒绝(本地 SDK 源码),不提供不执行的试运行;建议/落地:pipe 中用 `on_permission_request` 记录/拒绝高风险工具并在 hooks 做审计。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 67, "question": "What are the undocumented requirements for 'workspace' context updates?", "answer": "CLI 文档与本地 SDK 源码都未提到“workspace context updates”的未公开要求;SDK 只暴露 `working_directory` 与 `session.workspace_path`,以及 `attachments` 真实路径(来源:本地 SDK 源码)。 \n如果你指的是 CLI 的“工作区/会话文件”更新规则,请提供具体页面或日志片段,我可以进一步定位。", "model": "gpt-5.2-codex"}
|
||||||
|
{"index": 68, "question": "How does the CLI handle 'device code' re-authentication automatically?", "answer": "CLI 文档未说明“自动 device code 重新认证”的具体机制;只提到未登录时使用 `/login`,PAT 可通过 `GH_TOKEN/GITHUB_TOKEN` 提供(源:Copilot CLI 文档)。因此无法确认是否有自动 re-auth 或缓存刷新策略。 \n若需无交互场景,请使用 SDK 的 `github_token` 注入并自行实现 device flow 轮询与持久化。", "model": "gpt-5.2-codex"}
|
||||||
File diff suppressed because it is too large
Load Diff
186
plugins/debug/copilot_sdk_research/test_multi_rule_permission.py
Normal file
186
plugins/debug/copilot_sdk_research/test_multi_rule_permission.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
"""
|
||||||
|
Test multi-rule permission control pattern (VSCode-style)
|
||||||
|
Tests ordered rule matching like VSCode's chat.tools.terminal.autoApprove
|
||||||
|
SAFE VERSION: Uses harmless commands (echo/ls) only. No rm, no git.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
|
from copilot import CopilotClient
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def build_multi_rule_handler(rules_json: str):
|
||||||
|
"""
|
||||||
|
Build permission handler with ordered rules (VSCode-style)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rules = json.loads(rules_json) if rules_json else {}
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error("Invalid rules JSON: %s", e)
|
||||||
|
rules = {}
|
||||||
|
|
||||||
|
async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]):
|
||||||
|
kind = request.get("kind")
|
||||||
|
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||||
|
|
||||||
|
# Always approve read and url
|
||||||
|
if kind in ("read", "url"):
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
# For shell commands, apply ordered rules
|
||||||
|
if kind == "shell" and command:
|
||||||
|
for pattern, approved in rules.items():
|
||||||
|
try:
|
||||||
|
if re.match(pattern, command):
|
||||||
|
if approved:
|
||||||
|
logger.info(
|
||||||
|
"✅ Approved (rule match): pattern=%r command=%r",
|
||||||
|
pattern,
|
||||||
|
command,
|
||||||
|
)
|
||||||
|
return {"kind": "approved"}
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"❌ Denied (rule match): pattern=%r command=%r",
|
||||||
|
pattern,
|
||||||
|
command,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"kind": "denied-by-rules",
|
||||||
|
"rules": [
|
||||||
|
{"kind": "multi-rule-deny", "pattern": pattern}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
except re.error as exc:
|
||||||
|
logger.error("Invalid pattern %r: %s", pattern, exc)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Default deny for shell without matching rule
|
||||||
|
logger.warning("❌ Denied (no matching rule): command=%r", command)
|
||||||
|
return {"kind": "denied-by-rules", "rules": [{"kind": "no-rule-match"}]}
|
||||||
|
|
||||||
|
return on_permission_request
|
||||||
|
|
||||||
|
|
||||||
|
async def run_test(model: str, rules_json: str, prompt: str) -> Tuple[bool, str]:
|
||||||
|
"""Run a single test and return (approved, response)"""
|
||||||
|
try:
|
||||||
|
client = CopilotClient()
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
session = await client.create_session(
|
||||||
|
{
|
||||||
|
"model": model,
|
||||||
|
"on_permission_request": build_multi_rule_handler(rules_json),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set a short timeout
|
||||||
|
try:
|
||||||
|
response = await asyncio.wait_for(
|
||||||
|
session.send_and_wait({"prompt": prompt}), timeout=15.0
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.error("Test Timed Out")
|
||||||
|
return (False, "Timeout")
|
||||||
|
finally:
|
||||||
|
await client.stop()
|
||||||
|
|
||||||
|
content = response.data.content
|
||||||
|
# Heuristics to detect denial in response
|
||||||
|
denied_keywords = [
|
||||||
|
"不允许",
|
||||||
|
"无法",
|
||||||
|
"对不起",
|
||||||
|
"Sorry",
|
||||||
|
"can't",
|
||||||
|
"cannot",
|
||||||
|
"not have permission",
|
||||||
|
"denied",
|
||||||
|
]
|
||||||
|
is_denied = any(kw in content for kw in denied_keywords)
|
||||||
|
|
||||||
|
return (not is_denied, content)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Test failed: %s", e)
|
||||||
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--model", default="gpt-4.1", help="Model ID")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# LOGIC TEST RULES
|
||||||
|
# 1. Deny "echo secret" explicitly (Specific Deny)
|
||||||
|
# 2. Allow "echo" anything else (General Allow)
|
||||||
|
# 3. Allow "ls" (General Allow)
|
||||||
|
# 4. Deny everything else (Default Deny)
|
||||||
|
logic_test_rules = {
|
||||||
|
"^echo\\s+secret": False, # Higher priority: Deny specific echo
|
||||||
|
"^echo": True, # Lower priority: Allow general echo
|
||||||
|
"^ls": True, # Allow ls
|
||||||
|
".*": False, # Deny everything else (e.g. whoami)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules_json = json.dumps(logic_test_rules)
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
# 1. Matches Rule 2 (^echo) -> Should be Approved
|
||||||
|
("Allowed: Normal Echo", "请执行: echo 'hello world'", True),
|
||||||
|
# 2. Matches Rule 3 (^ls) -> Should be Approved
|
||||||
|
("Allowed: LS", "请执行: ls -la", True),
|
||||||
|
# 3. Matches Rule 1 (^echo\s+secret) -> Should be DENIED
|
||||||
|
# This proves the ORDER matters. If it matched Rule 2 first, it would be allowed.
|
||||||
|
("Denied: Restricted Echo", "请执行: echo secret data", False),
|
||||||
|
# 4. Matches Rule 4 (.*) -> Should be DENIED
|
||||||
|
("Denied: Unknown Command", "请执行: whoami", False),
|
||||||
|
]
|
||||||
|
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("Safe Multi-Rule Logic Test (Proving Precedence)")
|
||||||
|
logger.info("Rules: %s", json.dumps(logic_test_rules, indent=2))
|
||||||
|
logger.info("=" * 80)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for i, (name, prompt, expected) in enumerate(test_cases, 1):
|
||||||
|
logger.info("\n[Test %d/%d] %s", i, len(test_cases), name)
|
||||||
|
logger.info(" Prompt: %s", prompt)
|
||||||
|
|
||||||
|
approved, response = await run_test(args.model, rules_json, prompt)
|
||||||
|
passed = approved == expected
|
||||||
|
|
||||||
|
status = "✅ PASS" if passed else "❌ FAIL"
|
||||||
|
results.append((name, passed))
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
" Expected: %s, Got: %s - %s",
|
||||||
|
"Approved" if expected else "Denied",
|
||||||
|
"Approved" if approved else "Denied",
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
logger.info(" Response: %s", response[:100].replace("\n", " "))
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
logger.info("\n" + "=" * 80)
|
||||||
|
logger.info("Test Summary")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
passed_count = sum(1 for _, passed in results if passed)
|
||||||
|
for name, passed in results:
|
||||||
|
logger.info("%s %s", "✅" if passed else "❌", name)
|
||||||
|
logger.info("Total: %d/%d tests passed", passed_count, len(results))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
"""
|
||||||
|
Comprehensive Permission Control Test Suite
|
||||||
|
Tests all permission control scenarios for GitHub Copilot SDK
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
|
from copilot import CopilotClient
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def build_permission_handler(allow_all: bool, allow_shell: bool, pattern: str):
|
||||||
|
async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]):
|
||||||
|
kind = request.get("kind")
|
||||||
|
# Shell requests use 'fullCommandText' not 'command'
|
||||||
|
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||||
|
|
||||||
|
if allow_all:
|
||||||
|
logger.info("✅ Approved (allow-all): kind=%s command=%r", kind, command)
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
if kind in ("read", "url"):
|
||||||
|
logger.info("✅ Approved (safe): kind=%s", kind)
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
if kind == "shell":
|
||||||
|
if allow_shell:
|
||||||
|
logger.info("✅ Approved (allow-shell): command=%r", command)
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
if pattern and command:
|
||||||
|
try:
|
||||||
|
if re.match(pattern, command):
|
||||||
|
logger.info(
|
||||||
|
"✅ Approved (regex match): pattern=%r command=%r",
|
||||||
|
pattern,
|
||||||
|
command,
|
||||||
|
)
|
||||||
|
return {"kind": "approved"}
|
||||||
|
except re.error as exc:
|
||||||
|
logger.error("Invalid regex pattern: %s (%s)", pattern, exc)
|
||||||
|
|
||||||
|
logger.warning("❌ Denied: kind=%s command=%r", kind, command)
|
||||||
|
return {"kind": "denied-by-rules", "rules": [{"kind": "test-suite"}]}
|
||||||
|
|
||||||
|
return on_permission_request
|
||||||
|
|
||||||
|
|
||||||
|
async def run_test(
|
||||||
|
model: str, allow_all: bool, allow_shell: bool, pattern: str, prompt: str
|
||||||
|
) -> Tuple[bool, str]:
|
||||||
|
"""Run a single test and return (success, response)"""
|
||||||
|
try:
|
||||||
|
client = CopilotClient()
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
session = await client.create_session(
|
||||||
|
{
|
||||||
|
"model": model,
|
||||||
|
"on_permission_request": build_permission_handler(
|
||||||
|
allow_all=allow_all,
|
||||||
|
allow_shell=allow_shell,
|
||||||
|
pattern=pattern,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await session.send_and_wait({"prompt": prompt})
|
||||||
|
await client.stop()
|
||||||
|
|
||||||
|
content = response.data.content
|
||||||
|
# Check if response indicates success or denial
|
||||||
|
denied_keywords = [
|
||||||
|
"不允许",
|
||||||
|
"无法",
|
||||||
|
"对不起",
|
||||||
|
"Sorry",
|
||||||
|
"can't",
|
||||||
|
"cannot",
|
||||||
|
"not have permission",
|
||||||
|
]
|
||||||
|
is_denied = any(kw in content for kw in denied_keywords)
|
||||||
|
|
||||||
|
return (not is_denied, content)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Test failed with exception: %s", e)
|
||||||
|
return (False, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Comprehensive permission control test suite."
|
||||||
|
)
|
||||||
|
parser.add_argument("--model", default="gpt-4.1", help="Model ID for testing.")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Test cases: (name, allow_all, allow_shell, pattern, prompt, expected_approved)
|
||||||
|
test_cases = [
|
||||||
|
("Default Deny Shell", False, False, "", "请执行: ls -la", False),
|
||||||
|
("Allow All", True, False, "", "请执行: ls -la", True),
|
||||||
|
("Allow Shell", False, True, "", "请执行: pwd", True),
|
||||||
|
("Regex Match: ^ls", False, False, "^ls", "请执行: ls -la", True),
|
||||||
|
("Regex No Match: ^ls vs pwd", False, False, "^ls", "请执行: pwd", False),
|
||||||
|
(
|
||||||
|
"Regex Complex: ^(ls|pwd|echo)",
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
"^(ls|pwd|echo)",
|
||||||
|
"请执行: pwd",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Regex Complex No Match: git",
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
"^(ls|pwd|echo)",
|
||||||
|
"请执行: git status",
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Read Permission (Always Allow)",
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
"",
|
||||||
|
"Read the file: README.md",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("Starting Comprehensive Permission Control Test Suite")
|
||||||
|
logger.info("Model: %s", args.model)
|
||||||
|
logger.info("=" * 80)
|
||||||
|
|
||||||
|
for i, (name, allow_all, allow_shell, pattern, prompt, expected) in enumerate(
|
||||||
|
test_cases, 1
|
||||||
|
):
|
||||||
|
logger.info("\n[Test %d/%d] %s", i, len(test_cases), name)
|
||||||
|
logger.info(
|
||||||
|
" Config: allow_all=%s, allow_shell=%s, pattern=%r",
|
||||||
|
allow_all,
|
||||||
|
allow_shell,
|
||||||
|
pattern,
|
||||||
|
)
|
||||||
|
logger.info(" Prompt: %s", prompt)
|
||||||
|
|
||||||
|
approved, response = await run_test(
|
||||||
|
args.model, allow_all, allow_shell, pattern, prompt
|
||||||
|
)
|
||||||
|
passed = approved == expected
|
||||||
|
|
||||||
|
status = "✅ PASS" if passed else "❌ FAIL"
|
||||||
|
results.append((name, passed))
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
" Expected: %s, Got: %s - %s",
|
||||||
|
"Approved" if expected else "Denied",
|
||||||
|
"Approved" if approved else "Denied",
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
" Response: %s",
|
||||||
|
response[:100] + "..." if len(response) > 100 else response,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
logger.info("\n" + "=" * 80)
|
||||||
|
logger.info("Test Summary")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
passed_count = sum(1 for _, passed in results if passed)
|
||||||
|
total_count = len(results)
|
||||||
|
|
||||||
|
for name, passed in results:
|
||||||
|
logger.info("%s %s", "✅" if passed else "❌", name)
|
||||||
|
|
||||||
|
logger.info("-" * 80)
|
||||||
|
logger.info(
|
||||||
|
"Total: %d/%d tests passed (%.1f%%)",
|
||||||
|
passed_count,
|
||||||
|
total_count,
|
||||||
|
100 * passed_count / total_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
if passed_count == total_count:
|
||||||
|
logger.info("🎉 All tests passed!")
|
||||||
|
else:
|
||||||
|
logger.warning("⚠️ Some tests failed. Please review the logs.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from copilot import CopilotClient
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def build_permission_handler(allow_all: bool, allow_shell: bool, pattern: str):
|
||||||
|
async def on_permission_request(request: Dict[str, Any], context: Dict[str, str]):
|
||||||
|
kind = request.get("kind")
|
||||||
|
# Shell requests use 'fullCommandText' not 'command'
|
||||||
|
command = request.get("fullCommandText", "") or request.get("command", "")
|
||||||
|
logger.info("permission.request FULL: %s", request)
|
||||||
|
logger.info("permission.request kind=%s command=%r", kind, command)
|
||||||
|
|
||||||
|
if allow_all:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
if kind in ("read", "url"):
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
if kind == "shell":
|
||||||
|
if allow_shell:
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
if pattern and command:
|
||||||
|
try:
|
||||||
|
if re.match(pattern, command):
|
||||||
|
return {"kind": "approved"}
|
||||||
|
except re.error as exc:
|
||||||
|
logger.error("Invalid regex pattern: %s (%s)", pattern, exc)
|
||||||
|
|
||||||
|
return {"kind": "denied-by-rules", "rules": [{"kind": "debug-shell-pattern"}]}
|
||||||
|
|
||||||
|
return on_permission_request
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Test shell permission regex with GitHub Copilot SDK."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pattern", default="", help="Regex pattern for auto-approving shell commands."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--allow-shell", action="store_true", help="Auto-approve all shell commands."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--allow-all", action="store_true", help="Auto-approve all permission requests."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--prompt",
|
||||||
|
default="请执行: ls -la",
|
||||||
|
help="Prompt to trigger a shell tool request.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--model", default="gpt-5-mini", help="Model ID for testing.")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
client = CopilotClient()
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
session = await client.create_session(
|
||||||
|
{
|
||||||
|
"model": args.model,
|
||||||
|
"on_permission_request": build_permission_handler(
|
||||||
|
allow_all=args.allow_all,
|
||||||
|
allow_shell=args.allow_shell,
|
||||||
|
pattern=args.pattern,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Sending prompt: %s", args.prompt)
|
||||||
|
response = await session.send_and_wait({"prompt": args.prompt})
|
||||||
|
logger.info("Response: %s", response.data.content)
|
||||||
|
|
||||||
|
await client.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
359
plugins/debug/language-debug/language_debug.py
Normal file
359
plugins/debug/language-debug/language_debug.py
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
"""
|
||||||
|
title: UI Language Debugger
|
||||||
|
author: Fu-Jie
|
||||||
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
|
funding_url: https://github.com/open-webui
|
||||||
|
version: 0.1.0
|
||||||
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogIDxwYXRoIGQ9Im01IDggNiA2Ii8+CiAgPHBhdGggZD0ibTQgMTQgNi02IDItMiIvPgogIDxwYXRoIGQ9Ik0yIDVoMTIiLz4KICA8cGF0aCBkPSJNNyAyaDEiLz4KICA8cGF0aCBkPSJtMjIgMjItNS0xMC01IDEwIi8+CiAgPHBhdGggZD0iTTE0IDE4aDYiLz4KPC9zdmc+Cg==
|
||||||
|
description: Debug UI language detection in the browser console and on-page panel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any, Callable, Awaitable
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
HTML_WRAPPER_TEMPLATE = """
|
||||||
|
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{user_language}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
#main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
/* STYLES_INSERTION_POINT */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main-container">
|
||||||
|
<!-- CONTENT_INSERTION_POINT -->
|
||||||
|
</div>
|
||||||
|
<!-- SCRIPTS_INSERTION_POINT -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
CONTENT_TEMPLATE = """
|
||||||
|
<div class="lang-debug-card" id="lang-debug-card-{unique_id}">
|
||||||
|
<div class="lang-debug-header">
|
||||||
|
🧭 UI Language Debugger
|
||||||
|
</div>
|
||||||
|
<div class="lang-debug-body">
|
||||||
|
<div class="lang-debug-row"><span>python.ui_language</span><code id="lang-py-{unique_id}">{python_language}</code></div>
|
||||||
|
<div class="lang-debug-row"><span>document.documentElement.lang</span><code id="lang-html-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>document.documentElement.getAttribute('lang')</span><code id="lang-attr-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>document.documentElement.dir</span><code id="lang-dir-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>document.body.lang</span><code id="lang-body-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>navigator.language</span><code id="lang-nav-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>navigator.languages</span><code id="lang-navs-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>localStorage.language</span><code id="lang-store-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>localStorage.locale</span><code id="lang-locale-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>localStorage.i18n</span><code id="lang-i18n-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>localStorage.settings</span><code id="lang-settings-{unique_id}">-</code></div>
|
||||||
|
<div class="lang-debug-row"><span>document.documentElement.dataset</span><code id="lang-dataset-{unique_id}">-</code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
STYLE_TEMPLATE = """
|
||||||
|
.lang-debug-card {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
.lang-debug-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.lang-debug-body {
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.lang-debug-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
.lang-debug-row code {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
SCRIPT_TEMPLATE = """
|
||||||
|
<script>
|
||||||
|
(function() {{
|
||||||
|
const uniqueId = "{unique_id}";
|
||||||
|
const get = (id) => document.getElementById(id + '-' + uniqueId);
|
||||||
|
|
||||||
|
const safe = (value) => {
|
||||||
|
if (value === undefined || value === null || value === "") return "-";
|
||||||
|
if (Array.isArray(value)) return value.join(", ");
|
||||||
|
if (typeof value === "object") return JSON.stringify(value);
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeJson = (value) => {
|
||||||
|
try {
|
||||||
|
return value ? JSON.stringify(JSON.parse(value)) : "-";
|
||||||
|
} catch (e) {
|
||||||
|
return value ? String(value) : "-";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsRaw = localStorage.getItem('settings');
|
||||||
|
const i18nRaw = localStorage.getItem('i18n');
|
||||||
|
const localeRaw = localStorage.getItem('locale');
|
||||||
|
|
||||||
|
const payload = {{
|
||||||
|
htmlLang: document.documentElement.lang,
|
||||||
|
htmlAttr: document.documentElement.getAttribute('lang'),
|
||||||
|
htmlDir: document.documentElement.dir,
|
||||||
|
bodyLang: document.body ? document.body.lang : "",
|
||||||
|
navigatorLanguage: navigator.language,
|
||||||
|
navigatorLanguages: navigator.languages,
|
||||||
|
localStorageLanguage: localStorage.getItem('language'),
|
||||||
|
localStorageLocale: localeRaw,
|
||||||
|
localStorageI18n: i18nRaw,
|
||||||
|
localStorageSettings: settingsRaw,
|
||||||
|
htmlDataset: document.documentElement.dataset,
|
||||||
|
}};
|
||||||
|
|
||||||
|
get('lang-html').textContent = safe(payload.htmlLang);
|
||||||
|
get('lang-attr').textContent = safe(payload.htmlAttr);
|
||||||
|
get('lang-dir').textContent = safe(payload.htmlDir);
|
||||||
|
get('lang-body').textContent = safe(payload.bodyLang);
|
||||||
|
get('lang-nav').textContent = safe(payload.navigatorLanguage);
|
||||||
|
get('lang-navs').textContent = safe(payload.navigatorLanguages);
|
||||||
|
get('lang-store').textContent = safe(payload.localStorageLanguage);
|
||||||
|
get('lang-locale').textContent = safe(payload.localStorageLocale);
|
||||||
|
get('lang-i18n').textContent = safeJson(payload.localStorageI18n);
|
||||||
|
get('lang-settings').textContent = safeJson(payload.localStorageSettings);
|
||||||
|
get('lang-dataset').textContent = safe(payload.htmlDataset);
|
||||||
|
|
||||||
|
console.group('🧭 UI Language Debugger');
|
||||||
|
console.log(payload);
|
||||||
|
console.groupEnd();
|
||||||
|
}})();
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Action:
|
||||||
|
class Valves(BaseModel):
|
||||||
|
SHOW_STATUS: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Whether to show operation status updates.",
|
||||||
|
)
|
||||||
|
SHOW_DEBUG_LOG: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Whether to print debug logs in the browser console.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.valves = self.Valves()
|
||||||
|
|
||||||
|
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||||
|
if isinstance(__user__, (list, tuple)):
|
||||||
|
user_data = __user__[0] if __user__ else {}
|
||||||
|
elif isinstance(__user__, dict):
|
||||||
|
user_data = __user__
|
||||||
|
else:
|
||||||
|
user_data = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": user_data.get("id", "unknown_user"),
|
||||||
|
"user_name": user_data.get("name", "User"),
|
||||||
|
"user_language": user_data.get("language", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_chat_context(
|
||||||
|
self, body: dict, __metadata__: Optional[dict] = None
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
chat_id = ""
|
||||||
|
message_id = ""
|
||||||
|
|
||||||
|
if isinstance(body, dict):
|
||||||
|
chat_id = body.get("chat_id", "")
|
||||||
|
message_id = body.get("id", "")
|
||||||
|
|
||||||
|
if not chat_id or not message_id:
|
||||||
|
body_metadata = body.get("metadata", {})
|
||||||
|
if isinstance(body_metadata, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = body_metadata.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = body_metadata.get("message_id", "")
|
||||||
|
|
||||||
|
if __metadata__ and isinstance(__metadata__, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = __metadata__.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = __metadata__.get("message_id", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chat_id": str(chat_id).strip(),
|
||||||
|
"message_id": str(message_id).strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _emit_status(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
description: str,
|
||||||
|
done: bool = False,
|
||||||
|
):
|
||||||
|
if self.valves.SHOW_STATUS and emitter:
|
||||||
|
await emitter(
|
||||||
|
{"type": "status", "data": {"description": description, "done": done}}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _emit_debug_log(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
title: str,
|
||||||
|
data: dict,
|
||||||
|
):
|
||||||
|
if not self.valves.SHOW_DEBUG_LOG or not emitter:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
js_code = f"""
|
||||||
|
(async function() {{
|
||||||
|
console.group("🛠️ {title}");
|
||||||
|
console.log({json.dumps(data, ensure_ascii=False)});
|
||||||
|
console.groupEnd();
|
||||||
|
}})();
|
||||||
|
"""
|
||||||
|
|
||||||
|
await emitter({"type": "execute", "data": {"code": js_code}})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error emitting debug log: %s", e, exc_info=True)
|
||||||
|
|
||||||
|
def _merge_html(
|
||||||
|
self,
|
||||||
|
existing_html: str,
|
||||||
|
new_content: str,
|
||||||
|
new_styles: str = "",
|
||||||
|
new_scripts: str = "",
|
||||||
|
user_language: str = "en-US",
|
||||||
|
) -> str:
|
||||||
|
if not existing_html:
|
||||||
|
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
|
||||||
|
else:
|
||||||
|
base_html = existing_html
|
||||||
|
|
||||||
|
if "<!-- CONTENT_INSERTION_POINT -->" in base_html:
|
||||||
|
base_html = base_html.replace(
|
||||||
|
"<!-- CONTENT_INSERTION_POINT -->",
|
||||||
|
f"{new_content}\n <!-- CONTENT_INSERTION_POINT -->",
|
||||||
|
)
|
||||||
|
|
||||||
|
if new_styles and "/* STYLES_INSERTION_POINT */" in base_html:
|
||||||
|
base_html = base_html.replace(
|
||||||
|
"/* STYLES_INSERTION_POINT */",
|
||||||
|
f"{new_styles}\n /* STYLES_INSERTION_POINT */",
|
||||||
|
)
|
||||||
|
|
||||||
|
if new_scripts and "<!-- SCRIPTS_INSERTION_POINT -->" in base_html:
|
||||||
|
base_html = base_html.replace(
|
||||||
|
"<!-- SCRIPTS_INSERTION_POINT -->",
|
||||||
|
f"{new_scripts}\n <!-- SCRIPTS_INSERTION_POINT -->",
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_html
|
||||||
|
|
||||||
|
async def action(
|
||||||
|
self,
|
||||||
|
body: dict,
|
||||||
|
__user__: Optional[Dict[str, Any]] = None,
|
||||||
|
__event_emitter__: Optional[Any] = None,
|
||||||
|
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
__metadata__: Optional[dict] = None,
|
||||||
|
__request__: Optional[Any] = None,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
await self._emit_status(__event_emitter__, "Detecting UI language...", False)
|
||||||
|
|
||||||
|
user_ctx = self._get_user_context(__user__)
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Language Debugger: user context",
|
||||||
|
user_ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
ui_language = ""
|
||||||
|
if __event_call__:
|
||||||
|
try:
|
||||||
|
response = await __event_call__(
|
||||||
|
{
|
||||||
|
"type": "execute",
|
||||||
|
"data": {
|
||||||
|
"code": "return (localStorage.getItem('locale') || localStorage.getItem('language') || (navigator.languages && navigator.languages[0]) || navigator.language || document.documentElement.lang || '')",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Language Debugger: execute response",
|
||||||
|
{"response": response},
|
||||||
|
)
|
||||||
|
if isinstance(response, dict) and "value" in response:
|
||||||
|
ui_language = response.get("value", "") or ""
|
||||||
|
elif isinstance(response, str):
|
||||||
|
ui_language = response
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"Failed to read UI language from frontend: %s", e, exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
|
unique_id = f"lang_{int(__import__('time').time() * 1000)}"
|
||||||
|
content_html = CONTENT_TEMPLATE.replace("{unique_id}", unique_id).replace(
|
||||||
|
"{python_language}", ui_language or "-"
|
||||||
|
)
|
||||||
|
script_html = SCRIPT_TEMPLATE.replace("{unique_id}", unique_id)
|
||||||
|
script_html = script_html.replace("{{", "{").replace("}}", "}")
|
||||||
|
|
||||||
|
final_html = self._merge_html(
|
||||||
|
"",
|
||||||
|
content_html,
|
||||||
|
STYLE_TEMPLATE,
|
||||||
|
script_html,
|
||||||
|
"en",
|
||||||
|
)
|
||||||
|
|
||||||
|
html_embed_tag = f"```html\n{final_html}\n```"
|
||||||
|
body["messages"][-1]["content"] = (
|
||||||
|
body["messages"][-1].get("content", "") + "\n\n" + html_embed_tag
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._emit_status(__event_emitter__, "UI language captured.", True)
|
||||||
|
return body
|
||||||
568
plugins/debug/legacy_docs/guides/COPILOT_TOOLS_QUICKSTART.md
Normal file
568
plugins/debug/legacy_docs/guides/COPILOT_TOOLS_QUICKSTART.md
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
# GitHub Copilot SDK 自定义工具快速入门
|
||||||
|
|
||||||
|
## 🎯 目标
|
||||||
|
|
||||||
|
在 OpenWebUI Pipe 中直接使用 GitHub Copilot SDK 的自定义工具功能,无需集成 OpenWebUI Function 系统。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 基础概念
|
||||||
|
|
||||||
|
### Copilot SDK Tool 的三要素
|
||||||
|
|
||||||
|
```python
|
||||||
|
from copilot.types import Tool, ToolInvocation, ToolResult
|
||||||
|
|
||||||
|
# 1. Tool Definition(工具定义)
|
||||||
|
tool = Tool(
|
||||||
|
name="tool_name", # 工具名称
|
||||||
|
description="What it does", # 描述(给 AI 看的)
|
||||||
|
parameters={...}, # JSON Schema 参数定义
|
||||||
|
handler=handler_function # 处理函数
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. Tool Handler(处理函数)
|
||||||
|
async def handler_function(invocation: ToolInvocation) -> ToolResult:
|
||||||
|
# invocation 包含:
|
||||||
|
# - session_id: 会话 ID
|
||||||
|
# - tool_call_id: 调用 ID
|
||||||
|
# - tool_name: 工具名称
|
||||||
|
# - arguments: dict(实际参数)
|
||||||
|
|
||||||
|
result = do_something(invocation["arguments"])
|
||||||
|
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm="结果文本",
|
||||||
|
resultType="success", # 或 "failure"
|
||||||
|
error=None,
|
||||||
|
toolTelemetry={}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Session Configuration(会话配置)
|
||||||
|
session_config = SessionConfig(
|
||||||
|
model="claude-sonnet-4.5",
|
||||||
|
tools=[tool1, tool2, tool3], # ✅ 传入工具列表
|
||||||
|
streaming=True
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 完整实现示例
|
||||||
|
|
||||||
|
### 示例 1:获取当前时间
|
||||||
|
|
||||||
|
```python
|
||||||
|
from datetime import datetime
|
||||||
|
from copilot.types import Tool, ToolInvocation, ToolResult
|
||||||
|
|
||||||
|
def create_time_tool():
|
||||||
|
"""创建获取时间的工具"""
|
||||||
|
|
||||||
|
async def get_time_handler(invocation: ToolInvocation) -> ToolResult:
|
||||||
|
"""工具处理函数"""
|
||||||
|
try:
|
||||||
|
# 获取参数
|
||||||
|
timezone = invocation["arguments"].get("timezone", "UTC")
|
||||||
|
format_str = invocation["arguments"].get("format", "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# 执行逻辑
|
||||||
|
current_time = datetime.now().strftime(format_str)
|
||||||
|
result_text = f"Current time: {current_time}"
|
||||||
|
|
||||||
|
# 返回结果
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=result_text,
|
||||||
|
resultType="success",
|
||||||
|
error=None,
|
||||||
|
toolTelemetry={"execution_time": "fast"}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=f"Error getting time: {str(e)}",
|
||||||
|
resultType="failure",
|
||||||
|
error=str(e),
|
||||||
|
toolTelemetry={}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建工具定义
|
||||||
|
return Tool(
|
||||||
|
name="get_current_time",
|
||||||
|
description="Get the current date and time. Useful when user asks 'what time is it' or needs to know the current date.",
|
||||||
|
parameters={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"timezone": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Timezone name (e.g., 'UTC', 'Asia/Shanghai')",
|
||||||
|
"default": "UTC"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Time format string",
|
||||||
|
"default": "%Y-%m-%d %H:%M:%S"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler=get_time_handler
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2:数学计算器
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_calculator_tool():
|
||||||
|
"""创建计算器工具"""
|
||||||
|
|
||||||
|
async def calculate_handler(invocation: ToolInvocation) -> ToolResult:
|
||||||
|
try:
|
||||||
|
expression = invocation["arguments"].get("expression", "")
|
||||||
|
|
||||||
|
# 安全检查
|
||||||
|
allowed_chars = set("0123456789+-*/()., ")
|
||||||
|
if not all(c in allowed_chars for c in expression):
|
||||||
|
raise ValueError("Expression contains invalid characters")
|
||||||
|
|
||||||
|
# 计算(安全的 eval)
|
||||||
|
result = eval(expression, {"__builtins__": {}})
|
||||||
|
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=f"The result of {expression} is {result}",
|
||||||
|
resultType="success",
|
||||||
|
error=None,
|
||||||
|
toolTelemetry={}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=f"Calculation error: {str(e)}",
|
||||||
|
resultType="failure",
|
||||||
|
error=str(e),
|
||||||
|
toolTelemetry={}
|
||||||
|
)
|
||||||
|
|
||||||
|
return Tool(
|
||||||
|
name="calculate",
|
||||||
|
description="Perform mathematical calculations. Supports basic arithmetic operations (+, -, *, /).",
|
||||||
|
parameters={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expression": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Mathematical expression to evaluate (e.g., '2 + 2 * 3')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["expression"]
|
||||||
|
},
|
||||||
|
handler=calculate_handler
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3:随机数生成器
|
||||||
|
|
||||||
|
```python
|
||||||
|
import random
|
||||||
|
|
||||||
|
def create_random_number_tool():
|
||||||
|
"""创建随机数生成工具"""
|
||||||
|
|
||||||
|
async def random_handler(invocation: ToolInvocation) -> ToolResult:
|
||||||
|
try:
|
||||||
|
min_val = invocation["arguments"].get("min", 1)
|
||||||
|
max_val = invocation["arguments"].get("max", 100)
|
||||||
|
|
||||||
|
if min_val >= max_val:
|
||||||
|
raise ValueError("min must be less than max")
|
||||||
|
|
||||||
|
number = random.randint(min_val, max_val)
|
||||||
|
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=f"Generated random number: {number}",
|
||||||
|
resultType="success",
|
||||||
|
error=None,
|
||||||
|
toolTelemetry={}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=f"Error: {str(e)}",
|
||||||
|
resultType="failure",
|
||||||
|
error=str(e),
|
||||||
|
toolTelemetry={}
|
||||||
|
)
|
||||||
|
|
||||||
|
return Tool(
|
||||||
|
name="generate_random_number",
|
||||||
|
description="Generate a random integer within a specified range.",
|
||||||
|
parameters={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"min": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Minimum value (inclusive)",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum value (inclusive)",
|
||||||
|
"default": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler=random_handler
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 集成到 Pipe
|
||||||
|
|
||||||
|
### 完整的 Pipe 实现
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Pipe:
|
||||||
|
class Valves(BaseModel):
|
||||||
|
# ... 现有 Valves ...
|
||||||
|
|
||||||
|
ENABLE_TOOLS: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Enable custom tools (time, calculator, random)"
|
||||||
|
)
|
||||||
|
AVAILABLE_TOOLS: str = Field(
|
||||||
|
default="all",
|
||||||
|
description="Available tools: 'all' or comma-separated list (e.g., 'get_current_time,calculate')"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# ... 现有初始化 ...
|
||||||
|
self._custom_tools = []
|
||||||
|
|
||||||
|
def _initialize_custom_tools(self):
|
||||||
|
"""初始化自定义工具"""
|
||||||
|
if not self.valves.ENABLE_TOOLS:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 定义所有可用工具
|
||||||
|
all_tools = {
|
||||||
|
"get_current_time": create_time_tool(),
|
||||||
|
"calculate": create_calculator_tool(),
|
||||||
|
"generate_random_number": create_random_number_tool(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根据配置过滤工具
|
||||||
|
if self.valves.AVAILABLE_TOOLS == "all":
|
||||||
|
return list(all_tools.values())
|
||||||
|
|
||||||
|
# 只启用指定的工具
|
||||||
|
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||||
|
return [all_tools[name] for name in enabled if name in all_tools]
|
||||||
|
|
||||||
|
async def pipe(
|
||||||
|
self,
|
||||||
|
body: dict,
|
||||||
|
__metadata__: Optional[dict] = None,
|
||||||
|
__event_emitter__=None,
|
||||||
|
__event_call__=None,
|
||||||
|
) -> Union[str, AsyncGenerator]:
|
||||||
|
# ... 现有代码 ...
|
||||||
|
|
||||||
|
# ✅ 初始化工具
|
||||||
|
custom_tools = self._initialize_custom_tools()
|
||||||
|
|
||||||
|
if custom_tools:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
f"Enabled {len(custom_tools)} custom tools: {[t.name for t in custom_tools]}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ 创建会话配置(传入工具)
|
||||||
|
from copilot.types import SessionConfig, InfiniteSessionConfig
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
session_id=chat_id if chat_id else None,
|
||||||
|
model=real_model_id,
|
||||||
|
streaming=body.get("stream", False),
|
||||||
|
tools=custom_tools, # ✅✅✅ 关键:传入工具列表
|
||||||
|
infinite_sessions=infinite_session_config if self.valves.INFINITE_SESSION else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
session = await client.create_session(config=session_config)
|
||||||
|
|
||||||
|
# ... 其余代码保持不变 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 处理工具调用事件
|
||||||
|
|
||||||
|
### 在 stream_response 中显示工具调用
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def stream_response(
|
||||||
|
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||||
|
) -> AsyncGenerator:
|
||||||
|
# ... 现有代码 ...
|
||||||
|
|
||||||
|
def handler(event):
|
||||||
|
event_type = str(getattr(event.type, "value", event.type))
|
||||||
|
|
||||||
|
# ✅ 工具调用开始
|
||||||
|
if "tool_invocation_started" in event_type or "tool_call_started" in event_type:
|
||||||
|
tool_name = get_event_data(event, "tool_name", "")
|
||||||
|
if tool_name:
|
||||||
|
queue.put_nowait(f"\n\n🔧 **Calling tool**: `{tool_name}`\n")
|
||||||
|
|
||||||
|
# ✅ 工具调用完成
|
||||||
|
elif "tool_invocation_completed" in event_type or "tool_call_completed" in event_type:
|
||||||
|
tool_name = get_event_data(event, "tool_name", "")
|
||||||
|
result = get_event_data(event, "result", "")
|
||||||
|
if tool_name:
|
||||||
|
queue.put_nowait(f"\n✅ **Tool `{tool_name}` completed**\n")
|
||||||
|
|
||||||
|
# ✅ 工具调用失败
|
||||||
|
elif "tool_invocation_failed" in event_type or "tool_call_failed" in event_type:
|
||||||
|
tool_name = get_event_data(event, "tool_name", "")
|
||||||
|
error = get_event_data(event, "error", "")
|
||||||
|
if tool_name:
|
||||||
|
queue.put_nowait(f"\n❌ **Tool `{tool_name}` failed**: {error}\n")
|
||||||
|
|
||||||
|
# ... 其他事件处理 ...
|
||||||
|
|
||||||
|
# ... 其余代码 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试示例
|
||||||
|
|
||||||
|
### 测试 1:询问时间
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "What time is it now?"
|
||||||
|
|
||||||
|
Expected Flow:
|
||||||
|
1. Copilot 识别需要调用 get_current_time 工具
|
||||||
|
2. 调用工具(无参数或默认参数)
|
||||||
|
3. 工具返回: "Current time: 2026-01-26 15:30:00"
|
||||||
|
4. Copilot 回答: "The current time is 2026-01-26 15:30:00"
|
||||||
|
|
||||||
|
Pipe Output:
|
||||||
|
---
|
||||||
|
🔧 **Calling tool**: `get_current_time`
|
||||||
|
✅ **Tool `get_current_time` completed**
|
||||||
|
The current time is 2026-01-26 15:30:00
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试 2:数学计算
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Calculate 123 * 456"
|
||||||
|
|
||||||
|
Expected Flow:
|
||||||
|
1. Copilot 调用 calculate 工具
|
||||||
|
2. 参数: {"expression": "123 * 456"}
|
||||||
|
3. 工具返回: "The result of 123 * 456 is 56088"
|
||||||
|
4. Copilot 回答: "123 multiplied by 456 equals 56,088"
|
||||||
|
|
||||||
|
Pipe Output:
|
||||||
|
---
|
||||||
|
🔧 **Calling tool**: `calculate`
|
||||||
|
✅ **Tool `calculate` completed**
|
||||||
|
123 multiplied by 456 equals 56,088
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试 3:生成随机数
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Give me a random number between 1 and 10"
|
||||||
|
|
||||||
|
Expected Flow:
|
||||||
|
1. Copilot 调用 generate_random_number 工具
|
||||||
|
2. 参数: {"min": 1, "max": 10}
|
||||||
|
3. 工具返回: "Generated random number: 7"
|
||||||
|
4. Copilot 回答: "I generated a random number for you: 7"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 调试技巧
|
||||||
|
|
||||||
|
### 1. 记录所有工具事件
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handler(event):
|
||||||
|
event_type = str(getattr(event.type, "value", event.type))
|
||||||
|
|
||||||
|
# 记录所有包含 "tool" 的事件
|
||||||
|
if "tool" in event_type.lower():
|
||||||
|
event_data = {}
|
||||||
|
if hasattr(event, "data"):
|
||||||
|
try:
|
||||||
|
event_data = {
|
||||||
|
"type": event_type,
|
||||||
|
"data": str(event.data)[:200] # 截断长数据
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"Tool Event: {json.dumps(event_data)}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 验证工具注册
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def pipe(...):
|
||||||
|
# ...
|
||||||
|
custom_tools = self._initialize_custom_tools()
|
||||||
|
|
||||||
|
# 调试:打印工具信息
|
||||||
|
if self.valves.DEBUG:
|
||||||
|
tool_info = [
|
||||||
|
{
|
||||||
|
"name": t.name,
|
||||||
|
"description": t.description[:50],
|
||||||
|
"has_handler": t.handler is not None
|
||||||
|
}
|
||||||
|
for t in custom_tools
|
||||||
|
]
|
||||||
|
await self._emit_debug_log(
|
||||||
|
f"Registered tools: {json.dumps(tool_info, indent=2)}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试工具处理函数
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 单独测试工具
|
||||||
|
async def test_tool():
|
||||||
|
tool = create_time_tool()
|
||||||
|
|
||||||
|
# 模拟调用
|
||||||
|
invocation = {
|
||||||
|
"session_id": "test",
|
||||||
|
"tool_call_id": "test_call",
|
||||||
|
"tool_name": "get_current_time",
|
||||||
|
"arguments": {"format": "%H:%M:%S"}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await tool.handler(invocation)
|
||||||
|
print(f"Result: {result}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 1. 工具描述的重要性
|
||||||
|
|
||||||
|
工具的 `description` 字段非常重要,它告诉 AI 何时应该使用这个工具:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ 差的描述
|
||||||
|
description="Get time"
|
||||||
|
|
||||||
|
# ✅ 好的描述
|
||||||
|
description="Get the current date and time. Use this when the user asks 'what time is it', 'what's the date', or needs to know the current timestamp."
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 参数定义
|
||||||
|
|
||||||
|
使用标准的 JSON Schema 定义参数:
|
||||||
|
|
||||||
|
```python
|
||||||
|
parameters={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"param_name": {
|
||||||
|
"type": "string", # string, integer, boolean, array, object
|
||||||
|
"description": "Clear description",
|
||||||
|
"enum": ["option1", "option2"], # 可选:枚举值
|
||||||
|
"default": "default_value" # 可选:默认值
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["param_name"] # 必需参数
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 错误处理
|
||||||
|
|
||||||
|
总是捕获异常并返回有意义的错误:
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
result = do_something()
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=f"Success: {result}",
|
||||||
|
resultType="success",
|
||||||
|
error=None,
|
||||||
|
toolTelemetry={}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=f"Error occurred: {str(e)}",
|
||||||
|
resultType="failure",
|
||||||
|
error=str(e), # 用于调试
|
||||||
|
toolTelemetry={}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 异步 vs 同步
|
||||||
|
|
||||||
|
工具处理函数可以是同步或异步:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 同步工具
|
||||||
|
def sync_handler(invocation):
|
||||||
|
result = calculate(invocation["arguments"])
|
||||||
|
return ToolResult(...)
|
||||||
|
|
||||||
|
# 异步工具(推荐)
|
||||||
|
async def async_handler(invocation):
|
||||||
|
result = await fetch_data(invocation["arguments"])
|
||||||
|
return ToolResult(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始清单
|
||||||
|
|
||||||
|
- [ ] 1. 在 Valves 中添加 `ENABLE_TOOLS` 配置
|
||||||
|
- [ ] 2. 定义 2-3 个简单的工具函数
|
||||||
|
- [ ] 3. 实现 `_initialize_custom_tools()` 方法
|
||||||
|
- [ ] 4. 修改 `SessionConfig` 传入 `tools` 参数
|
||||||
|
- [ ] 5. 在 `stream_response` 中添加工具事件处理
|
||||||
|
- [ ] 6. 测试:询问时间、计算数学、生成随机数
|
||||||
|
- [ ] 7. 添加调试日志
|
||||||
|
- [ ] 8. 同步中文版本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 完整的工具事件列表
|
||||||
|
|
||||||
|
根据 SDK 源码,可能的工具相关事件:
|
||||||
|
|
||||||
|
- `tool_invocation_started` / `tool_call_started`
|
||||||
|
- `tool_invocation_completed` / `tool_call_completed`
|
||||||
|
- `tool_invocation_failed` / `tool_call_failed`
|
||||||
|
- `tool_parameter_validation_failed`
|
||||||
|
|
||||||
|
实际事件名称可能因 SDK 版本而异,建议先记录所有事件类型:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handler(event):
|
||||||
|
print(f"Event type: {event.type}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**快速实现入口:** 从示例 1(获取时间)开始,这是最简单的工具,可以快速验证整个流程!
|
||||||
|
|
||||||
|
**作者:** Fu-Jie
|
||||||
|
**日期:** 2026-01-26
|
||||||
480
plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_GUIDE.md
Normal file
480
plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_GUIDE.md
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
# OpenWebUI Native Tool Call Display Implementation Guide
|
||||||
|
|
||||||
|
**Date:** 2026-01-27
|
||||||
|
**Purpose:** Analyze and implement OpenWebUI's native tool call display mechanism
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 Current vs Native Display
|
||||||
|
|
||||||
|
### Current Implementation
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
> 🔧 **Running Tool**: `search_chats`
|
||||||
|
|
||||||
|
> ✅ **Tool Completed**: {...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenWebUI Native Display (from screenshot)
|
||||||
|
|
||||||
|
- ✅ Collapsible panel: "查看来自 search_chats 的结果"
|
||||||
|
- ✅ Formatted JSON display
|
||||||
|
- ✅ Syntax highlighting
|
||||||
|
- ✅ Expand/collapse functionality
|
||||||
|
- ✅ Clean visual separation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Understanding OpenWebUI's Tool Call Format
|
||||||
|
|
||||||
|
### Standard OpenAI Tool Call Message Format
|
||||||
|
|
||||||
|
OpenWebUI follows the OpenAI Chat Completion API format for tool calls:
|
||||||
|
|
||||||
|
#### 1. Assistant Message with Tool Calls
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None, # or explanatory text
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_abc123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "search_chats",
|
||||||
|
"arguments": '{"query": ""}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Tool Response Message
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "call_abc123",
|
||||||
|
"name": "search_chats", # Optional but recommended
|
||||||
|
"content": '{"count": 5, "results": [...]}' # JSON string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Implementation Strategy for Native Display
|
||||||
|
|
||||||
|
### Option 1: Event Emitter Approach (Recommended)
|
||||||
|
|
||||||
|
Use OpenWebUI's event emitter to send structured tool call data:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def stream_response(self, ...):
|
||||||
|
# When tool execution starts
|
||||||
|
if event_type == "tool.execution_start":
|
||||||
|
await self._emit_tool_call_start(
|
||||||
|
emitter=__event_call__,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
arguments=arguments
|
||||||
|
)
|
||||||
|
|
||||||
|
# When tool execution completes
|
||||||
|
elif event_type == "tool.execution_complete":
|
||||||
|
await self._emit_tool_call_result(
|
||||||
|
emitter=__event_call__,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
result=result_content
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Helper Methods
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _emit_tool_call_start(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
tool_call_id: str,
|
||||||
|
tool_name: str,
|
||||||
|
arguments: dict
|
||||||
|
):
|
||||||
|
"""Emit a tool call start event to OpenWebUI."""
|
||||||
|
if not emitter:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# OpenWebUI expects tool_calls in assistant message format
|
||||||
|
await emitter({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": tool_call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": json.dumps(arguments, ensure_ascii=False)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to emit tool call start: {e}")
|
||||||
|
|
||||||
|
async def _emit_tool_call_result(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
tool_call_id: str,
|
||||||
|
tool_name: str,
|
||||||
|
result: Any
|
||||||
|
):
|
||||||
|
"""Emit a tool call result to OpenWebUI."""
|
||||||
|
if not emitter:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Format result as JSON string
|
||||||
|
if isinstance(result, str):
|
||||||
|
result_content = result
|
||||||
|
else:
|
||||||
|
result_content = json.dumps(result, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# OpenWebUI expects tool results in tool message format
|
||||||
|
await emitter({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"content": result_content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to emit tool result: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Message History Injection
|
||||||
|
|
||||||
|
Modify the conversation history to include tool calls:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# After tool execution, append to messages
|
||||||
|
messages.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": tool_call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": json.dumps(arguments)
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"content": json.dumps(result)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Challenges with Current Architecture
|
||||||
|
|
||||||
|
### 1. Streaming Context
|
||||||
|
|
||||||
|
Our current implementation uses:
|
||||||
|
|
||||||
|
- **Queue-based streaming**: Events → Queue → Yield chunks
|
||||||
|
- **Text chunks only**: We yield plain text, not structured messages
|
||||||
|
|
||||||
|
OpenWebUI's native display requires:
|
||||||
|
|
||||||
|
- **Structured message events**: Not text chunks
|
||||||
|
- **Message-level control**: Need to emit complete messages
|
||||||
|
|
||||||
|
### 2. Event Emitter Compatibility
|
||||||
|
|
||||||
|
**Current usage:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# We use event_emitter for status/notifications
|
||||||
|
await event_emitter({
|
||||||
|
"type": "status",
|
||||||
|
"data": {"description": "Processing..."}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Need for tool calls:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Need to emit message-type events
|
||||||
|
await event_emitter({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "tool",
|
||||||
|
"content": "..."
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Question:** Does `__event_emitter__` support `message` type events?
|
||||||
|
|
||||||
|
### 3. Session SDK Events vs OpenWebUI Messages
|
||||||
|
|
||||||
|
**Copilot SDK events:**
|
||||||
|
|
||||||
|
- `tool.execution_start` → We get tool name, arguments
|
||||||
|
- `tool.execution_complete` → We get tool result
|
||||||
|
- Designed for streaming text output
|
||||||
|
|
||||||
|
**OpenWebUI messages:**
|
||||||
|
|
||||||
|
- Expect structured message objects
|
||||||
|
- Not designed for mid-stream injection
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Experimental Implementation
|
||||||
|
|
||||||
|
### Step 1: Add Valve for Native Display
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Use OpenWebUI's native tool call display instead of markdown formatting"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Modify Tool Event Handling
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def stream_response(self, ...):
|
||||||
|
# ...existing code...
|
||||||
|
|
||||||
|
def handler(event):
|
||||||
|
event_type = get_event_type(event)
|
||||||
|
|
||||||
|
if event_type == "tool.execution_start":
|
||||||
|
tool_name = safe_get_data_attr(event, "name")
|
||||||
|
|
||||||
|
# Get tool arguments
|
||||||
|
tool_input = safe_get_data_attr(event, "input") or {}
|
||||||
|
tool_call_id = safe_get_data_attr(event, "tool_call_id", f"call_{time.time()}")
|
||||||
|
|
||||||
|
if tool_call_id:
|
||||||
|
active_tools[tool_call_id] = {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": tool_input
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||||
|
# Emit structured tool call
|
||||||
|
asyncio.create_task(
|
||||||
|
self._emit_tool_call_start(
|
||||||
|
__event_call__,
|
||||||
|
tool_call_id,
|
||||||
|
tool_name,
|
||||||
|
tool_input
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Current markdown display
|
||||||
|
queue.put_nowait(f"\n\n> 🔧 **Running Tool**: `{tool_name}`\n\n")
|
||||||
|
|
||||||
|
elif event_type == "tool.execution_complete":
|
||||||
|
tool_call_id = safe_get_data_attr(event, "tool_call_id")
|
||||||
|
tool_info = active_tools.get(tool_call_id, {})
|
||||||
|
tool_name = tool_info.get("name", "Unknown")
|
||||||
|
|
||||||
|
# Extract result
|
||||||
|
result_obj = safe_get_data_attr(event, "result")
|
||||||
|
result_content = ""
|
||||||
|
if hasattr(result_obj, "content"):
|
||||||
|
result_content = result_obj.content
|
||||||
|
elif isinstance(result_obj, dict):
|
||||||
|
result_content = result_obj.get("content", "")
|
||||||
|
|
||||||
|
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||||
|
# Emit structured tool result
|
||||||
|
asyncio.create_task(
|
||||||
|
self._emit_tool_call_result(
|
||||||
|
__event_call__,
|
||||||
|
tool_call_id,
|
||||||
|
tool_name,
|
||||||
|
result_content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Current markdown display
|
||||||
|
queue.put_nowait(f"> ✅ **Tool Completed**: {result_content}\n\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔬 Testing Plan
|
||||||
|
|
||||||
|
### Test 1: Event Emitter Message Type Support
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In a test conversation, try:
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Test message"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** Message appears in chat
|
||||||
|
**If fails:** Event emitter doesn't support message type
|
||||||
|
|
||||||
|
### Test 2: Tool Call Message Format
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Send a tool call message
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": "test_123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "test_tool",
|
||||||
|
"arguments": '{"param": "value"}'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Send tool result
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "test_123",
|
||||||
|
"name": "test_tool",
|
||||||
|
"content": '{"result": "success"}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** OpenWebUI displays collapsible tool panel
|
||||||
|
**If fails:** Event format doesn't match OpenWebUI expectations
|
||||||
|
|
||||||
|
### Test 3: Mid-Stream Tool Call Injection
|
||||||
|
|
||||||
|
Test if tool call messages can be injected during streaming:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Start streaming text
|
||||||
|
yield "Processing your request..."
|
||||||
|
|
||||||
|
# Mid-stream: emit tool call
|
||||||
|
await __event_emitter__({"type": "message", "data": {...}})
|
||||||
|
|
||||||
|
# Continue streaming
|
||||||
|
yield "Done!"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** Tool panel appears mid-response
|
||||||
|
**Risk:** May break streaming flow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Implementation Checklist
|
||||||
|
|
||||||
|
- [x] Add `REASONING_EFFORT` valve (completed)
|
||||||
|
- [ ] Add `USE_NATIVE_TOOL_DISPLAY` valve
|
||||||
|
- [ ] Implement `_emit_tool_call_start()` helper
|
||||||
|
- [ ] Implement `_emit_tool_call_result()` helper
|
||||||
|
- [ ] Modify tool event handling in `stream_response()`
|
||||||
|
- [ ] Test event emitter message type support
|
||||||
|
- [ ] Test tool call message format
|
||||||
|
- [ ] Test mid-stream injection
|
||||||
|
- [ ] Update documentation
|
||||||
|
- [ ] Add user configuration guide
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 Recommendation
|
||||||
|
|
||||||
|
### Hybrid Approach (Safest)
|
||||||
|
|
||||||
|
Keep both display modes:
|
||||||
|
|
||||||
|
1. **Default (Current):** Markdown-based display
|
||||||
|
- ✅ Works reliably with streaming
|
||||||
|
- ✅ No OpenWebUI API dependencies
|
||||||
|
- ✅ Consistent across versions
|
||||||
|
|
||||||
|
2. **Experimental (Native):** Structured tool messages
|
||||||
|
- ✅ Better visual integration
|
||||||
|
- ⚠️ Requires testing with OpenWebUI internals
|
||||||
|
- ⚠️ May not work in all scenarios
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="[EXPERIMENTAL] Use OpenWebUI's native tool call display"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why Markdown Display is Currently Better
|
||||||
|
|
||||||
|
1. **Reliability:** Always works with streaming
|
||||||
|
2. **Flexibility:** Can customize format easily
|
||||||
|
3. **Context:** Shows tools inline with reasoning
|
||||||
|
4. **Compatibility:** Works across OpenWebUI versions
|
||||||
|
|
||||||
|
### When to Use Native Display
|
||||||
|
|
||||||
|
- Non-streaming mode (easier to inject messages)
|
||||||
|
- After confirming event emitter supports message type
|
||||||
|
- For tools with large JSON results (better formatting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Next Steps
|
||||||
|
|
||||||
|
1. **Research OpenWebUI Source Code**
|
||||||
|
- Check `__event_emitter__` implementation
|
||||||
|
- Verify supported event types
|
||||||
|
- Test message injection patterns
|
||||||
|
|
||||||
|
2. **Create Proof of Concept**
|
||||||
|
- Simple test plugin
|
||||||
|
- Emit tool call messages
|
||||||
|
- Verify UI rendering
|
||||||
|
|
||||||
|
3. **Document Findings**
|
||||||
|
- Update this guide with test results
|
||||||
|
- Add code examples that work
|
||||||
|
- Create migration guide if successful
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 References
|
||||||
|
|
||||||
|
- [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create)
|
||||||
|
- [OpenWebUI Plugin Development](https://docs.openwebui.com/)
|
||||||
|
- [Copilot SDK Events](https://github.com/github/copilot-sdk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Author:** Fu-Jie
|
||||||
|
**Status:** Analysis Complete - Implementation Pending Testing
|
||||||
480
plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md
Normal file
480
plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_GUIDE_CN.md
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
# OpenWebUI 原生工具调用展示实现指南
|
||||||
|
|
||||||
|
**日期:** 2026-01-27
|
||||||
|
**目的:** 分析并实现 OpenWebUI 的原生工具调用展示机制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 当前展示 vs 原生展示
|
||||||
|
|
||||||
|
### 当前实现
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
> 🔧 **Running Tool**: `search_chats`
|
||||||
|
|
||||||
|
> ✅ **Tool Completed**: {...}
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenWebUI 原生展示(来自截图)
|
||||||
|
|
||||||
|
- ✅ 可折叠面板:"查看来自 search_chats 的结果"
|
||||||
|
- ✅ 格式化的 JSON 显示
|
||||||
|
- ✅ 语法高亮
|
||||||
|
- ✅ 展开/折叠功能
|
||||||
|
- ✅ 清晰的视觉分隔
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 理解 OpenWebUI 的工具调用格式
|
||||||
|
|
||||||
|
### 标准 OpenAI 工具调用消息格式
|
||||||
|
|
||||||
|
OpenWebUI 遵循 OpenAI Chat Completion API 的工具调用格式:
|
||||||
|
|
||||||
|
#### 1. 带工具调用的助手消息
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None, # 或解释性文本
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_abc123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "search_chats",
|
||||||
|
"arguments": '{"query": ""}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 工具响应消息
|
||||||
|
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "call_abc123",
|
||||||
|
"name": "search_chats", # 可选但推荐
|
||||||
|
"content": '{"count": 5, "results": [...]}' # JSON 字符串
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 原生展示的实现策略
|
||||||
|
|
||||||
|
### 方案 1:事件发射器方法(推荐)
|
||||||
|
|
||||||
|
使用 OpenWebUI 的事件发射器发送结构化工具调用数据:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def stream_response(self, ...):
|
||||||
|
# 工具执行开始时
|
||||||
|
if event_type == "tool.execution_start":
|
||||||
|
await self._emit_tool_call_start(
|
||||||
|
emitter=__event_call__,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
arguments=arguments
|
||||||
|
)
|
||||||
|
|
||||||
|
# 工具执行完成时
|
||||||
|
elif event_type == "tool.execution_complete":
|
||||||
|
await self._emit_tool_call_result(
|
||||||
|
emitter=__event_call__,
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
result=result_content
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 辅助方法
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _emit_tool_call_start(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
tool_call_id: str,
|
||||||
|
tool_name: str,
|
||||||
|
arguments: dict
|
||||||
|
):
|
||||||
|
"""向 OpenWebUI 发射工具调用开始事件。"""
|
||||||
|
if not emitter:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# OpenWebUI 期望 assistant 消息格式的 tool_calls
|
||||||
|
await emitter({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": tool_call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": json.dumps(arguments, ensure_ascii=False)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发射工具调用开始事件失败: {e}")
|
||||||
|
|
||||||
|
async def _emit_tool_call_result(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
tool_call_id: str,
|
||||||
|
tool_name: str,
|
||||||
|
result: Any
|
||||||
|
):
|
||||||
|
"""向 OpenWebUI 发射工具调用结果。"""
|
||||||
|
if not emitter:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 将结果格式化为 JSON 字符串
|
||||||
|
if isinstance(result, str):
|
||||||
|
result_content = result
|
||||||
|
else:
|
||||||
|
result_content = json.dumps(result, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# OpenWebUI 期望 tool 消息格式的工具结果
|
||||||
|
await emitter({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"content": result_content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发射工具结果失败: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方案 2:消息历史注入
|
||||||
|
|
||||||
|
修改对话历史以包含工具调用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 工具执行后,追加到消息中
|
||||||
|
messages.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": tool_call_id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": json.dumps(arguments)
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
messages.append({
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": tool_call_id,
|
||||||
|
"name": tool_name,
|
||||||
|
"content": json.dumps(result)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 当前架构的挑战
|
||||||
|
|
||||||
|
### 1. 流式上下文
|
||||||
|
|
||||||
|
我们当前的实现使用:
|
||||||
|
|
||||||
|
- **基于队列的流式传输**:事件 → 队列 → 产出块
|
||||||
|
- **仅文本块**:我们产出纯文本,而非结构化消息
|
||||||
|
|
||||||
|
OpenWebUI 的原生展示需要:
|
||||||
|
|
||||||
|
- **结构化消息事件**:不是文本块
|
||||||
|
- **消息级别控制**:需要发射完整消息
|
||||||
|
|
||||||
|
### 2. 事件发射器兼容性
|
||||||
|
|
||||||
|
**当前用法:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 我们使用 event_emitter 发送状态/通知
|
||||||
|
await event_emitter({
|
||||||
|
"type": "status",
|
||||||
|
"data": {"description": "处理中..."}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**工具调用所需:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 需要发射 message 类型事件
|
||||||
|
await event_emitter({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "tool",
|
||||||
|
"content": "..."
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题:** `__event_emitter__` 是否支持 `message` 类型事件?
|
||||||
|
|
||||||
|
### 3. Session SDK 事件 vs OpenWebUI 消息
|
||||||
|
|
||||||
|
**Copilot SDK 事件:**
|
||||||
|
|
||||||
|
- `tool.execution_start` → 获取工具名称、参数
|
||||||
|
- `tool.execution_complete` → 获取工具结果
|
||||||
|
- 为流式文本输出设计
|
||||||
|
|
||||||
|
**OpenWebUI 消息:**
|
||||||
|
|
||||||
|
- 期望结构化消息对象
|
||||||
|
- 不为中间流注入设计
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 实验性实现
|
||||||
|
|
||||||
|
### 步骤 1:添加原生展示 Valve
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="使用 OpenWebUI 的原生工具调用展示,而非 Markdown 格式"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2:修改工具事件处理
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def stream_response(self, ...):
|
||||||
|
# ...现有代码...
|
||||||
|
|
||||||
|
def handler(event):
|
||||||
|
event_type = get_event_type(event)
|
||||||
|
|
||||||
|
if event_type == "tool.execution_start":
|
||||||
|
tool_name = safe_get_data_attr(event, "name")
|
||||||
|
|
||||||
|
# 获取工具参数
|
||||||
|
tool_input = safe_get_data_attr(event, "input") or {}
|
||||||
|
tool_call_id = safe_get_data_attr(event, "tool_call_id", f"call_{time.time()}")
|
||||||
|
|
||||||
|
if tool_call_id:
|
||||||
|
active_tools[tool_call_id] = {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": tool_input
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||||
|
# 发射结构化工具调用
|
||||||
|
asyncio.create_task(
|
||||||
|
self._emit_tool_call_start(
|
||||||
|
__event_call__,
|
||||||
|
tool_call_id,
|
||||||
|
tool_name,
|
||||||
|
tool_input
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 当前 Markdown 展示
|
||||||
|
queue.put_nowait(f"\n\n> 🔧 **运行工具**: `{tool_name}`\n\n")
|
||||||
|
|
||||||
|
elif event_type == "tool.execution_complete":
|
||||||
|
tool_call_id = safe_get_data_attr(event, "tool_call_id")
|
||||||
|
tool_info = active_tools.get(tool_call_id, {})
|
||||||
|
tool_name = tool_info.get("name", "未知")
|
||||||
|
|
||||||
|
# 提取结果
|
||||||
|
result_obj = safe_get_data_attr(event, "result")
|
||||||
|
result_content = ""
|
||||||
|
if hasattr(result_obj, "content"):
|
||||||
|
result_content = result_obj.content
|
||||||
|
elif isinstance(result_obj, dict):
|
||||||
|
result_content = result_obj.get("content", "")
|
||||||
|
|
||||||
|
if self.valves.USE_NATIVE_TOOL_DISPLAY:
|
||||||
|
# 发射结构化工具结果
|
||||||
|
asyncio.create_task(
|
||||||
|
self._emit_tool_call_result(
|
||||||
|
__event_call__,
|
||||||
|
tool_call_id,
|
||||||
|
tool_name,
|
||||||
|
result_content
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 当前 Markdown 展示
|
||||||
|
queue.put_nowait(f"> ✅ **工具完成**: {result_content}\n\n")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔬 测试计划
|
||||||
|
|
||||||
|
### 测试 1:事件发射器消息类型支持
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 在测试对话中尝试:
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "测试消息"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期:** 消息出现在聊天中
|
||||||
|
**如果失败:** 事件发射器不支持 message 类型
|
||||||
|
|
||||||
|
### 测试 2:工具调用消息格式
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 发送工具调用消息
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{
|
||||||
|
"id": "test_123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "test_tool",
|
||||||
|
"arguments": '{"param": "value"}'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 发送工具结果
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "message",
|
||||||
|
"data": {
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "test_123",
|
||||||
|
"name": "test_tool",
|
||||||
|
"content": '{"result": "success"}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期:** OpenWebUI 显示可折叠工具面板
|
||||||
|
**如果失败:** 事件格式与 OpenWebUI 期望不符
|
||||||
|
|
||||||
|
### 测试 3:中间流工具调用注入
|
||||||
|
|
||||||
|
测试是否可以在流式传输期间注入工具调用消息:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 开始流式文本
|
||||||
|
yield "正在处理您的请求..."
|
||||||
|
|
||||||
|
# 中间流:发射工具调用
|
||||||
|
await __event_emitter__({"type": "message", "data": {...}})
|
||||||
|
|
||||||
|
# 继续流式传输
|
||||||
|
yield "完成!"
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期:** 工具面板出现在响应中间
|
||||||
|
**风险:** 可能破坏流式传输流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 实施检查清单
|
||||||
|
|
||||||
|
- [x] 添加 `REASONING_EFFORT` valve(已完成)
|
||||||
|
- [ ] 添加 `USE_NATIVE_TOOL_DISPLAY` valve
|
||||||
|
- [ ] 实现 `_emit_tool_call_start()` 辅助方法
|
||||||
|
- [ ] 实现 `_emit_tool_call_result()` 辅助方法
|
||||||
|
- [ ] 修改 `stream_response()` 中的工具事件处理
|
||||||
|
- [ ] 测试事件发射器消息类型支持
|
||||||
|
- [ ] 测试工具调用消息格式
|
||||||
|
- [ ] 测试中间流注入
|
||||||
|
- [ ] 更新文档
|
||||||
|
- [ ] 添加用户配置指南
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 建议
|
||||||
|
|
||||||
|
### 混合方法(最安全)
|
||||||
|
|
||||||
|
保留两种展示模式:
|
||||||
|
|
||||||
|
1. **默认(当前):** 基于 Markdown 的展示
|
||||||
|
- ✅ 与流式传输可靠工作
|
||||||
|
- ✅ 无 OpenWebUI API 依赖
|
||||||
|
- ✅ 跨版本一致
|
||||||
|
|
||||||
|
2. **实验性(原生):** 结构化工具消息
|
||||||
|
- ✅ 更好的视觉集成
|
||||||
|
- ⚠️ 需要测试 OpenWebUI 内部
|
||||||
|
- ⚠️ 可能不适用于所有场景
|
||||||
|
|
||||||
|
**配置:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
USE_NATIVE_TOOL_DISPLAY: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="[实验性] 使用 OpenWebUI 的原生工具调用展示"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 为什么 Markdown 展示目前更好
|
||||||
|
|
||||||
|
1. **可靠性:** 始终与流式传输兼容
|
||||||
|
2. **灵活性:** 可以轻松自定义格式
|
||||||
|
3. **上下文:** 与推理内联显示工具
|
||||||
|
4. **兼容性:** 跨 OpenWebUI 版本工作
|
||||||
|
|
||||||
|
### 何时使用原生展示
|
||||||
|
|
||||||
|
- 非流式模式(更容易注入消息)
|
||||||
|
- 确认事件发射器支持 message 类型后
|
||||||
|
- 对于具有大型 JSON 结果的工具(更好的格式化)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 后续步骤
|
||||||
|
|
||||||
|
1. **研究 OpenWebUI 源代码**
|
||||||
|
- 检查 `__event_emitter__` 实现
|
||||||
|
- 验证支持的事件类型
|
||||||
|
- 测试消息注入模式
|
||||||
|
|
||||||
|
2. **创建概念验证**
|
||||||
|
- 简单测试插件
|
||||||
|
- 发射工具调用消息
|
||||||
|
- 验证 UI 渲染
|
||||||
|
|
||||||
|
3. **记录发现**
|
||||||
|
- 使用测试结果更新本指南
|
||||||
|
- 添加有效的代码示例
|
||||||
|
- 如果成功,创建迁移指南
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 参考资料
|
||||||
|
|
||||||
|
- [OpenAI Chat Completion API](https://platform.openai.com/docs/api-reference/chat/create)
|
||||||
|
- [OpenWebUI 插件开发](https://docs.openwebui.com/)
|
||||||
|
- [Copilot SDK 事件](https://github.com/github/copilot-sdk)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**作者:** Fu-Jie
|
||||||
|
**状态:** 分析完成 - 实施等待测试
|
||||||
182
plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_USAGE.md
Normal file
182
plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_USAGE.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# Native Tool Display Usage Guide
|
||||||
|
|
||||||
|
## 🎨 What is Native Tool Display?
|
||||||
|
|
||||||
|
Native Tool Display is an experimental feature that integrates with OpenWebUI's built-in tool call visualization system. When enabled, tool calls and their results are displayed in **collapsible JSON panels** instead of plain markdown text.
|
||||||
|
|
||||||
|
### Visual Comparison
|
||||||
|
|
||||||
|
**Traditional Display (markdown):**
|
||||||
|
|
||||||
|
```
|
||||||
|
> 🔧 Running Tool: `get_current_time`
|
||||||
|
> ✅ Tool Completed: 2026-01-27 10:30:00
|
||||||
|
```
|
||||||
|
|
||||||
|
**Native Display (collapsible panels):**
|
||||||
|
|
||||||
|
- Tool call appears in a collapsible `assistant.tool_calls` panel
|
||||||
|
- Tool result appears in a separate collapsible `tool.content` panel
|
||||||
|
- JSON syntax highlighting for better readability
|
||||||
|
- Compact by default, expandable on click
|
||||||
|
|
||||||
|
## 🚀 How to Enable
|
||||||
|
|
||||||
|
1. Open the GitHub Copilot SDK Pipe configuration (Valves)
|
||||||
|
2. Set `USE_NATIVE_TOOL_DISPLAY` to `true`
|
||||||
|
3. Save the configuration
|
||||||
|
4. Start a new conversation with tool calls
|
||||||
|
|
||||||
|
## 📋 Requirements
|
||||||
|
|
||||||
|
- OpenWebUI with native tool display support
|
||||||
|
- `__event_emitter__` must support `message` type events
|
||||||
|
- Tool-enabled models (e.g., GPT-4, Claude Sonnet)
|
||||||
|
|
||||||
|
## ⚙️ How It Works
|
||||||
|
|
||||||
|
### OpenAI Standard Format
|
||||||
|
|
||||||
|
The native display uses the OpenAI standard message format:
|
||||||
|
|
||||||
|
**Tool Call (Assistant Message):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_abc123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_current_time",
|
||||||
|
"arguments": "{\"timezone\":\"UTC\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tool Result (Tool Message):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "call_abc123",
|
||||||
|
"content": "2026-01-27 10:30:00 UTC"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Message Flow
|
||||||
|
|
||||||
|
1. **Tool Execution Start**:
|
||||||
|
- SDK emits `tool.execution_start` event
|
||||||
|
- Plugin sends `assistant` message with `tool_calls` array
|
||||||
|
- OpenWebUI displays collapsible tool call panel
|
||||||
|
|
||||||
|
2. **Tool Execution Complete**:
|
||||||
|
- SDK emits `tool.execution_complete` event
|
||||||
|
- Plugin sends `tool` message with `tool_call_id` and `content`
|
||||||
|
- OpenWebUI displays collapsible result panel
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
|
### Panel Not Showing
|
||||||
|
|
||||||
|
**Symptoms:** Tool calls still appear as markdown text
|
||||||
|
|
||||||
|
**Possible Causes:**
|
||||||
|
|
||||||
|
1. `__event_emitter__` doesn't support `message` type events
|
||||||
|
2. OpenWebUI version too old
|
||||||
|
3. Feature not enabled (`USE_NATIVE_TOOL_DISPLAY = false`)
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
- Enable DEBUG mode to see error messages in browser console
|
||||||
|
- Check browser console for "Native message emission failed" warnings
|
||||||
|
- Update OpenWebUI to latest version
|
||||||
|
- Keep `USE_NATIVE_TOOL_DISPLAY = false` to use traditional markdown display
|
||||||
|
|
||||||
|
### Duplicate Tool Information
|
||||||
|
|
||||||
|
**Symptoms:** Tool calls appear in both native panels and markdown
|
||||||
|
|
||||||
|
**Cause:** Mixed display modes
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
- Ensure `USE_NATIVE_TOOL_DISPLAY` is either `true` (native only) or `false` (markdown only)
|
||||||
|
- Restart the conversation after changing this setting
|
||||||
|
|
||||||
|
## 🧪 Experimental Status
|
||||||
|
|
||||||
|
This feature is marked as **EXPERIMENTAL** because:
|
||||||
|
|
||||||
|
1. **Event Emitter API**: The `__event_emitter__` support for `message` type events is not fully documented
|
||||||
|
2. **OpenWebUI Version Dependency**: Requires recent versions of OpenWebUI with native tool display support
|
||||||
|
3. **Streaming Architecture**: May have compatibility issues with streaming responses
|
||||||
|
|
||||||
|
### Fallback Behavior
|
||||||
|
|
||||||
|
If native message emission fails:
|
||||||
|
|
||||||
|
- Plugin automatically falls back to markdown display
|
||||||
|
- Error logged to browser console (when DEBUG is enabled)
|
||||||
|
- No interruption to conversation flow
|
||||||
|
|
||||||
|
## 📊 Performance Considerations
|
||||||
|
|
||||||
|
Native display has slightly better performance characteristics:
|
||||||
|
|
||||||
|
| Aspect | Native Display | Markdown Display |
|
||||||
|
|--------|----------------|------------------|
|
||||||
|
| **Rendering** | Native UI components | Markdown parser |
|
||||||
|
| **Interactivity** | Collapsible panels | Static text |
|
||||||
|
| **JSON Parsing** | Handled by UI | Not formatted |
|
||||||
|
| **Token Usage** | Minimal overhead | Formatting tokens |
|
||||||
|
|
||||||
|
## 🔮 Future Enhancements
|
||||||
|
|
||||||
|
Planned improvements for native tool display:
|
||||||
|
|
||||||
|
- [ ] Automatic fallback detection
|
||||||
|
- [ ] Tool call history persistence
|
||||||
|
- [ ] Rich metadata display (execution time, arguments preview)
|
||||||
|
- [ ] Copy tool call JSON button
|
||||||
|
- [ ] Tool call replay functionality
|
||||||
|
|
||||||
|
## 💡 Best Practices
|
||||||
|
|
||||||
|
1. **Enable DEBUG First**: Test with DEBUG mode before using in production
|
||||||
|
2. **Monitor Browser Console**: Check for warning messages during tool calls
|
||||||
|
3. **Test with Simple Tools**: Verify with built-in tools before custom implementations
|
||||||
|
4. **Keep Fallback Option**: Don't rely solely on native display until it exits experimental status
|
||||||
|
|
||||||
|
## 📖 Related Documentation
|
||||||
|
|
||||||
|
- [TOOLS_USAGE.md](TOOLS_USAGE.md) - How to create and use custom tools
|
||||||
|
- [NATIVE_TOOL_DISPLAY_GUIDE.md](NATIVE_TOOL_DISPLAY_GUIDE.md) - Technical implementation details
|
||||||
|
- [WORKFLOW.md](WORKFLOW.md) - Complete integration workflow
|
||||||
|
|
||||||
|
## 🐛 Reporting Issues
|
||||||
|
|
||||||
|
If you encounter issues with native tool display:
|
||||||
|
|
||||||
|
1. Enable `DEBUG` and `USE_NATIVE_TOOL_DISPLAY`
|
||||||
|
2. Open browser console (F12)
|
||||||
|
3. Trigger a tool call
|
||||||
|
4. Copy any error messages
|
||||||
|
5. Report to [GitHub Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- OpenWebUI version
|
||||||
|
- Browser and version
|
||||||
|
- Error messages from console
|
||||||
|
- Steps to reproduce
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Author:** Fu-Jie | **Version:** 0.2.0 | **License:** MIT
|
||||||
182
plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md
Normal file
182
plugins/debug/legacy_docs/guides/NATIVE_TOOL_DISPLAY_USAGE_CN.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# 原生工具显示使用指南
|
||||||
|
|
||||||
|
## 🎨 什么是原生工具显示?
|
||||||
|
|
||||||
|
原生工具显示是一项实验性功能,与 OpenWebUI 的内置工具调用可视化系统集成。启用后,工具调用及其结果将以**可折叠的 JSON 面板**显示,而不是纯文本 markdown。
|
||||||
|
|
||||||
|
### 视觉对比
|
||||||
|
|
||||||
|
**传统显示 (markdown):**
|
||||||
|
|
||||||
|
```
|
||||||
|
> 🔧 正在运行工具: `get_current_time`
|
||||||
|
> ✅ 工具已完成: 2026-01-27 10:30:00
|
||||||
|
```
|
||||||
|
|
||||||
|
**原生显示 (可折叠面板):**
|
||||||
|
|
||||||
|
- 工具调用显示在可折叠的 `assistant.tool_calls` 面板中
|
||||||
|
- 工具结果显示在单独的可折叠 `tool.content` 面板中
|
||||||
|
- JSON 语法高亮,提高可读性
|
||||||
|
- 默认折叠,点击即可展开
|
||||||
|
|
||||||
|
## 🚀 如何启用
|
||||||
|
|
||||||
|
1. 打开 GitHub Copilot SDK Pipe 配置 (Valves)
|
||||||
|
2. 将 `USE_NATIVE_TOOL_DISPLAY` 设置为 `true`
|
||||||
|
3. 保存配置
|
||||||
|
4. 开始新的对话并使用工具调用
|
||||||
|
|
||||||
|
## 📋 要求
|
||||||
|
|
||||||
|
- 支持原生工具显示的 OpenWebUI
|
||||||
|
- `__event_emitter__` 必须支持 `message` 类型事件
|
||||||
|
- 支持工具的模型(例如 GPT-4、Claude Sonnet)
|
||||||
|
|
||||||
|
## ⚙️ 工作原理
|
||||||
|
|
||||||
|
### OpenAI 标准格式
|
||||||
|
|
||||||
|
原生显示使用 OpenAI 标准消息格式:
|
||||||
|
|
||||||
|
**工具调用(助手消息):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "",
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_abc123",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_current_time",
|
||||||
|
"arguments": "{\"timezone\":\"UTC\"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**工具结果(工具消息):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"role": "tool",
|
||||||
|
"tool_call_id": "call_abc123",
|
||||||
|
"content": "2026-01-27 10:30:00 UTC"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 消息流程
|
||||||
|
|
||||||
|
1. **工具执行开始**:
|
||||||
|
- SDK 发出 `tool.execution_start` 事件
|
||||||
|
- 插件发送带有 `tool_calls` 数组的 `assistant` 消息
|
||||||
|
- OpenWebUI 显示可折叠的工具调用面板
|
||||||
|
|
||||||
|
2. **工具执行完成**:
|
||||||
|
- SDK 发出 `tool.execution_complete` 事件
|
||||||
|
- 插件发送带有 `tool_call_id` 和 `content` 的 `tool` 消息
|
||||||
|
- OpenWebUI 显示可折叠的结果面板
|
||||||
|
|
||||||
|
## 🔧 故障排除
|
||||||
|
|
||||||
|
### 面板未显示
|
||||||
|
|
||||||
|
**症状:** 工具调用仍以 markdown 文本形式显示
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
|
||||||
|
1. `__event_emitter__` 不支持 `message` 类型事件
|
||||||
|
2. OpenWebUI 版本过旧
|
||||||
|
3. 功能未启用(`USE_NATIVE_TOOL_DISPLAY = false`)
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
|
||||||
|
- 启用 DEBUG 模式查看浏览器控制台中的错误消息
|
||||||
|
- 检查浏览器控制台的 "Native message emission failed" 警告
|
||||||
|
- 更新 OpenWebUI 到最新版本
|
||||||
|
- 保持 `USE_NATIVE_TOOL_DISPLAY = false` 使用传统 markdown 显示
|
||||||
|
|
||||||
|
### 重复的工具信息
|
||||||
|
|
||||||
|
**症状:** 工具调用同时出现在原生面板和 markdown 中
|
||||||
|
|
||||||
|
**原因:** 混合显示模式
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
|
||||||
|
- 确保 `USE_NATIVE_TOOL_DISPLAY` 为 `true`(仅原生)或 `false`(仅 markdown)
|
||||||
|
- 更改设置后重启对话
|
||||||
|
|
||||||
|
## 🧪 实验性状态
|
||||||
|
|
||||||
|
此功能标记为**实验性**,因为:
|
||||||
|
|
||||||
|
1. **事件发射器 API**:`__event_emitter__` 对 `message` 类型事件的支持未完全文档化
|
||||||
|
2. **OpenWebUI 版本依赖**:需要支持原生工具显示的较新 OpenWebUI 版本
|
||||||
|
3. **流式架构**:可能与流式响应存在兼容性问题
|
||||||
|
|
||||||
|
### 回退行为
|
||||||
|
|
||||||
|
如果原生消息发送失败:
|
||||||
|
|
||||||
|
- 插件自动回退到 markdown 显示
|
||||||
|
- 错误记录到浏览器控制台(启用 DEBUG 时)
|
||||||
|
- 不会中断对话流程
|
||||||
|
|
||||||
|
## 📊 性能考虑
|
||||||
|
|
||||||
|
原生显示具有略好的性能特征:
|
||||||
|
|
||||||
|
| 方面 | 原生显示 | Markdown 显示 |
|
||||||
|
|------|----------|---------------|
|
||||||
|
| **渲染** | 原生 UI 组件 | Markdown 解析器 |
|
||||||
|
| **交互性** | 可折叠面板 | 静态文本 |
|
||||||
|
| **JSON 解析** | 由 UI 处理 | 未格式化 |
|
||||||
|
| **Token 使用** | 最小开销 | 格式化 token |
|
||||||
|
|
||||||
|
## 🔮 未来增强
|
||||||
|
|
||||||
|
原生工具显示的计划改进:
|
||||||
|
|
||||||
|
- [ ] 自动回退检测
|
||||||
|
- [ ] 工具调用历史持久化
|
||||||
|
- [ ] 丰富的元数据显示(执行时间、参数预览)
|
||||||
|
- [ ] 复制工具调用 JSON 按钮
|
||||||
|
- [ ] 工具调用重放功能
|
||||||
|
|
||||||
|
## 💡 最佳实践
|
||||||
|
|
||||||
|
1. **先启用 DEBUG**:在生产环境使用前先在 DEBUG 模式下测试
|
||||||
|
2. **监控浏览器控制台**:在工具调用期间检查警告消息
|
||||||
|
3. **使用简单工具测试**:在自定义实现前先用内置工具验证
|
||||||
|
4. **保留回退选项**:在退出实验性状态前不要完全依赖原生显示
|
||||||
|
|
||||||
|
## 📖 相关文档
|
||||||
|
|
||||||
|
- [TOOLS_USAGE.md](TOOLS_USAGE.md) - 如何创建和使用自定义工具
|
||||||
|
- [NATIVE_TOOL_DISPLAY_GUIDE.md](NATIVE_TOOL_DISPLAY_GUIDE.md) - 技术实现细节
|
||||||
|
- [WORKFLOW.md](WORKFLOW.md) - 完整集成工作流程
|
||||||
|
|
||||||
|
## 🐛 报告问题
|
||||||
|
|
||||||
|
如果您在使用原生工具显示时遇到问题:
|
||||||
|
|
||||||
|
1. 启用 `DEBUG` 和 `USE_NATIVE_TOOL_DISPLAY`
|
||||||
|
2. 打开浏览器控制台(F12)
|
||||||
|
3. 触发工具调用
|
||||||
|
4. 复制任何错误消息
|
||||||
|
5. 报告到 [GitHub Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
包含:
|
||||||
|
|
||||||
|
- OpenWebUI 版本
|
||||||
|
- 浏览器和版本
|
||||||
|
- 控制台的错误消息
|
||||||
|
- 复现步骤
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**作者:** Fu-Jie | **版本:** 0.2.0 | **许可证:** MIT
|
||||||
@@ -0,0 +1,509 @@
|
|||||||
|
# OpenWebUI Function 集成方案
|
||||||
|
|
||||||
|
## 🎯 核心挑战
|
||||||
|
|
||||||
|
在 Copilot Tool Handler 中调用 OpenWebUI Functions 的关键问题:
|
||||||
|
|
||||||
|
**问题:** Copilot SDK 的 Tool Handler 是一个独立的回调函数,如何在这个上下文中访问和执行 OpenWebUI 的 Function?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 OpenWebUI Function 系统分析
|
||||||
|
|
||||||
|
### 1. Function 数据结构
|
||||||
|
|
||||||
|
OpenWebUI 的 Function/Tool 传递格式:
|
||||||
|
|
||||||
|
```python
|
||||||
|
body = {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"description": "Get current weather",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["location"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Function 执行机制
|
||||||
|
|
||||||
|
OpenWebUI Functions 的执行方式有几种可能:
|
||||||
|
|
||||||
|
#### 选项 A: 通过 Function ID 调用内部 API
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 假设 OpenWebUI 提供内部 API
|
||||||
|
from open_webui.apps.webui.models.functions import Functions
|
||||||
|
|
||||||
|
function_id = "function_uuid" # 需要从配置中获取
|
||||||
|
result = await Functions.execute_function(
|
||||||
|
function_id=function_id,
|
||||||
|
arguments={"location": "Beijing"}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 选项 B: 通过 **event_emitter** 触发
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 通过事件系统触发 function 执行
|
||||||
|
if __event_emitter__:
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "function_call",
|
||||||
|
"data": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"arguments": {"location": "Beijing"}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 选项 C: 自己实现 Function 逻辑
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 在 Pipe 内部实现常用功能
|
||||||
|
class Pipe:
|
||||||
|
def _builtin_get_weather(self, location: str) -> dict:
|
||||||
|
# 实现天气查询
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _builtin_search_web(self, query: str) -> dict:
|
||||||
|
# 实现网页搜索
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 推荐方案:混合架构
|
||||||
|
|
||||||
|
### 架构设计
|
||||||
|
|
||||||
|
```
|
||||||
|
User Message
|
||||||
|
↓
|
||||||
|
OpenWebUI UI (Functions 已配置)
|
||||||
|
↓
|
||||||
|
Pipe.pipe(body) - body 包含 tools[]
|
||||||
|
↓
|
||||||
|
转换为 Copilot Tools + 存储 Function Registry
|
||||||
|
↓
|
||||||
|
Copilot 决定调用 Tool
|
||||||
|
↓
|
||||||
|
Tool Handler 查询 Registry → 执行对应逻辑
|
||||||
|
↓
|
||||||
|
返回结果给 Copilot
|
||||||
|
↓
|
||||||
|
继续生成回答
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心实现
|
||||||
|
|
||||||
|
#### 1. Function Registry(函数注册表)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Pipe:
|
||||||
|
def __init__(self):
|
||||||
|
# ...
|
||||||
|
self._function_registry = {} # {function_name: callable}
|
||||||
|
self._function_metadata = {} # {function_name: metadata}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 注册 Functions
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _register_openwebui_functions(
|
||||||
|
self,
|
||||||
|
owui_functions: List[dict],
|
||||||
|
__event_emitter__=None,
|
||||||
|
__event_call__=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
注册 OpenWebUI Functions 到内部 registry
|
||||||
|
|
||||||
|
关键:将 function 定义和执行逻辑关联起来
|
||||||
|
"""
|
||||||
|
for func_def in owui_functions:
|
||||||
|
if func_def.get("type") != "function":
|
||||||
|
continue
|
||||||
|
|
||||||
|
func_info = func_def.get("function", {})
|
||||||
|
func_name = func_info.get("name")
|
||||||
|
|
||||||
|
if not func_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 存储元数据
|
||||||
|
self._function_metadata[func_name] = {
|
||||||
|
"description": func_info.get("description", ""),
|
||||||
|
"parameters": func_info.get("parameters", {}),
|
||||||
|
"original_def": func_def
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建执行器(关键)
|
||||||
|
executor = self._create_function_executor(
|
||||||
|
func_name,
|
||||||
|
func_def,
|
||||||
|
__event_emitter__,
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
self._function_registry[func_name] = executor
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Function Executor 工厂
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _create_function_executor(
|
||||||
|
self,
|
||||||
|
func_name: str,
|
||||||
|
func_def: dict,
|
||||||
|
__event_emitter__=None,
|
||||||
|
__event_call__=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
为每个 function 创建执行器
|
||||||
|
|
||||||
|
策略:
|
||||||
|
1. 优先使用内置实现
|
||||||
|
2. 尝试调用 OpenWebUI API
|
||||||
|
3. 返回错误
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def executor(arguments: dict) -> dict:
|
||||||
|
# 策略 1: 检查是否有内置实现
|
||||||
|
builtin_method = getattr(self, f"_builtin_{func_name}", None)
|
||||||
|
if builtin_method:
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"Using builtin implementation for {func_name}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = builtin_method(arguments)
|
||||||
|
if inspect.iscoroutine(result):
|
||||||
|
result = await result
|
||||||
|
return {"success": True, "result": result}
|
||||||
|
except Exception as e:
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
# 策略 2: 尝试通过 Event Emitter 调用
|
||||||
|
if __event_emitter__:
|
||||||
|
try:
|
||||||
|
# 尝试触发 function_call 事件
|
||||||
|
response_queue = asyncio.Queue()
|
||||||
|
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "function_call",
|
||||||
|
"data": {
|
||||||
|
"name": func_name,
|
||||||
|
"arguments": arguments,
|
||||||
|
"response_queue": response_queue # 回调队列
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 等待结果(带超时)
|
||||||
|
result = await asyncio.wait_for(
|
||||||
|
response_queue.get(),
|
||||||
|
timeout=self.valves.TOOL_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"success": True, "result": result}
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return {"success": False, "error": "Function execution timeout"}
|
||||||
|
except Exception as e:
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"Event emitter call failed: {e}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
# 继续尝试其他方法
|
||||||
|
|
||||||
|
# 策略 3: 尝试调用 OpenWebUI internal API
|
||||||
|
try:
|
||||||
|
# 这需要研究 OpenWebUI 源码确定正确的调用方式
|
||||||
|
from open_webui.apps.webui.models.functions import Functions
|
||||||
|
|
||||||
|
# 需要获取 function_id(这是关键问题)
|
||||||
|
function_id = self._get_function_id_by_name(func_name)
|
||||||
|
|
||||||
|
if function_id:
|
||||||
|
result = await Functions.execute(
|
||||||
|
function_id=function_id,
|
||||||
|
params=arguments
|
||||||
|
)
|
||||||
|
return {"success": True, "result": result}
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"OpenWebUI API call failed: {e}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
# 策略 4: 返回"未实现"错误
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Function '{func_name}' is not implemented. "
|
||||||
|
"Please implement it as a builtin method or ensure OpenWebUI API is available."
|
||||||
|
}
|
||||||
|
|
||||||
|
return executor
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Tool Handler 实现
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _create_tool_handler(self, tool_name: str, __event_call__=None):
|
||||||
|
"""为 Copilot SDK 创建 Tool Handler"""
|
||||||
|
|
||||||
|
async def handler(invocation: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Copilot Tool Handler
|
||||||
|
|
||||||
|
invocation: {
|
||||||
|
"session_id": str,
|
||||||
|
"tool_call_id": str,
|
||||||
|
"tool_name": str,
|
||||||
|
"arguments": dict
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 从 registry 获取 executor
|
||||||
|
executor = self._function_registry.get(invocation["tool_name"])
|
||||||
|
|
||||||
|
if not executor:
|
||||||
|
return {
|
||||||
|
"textResultForLlm": f"Function '{invocation['tool_name']}' not found.",
|
||||||
|
"resultType": "failure",
|
||||||
|
"error": "function_not_found",
|
||||||
|
"toolTelemetry": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行 function
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"Executing function: {invocation['tool_name']}({invocation['arguments']})",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
exec_result = await executor(invocation["arguments"])
|
||||||
|
|
||||||
|
# 处理结果
|
||||||
|
if exec_result.get("success"):
|
||||||
|
result_text = str(exec_result.get("result", ""))
|
||||||
|
return {
|
||||||
|
"textResultForLlm": result_text,
|
||||||
|
"resultType": "success",
|
||||||
|
"error": None,
|
||||||
|
"toolTelemetry": {}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
error_msg = exec_result.get("error", "Unknown error")
|
||||||
|
return {
|
||||||
|
"textResultForLlm": f"Function execution failed: {error_msg}",
|
||||||
|
"resultType": "failure",
|
||||||
|
"error": error_msg,
|
||||||
|
"toolTelemetry": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"Tool handler error: {e}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"textResultForLlm": "An unexpected error occurred during function execution.",
|
||||||
|
"resultType": "failure",
|
||||||
|
"error": str(e),
|
||||||
|
"toolTelemetry": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 内置 Functions 实现示例
|
||||||
|
|
||||||
|
### 示例 1: 获取当前时间
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _builtin_get_current_time(self, arguments: dict) -> str:
|
||||||
|
"""内置实现:获取当前时间"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
timezone = arguments.get("timezone", "UTC")
|
||||||
|
format_str = arguments.get("format", "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
return now.strftime(format_str)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2: 简单计算器
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _builtin_calculate(self, arguments: dict) -> str:
|
||||||
|
"""内置实现:数学计算"""
|
||||||
|
expression = arguments.get("expression", "")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 安全的数学计算(仅允许基本运算)
|
||||||
|
allowed_chars = set("0123456789+-*/()., ")
|
||||||
|
if not all(c in allowed_chars for c in expression):
|
||||||
|
raise ValueError("Invalid characters in expression")
|
||||||
|
|
||||||
|
result = eval(expression, {"__builtins__": {}})
|
||||||
|
return str(result)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Calculation error: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3: 网页搜索(需要外部 API)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _builtin_search_web(self, arguments: dict) -> str:
|
||||||
|
"""内置实现:网页搜索(使用 DuckDuckGo)"""
|
||||||
|
query = arguments.get("query", "")
|
||||||
|
max_results = arguments.get("max_results", 5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 使用 duckduckgo_search 库
|
||||||
|
from duckduckgo_search import DDGS
|
||||||
|
|
||||||
|
results = []
|
||||||
|
with DDGS() as ddgs:
|
||||||
|
for r in ddgs.text(query, max_results=max_results):
|
||||||
|
results.append({
|
||||||
|
"title": r.get("title", ""),
|
||||||
|
"url": r.get("href", ""),
|
||||||
|
"snippet": r.get("body", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
# 格式化结果
|
||||||
|
formatted = "\n\n".join([
|
||||||
|
f"**{r['title']}**\n{r['url']}\n{r['snippet']}"
|
||||||
|
for r in results
|
||||||
|
])
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Search failed: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 完整集成流程
|
||||||
|
|
||||||
|
### pipe() 方法中的集成
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def pipe(
|
||||||
|
self,
|
||||||
|
body: dict,
|
||||||
|
__metadata__: Optional[dict] = None,
|
||||||
|
__event_emitter__=None,
|
||||||
|
__event_call__=None,
|
||||||
|
) -> Union[str, AsyncGenerator]:
|
||||||
|
# ... 现有代码 ...
|
||||||
|
|
||||||
|
# ✅ Step 1: 提取 OpenWebUI Functions
|
||||||
|
owui_functions = body.get("tools", [])
|
||||||
|
|
||||||
|
# ✅ Step 2: 注册 Functions
|
||||||
|
if self.valves.ENABLE_TOOLS and owui_functions:
|
||||||
|
self._register_openwebui_functions(
|
||||||
|
owui_functions,
|
||||||
|
__event_emitter__,
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ Step 3: 转换为 Copilot Tools
|
||||||
|
copilot_tools = []
|
||||||
|
for func_name in self._function_registry.keys():
|
||||||
|
metadata = self._function_metadata[func_name]
|
||||||
|
copilot_tools.append({
|
||||||
|
"name": func_name,
|
||||||
|
"description": metadata["description"],
|
||||||
|
"parameters": metadata["parameters"],
|
||||||
|
"handler": self._create_tool_handler(func_name, __event_call__)
|
||||||
|
})
|
||||||
|
|
||||||
|
# ✅ Step 4: 创建 Session 并传递 Tools
|
||||||
|
session_config = SessionConfig(
|
||||||
|
model=real_model_id,
|
||||||
|
tools=copilot_tools, # ✅ 关键
|
||||||
|
...
|
||||||
|
)
|
||||||
|
|
||||||
|
session = await client.create_session(config=session_config)
|
||||||
|
|
||||||
|
# ... 后续代码 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 待解决问题
|
||||||
|
|
||||||
|
### 1. Function ID 映射
|
||||||
|
|
||||||
|
**问题:** OpenWebUI Functions 通常通过 UUID 标识,但 body 中只有 name
|
||||||
|
|
||||||
|
**解决思路:**
|
||||||
|
|
||||||
|
- 在 OpenWebUI 启动时建立 name → id 映射表
|
||||||
|
- 或者修改 OpenWebUI 在 body 中同时传递 id
|
||||||
|
|
||||||
|
### 2. Event Emitter 回调机制
|
||||||
|
|
||||||
|
**问题:** 不确定 **event_emitter** 是否支持 function_call 事件
|
||||||
|
|
||||||
|
**验证方法:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 测试代码
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "function_call",
|
||||||
|
"data": {"name": "test_func", "arguments": {}}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 异步执行超时
|
||||||
|
|
||||||
|
**问题:** 某些 Functions 可能执行很慢
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
|
||||||
|
- 实现 timeout 机制(已在 executor 中实现)
|
||||||
|
- 对于长时间运行的任务,考虑返回"processing"状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 实现清单
|
||||||
|
|
||||||
|
- [ ] 实现 _function_registry 和 _function_metadata
|
||||||
|
- [ ] 实现 _register_openwebui_functions()
|
||||||
|
- [ ] 实现 _create_function_executor()
|
||||||
|
- [ ] 实现 _create_tool_handler()
|
||||||
|
- [ ] 实现 3-5 个常用内置 Functions
|
||||||
|
- [ ] 测试 Function 注册和调用流程
|
||||||
|
- [ ] 验证 **event_emitter** 机制
|
||||||
|
- [ ] 研究 OpenWebUI Functions API
|
||||||
|
- [ ] 添加错误处理和超时机制
|
||||||
|
- [ ] 更新文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**下一步行动:**
|
||||||
|
|
||||||
|
1. 实现基础的 Function Registry
|
||||||
|
2. 添加 2-3 个简单的内置 Functions(如 get_time, calculate)
|
||||||
|
3. 测试基本的 Tool Calling 流程
|
||||||
|
4. 根据测试结果调整架构
|
||||||
|
|
||||||
|
**作者:** Fu-Jie
|
||||||
|
**日期:** 2026-01-26
|
||||||
@@ -0,0 +1,708 @@
|
|||||||
|
# SessionConfig 完整功能集成指南
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
本文档详细说明如何将 GitHub Copilot SDK 的 `SessionConfig` 所有功能集成到 OpenWebUI Pipe 中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 功能清单与集成状态
|
||||||
|
|
||||||
|
| 功能 | 状态 | 优先级 | 说明 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `session_id` | ✅ 已实现 | 高 | 使用 OpenWebUI chat_id |
|
||||||
|
| `model` | ✅ 已实现 | 高 | 从 body 动态获取 |
|
||||||
|
| `tools` | ✅ 已实现 | 高 | v0.2.0 新增示例工具 |
|
||||||
|
| `streaming` | ✅ 已实现 | 高 | 支持流式输出 |
|
||||||
|
| `infinite_sessions` | ✅ 已实现 | 高 | 自动上下文压缩 |
|
||||||
|
| `system_message` | ⚠️ 部分支持 | 中 | 可通过 Valves 添加 |
|
||||||
|
| `available_tools` | ⚠️ 部分支持 | 中 | 已有 AVAILABLE_TOOLS |
|
||||||
|
| `excluded_tools` | 🔲 未实现 | 低 | 可添加到 Valves |
|
||||||
|
| `on_permission_request` | 🔲 未实现 | 中 | 需要 UI 交互支持 |
|
||||||
|
| `provider` (BYOK) | 🔲 未实现 | 低 | 高级功能 |
|
||||||
|
| `mcp_servers` | 🔲 未实现 | 低 | MCP 协议支持 |
|
||||||
|
| `custom_agents` | 🔲 未实现 | 低 | 自定义代理 |
|
||||||
|
| `config_dir` | 🔲 未实现 | 低 | 可通过 WORKSPACE_DIR |
|
||||||
|
| `skill_directories` | 🔲 未实现 | 低 | 技能系统 |
|
||||||
|
| `disabled_skills` | 🔲 未实现 | 低 | 技能过滤 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 详细集成方案
|
||||||
|
|
||||||
|
### 1. ✅ session_id(已实现)
|
||||||
|
|
||||||
|
**功能:** 持久化会话 ID
|
||||||
|
|
||||||
|
**当前实现:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
session_config = SessionConfig(
|
||||||
|
session_id=chat_id if chat_id else None, # 使用 OpenWebUI 的 chat_id
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作原理:**
|
||||||
|
|
||||||
|
- OpenWebUI 的 `chat_id` 直接映射为 Copilot 的 `session_id`
|
||||||
|
- 会话状态持久化到磁盘
|
||||||
|
- 支持跨重启恢复对话
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. ✅ model(已实现)
|
||||||
|
|
||||||
|
**功能:** 选择 Copilot 模型
|
||||||
|
|
||||||
|
**当前实现:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 从用户选择的模型中提取
|
||||||
|
request_model = body.get("model", "")
|
||||||
|
if request_model.startswith(f"{self.id}-"):
|
||||||
|
real_model_id = request_model[len(f"{self.id}-"):]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Valves 配置:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
MODEL_ID: str = Field(
|
||||||
|
default="claude-sonnet-4.5",
|
||||||
|
description="默认模型(动态获取失败时使用)"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. ✅ tools(已实现 - v0.2.0)
|
||||||
|
|
||||||
|
**功能:** 自定义工具/函数调用
|
||||||
|
|
||||||
|
**当前实现:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
custom_tools = self._initialize_custom_tools()
|
||||||
|
session_config = SessionConfig(
|
||||||
|
tools=custom_tools,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Valves 配置:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
ENABLE_TOOLS: bool = Field(default=False)
|
||||||
|
AVAILABLE_TOOLS: str = Field(default="all")
|
||||||
|
```
|
||||||
|
|
||||||
|
**内置示例工具:**
|
||||||
|
|
||||||
|
- `get_current_time` - 获取当前时间
|
||||||
|
- `calculate` - 数学计算
|
||||||
|
- `generate_random_number` - 随机数生成
|
||||||
|
|
||||||
|
**扩展方法:** 参考 [TOOLS_USAGE.md](TOOLS_USAGE.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. ⚠️ system_message(部分支持)
|
||||||
|
|
||||||
|
**功能:** 自定义系统提示词
|
||||||
|
|
||||||
|
**集成方案:**
|
||||||
|
|
||||||
|
#### 方案 A:通过 Valves 添加(推荐)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
SYSTEM_MESSAGE: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Custom system message (append mode)"
|
||||||
|
)
|
||||||
|
SYSTEM_MESSAGE_MODE: str = Field(
|
||||||
|
default="append",
|
||||||
|
description="System message mode: 'append' or 'replace'"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def pipe(self, body, ...):
|
||||||
|
system_message_config = None
|
||||||
|
|
||||||
|
if self.valves.SYSTEM_MESSAGE:
|
||||||
|
if self.valves.SYSTEM_MESSAGE_MODE == "replace":
|
||||||
|
system_message_config = {
|
||||||
|
"mode": "replace",
|
||||||
|
"content": self.valves.SYSTEM_MESSAGE
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
system_message_config = {
|
||||||
|
"mode": "append",
|
||||||
|
"content": self.valves.SYSTEM_MESSAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
system_message=system_message_config,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案 B:从 OpenWebUI 系统提示词读取
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 从 body 中获取系统提示词
|
||||||
|
system_prompt = body.get("system", "")
|
||||||
|
if system_prompt:
|
||||||
|
system_message_config = {
|
||||||
|
"mode": "append",
|
||||||
|
"content": system_prompt
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事项:**
|
||||||
|
|
||||||
|
- `append` 模式:在默认系统提示词后追加
|
||||||
|
- `replace` 模式:完全替换(移除 SDK 安全保护)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. ⚠️ available_tools / excluded_tools
|
||||||
|
|
||||||
|
**功能:** 工具白名单/黑名单
|
||||||
|
|
||||||
|
**当前部分支持:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
AVAILABLE_TOOLS: str = Field(
|
||||||
|
default="all",
|
||||||
|
description="'all' or comma-separated list"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**增强实现:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
AVAILABLE_TOOLS: str = Field(
|
||||||
|
default="all",
|
||||||
|
description="Available tools (comma-separated or 'all')"
|
||||||
|
)
|
||||||
|
EXCLUDED_TOOLS: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Excluded tools (comma-separated)"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**应用到 SessionConfig:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
session_config = SessionConfig(
|
||||||
|
tools=custom_tools,
|
||||||
|
available_tools=self._parse_tool_list(self.valves.AVAILABLE_TOOLS),
|
||||||
|
excluded_tools=self._parse_tool_list(self.valves.EXCLUDED_TOOLS),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_tool_list(self, value: str) -> list[str]:
|
||||||
|
"""解析工具列表"""
|
||||||
|
if not value or value == "all":
|
||||||
|
return []
|
||||||
|
return [t.strip() for t in value.split(",") if t.strip()]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 🔲 on_permission_request(未实现)
|
||||||
|
|
||||||
|
**功能:** 处理权限请求(shell 命令、文件写入等)
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
|
||||||
|
- Copilot 需要执行 shell 命令
|
||||||
|
- 需要写入文件
|
||||||
|
- 需要访问 URL
|
||||||
|
|
||||||
|
**集成挑战:**
|
||||||
|
|
||||||
|
- 需要 OpenWebUI 前端支持实时权限弹窗
|
||||||
|
- 需要异步处理用户确认
|
||||||
|
|
||||||
|
**推荐方案:**
|
||||||
|
|
||||||
|
#### 方案 A:自动批准(开发/测试环境)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def auto_approve_permission_handler(
|
||||||
|
request: dict,
|
||||||
|
context: dict
|
||||||
|
) -> dict:
|
||||||
|
"""自动批准所有权限请求(危险!)"""
|
||||||
|
return {
|
||||||
|
"kind": "approved",
|
||||||
|
"rules": []
|
||||||
|
}
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
on_permission_request=auto_approve_permission_handler,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案 B:基于规则的批准
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
ALLOW_SHELL_COMMANDS: bool = Field(default=False)
|
||||||
|
ALLOW_FILE_WRITE: bool = Field(default=False)
|
||||||
|
ALLOW_URL_ACCESS: bool = Field(default=True)
|
||||||
|
|
||||||
|
async def rule_based_permission_handler(
|
||||||
|
request: dict,
|
||||||
|
context: dict
|
||||||
|
) -> dict:
|
||||||
|
kind = request.get("kind")
|
||||||
|
|
||||||
|
if kind == "shell" and not self.valves.ALLOW_SHELL_COMMANDS:
|
||||||
|
return {"kind": "denied-by-rules"}
|
||||||
|
|
||||||
|
if kind == "write" and not self.valves.ALLOW_FILE_WRITE:
|
||||||
|
return {"kind": "denied-by-rules"}
|
||||||
|
|
||||||
|
if kind == "url" and not self.valves.ALLOW_URL_ACCESS:
|
||||||
|
return {"kind": "denied-by-rules"}
|
||||||
|
|
||||||
|
return {"kind": "approved", "rules": []}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案 C:通过 Event Emitter 请求用户确认(理想)
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def interactive_permission_handler(
|
||||||
|
request: dict,
|
||||||
|
context: dict
|
||||||
|
) -> dict:
|
||||||
|
"""通过前端请求用户确认"""
|
||||||
|
if not __event_emitter__:
|
||||||
|
return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"}
|
||||||
|
|
||||||
|
# 发送权限请求到前端
|
||||||
|
response_queue = asyncio.Queue()
|
||||||
|
await __event_emitter__({
|
||||||
|
"type": "permission_request",
|
||||||
|
"data": {
|
||||||
|
"kind": request.get("kind"),
|
||||||
|
"description": request.get("description"),
|
||||||
|
"response_queue": response_queue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 等待用户响应(带超时)
|
||||||
|
try:
|
||||||
|
user_response = await asyncio.wait_for(
|
||||||
|
response_queue.get(),
|
||||||
|
timeout=30.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_response.get("approved"):
|
||||||
|
return {"kind": "approved", "rules": []}
|
||||||
|
else:
|
||||||
|
return {"kind": "denied-interactively-by-user"}
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 🔲 provider(BYOK - Bring Your Own Key)
|
||||||
|
|
||||||
|
**功能:** 使用自己的 API 密钥连接 OpenAI/Azure/Anthropic
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
|
||||||
|
- 不使用 GitHub Copilot 配额
|
||||||
|
- 直接连接云服务提供商
|
||||||
|
- 使用 Azure OpenAI 部署
|
||||||
|
|
||||||
|
**集成方案:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
USE_CUSTOM_PROVIDER: bool = Field(default=False)
|
||||||
|
PROVIDER_TYPE: str = Field(
|
||||||
|
default="openai",
|
||||||
|
description="Provider type: openai, azure, anthropic"
|
||||||
|
)
|
||||||
|
PROVIDER_BASE_URL: str = Field(default="")
|
||||||
|
PROVIDER_API_KEY: str = Field(default="")
|
||||||
|
PROVIDER_BEARER_TOKEN: str = Field(default="")
|
||||||
|
AZURE_API_VERSION: str = Field(default="2024-10-21")
|
||||||
|
|
||||||
|
def _build_provider_config(self) -> dict | None:
|
||||||
|
"""构建 Provider 配置"""
|
||||||
|
if not self.valves.USE_CUSTOM_PROVIDER:
|
||||||
|
return None
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"type": self.valves.PROVIDER_TYPE,
|
||||||
|
"base_url": self.valves.PROVIDER_BASE_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.valves.PROVIDER_API_KEY:
|
||||||
|
config["api_key"] = self.valves.PROVIDER_API_KEY
|
||||||
|
|
||||||
|
if self.valves.PROVIDER_BEARER_TOKEN:
|
||||||
|
config["bearer_token"] = self.valves.PROVIDER_BEARER_TOKEN
|
||||||
|
|
||||||
|
if self.valves.PROVIDER_TYPE == "azure":
|
||||||
|
config["azure"] = {
|
||||||
|
"api_version": self.valves.AZURE_API_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
# 自动推断 wire_api
|
||||||
|
if self.valves.PROVIDER_TYPE == "anthropic":
|
||||||
|
config["wire_api"] = "responses"
|
||||||
|
else:
|
||||||
|
config["wire_api"] = "completions"
|
||||||
|
|
||||||
|
return config
|
||||||
|
```
|
||||||
|
|
||||||
|
**应用:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
session_config = SessionConfig(
|
||||||
|
provider=self._build_provider_config(),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. ✅ streaming(已实现)
|
||||||
|
|
||||||
|
**功能:** 流式输出
|
||||||
|
|
||||||
|
**当前实现:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
session_config = SessionConfig(
|
||||||
|
streaming=body.get("stream", False),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. 🔲 mcp_servers(MCP 协议)
|
||||||
|
|
||||||
|
**功能:** Model Context Protocol 服务器集成
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
|
||||||
|
- 连接外部数据源(数据库、API)
|
||||||
|
- 集成第三方服务
|
||||||
|
|
||||||
|
**集成方案:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
MCP_SERVERS_CONFIG: str = Field(
|
||||||
|
default="{}",
|
||||||
|
description="MCP servers configuration (JSON format)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_mcp_servers(self) -> dict | None:
|
||||||
|
"""解析 MCP 服务器配置"""
|
||||||
|
if not self.valves.MCP_SERVERS_CONFIG:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(self.valves.MCP_SERVERS_CONFIG)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"database": {
|
||||||
|
"type": "local",
|
||||||
|
"command": "mcp-server-sqlite",
|
||||||
|
"args": ["--db", "/path/to/db.sqlite"],
|
||||||
|
"tools": ["*"]
|
||||||
|
},
|
||||||
|
"weather": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://weather-api.example.com/mcp",
|
||||||
|
"tools": ["get_weather", "get_forecast"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. 🔲 custom_agents
|
||||||
|
|
||||||
|
**功能:** 自定义 AI 代理
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
|
||||||
|
- 专门化的子代理(如代码审查、文档编写)
|
||||||
|
- 不同的提示词策略
|
||||||
|
|
||||||
|
**集成方案:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
CUSTOM_AGENTS_CONFIG: str = Field(
|
||||||
|
default="[]",
|
||||||
|
description="Custom agents configuration (JSON array)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_custom_agents(self) -> list | None:
|
||||||
|
"""解析自定义代理配置"""
|
||||||
|
if not self.valves.CUSTOM_AGENTS_CONFIG:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(self.valves.CUSTOM_AGENTS_CONFIG)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "code_reviewer",
|
||||||
|
"display_name": "Code Reviewer",
|
||||||
|
"description": "Reviews code for best practices",
|
||||||
|
"prompt": "You are an expert code reviewer. Focus on security, performance, and maintainability.",
|
||||||
|
"tools": ["read_file", "write_file"],
|
||||||
|
"infer": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. 🔲 config_dir
|
||||||
|
|
||||||
|
**功能:** 自定义配置目录
|
||||||
|
|
||||||
|
**当前支持:**
|
||||||
|
|
||||||
|
- 已有 `WORKSPACE_DIR` 控制工作目录
|
||||||
|
|
||||||
|
**增强方案:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
CONFIG_DIR: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Custom config directory for session state"
|
||||||
|
)
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
config_dir=self.valves.CONFIG_DIR if self.valves.CONFIG_DIR else None,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. 🔲 skill_directories / disabled_skills
|
||||||
|
|
||||||
|
**功能:** Copilot Skills 系统
|
||||||
|
|
||||||
|
**使用场景:**
|
||||||
|
|
||||||
|
- 加载自定义技能包
|
||||||
|
- 禁用特定技能
|
||||||
|
|
||||||
|
**集成方案:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
SKILL_DIRECTORIES: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Comma-separated skill directories"
|
||||||
|
)
|
||||||
|
DISABLED_SKILLS: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Comma-separated disabled skills"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_skills_config(self):
|
||||||
|
"""解析技能配置"""
|
||||||
|
skill_dirs = []
|
||||||
|
if self.valves.SKILL_DIRECTORIES:
|
||||||
|
skill_dirs = [
|
||||||
|
d.strip()
|
||||||
|
for d in self.valves.SKILL_DIRECTORIES.split(",")
|
||||||
|
if d.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
disabled = []
|
||||||
|
if self.valves.DISABLED_SKILLS:
|
||||||
|
disabled = [
|
||||||
|
s.strip()
|
||||||
|
for s in self.valves.DISABLED_SKILLS.split(",")
|
||||||
|
if s.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
return skill_dirs, disabled
|
||||||
|
|
||||||
|
# 应用
|
||||||
|
skill_dirs, disabled_skills = self._parse_skills_config()
|
||||||
|
session_config = SessionConfig(
|
||||||
|
skill_directories=skill_dirs if skill_dirs else None,
|
||||||
|
disabled_skills=disabled_skills if disabled_skills else None,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. ✅ infinite_sessions(已实现)
|
||||||
|
|
||||||
|
**功能:** 无限会话与自动上下文压缩
|
||||||
|
|
||||||
|
**当前实现:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
INFINITE_SESSION: bool = Field(default=True)
|
||||||
|
COMPACTION_THRESHOLD: float = Field(default=0.8)
|
||||||
|
BUFFER_THRESHOLD: float = Field(default=0.95)
|
||||||
|
|
||||||
|
infinite_session_config = None
|
||||||
|
if self.valves.INFINITE_SESSION:
|
||||||
|
infinite_session_config = {
|
||||||
|
"enabled": True,
|
||||||
|
"background_compaction_threshold": self.valves.COMPACTION_THRESHOLD,
|
||||||
|
"buffer_exhaustion_threshold": self.valves.BUFFER_THRESHOLD,
|
||||||
|
}
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
infinite_sessions=infinite_session_config,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 实施优先级建议
|
||||||
|
|
||||||
|
### 🔥 高优先级(立即实现)
|
||||||
|
|
||||||
|
1. **system_message** - 用户最常需要的功能
|
||||||
|
2. **on_permission_request (基于规则)** - 安全性需求
|
||||||
|
|
||||||
|
### 📌 中优先级(下一阶段)
|
||||||
|
|
||||||
|
3. **excluded_tools** - 完善工具管理
|
||||||
|
4. **provider (BYOK)** - 高级用户需求
|
||||||
|
5. **config_dir** - 增强会话管理
|
||||||
|
|
||||||
|
### 📋 低优先级(可选)
|
||||||
|
|
||||||
|
6. **mcp_servers** - 高级集成
|
||||||
|
7. **custom_agents** - 专业化功能
|
||||||
|
8. **skill_directories** - 生态系统功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速实施计划
|
||||||
|
|
||||||
|
### Phase 1: 基础增强(1-2小时)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 添加到 Valves
|
||||||
|
SYSTEM_MESSAGE: str = Field(default="")
|
||||||
|
SYSTEM_MESSAGE_MODE: str = Field(default="append")
|
||||||
|
EXCLUDED_TOOLS: str = Field(default="")
|
||||||
|
|
||||||
|
# 添加到 pipe() 方法
|
||||||
|
system_message_config = self._build_system_message_config()
|
||||||
|
excluded_tools = self._parse_tool_list(self.valves.EXCLUDED_TOOLS)
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
system_message=system_message_config,
|
||||||
|
excluded_tools=excluded_tools,
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: 权限管理(2-3小时)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 添加权限控制 Valves
|
||||||
|
ALLOW_SHELL_COMMANDS: bool = Field(default=False)
|
||||||
|
ALLOW_FILE_WRITE: bool = Field(default=False)
|
||||||
|
ALLOW_URL_ACCESS: bool = Field(default=True)
|
||||||
|
|
||||||
|
# 实现权限处理器
|
||||||
|
session_config = SessionConfig(
|
||||||
|
on_permission_request=self._create_permission_handler(),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: BYOK 支持(3-4小时)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 添加 Provider Valves
|
||||||
|
USE_CUSTOM_PROVIDER: bool = Field(default=False)
|
||||||
|
PROVIDER_TYPE: str = Field(default="openai")
|
||||||
|
PROVIDER_BASE_URL: str = Field(default="")
|
||||||
|
PROVIDER_API_KEY: str = Field(default="")
|
||||||
|
|
||||||
|
# 实现 Provider 配置
|
||||||
|
session_config = SessionConfig(
|
||||||
|
provider=self._build_provider_config(),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 参考资源
|
||||||
|
|
||||||
|
- **SDK 类型定义**: `/opt/homebrew/.../copilot/types.py`
|
||||||
|
- **工具系统**: [TOOLS_USAGE.md](TOOLS_USAGE.md)
|
||||||
|
- **SDK 文档**: <https://github.com/github/copilot-sdk>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 实施检查清单
|
||||||
|
|
||||||
|
使用此清单跟踪实施进度:
|
||||||
|
|
||||||
|
- [x] session_id
|
||||||
|
- [x] model
|
||||||
|
- [x] tools
|
||||||
|
- [x] streaming
|
||||||
|
- [x] infinite_sessions
|
||||||
|
- [ ] system_message
|
||||||
|
- [ ] available_tools (完善)
|
||||||
|
- [ ] excluded_tools
|
||||||
|
- [ ] on_permission_request
|
||||||
|
- [ ] provider (BYOK)
|
||||||
|
- [ ] mcp_servers
|
||||||
|
- [ ] custom_agents
|
||||||
|
- [ ] config_dir
|
||||||
|
- [ ] skill_directories
|
||||||
|
- [ ] disabled_skills
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**作者:** Fu-Jie
|
||||||
|
**版本:** v1.0
|
||||||
|
**日期:** 2026-01-26
|
||||||
|
**更新:** 随功能实施持续更新
|
||||||
191
plugins/debug/legacy_docs/guides/TOOLS_USAGE.md
Normal file
191
plugins/debug/legacy_docs/guides/TOOLS_USAGE.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# 🛠️ Custom Tools Usage / 自定义工具使用指南
|
||||||
|
|
||||||
|
## Overview / 概览
|
||||||
|
|
||||||
|
This pipe includes **1 example custom tool** that demonstrates how to use GitHub Copilot SDK's tool calling feature.
|
||||||
|
|
||||||
|
本 Pipe 包含 **1 个示例自定义工具**,展示如何使用 GitHub Copilot SDK 的工具调用功能。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start / 快速开始
|
||||||
|
|
||||||
|
### 1. Enable Tools / 启用工具
|
||||||
|
|
||||||
|
In Valves configuration:
|
||||||
|
在 Valves 配置中:
|
||||||
|
|
||||||
|
```
|
||||||
|
ENABLE_TOOLS: true
|
||||||
|
AVAILABLE_TOOLS: all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test with Conversations / 测试对话
|
||||||
|
|
||||||
|
Try these examples:
|
||||||
|
尝试这些示例:
|
||||||
|
|
||||||
|
**English:**
|
||||||
|
|
||||||
|
- "Give me a random number between 1 and 100"
|
||||||
|
|
||||||
|
**中文:**
|
||||||
|
|
||||||
|
- "给我一个 1 到 100 之间的随机数"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Included Tools / 内置工具
|
||||||
|
|
||||||
|
### 1. `generate_random_number` / 生成随机数
|
||||||
|
|
||||||
|
**Description:** Generate a random integer
|
||||||
|
**描述:** 生成随机整数
|
||||||
|
|
||||||
|
**Parameters / 参数:**
|
||||||
|
|
||||||
|
- `min` (optional): Minimum value (default: 1)
|
||||||
|
- `max` (optional): Maximum value (default: 100)
|
||||||
|
- `min` (可选): 最小值 (默认: 1)
|
||||||
|
- `max` (可选): 最大值 (默认: 100)
|
||||||
|
|
||||||
|
**Example / 示例:**
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Give me a random number between 1 and 10"
|
||||||
|
Copilot: [calls generate_random_number with min=1, max=10] "Generated random number: 7"
|
||||||
|
|
||||||
|
用户: "给我一个 1 到 10 之间的随机数"
|
||||||
|
Copilot: [调用 generate_random_number,参数 min=1, max=10] "生成的随机数: 7"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Advanced Configuration / 高级配置
|
||||||
|
|
||||||
|
### Select Specific Tools / 选择特定工具
|
||||||
|
|
||||||
|
Instead of enabling all tools, specify which ones to use:
|
||||||
|
不启用所有工具,而是指定要使用的工具:
|
||||||
|
|
||||||
|
```
|
||||||
|
ENABLE_TOOLS: true
|
||||||
|
AVAILABLE_TOOLS: generate_random_number
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 How Tool Calling Works / 工具调用的工作原理
|
||||||
|
|
||||||
|
```
|
||||||
|
1. User asks a question / 用户提问
|
||||||
|
↓
|
||||||
|
2. Copilot decides if it needs a tool / Copilot 决定是否需要工具
|
||||||
|
↓
|
||||||
|
3. If yes, Copilot calls the appropriate tool / 如果需要,调用相应工具
|
||||||
|
↓
|
||||||
|
4. Tool executes and returns result / 工具执行并返回结果
|
||||||
|
↓
|
||||||
|
5. Copilot uses the result to answer / Copilot 使用结果回答
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Feedback / 可视化反馈
|
||||||
|
|
||||||
|
When tools are called, you'll see:
|
||||||
|
当工具被调用时,你会看到:
|
||||||
|
|
||||||
|
```
|
||||||
|
🔧 **Calling tool**: `generate_random_number`
|
||||||
|
✅ **Tool `generate_random_number` completed**
|
||||||
|
|
||||||
|
Generated random number: 7
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Creating Your Own Tools / 创建自定义工具
|
||||||
|
|
||||||
|
Want to add your own tools? Follow this pattern (module-level tools):
|
||||||
|
想要添加自己的工具?遵循这个模式(模块级工具):
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from copilot import define_tool
|
||||||
|
|
||||||
|
class MyToolParams(BaseModel):
|
||||||
|
param_name: str = Field(description="Parameter description")
|
||||||
|
|
||||||
|
|
||||||
|
@define_tool(description="Clear description of what the tool does and when to use it")
|
||||||
|
async def my_tool(params: MyToolParams) -> str:
|
||||||
|
# Do something
|
||||||
|
result = do_something(params.param_name)
|
||||||
|
return f"Result: {result}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then register it in `_initialize_custom_tools()`:
|
||||||
|
然后将它添加到 `_initialize_custom_tools()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _initialize_custom_tools(self):
|
||||||
|
if not self.valves.ENABLE_TOOLS:
|
||||||
|
return []
|
||||||
|
|
||||||
|
all_tools = {
|
||||||
|
"generate_random_number": generate_random_number,
|
||||||
|
"my_tool": my_tool, # ✅ Add here
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.valves.AVAILABLE_TOOLS == "all":
|
||||||
|
return list(all_tools.values())
|
||||||
|
|
||||||
|
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||||
|
return [all_tools[name] for name in enabled if name in all_tools]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Important Notes / 重要说明
|
||||||
|
|
||||||
|
### Security / 安全性
|
||||||
|
|
||||||
|
- Tools run in the same process as the pipe
|
||||||
|
- Be careful with tools that execute code or access files
|
||||||
|
- Always validate input parameters
|
||||||
|
|
||||||
|
- 工具在与 Pipe 相同的进程中运行
|
||||||
|
- 谨慎处理执行代码或访问文件的工具
|
||||||
|
- 始终验证输入参数
|
||||||
|
|
||||||
|
### Performance / 性能
|
||||||
|
|
||||||
|
- Tool execution is synchronous during streaming
|
||||||
|
- Long-running tools may cause delays
|
||||||
|
- Consider adding timeouts for external API calls
|
||||||
|
|
||||||
|
- 工具执行在流式传输期间是同步的
|
||||||
|
- 长时间运行的工具可能导致延迟
|
||||||
|
- 考虑为外部 API 调用添加超时
|
||||||
|
|
||||||
|
### Debugging / 调试
|
||||||
|
|
||||||
|
- Enable `DEBUG: true` to see tool events in the browser console
|
||||||
|
- Check tool calls in `🔧 Calling tool` messages
|
||||||
|
- Tool errors are displayed in the response
|
||||||
|
|
||||||
|
- 启用 `DEBUG: true` 在浏览器控制台查看工具事件
|
||||||
|
- 在 `🔧 Calling tool` 消息中检查工具调用
|
||||||
|
- 工具错误会显示在响应中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 References / 参考资料
|
||||||
|
|
||||||
|
- [Copilot SDK Documentation](https://github.com/github/copilot-sdk)
|
||||||
|
- [COPILOT_TOOLS_QUICKSTART.md](COPILOT_TOOLS_QUICKSTART.md) - Detailed implementation guide
|
||||||
|
- [JSON Schema](https://json-schema.org/) - For parameter definitions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version:** 0.2.3
|
||||||
|
**Last Updated:** 2026-01-27
|
||||||
431
plugins/debug/legacy_docs/guides/TOOL_IMPLEMENTATION_GUIDE.md
Normal file
431
plugins/debug/legacy_docs/guides/TOOL_IMPLEMENTATION_GUIDE.md
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
# GitHub Copilot SDK - Tool 功能实现指南
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
本指南介绍如何在 GitHub Copilot SDK Pipe 中实现 Function/Tool Calling 功能。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 架构设计
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
OpenWebUI Tools/Functions
|
||||||
|
↓ (转换)
|
||||||
|
Copilot SDK Tool Definition
|
||||||
|
↓ (注册)
|
||||||
|
Session Tool Handlers
|
||||||
|
↓ (调用)
|
||||||
|
Tool Execution → Result
|
||||||
|
↓ (返回)
|
||||||
|
Continue Conversation
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心接口
|
||||||
|
|
||||||
|
#### 1. Tool Definition(工具定义)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from copilot.types import Tool
|
||||||
|
|
||||||
|
tool = Tool(
|
||||||
|
name="get_weather",
|
||||||
|
description="Get current weather for a location",
|
||||||
|
parameters={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "City name, e.g., 'San Francisco'"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["celsius", "fahrenheit"],
|
||||||
|
"description": "Temperature unit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["location"]
|
||||||
|
},
|
||||||
|
handler=weather_handler # 处理函数
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Tool Handler(处理函数)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from copilot.types import ToolInvocation, ToolResult
|
||||||
|
|
||||||
|
async def weather_handler(invocation: ToolInvocation) -> ToolResult:
|
||||||
|
"""
|
||||||
|
invocation 包含:
|
||||||
|
- session_id: str
|
||||||
|
- tool_call_id: str
|
||||||
|
- tool_name: str
|
||||||
|
- arguments: dict # {"location": "San Francisco", "unit": "celsius"}
|
||||||
|
"""
|
||||||
|
location = invocation["arguments"]["location"]
|
||||||
|
|
||||||
|
# 执行实际逻辑
|
||||||
|
weather_data = await fetch_weather(location)
|
||||||
|
|
||||||
|
# 返回结果
|
||||||
|
return ToolResult(
|
||||||
|
textResultForLlm=f"Weather in {location}: {weather_data['temp']}°C, {weather_data['condition']}",
|
||||||
|
resultType="success", # or "failure"
|
||||||
|
error=None,
|
||||||
|
toolTelemetry={"execution_time_ms": 150}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Session Configuration(会话配置)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from copilot.types import SessionConfig
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
model="claude-sonnet-4.5",
|
||||||
|
tools=[tool1, tool2, tool3], # ✅ 传递工具列表
|
||||||
|
available_tools=["get_weather", "search_web"], # 可选:过滤可用工具
|
||||||
|
excluded_tools=["dangerous_tool"], # 可选:排除工具
|
||||||
|
)
|
||||||
|
|
||||||
|
session = await client.create_session(config=session_config)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 实现方案
|
||||||
|
|
||||||
|
### 方案 A:桥接 OpenWebUI Tools(推荐)
|
||||||
|
|
||||||
|
#### 1. 添加 Valves 配置
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
ENABLE_TOOLS: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Enable OpenWebUI tool integration"
|
||||||
|
)
|
||||||
|
TOOL_TIMEOUT: int = Field(
|
||||||
|
default=30,
|
||||||
|
description="Tool execution timeout (seconds)"
|
||||||
|
)
|
||||||
|
AVAILABLE_TOOLS: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Filter specific tools (comma separated, empty = all)"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 实现 Tool 转换器
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _convert_openwebui_tools_to_copilot(
|
||||||
|
self,
|
||||||
|
owui_tools: List[dict],
|
||||||
|
__event_call__=None
|
||||||
|
) -> List[dict]:
|
||||||
|
"""
|
||||||
|
将 OpenWebUI tools 转换为 Copilot SDK 格式
|
||||||
|
|
||||||
|
OpenWebUI Tool 格式:
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_weather",
|
||||||
|
"description": "Get weather info",
|
||||||
|
"parameters": {...} # JSON Schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
copilot_tools = []
|
||||||
|
|
||||||
|
for tool in owui_tools:
|
||||||
|
if tool.get("type") != "function":
|
||||||
|
continue
|
||||||
|
|
||||||
|
func = tool.get("function", {})
|
||||||
|
tool_name = func.get("name")
|
||||||
|
|
||||||
|
if not tool_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 应用过滤器
|
||||||
|
if self.valves.AVAILABLE_TOOLS:
|
||||||
|
allowed = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||||
|
if tool_name not in allowed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
copilot_tools.append({
|
||||||
|
"name": tool_name,
|
||||||
|
"description": func.get("description", ""),
|
||||||
|
"parameters": func.get("parameters", {}),
|
||||||
|
"handler": self._create_tool_handler(tool_name, __event_call__)
|
||||||
|
})
|
||||||
|
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"Registered tool: {tool_name}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
return copilot_tools
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 实现动态 Tool Handler
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _create_tool_handler(self, tool_name: str, __event_call__=None):
|
||||||
|
"""为每个 tool 创建 handler 函数"""
|
||||||
|
|
||||||
|
async def handler(invocation: dict) -> dict:
|
||||||
|
"""
|
||||||
|
Tool handler 实现
|
||||||
|
|
||||||
|
invocation 结构:
|
||||||
|
{
|
||||||
|
"session_id": "...",
|
||||||
|
"tool_call_id": "...",
|
||||||
|
"tool_name": "get_weather",
|
||||||
|
"arguments": {"location": "Beijing"}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"Tool called: {invocation['tool_name']} with {invocation['arguments']}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
# 方法 1: 调用 OpenWebUI 内部 Function API
|
||||||
|
result = await self._execute_openwebui_function(
|
||||||
|
function_name=invocation["tool_name"],
|
||||||
|
arguments=invocation["arguments"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 方法 2: 通过 __event_emitter__ 触发(需要测试)
|
||||||
|
# 方法 3: 直接实现工具逻辑
|
||||||
|
|
||||||
|
return {
|
||||||
|
"textResultForLlm": str(result),
|
||||||
|
"resultType": "success",
|
||||||
|
"error": None,
|
||||||
|
"toolTelemetry": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return {
|
||||||
|
"textResultForLlm": "Tool execution timed out.",
|
||||||
|
"resultType": "failure",
|
||||||
|
"error": "timeout",
|
||||||
|
"toolTelemetry": {}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
self._emit_debug_log_sync(
|
||||||
|
f"Tool error: {e}",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"textResultForLlm": f"Tool execution failed: {str(e)}",
|
||||||
|
"resultType": "failure",
|
||||||
|
"error": str(e),
|
||||||
|
"toolTelemetry": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 集成到 pipe() 方法
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def pipe(
|
||||||
|
self,
|
||||||
|
body: dict,
|
||||||
|
__metadata__: Optional[dict] = None,
|
||||||
|
__event_emitter__=None,
|
||||||
|
__event_call__=None,
|
||||||
|
) -> Union[str, AsyncGenerator]:
|
||||||
|
# ... 现有代码 ...
|
||||||
|
|
||||||
|
# ✅ 提取并转换 tools
|
||||||
|
copilot_tools = []
|
||||||
|
if self.valves.ENABLE_TOOLS and body.get("tools"):
|
||||||
|
copilot_tools = self._convert_openwebui_tools_to_copilot(
|
||||||
|
body["tools"],
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._emit_debug_log(
|
||||||
|
f"Enabled {len(copilot_tools)} tools",
|
||||||
|
__event_call__
|
||||||
|
)
|
||||||
|
|
||||||
|
# ✅ 传递给 SessionConfig
|
||||||
|
session_config = SessionConfig(
|
||||||
|
session_id=chat_id if chat_id else None,
|
||||||
|
model=real_model_id,
|
||||||
|
streaming=body.get("stream", False),
|
||||||
|
tools=copilot_tools, # ✅ 关键
|
||||||
|
infinite_sessions=infinite_session_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
session = await client.create_session(config=session_config)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. 处理 Tool 调用事件
|
||||||
|
|
||||||
|
```python
|
||||||
|
def stream_response(...):
|
||||||
|
def handler(event):
|
||||||
|
event_type = str(event.type)
|
||||||
|
|
||||||
|
# ✅ Tool 调用开始
|
||||||
|
if "tool_invocation_started" in event_type:
|
||||||
|
tool_name = get_event_data(event, "tool_name", "")
|
||||||
|
yield f"\n🔧 **Calling tool**: `{tool_name}`\n"
|
||||||
|
|
||||||
|
# ✅ Tool 调用完成
|
||||||
|
elif "tool_invocation_completed" in event_type:
|
||||||
|
tool_name = get_event_data(event, "tool_name", "")
|
||||||
|
result = get_event_data(event, "result", "")
|
||||||
|
yield f"\n✅ **Tool result**: {result}\n"
|
||||||
|
|
||||||
|
# ✅ Tool 调用失败
|
||||||
|
elif "tool_invocation_failed" in event_type:
|
||||||
|
tool_name = get_event_data(event, "tool_name", "")
|
||||||
|
error = get_event_data(event, "error", "")
|
||||||
|
yield f"\n❌ **Tool failed**: `{tool_name}` - {error}\n"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案 B:自定义 Tool 实现
|
||||||
|
|
||||||
|
#### Valves 配置
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Valves(BaseModel):
|
||||||
|
CUSTOM_TOOLS: str = Field(
|
||||||
|
default="[]",
|
||||||
|
description="Custom tools JSON: [{name, description, parameters, implementation}]"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 工具定义示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "calculate",
|
||||||
|
"description": "Perform mathematical calculations",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expression": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Math expression, e.g., '2 + 2 * 3'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["expression"]
|
||||||
|
},
|
||||||
|
"implementation": "eval" // 或指定 Python 函数名
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试方案
|
||||||
|
|
||||||
|
### 1. 测试 Tool 定义
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 在 OpenWebUI 中创建一个简单的 Function:
|
||||||
|
# Name: get_time
|
||||||
|
# Description: Get current time
|
||||||
|
# Parameters: {"type": "object", "properties": {}}
|
||||||
|
|
||||||
|
# 测试对话:
|
||||||
|
# User: "What time is it?"
|
||||||
|
# Expected: Copilot 调用 get_time tool,返回当前时间
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试 Tool 调用链
|
||||||
|
|
||||||
|
```python
|
||||||
|
# User: "Search for Python tutorials and summarize the top 3 results"
|
||||||
|
# Expected Flow:
|
||||||
|
# 1. Copilot calls search_web(query="Python tutorials")
|
||||||
|
# 2. Copilot receives search results
|
||||||
|
# 3. Copilot summarizes top 3
|
||||||
|
# 4. Returns final answer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 测试错误处理
|
||||||
|
|
||||||
|
```python
|
||||||
|
# User: "Call a non-existent tool"
|
||||||
|
# Expected: 返回 "Tool not supported" error
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 事件监听
|
||||||
|
|
||||||
|
Tool 相关事件类型:
|
||||||
|
|
||||||
|
- `tool_invocation_started` - Tool 调用开始
|
||||||
|
- `tool_invocation_completed` - Tool 完成
|
||||||
|
- `tool_invocation_failed` - Tool 失败
|
||||||
|
- `tool_parameter_validation_failed` - 参数验证失败
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 1. 安全性
|
||||||
|
|
||||||
|
- ✅ 验证 tool parameters
|
||||||
|
- ✅ 限制执行超时
|
||||||
|
- ✅ 不暴露详细错误信息给 LLM
|
||||||
|
- ❌ 禁止执行危险命令(如 `rm -rf`)
|
||||||
|
|
||||||
|
### 2. 性能
|
||||||
|
|
||||||
|
- ⏱️ 设置合理的 timeout
|
||||||
|
- 🔄 考虑异步执行长时间运行的 tool
|
||||||
|
- 📊 记录 tool 执行时间(toolTelemetry)
|
||||||
|
|
||||||
|
### 3. 调试
|
||||||
|
|
||||||
|
- 🐛 在 DEBUG 模式下记录所有 tool 调用
|
||||||
|
- 📝 记录 arguments 和 results
|
||||||
|
- 🔍 使用前端 console 显示 tool 流程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 参考资源
|
||||||
|
|
||||||
|
- [GitHub Copilot SDK 官方文档](https://github.com/github/copilot-sdk)
|
||||||
|
- [OpenWebUI Function API](https://docs.openwebui.com/features/plugin-system)
|
||||||
|
- [JSON Schema 规范](https://json-schema.org/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 实现清单
|
||||||
|
|
||||||
|
- [ ] 添加 ENABLE_TOOLS Valve
|
||||||
|
- [ ] 实现 _convert_openwebui_tools_to_copilot()
|
||||||
|
- [ ] 实现 _create_tool_handler()
|
||||||
|
- [ ] 修改 SessionConfig 传递 tools
|
||||||
|
- [ ] 处理 tool 事件流
|
||||||
|
- [ ] 添加调试日志
|
||||||
|
- [ ] 测试基础 tool 调用
|
||||||
|
- [ ] 测试错误处理
|
||||||
|
- [ ] 更新文档和 README
|
||||||
|
- [ ] 同步中文版本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**作者:** Fu-Jie
|
||||||
|
**版本:** v1.0
|
||||||
|
**日期:** 2026-01-26
|
||||||
835
plugins/debug/legacy_docs/guides/WORKFLOW.md
Normal file
835
plugins/debug/legacy_docs/guides/WORKFLOW.md
Normal file
@@ -0,0 +1,835 @@
|
|||||||
|
# GitHub Copilot SDK Integration Workflow
|
||||||
|
|
||||||
|
**Author:** Fu-Jie
|
||||||
|
**Version:** 0.2.3
|
||||||
|
**Last Updated:** 2026-01-27
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Architecture Overview](#architecture-overview)
|
||||||
|
2. [Request Processing Flow](#request-processing-flow)
|
||||||
|
3. [Session Management](#session-management)
|
||||||
|
4. [Streaming Response Handling](#streaming-response-handling)
|
||||||
|
5. [Event Processing Mechanism](#event-processing-mechanism)
|
||||||
|
6. [Tool Execution Flow](#tool-execution-flow)
|
||||||
|
7. [System Prompt Extraction](#system-prompt-extraction)
|
||||||
|
8. [Configuration Parameters](#configuration-parameters)
|
||||||
|
9. [Key Functions Reference](#key-functions-reference)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
### Component Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ OpenWebUI │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Pipe Interface (Entry Point) │ │
|
||||||
|
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ _pipe_impl (Main Logic) │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ 1. Environment Setup (_setup_env) │ │ │
|
||||||
|
│ │ │ 2. Model Selection (request_model parsing) │ │ │
|
||||||
|
│ │ │ 3. Chat Context Extraction │ │ │
|
||||||
|
│ │ │ 4. System Prompt Extraction │ │ │
|
||||||
|
│ │ │ 5. Session Management (create/resume) │ │ │
|
||||||
|
│ │ │ 6. Streaming/Non-streaming Response │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ GitHub Copilot Client │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ • CopilotClient (SDK instance) │ │ │
|
||||||
|
│ │ │ • Session (conversation context) │ │ │
|
||||||
|
│ │ │ • Event Stream (async events) │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
└────────────────────────┼─────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Copilot CLI Process │
|
||||||
|
│ (Backend Agent) │
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
1. **Pipe Interface**: OpenWebUI's standard entry point
|
||||||
|
2. **Environment Manager**: CLI setup, token validation, environment variables
|
||||||
|
3. **Session Manager**: Persistent conversation state with automatic compaction
|
||||||
|
4. **Event Processor**: Asynchronous streaming event handler
|
||||||
|
5. **Tool System**: Custom tool registration and execution
|
||||||
|
6. **Debug Logger**: Frontend console logging for troubleshooting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Request Processing Flow
|
||||||
|
|
||||||
|
### Complete Request Lifecycle
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[OpenWebUI Request] --> B[pipe Entry Point]
|
||||||
|
B --> C[_pipe_impl]
|
||||||
|
C --> D{Setup Environment}
|
||||||
|
D --> E[Parse Model ID]
|
||||||
|
E --> F[Extract Chat Context]
|
||||||
|
F --> G[Extract System Prompt]
|
||||||
|
G --> H{Session Exists?}
|
||||||
|
H -->|Yes| I[Resume Session]
|
||||||
|
H -->|No| J[Create New Session]
|
||||||
|
I --> K[Initialize Tools]
|
||||||
|
J --> K
|
||||||
|
K --> L[Process Images]
|
||||||
|
L --> M{Streaming Mode?}
|
||||||
|
M -->|Yes| N[stream_response]
|
||||||
|
M -->|No| O[send_and_wait]
|
||||||
|
N --> P[Async Event Stream]
|
||||||
|
O --> Q[Direct Response]
|
||||||
|
P --> R[Return to OpenWebUI]
|
||||||
|
Q --> R
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step-by-Step Breakdown
|
||||||
|
|
||||||
|
#### 1. Environment Setup (`_setup_env`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _setup_env(self, __event_call__=None):
|
||||||
|
"""
|
||||||
|
Priority:
|
||||||
|
1. Check VALVES.CLI_PATH
|
||||||
|
2. Search system PATH
|
||||||
|
3. Auto-install via curl (if not found)
|
||||||
|
4. Set GH_TOKEN environment variables
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
|
||||||
|
- Locate Copilot CLI binary
|
||||||
|
- Set `COPILOT_CLI_PATH` environment variable
|
||||||
|
- Configure `GH_TOKEN` for authentication
|
||||||
|
- Apply custom environment variables
|
||||||
|
|
||||||
|
#### 2. Model Selection
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Input: body["model"] = "copilotsdk-claude-sonnet-4.5"
|
||||||
|
request_model = body.get("model", "")
|
||||||
|
if request_model.startswith(f"{self.id}-"):
|
||||||
|
real_model_id = request_model[len(f"{self.id}-"):] # "claude-sonnet-4.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Chat Context Extraction (`_get_chat_context`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Priority order for chat_id:
|
||||||
|
# 1. __metadata__ (most reliable)
|
||||||
|
# 2. body["chat_id"]
|
||||||
|
# 3. body["metadata"]["chat_id"]
|
||||||
|
chat_ctx = self._get_chat_context(body, __metadata__, __event_call__)
|
||||||
|
chat_id = chat_ctx.get("chat_id")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. System Prompt Extraction (`_extract_system_prompt`)
|
||||||
|
|
||||||
|
Multi-source fallback strategy:
|
||||||
|
|
||||||
|
1. `metadata.model.params.system`
|
||||||
|
2. Model database lookup (by model_id)
|
||||||
|
3. `body.params.system`
|
||||||
|
4. Messages with `role="system"`
|
||||||
|
|
||||||
|
#### 5. Session Creation/Resumption
|
||||||
|
|
||||||
|
**New Session:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
session_config = SessionConfig(
|
||||||
|
session_id=chat_id,
|
||||||
|
model=real_model_id,
|
||||||
|
streaming=is_streaming,
|
||||||
|
tools=custom_tools,
|
||||||
|
system_message={"mode": "append", "content": system_prompt_content},
|
||||||
|
infinite_sessions=InfiniteSessionConfig(
|
||||||
|
enabled=True,
|
||||||
|
background_compaction_threshold=0.8,
|
||||||
|
buffer_exhaustion_threshold=0.95
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session = await client.create_session(config=session_config)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resume Session:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
session = await client.resume_session(chat_id)
|
||||||
|
# Session state preserved: history, tools, workspace
|
||||||
|
except Exception:
|
||||||
|
# Fallback to creating new session
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Management
|
||||||
|
|
||||||
|
### Infinite Sessions Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Session Lifecycle │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ create ┌──────────┐ resume ┌───────┴───┐
|
||||||
|
│ │ Chat ID │─────────▶ │ Session │ ◀────────│ OpenWebUI │
|
||||||
|
│ └──────────┘ │ State │ └───────────┘
|
||||||
|
│ └─────┬────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Context Window Management │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Messages [user, assistant, tool_results...] │ │ │
|
||||||
|
│ │ │ Token Usage: ████████████░░░░ (80%) │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ▼ │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Threshold Reached (0.8) │ │ │
|
||||||
|
│ │ │ → Background Compaction Triggered │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ▼ │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ Compacted Summary + Recent Messages │ │ │
|
||||||
|
│ │ │ Token Usage: ██████░░░░░░░░░░░ (40%) │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Parameters
|
||||||
|
|
||||||
|
```python
|
||||||
|
InfiniteSessionConfig(
|
||||||
|
enabled=True, # Enable infinite sessions
|
||||||
|
background_compaction_threshold=0.8, # Start compaction at 80% token usage
|
||||||
|
buffer_exhaustion_threshold=0.95 # Emergency threshold at 95%
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
|
||||||
|
- **< 80%**: Normal operation, no compaction
|
||||||
|
- **80-95%**: Background compaction (summarize older messages)
|
||||||
|
- **> 95%**: Force compaction before next request
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Streaming Response Handling
|
||||||
|
|
||||||
|
### Event-Driven Architecture
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def stream_response(
|
||||||
|
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||||
|
) -> AsyncGenerator:
|
||||||
|
"""
|
||||||
|
Asynchronous event processing with queue-based buffering.
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Start async send task
|
||||||
|
2. Register event handler
|
||||||
|
3. Process events via queue
|
||||||
|
4. Yield chunks to OpenWebUI
|
||||||
|
5. Clean up resources
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Processing Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Copilot SDK Event Stream │
|
||||||
|
└────────────────────┬───────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ Event Handler │
|
||||||
|
│ (Sync Callback) │
|
||||||
|
└────────┬───────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ Async Queue │
|
||||||
|
│ (Thread-safe) │
|
||||||
|
└────────┬───────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ Consumer Loop │
|
||||||
|
│ (async for) │
|
||||||
|
└────────┬───────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ yield to OpenWebUI │
|
||||||
|
└────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Management During Streaming
|
||||||
|
|
||||||
|
```python
|
||||||
|
state = {
|
||||||
|
"thinking_started": False, # <think> tags opened
|
||||||
|
"content_sent": False # Main content has started
|
||||||
|
}
|
||||||
|
active_tools = {} # Track concurrent tool executions
|
||||||
|
```
|
||||||
|
|
||||||
|
**State Transitions:**
|
||||||
|
|
||||||
|
1. `reasoning_delta` arrives → `thinking_started = True` → Output: `<think>\n{reasoning}`
|
||||||
|
2. `message_delta` arrives → Close `</think>` if open → `content_sent = True` → Output: `{content}`
|
||||||
|
3. `tool.execution_start` → Output tool indicator (inside/outside `<think>`)
|
||||||
|
4. `session.complete` → Finalize stream
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event Processing Mechanism
|
||||||
|
|
||||||
|
### Event Type Reference
|
||||||
|
|
||||||
|
Following official SDK patterns (from `copilot.SessionEventType`):
|
||||||
|
|
||||||
|
| Event Type | Description | Key Data Fields | Handler Action |
|
||||||
|
|-----------|-------------|-----------------|----------------|
|
||||||
|
| `assistant.message_delta` | Main content streaming | `delta_content` | Yield text chunk |
|
||||||
|
| `assistant.reasoning_delta` | Chain-of-thought | `delta_content` | Wrap in `<think>` tags |
|
||||||
|
| `tool.execution_start` | Tool call initiated | `name`, `tool_call_id` | Display tool indicator |
|
||||||
|
| `tool.execution_complete` | Tool finished | `result.content` | Show completion status |
|
||||||
|
| `session.compaction_start` | Context compaction begins | - | Log debug info |
|
||||||
|
| `session.compaction_complete` | Compaction done | - | Log debug info |
|
||||||
|
| `session.error` | Error occurred | `error`, `message` | Emit error notification |
|
||||||
|
|
||||||
|
### Event Handler Implementation
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handler(event):
|
||||||
|
"""Process streaming events following official SDK patterns."""
|
||||||
|
event_type = get_event_type(event) # Handle enum/string types
|
||||||
|
|
||||||
|
# Extract data using safe_get_data_attr (handles dict/object)
|
||||||
|
if event_type == "assistant.message_delta":
|
||||||
|
delta = safe_get_data_attr(event, "delta_content")
|
||||||
|
if delta:
|
||||||
|
queue.put_nowait(delta) # Thread-safe enqueue
|
||||||
|
```
|
||||||
|
|
||||||
|
### Official SDK Pattern Compliance
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_get_data_attr(event, attr: str, default=None):
|
||||||
|
"""
|
||||||
|
Official pattern: event.data.delta_content
|
||||||
|
Handles both dict and object access patterns.
|
||||||
|
"""
|
||||||
|
if not hasattr(event, "data") or event.data is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
data = event.data
|
||||||
|
|
||||||
|
# Dict access (JSON-like)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data.get(attr, default)
|
||||||
|
|
||||||
|
# Object attribute (Python SDK)
|
||||||
|
return getattr(data, attr, default)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tool Execution Flow
|
||||||
|
|
||||||
|
### Tool Registration
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. Define tool at module level
|
||||||
|
@define_tool(description="Generate a random integer within a specified range.")
|
||||||
|
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||||
|
number = random.randint(params.min, params.max)
|
||||||
|
return f"Generated random number: {number}"
|
||||||
|
|
||||||
|
# 2. Register in _initialize_custom_tools
|
||||||
|
def _initialize_custom_tools(self):
|
||||||
|
if not self.valves.ENABLE_TOOLS:
|
||||||
|
return []
|
||||||
|
|
||||||
|
all_tools = {
|
||||||
|
"generate_random_number": generate_random_number,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Filter based on AVAILABLE_TOOLS valve
|
||||||
|
if self.valves.AVAILABLE_TOOLS == "all":
|
||||||
|
return list(all_tools.values())
|
||||||
|
|
||||||
|
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||||
|
return [all_tools[name] for name in enabled if name in all_tools]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tool Execution Timeline
|
||||||
|
|
||||||
|
```
|
||||||
|
User Message: "Generate a random number between 1 and 100"
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Model Decision: Use tool `generate_random_number`
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Event: tool.execution_start
|
||||||
|
│ → Display: "🔧 Running Tool: generate_random_number"
|
||||||
|
▼
|
||||||
|
Tool Function Execution (async)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Event: tool.execution_complete
|
||||||
|
│ → Result: "Generated random number: 42"
|
||||||
|
│ → Display: "✅ Tool Completed: 42"
|
||||||
|
▼
|
||||||
|
Model generates response using tool result
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Event: assistant.message_delta
|
||||||
|
│ → "I generated the number 42 for you."
|
||||||
|
▼
|
||||||
|
Stream Complete
|
||||||
|
```
|
||||||
|
|
||||||
|
### Visual Indicators
|
||||||
|
|
||||||
|
**Before Content:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<think>
|
||||||
|
Running Tool: generate_random_number...
|
||||||
|
Tool `generate_random_number` Completed. Result: 42
|
||||||
|
</think>
|
||||||
|
|
||||||
|
I generated the number 42 for you.
|
||||||
|
```
|
||||||
|
|
||||||
|
**After Content Started:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
The number is
|
||||||
|
|
||||||
|
> 🔧 **Running Tool**: `generate_random_number`
|
||||||
|
|
||||||
|
> ✅ **Tool Completed**: 42
|
||||||
|
|
||||||
|
actually 42.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System Prompt Extraction
|
||||||
|
|
||||||
|
### Multi-Source Priority System
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _extract_system_prompt(self, body, messages, request_model, real_model_id):
|
||||||
|
"""
|
||||||
|
Priority order:
|
||||||
|
1. metadata.model.params.system (highest)
|
||||||
|
2. Model database lookup
|
||||||
|
3. body.params.system
|
||||||
|
4. messages[role="system"] (fallback)
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source 1: Metadata Model Params
|
||||||
|
|
||||||
|
```python
|
||||||
|
# OpenWebUI injects model configuration
|
||||||
|
metadata = body.get("metadata", {})
|
||||||
|
meta_model = metadata.get("model", {})
|
||||||
|
meta_params = meta_model.get("params", {})
|
||||||
|
system_prompt = meta_params.get("system") # Priority 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source 2: Model Database
|
||||||
|
|
||||||
|
```python
|
||||||
|
from open_webui.models.models import Models
|
||||||
|
|
||||||
|
# Try multiple model ID variations
|
||||||
|
model_ids_to_try = [
|
||||||
|
request_model, # "copilotsdk-claude-sonnet-4.5"
|
||||||
|
request_model.removeprefix(...), # "claude-sonnet-4.5"
|
||||||
|
real_model_id, # From valves
|
||||||
|
]
|
||||||
|
|
||||||
|
for mid in model_ids_to_try:
|
||||||
|
model_record = Models.get_model_by_id(mid)
|
||||||
|
if model_record and hasattr(model_record, "params"):
|
||||||
|
system_prompt = model_record.params.get("system")
|
||||||
|
if system_prompt:
|
||||||
|
break
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source 3: Body Params
|
||||||
|
|
||||||
|
```python
|
||||||
|
body_params = body.get("params", {})
|
||||||
|
system_prompt = body_params.get("system")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source 4: System Message
|
||||||
|
|
||||||
|
```python
|
||||||
|
for msg in messages:
|
||||||
|
if msg.get("role") == "system":
|
||||||
|
system_prompt = self._extract_text_from_content(msg.get("content"))
|
||||||
|
break
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration in SessionConfig
|
||||||
|
|
||||||
|
```python
|
||||||
|
system_message_config = {
|
||||||
|
"mode": "append", # Append to conversation context
|
||||||
|
"content": system_prompt_content
|
||||||
|
}
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
system_message=system_message_config,
|
||||||
|
# ... other params
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Parameters
|
||||||
|
|
||||||
|
### Valve Definitions
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `GH_TOKEN` | str | `""` | GitHub Fine-grained Token (requires 'Copilot Requests' permission) |
|
||||||
|
| `MODEL_ID` | str | `"claude-sonnet-4.5"` | Default model when dynamic fetching fails |
|
||||||
|
| `CLI_PATH` | str | `"/usr/local/bin/copilot"` | Path to Copilot CLI binary |
|
||||||
|
| `DEBUG` | bool | `False` | Enable frontend console debug logging |
|
||||||
|
| `LOG_LEVEL` | str | `"error"` | CLI log level: none, error, warning, info, debug, all |
|
||||||
|
| `SHOW_THINKING` | bool | `True` | Display model reasoning in `<think>` tags |
|
||||||
|
| `SHOW_WORKSPACE_INFO` | bool | `True` | Show session workspace path in debug mode |
|
||||||
|
| `EXCLUDE_KEYWORDS` | str | `""` | Comma-separated keywords to exclude models |
|
||||||
|
| `WORKSPACE_DIR` | str | `""` | Restricted workspace directory (empty = process cwd) |
|
||||||
|
| `INFINITE_SESSION` | bool | `True` | Enable automatic context compaction |
|
||||||
|
| `COMPACTION_THRESHOLD` | float | `0.8` | Background compaction at 80% token usage |
|
||||||
|
| `BUFFER_THRESHOLD` | float | `0.95` | Emergency threshold at 95% |
|
||||||
|
| `TIMEOUT` | int | `300` | Stream chunk timeout (seconds) |
|
||||||
|
| `CUSTOM_ENV_VARS` | str | `""` | JSON string of custom environment variables |
|
||||||
|
| `ENABLE_TOOLS` | bool | `False` | Enable custom tool system |
|
||||||
|
| `AVAILABLE_TOOLS` | str | `"all"` | Available tools: "all" or comma-separated list |
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set by _setup_env
|
||||||
|
export COPILOT_CLI_PATH="/usr/local/bin/copilot"
|
||||||
|
export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||||
|
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
# Custom variables (from CUSTOM_ENV_VARS valve)
|
||||||
|
export CUSTOM_VAR_1="value1"
|
||||||
|
export CUSTOM_VAR_2="value2"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Functions Reference
|
||||||
|
|
||||||
|
### Entry Points
|
||||||
|
|
||||||
|
#### `pipe(body, __metadata__, __event_emitter__, __event_call__)`
|
||||||
|
|
||||||
|
- **Purpose**: OpenWebUI stable entry point
|
||||||
|
- **Returns**: Delegates to `_pipe_impl`
|
||||||
|
|
||||||
|
#### `_pipe_impl(body, __metadata__, __event_emitter__, __event_call__)`
|
||||||
|
|
||||||
|
- **Purpose**: Main request processing logic
|
||||||
|
- **Flow**: Setup → Extract → Session → Response
|
||||||
|
- **Returns**: `str` (non-streaming) or `AsyncGenerator` (streaming)
|
||||||
|
|
||||||
|
#### `pipes()`
|
||||||
|
|
||||||
|
- **Purpose**: Dynamic model list fetching
|
||||||
|
- **Returns**: List of available models with multiplier info
|
||||||
|
- **Caching**: Uses `_model_cache` to avoid repeated API calls
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
|
||||||
|
#### `_build_session_config(chat_id, real_model_id, custom_tools, system_prompt_content, is_streaming)`
|
||||||
|
|
||||||
|
- **Purpose**: Construct SessionConfig object
|
||||||
|
- **Returns**: `SessionConfig` with infinite sessions and tools
|
||||||
|
|
||||||
|
#### `_get_chat_context(body, __metadata__, __event_call__)`
|
||||||
|
|
||||||
|
- **Purpose**: Extract chat_id with priority fallback
|
||||||
|
- **Returns**: `{"chat_id": str}`
|
||||||
|
|
||||||
|
### Streaming
|
||||||
|
|
||||||
|
#### `stream_response(client, session, send_payload, init_message, __event_call__)`
|
||||||
|
|
||||||
|
- **Purpose**: Async streaming event processor
|
||||||
|
- **Yields**: Text chunks to OpenWebUI
|
||||||
|
- **Resources**: Auto-cleanup client and session
|
||||||
|
|
||||||
|
#### `handler(event)`
|
||||||
|
|
||||||
|
- **Purpose**: Sync event callback (inside `stream_response`)
|
||||||
|
- **Action**: Parse event → Enqueue chunks → Update state
|
||||||
|
|
||||||
|
### Helpers
|
||||||
|
|
||||||
|
#### `_emit_debug_log(message, __event_call__)`
|
||||||
|
|
||||||
|
- **Purpose**: Send debug logs to frontend console
|
||||||
|
- **Condition**: Only when `DEBUG=True`
|
||||||
|
|
||||||
|
#### `_setup_env(__event_call__)`
|
||||||
|
|
||||||
|
- **Purpose**: Locate CLI, set environment variables
|
||||||
|
- **Side Effects**: Modifies `os.environ`
|
||||||
|
|
||||||
|
#### `_extract_system_prompt(body, messages, request_model, real_model_id, __event_call__)`
|
||||||
|
|
||||||
|
- **Purpose**: Multi-source system prompt extraction
|
||||||
|
- **Returns**: `(system_prompt_content, source_name)`
|
||||||
|
|
||||||
|
#### `_process_images(messages, __event_call__)`
|
||||||
|
|
||||||
|
- **Purpose**: Extract text and images from multimodal messages
|
||||||
|
- **Returns**: `(text_content, attachments_list)`
|
||||||
|
|
||||||
|
#### `_initialize_custom_tools()`
|
||||||
|
|
||||||
|
- **Purpose**: Register and filter custom tools
|
||||||
|
- **Returns**: List of tool functions
|
||||||
|
|
||||||
|
### Utility Functions
|
||||||
|
|
||||||
|
#### `get_event_type(event) -> str`
|
||||||
|
|
||||||
|
- **Purpose**: Extract event type string from enum/string
|
||||||
|
- **Handles**: `SessionEventType` enum → `.value` extraction
|
||||||
|
|
||||||
|
#### `safe_get_data_attr(event, attr: str, default=None)`
|
||||||
|
|
||||||
|
- **Purpose**: Safe attribute extraction from event.data
|
||||||
|
- **Handles**: Both dict access and object attribute access
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting Guide
|
||||||
|
|
||||||
|
### Enable Debug Mode
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In OpenWebUI Valves UI:
|
||||||
|
DEBUG = True
|
||||||
|
SHOW_WORKSPACE_INFO = True
|
||||||
|
LOG_LEVEL = "debug"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Output Location
|
||||||
|
|
||||||
|
**Frontend Console:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Open browser DevTools (F12)
|
||||||
|
// Look for logs with prefix: [Copilot Pipe]
|
||||||
|
console.debug("[Copilot Pipe] Extracted ChatID: abc123 (Source: __metadata__)")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backend Logs:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python logging output
|
||||||
|
logger.debug(f"[Copilot Pipe] Session resumed: {chat_id}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### 1. Session Not Resuming
|
||||||
|
|
||||||
|
**Symptom:** New session created every request
|
||||||
|
**Causes:**
|
||||||
|
|
||||||
|
- `chat_id` not extracted correctly
|
||||||
|
- Session expired on Copilot side
|
||||||
|
- `INFINITE_SESSION=False` (sessions not persistent)
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Check debug logs for:
|
||||||
|
"Extracted ChatID: <id> (Source: ...)"
|
||||||
|
"Session <id> not found (...), creating new."
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. System Prompt Not Applied
|
||||||
|
|
||||||
|
**Symptom:** Model ignores configured system prompt
|
||||||
|
**Causes:**
|
||||||
|
|
||||||
|
- Not found in any of 4 sources
|
||||||
|
- Session resumed (system prompt only set on creation)
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Check debug logs for:
|
||||||
|
"Extracted system prompt from <source> (length: X)"
|
||||||
|
"Configured system message (mode: append)"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Tools Not Available
|
||||||
|
|
||||||
|
**Symptom:** Model can't use custom tools
|
||||||
|
**Causes:**
|
||||||
|
|
||||||
|
- `ENABLE_TOOLS=False`
|
||||||
|
- Tool not registered in `_initialize_custom_tools`
|
||||||
|
- Wrong `AVAILABLE_TOOLS` filter
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Check debug logs for:
|
||||||
|
"Enabled X custom tools: ['tool1', 'tool2']"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Model List Caching
|
||||||
|
|
||||||
|
```python
|
||||||
|
# First request: Fetch from API
|
||||||
|
models = await client.list_models()
|
||||||
|
self._model_cache = [...] # Cache result
|
||||||
|
|
||||||
|
# Subsequent requests: Use cache
|
||||||
|
if self._model_cache:
|
||||||
|
return self._model_cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Persistence
|
||||||
|
|
||||||
|
**Impact:** Eliminates redundant model initialization on every request
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Without session:
|
||||||
|
# Each request: Initialize model → Load context → Generate → Discard
|
||||||
|
|
||||||
|
# With session (chat_id):
|
||||||
|
# First request: Initialize model → Load context → Generate → Save
|
||||||
|
# Later: Resume → Generate (instant)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Streaming vs Non-streaming
|
||||||
|
|
||||||
|
**Streaming:**
|
||||||
|
|
||||||
|
- Lower perceived latency (first token faster)
|
||||||
|
- Better UX for long responses
|
||||||
|
- Resource cleanup via generator exit
|
||||||
|
|
||||||
|
**Non-streaming:**
|
||||||
|
|
||||||
|
- Simpler error handling
|
||||||
|
- Atomic response (no partial output)
|
||||||
|
- Use for short responses
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Token Protection
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ Never log tokens
|
||||||
|
logger.debug(f"Token: {self.valves.GH_TOKEN}") # DON'T DO THIS
|
||||||
|
|
||||||
|
# ✅ Mask sensitive data
|
||||||
|
logger.debug(f"Token configured: {'*' * 10}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workspace Isolation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Set WORKSPACE_DIR to restrict file access
|
||||||
|
WORKSPACE_DIR = "/safe/sandbox/path"
|
||||||
|
|
||||||
|
# Copilot CLI respects this directory
|
||||||
|
client_config["cwd"] = WORKSPACE_DIR
|
||||||
|
```
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Validate chat_id format
|
||||||
|
if chat_id and not re.match(r'^[a-zA-Z0-9_-]+$', chat_id):
|
||||||
|
logger.warning(f"Invalid chat_id format: {chat_id}")
|
||||||
|
chat_id = None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Planned Features
|
||||||
|
|
||||||
|
1. **Multi-Session Management**: Support multiple parallel sessions per user
|
||||||
|
2. **Session Analytics**: Track token usage, compaction frequency
|
||||||
|
3. **Tool Result Caching**: Avoid redundant tool calls
|
||||||
|
4. **Custom Event Filters**: User-configurable event handling
|
||||||
|
5. **Workspace Templates**: Pre-configured workspace environments
|
||||||
|
6. **Streaming Abort**: Graceful cancellation of long-running requests
|
||||||
|
|
||||||
|
### API Evolution
|
||||||
|
|
||||||
|
Monitoring Copilot SDK updates for:
|
||||||
|
|
||||||
|
- New event types (e.g., `assistant.function_call`)
|
||||||
|
- Enhanced tool capabilities
|
||||||
|
- Improved session serialization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [GitHub Copilot SDK Documentation](https://github.com/github/copilot-sdk)
|
||||||
|
- [OpenWebUI Pipe Development](https://docs.openwebui.com/)
|
||||||
|
- [Awesome OpenWebUI Project](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**License:** MIT
|
||||||
|
**Maintainer:** Fu-Jie ([@Fu-Jie](https://github.com/Fu-Jie))
|
||||||
835
plugins/debug/legacy_docs/guides/WORKFLOW_CN.md
Normal file
835
plugins/debug/legacy_docs/guides/WORKFLOW_CN.md
Normal file
@@ -0,0 +1,835 @@
|
|||||||
|
# GitHub Copilot SDK 集成工作流程
|
||||||
|
|
||||||
|
**作者:** Fu-Jie
|
||||||
|
**版本:** 0.2.3
|
||||||
|
**最后更新:** 2026-01-27
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [架构概览](#架构概览)
|
||||||
|
2. [请求处理流程](#请求处理流程)
|
||||||
|
3. [会话管理](#会话管理)
|
||||||
|
4. [流式响应处理](#流式响应处理)
|
||||||
|
5. [事件处理机制](#事件处理机制)
|
||||||
|
6. [工具执行流程](#工具执行流程)
|
||||||
|
7. [系统提示词提取](#系统提示词提取)
|
||||||
|
8. [配置参数](#配置参数)
|
||||||
|
9. [核心函数参考](#核心函数参考)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构概览
|
||||||
|
|
||||||
|
### 组件图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ OpenWebUI │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Pipe 接口 (入口点) │ │
|
||||||
|
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ _pipe_impl (主逻辑) │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ 1. 环境设置 (_setup_env) │ │ │
|
||||||
|
│ │ │ 2. 模型选择 (request_model 解析) │ │ │
|
||||||
|
│ │ │ 3. 聊天上下文提取 │ │ │
|
||||||
|
│ │ │ 4. 系统提示词提取 │ │ │
|
||||||
|
│ │ │ 5. 会话管理 (创建/恢复) │ │ │
|
||||||
|
│ │ │ 6. 流式/非流式响应 │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ GitHub Copilot 客户端 │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ • CopilotClient (SDK 实例) │ │ │
|
||||||
|
│ │ │ • Session (对话上下文) │ │ │
|
||||||
|
│ │ │ • Event Stream (异步事件流) │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────┬─────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
└────────────────────────┼─────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌──────────────────────┐
|
||||||
|
│ Copilot CLI 进程 │
|
||||||
|
│ (后端代理) │
|
||||||
|
└──────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
1. **Pipe 接口**:OpenWebUI 的标准入口点
|
||||||
|
2. **环境管理器**:CLI 设置、令牌验证、环境变量
|
||||||
|
3. **会话管理器**:持久化对话状态,自动压缩
|
||||||
|
4. **事件处理器**:异步流式事件处理器
|
||||||
|
5. **工具系统**:自定义工具注册和执行
|
||||||
|
6. **调试日志器**:前端控制台日志,用于故障排除
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 请求处理流程
|
||||||
|
|
||||||
|
### 完整请求生命周期
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[OpenWebUI 请求] --> B[pipe 入口点]
|
||||||
|
B --> C[_pipe_impl]
|
||||||
|
C --> D{设置环境}
|
||||||
|
D --> E[解析模型 ID]
|
||||||
|
E --> F[提取聊天上下文]
|
||||||
|
F --> G[提取系统提示词]
|
||||||
|
G --> H{会话存在?}
|
||||||
|
H -->|是| I[恢复会话]
|
||||||
|
H -->|否| J[创建新会话]
|
||||||
|
I --> K[初始化工具]
|
||||||
|
J --> K
|
||||||
|
K --> L[处理图片]
|
||||||
|
L --> M{流式模式?}
|
||||||
|
M -->|是| N[stream_response]
|
||||||
|
M -->|否| O[send_and_wait]
|
||||||
|
N --> P[异步事件流]
|
||||||
|
O --> Q[直接响应]
|
||||||
|
P --> R[返回到 OpenWebUI]
|
||||||
|
Q --> R
|
||||||
|
```
|
||||||
|
|
||||||
|
### 逐步分解
|
||||||
|
|
||||||
|
#### 1. 环境设置 (`_setup_env`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _setup_env(self, __event_call__=None):
|
||||||
|
"""
|
||||||
|
优先级:
|
||||||
|
1. 检查 VALVES.CLI_PATH
|
||||||
|
2. 搜索系统 PATH
|
||||||
|
3. 自动通过 curl 安装(如果未找到)
|
||||||
|
4. 设置 GH_TOKEN 环境变量
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
**操作:**
|
||||||
|
|
||||||
|
- 定位 Copilot CLI 二进制文件
|
||||||
|
- 设置 `COPILOT_CLI_PATH` 环境变量
|
||||||
|
- 配置 `GH_TOKEN` 进行身份验证
|
||||||
|
- 应用自定义环境变量
|
||||||
|
|
||||||
|
#### 2. 模型选择
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 输入:body["model"] = "copilotsdk-claude-sonnet-4.5"
|
||||||
|
request_model = body.get("model", "")
|
||||||
|
if request_model.startswith(f"{self.id}-"):
|
||||||
|
real_model_id = request_model[len(f"{self.id}-"):] # "claude-sonnet-4.5"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 聊天上下文提取 (`_get_chat_context`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# chat_id 的优先级顺序:
|
||||||
|
# 1. __metadata__(最可靠)
|
||||||
|
# 2. body["chat_id"]
|
||||||
|
# 3. body["metadata"]["chat_id"]
|
||||||
|
chat_ctx = self._get_chat_context(body, __metadata__, __event_call__)
|
||||||
|
chat_id = chat_ctx.get("chat_id")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 系统提示词提取 (`_extract_system_prompt`)
|
||||||
|
|
||||||
|
多源回退策略:
|
||||||
|
|
||||||
|
1. `metadata.model.params.system`
|
||||||
|
2. 模型数据库查询(按 model_id)
|
||||||
|
3. `body.params.system`
|
||||||
|
4. 包含 `role="system"` 的消息
|
||||||
|
|
||||||
|
#### 5. 会话创建/恢复
|
||||||
|
|
||||||
|
**新会话:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
session_config = SessionConfig(
|
||||||
|
session_id=chat_id,
|
||||||
|
model=real_model_id,
|
||||||
|
streaming=is_streaming,
|
||||||
|
tools=custom_tools,
|
||||||
|
system_message={"mode": "append", "content": system_prompt_content},
|
||||||
|
infinite_sessions=InfiniteSessionConfig(
|
||||||
|
enabled=True,
|
||||||
|
background_compaction_threshold=0.8,
|
||||||
|
buffer_exhaustion_threshold=0.95
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session = await client.create_session(config=session_config)
|
||||||
|
```
|
||||||
|
|
||||||
|
**恢复会话:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
session = await client.resume_session(chat_id)
|
||||||
|
# 会话状态保留:历史、工具、工作区
|
||||||
|
except Exception:
|
||||||
|
# 回退到创建新会话
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 会话管理
|
||||||
|
|
||||||
|
### 无限会话架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 会话生命周期 │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ 创建 ┌──────────┐ 恢复 ┌───────────┐ │
|
||||||
|
│ │ Chat ID │─────▶ │ Session │ ◀────────│ OpenWebUI │ │
|
||||||
|
│ └──────────┘ │ State │ └───────────┘ │
|
||||||
|
│ └─────┬────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 上下文窗口管理 │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ 消息 [user, assistant, tool_results...] │ │ │
|
||||||
|
│ │ │ Token 使用率: ████████████░░░░ (80%) │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ▼ │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ 达到阈值 (0.8) │ │ │
|
||||||
|
│ │ │ → 后台压缩触发 │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ▼ │ │
|
||||||
|
│ │ ┌──────────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ 压缩摘要 + 最近消息 │ │ │
|
||||||
|
│ │ │ Token 使用率: ██████░░░░░░░░░░░ (40%) │ │ │
|
||||||
|
│ │ └──────────────────────────────────────────┘ │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置参数
|
||||||
|
|
||||||
|
```python
|
||||||
|
InfiniteSessionConfig(
|
||||||
|
enabled=True, # 启用无限会话
|
||||||
|
background_compaction_threshold=0.8, # 在 80% token 使用率时开始压缩
|
||||||
|
buffer_exhaustion_threshold=0.95 # 95% 紧急阈值
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为:**
|
||||||
|
|
||||||
|
- **< 80%**:正常操作,无压缩
|
||||||
|
- **80-95%**:后台压缩(总结旧消息)
|
||||||
|
- **> 95%**:在下一个请求前强制压缩
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 流式响应处理
|
||||||
|
|
||||||
|
### 事件驱动架构
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def stream_response(
|
||||||
|
self, client, session, send_payload, init_message: str = "", __event_call__=None
|
||||||
|
) -> AsyncGenerator:
|
||||||
|
"""
|
||||||
|
使用基于队列的缓冲进行异步事件处理。
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. 启动异步发送任务
|
||||||
|
2. 注册事件处理器
|
||||||
|
3. 通过队列处理事件
|
||||||
|
4. 向 OpenWebUI 产出块
|
||||||
|
5. 清理资源
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 事件处理管道
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Copilot SDK 事件流 │
|
||||||
|
└────────────────────┬───────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ 事件处理器 │
|
||||||
|
│ (同步回调) │
|
||||||
|
└────────┬───────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ 异步队列 │
|
||||||
|
│ (线程安全) │
|
||||||
|
└────────┬───────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ 消费者循环 │
|
||||||
|
│ (async for) │
|
||||||
|
└────────┬───────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌────────────────────────┐
|
||||||
|
│ yield 到 OpenWebUI │
|
||||||
|
└────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 流式传输期间的状态管理
|
||||||
|
|
||||||
|
```python
|
||||||
|
state = {
|
||||||
|
"thinking_started": False, # <think> 标签已打开
|
||||||
|
"content_sent": False # 主内容已开始
|
||||||
|
}
|
||||||
|
active_tools = {} # 跟踪并发工具执行
|
||||||
|
```
|
||||||
|
|
||||||
|
**状态转换:**
|
||||||
|
|
||||||
|
1. `reasoning_delta` 到达 → `thinking_started = True` → 输出:`<think>\n{reasoning}`
|
||||||
|
2. `message_delta` 到达 → 如果打开则关闭 `</think>` → `content_sent = True` → 输出:`{content}`
|
||||||
|
3. `tool.execution_start` → 输出工具指示器(在 `<think>` 内部/外部)
|
||||||
|
4. `session.complete` → 完成流
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 事件处理机制
|
||||||
|
|
||||||
|
### 事件类型参考
|
||||||
|
|
||||||
|
遵循官方 SDK 模式(来自 `copilot.SessionEventType`):
|
||||||
|
|
||||||
|
| 事件类型 | 描述 | 关键数据字段 | 处理器操作 |
|
||||||
|
|---------|------|-------------|-----------|
|
||||||
|
| `assistant.message_delta` | 主内容流式传输 | `delta_content` | 产出文本块 |
|
||||||
|
| `assistant.reasoning_delta` | 思维链 | `delta_content` | 用 `<think>` 标签包装 |
|
||||||
|
| `tool.execution_start` | 工具调用启动 | `name`, `tool_call_id` | 显示工具指示器 |
|
||||||
|
| `tool.execution_complete` | 工具完成 | `result.content` | 显示完成状态 |
|
||||||
|
| `session.compaction_start` | 上下文压缩开始 | - | 记录调试信息 |
|
||||||
|
| `session.compaction_complete` | 压缩完成 | - | 记录调试信息 |
|
||||||
|
| `session.error` | 发生错误 | `error`, `message` | 发出错误通知 |
|
||||||
|
|
||||||
|
### 事件处理器实现
|
||||||
|
|
||||||
|
```python
|
||||||
|
def handler(event):
|
||||||
|
"""遵循官方 SDK 模式处理流式事件。"""
|
||||||
|
event_type = get_event_type(event) # 处理枚举/字符串类型
|
||||||
|
|
||||||
|
# 使用 safe_get_data_attr 提取数据(处理 dict/object)
|
||||||
|
if event_type == "assistant.message_delta":
|
||||||
|
delta = safe_get_data_attr(event, "delta_content")
|
||||||
|
if delta:
|
||||||
|
queue.put_nowait(delta) # 线程安全入队
|
||||||
|
```
|
||||||
|
|
||||||
|
### 官方 SDK 模式合规性
|
||||||
|
|
||||||
|
```python
|
||||||
|
def safe_get_data_attr(event, attr: str, default=None):
|
||||||
|
"""
|
||||||
|
官方模式:event.data.delta_content
|
||||||
|
处理 dict 和对象访问模式。
|
||||||
|
"""
|
||||||
|
if not hasattr(event, "data") or event.data is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
data = event.data
|
||||||
|
|
||||||
|
# Dict 访问(类似 JSON)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data.get(attr, default)
|
||||||
|
|
||||||
|
# 对象属性(Python SDK)
|
||||||
|
return getattr(data, attr, default)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 工具执行流程
|
||||||
|
|
||||||
|
### 工具注册
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. 在模块级别定义工具
|
||||||
|
@define_tool(description="在指定范围内生成随机整数。")
|
||||||
|
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||||
|
number = random.randint(params.min, params.max)
|
||||||
|
return f"生成的随机数: {number}"
|
||||||
|
|
||||||
|
# 2. 在 _initialize_custom_tools 中注册
|
||||||
|
def _initialize_custom_tools(self):
|
||||||
|
if not self.valves.ENABLE_TOOLS:
|
||||||
|
return []
|
||||||
|
|
||||||
|
all_tools = {
|
||||||
|
"generate_random_number": generate_random_number,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根据 AVAILABLE_TOOLS valve 过滤
|
||||||
|
if self.valves.AVAILABLE_TOOLS == "all":
|
||||||
|
return list(all_tools.values())
|
||||||
|
|
||||||
|
enabled = [t.strip() for t in self.valves.AVAILABLE_TOOLS.split(",")]
|
||||||
|
return [all_tools[name] for name in enabled if name in all_tools]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工具执行时间线
|
||||||
|
|
||||||
|
```
|
||||||
|
用户消息:生成一个 1 到 100 之间的随机数
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
模型决策:使用工具 `generate_random_number`
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
事件:tool.execution_start
|
||||||
|
│ → 显示:"🔧 运行工具:generate_random_number"
|
||||||
|
▼
|
||||||
|
工具函数执行(异步)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
事件:tool.execution_complete
|
||||||
|
│ → 结果:"生成的随机数:42"
|
||||||
|
│ → 显示:"✅ 工具完成:42"
|
||||||
|
▼
|
||||||
|
模型使用工具结果生成响应
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
事件:assistant.message_delta
|
||||||
|
│ → "我为你生成了数字 42。"
|
||||||
|
▼
|
||||||
|
流完成
|
||||||
|
```
|
||||||
|
|
||||||
|
### 视觉指示器
|
||||||
|
|
||||||
|
**内容前:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
<think>
|
||||||
|
运行工具:generate_random_number...
|
||||||
|
工具 `generate_random_number` 完成。结果:42
|
||||||
|
</think>
|
||||||
|
|
||||||
|
我为你生成了数字 42。
|
||||||
|
```
|
||||||
|
|
||||||
|
**内容开始后:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
数字是
|
||||||
|
|
||||||
|
> 🔧 **运行工具**:`generate_random_number`
|
||||||
|
|
||||||
|
> ✅ **工具完成**:42
|
||||||
|
|
||||||
|
实际上是 42。
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统提示词提取
|
||||||
|
|
||||||
|
### 多源优先级系统
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def _extract_system_prompt(self, body, messages, request_model, real_model_id):
|
||||||
|
"""
|
||||||
|
优先级顺序:
|
||||||
|
1. metadata.model.params.system(最高)
|
||||||
|
2. 模型数据库查询
|
||||||
|
3. body.params.system
|
||||||
|
4. messages[role="system"](回退)
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 来源 1:元数据模型参数
|
||||||
|
|
||||||
|
```python
|
||||||
|
# OpenWebUI 注入模型配置
|
||||||
|
metadata = body.get("metadata", {})
|
||||||
|
meta_model = metadata.get("model", {})
|
||||||
|
meta_params = meta_model.get("params", {})
|
||||||
|
system_prompt = meta_params.get("system") # 优先级 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 来源 2:模型数据库
|
||||||
|
|
||||||
|
```python
|
||||||
|
from open_webui.models.models import Models
|
||||||
|
|
||||||
|
# 尝试多个模型 ID 变体
|
||||||
|
model_ids_to_try = [
|
||||||
|
request_model, # "copilotsdk-claude-sonnet-4.5"
|
||||||
|
request_model.removeprefix(...), # "claude-sonnet-4.5"
|
||||||
|
real_model_id, # 来自 valves
|
||||||
|
]
|
||||||
|
|
||||||
|
for mid in model_ids_to_try:
|
||||||
|
model_record = Models.get_model_by_id(mid)
|
||||||
|
if model_record and hasattr(model_record, "params"):
|
||||||
|
system_prompt = model_record.params.get("system")
|
||||||
|
if system_prompt:
|
||||||
|
break
|
||||||
|
```
|
||||||
|
|
||||||
|
### 来源 3:Body 参数
|
||||||
|
|
||||||
|
```python
|
||||||
|
body_params = body.get("params", {})
|
||||||
|
system_prompt = body_params.get("system")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 来源 4:系统消息
|
||||||
|
|
||||||
|
```python
|
||||||
|
for msg in messages:
|
||||||
|
if msg.get("role") == "system":
|
||||||
|
system_prompt = self._extract_text_from_content(msg.get("content"))
|
||||||
|
break
|
||||||
|
```
|
||||||
|
|
||||||
|
### SessionConfig 中的配置
|
||||||
|
|
||||||
|
```python
|
||||||
|
system_message_config = {
|
||||||
|
"mode": "append", # 追加到对话上下文
|
||||||
|
"content": system_prompt_content
|
||||||
|
}
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
system_message=system_message_config,
|
||||||
|
# ... 其他参数
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置参数
|
||||||
|
|
||||||
|
### Valve 定义
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 描述 |
|
||||||
|
|-----|------|--------|------|
|
||||||
|
| `GH_TOKEN` | str | `""` | GitHub 精细化令牌(需要 'Copilot Requests' 权限) |
|
||||||
|
| `MODEL_ID` | str | `"claude-sonnet-4.5"` | 动态获取失败时的默认模型 |
|
||||||
|
| `CLI_PATH` | str | `"/usr/local/bin/copilot"` | Copilot CLI 二进制文件路径 |
|
||||||
|
| `DEBUG` | bool | `False` | 启用前端控制台调试日志 |
|
||||||
|
| `LOG_LEVEL` | str | `"error"` | CLI 日志级别:none、error、warning、info、debug、all |
|
||||||
|
| `SHOW_THINKING` | bool | `True` | 在 `<think>` 标签中显示模型推理 |
|
||||||
|
| `SHOW_WORKSPACE_INFO` | bool | `True` | 在调试模式下显示会话工作区路径 |
|
||||||
|
| `EXCLUDE_KEYWORDS` | str | `""` | 逗号分隔的关键字,用于排除模型 |
|
||||||
|
| `WORKSPACE_DIR` | str | `""` | 限制的工作区目录(空 = 进程 cwd) |
|
||||||
|
| `INFINITE_SESSION` | bool | `True` | 启用自动上下文压缩 |
|
||||||
|
| `COMPACTION_THRESHOLD` | float | `0.8` | 80% token 使用率时后台压缩 |
|
||||||
|
| `BUFFER_THRESHOLD` | float | `0.95` | 95% 紧急阈值 |
|
||||||
|
| `TIMEOUT` | int | `300` | 流块超时(秒) |
|
||||||
|
| `CUSTOM_ENV_VARS` | str | `""` | 自定义环境变量的 JSON 字符串 |
|
||||||
|
| `ENABLE_TOOLS` | bool | `False` | 启用自定义工具系统 |
|
||||||
|
| `AVAILABLE_TOOLS` | str | `"all"` | 可用工具:"all" 或逗号分隔列表 |
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 由 _setup_env 设置
|
||||||
|
export COPILOT_CLI_PATH="/usr/local/bin/copilot"
|
||||||
|
export GH_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||||
|
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
||||||
|
# 自定义变量(来自 CUSTOM_ENV_VARS valve)
|
||||||
|
export CUSTOM_VAR_1="value1"
|
||||||
|
export CUSTOM_VAR_2="value2"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心函数参考
|
||||||
|
|
||||||
|
### 入口点
|
||||||
|
|
||||||
|
#### `pipe(body, __metadata__, __event_emitter__, __event_call__)`
|
||||||
|
|
||||||
|
- **目的**:OpenWebUI 稳定入口点
|
||||||
|
- **返回**:委托给 `_pipe_impl`
|
||||||
|
|
||||||
|
#### `_pipe_impl(body, __metadata__, __event_emitter__, __event_call__)`
|
||||||
|
|
||||||
|
- **目的**:主请求处理逻辑
|
||||||
|
- **流程**:设置 → 提取 → 会话 → 响应
|
||||||
|
- **返回**:`str`(非流式)或 `AsyncGenerator`(流式)
|
||||||
|
|
||||||
|
#### `pipes()`
|
||||||
|
|
||||||
|
- **目的**:动态模型列表获取
|
||||||
|
- **返回**:带有倍数信息的可用模型列表
|
||||||
|
- **缓存**:使用 `_model_cache` 避免重复 API 调用
|
||||||
|
|
||||||
|
### 会话管理
|
||||||
|
|
||||||
|
#### `_build_session_config(chat_id, real_model_id, custom_tools, system_prompt_content, is_streaming)`
|
||||||
|
|
||||||
|
- **目的**:构建 SessionConfig 对象
|
||||||
|
- **返回**:带有无限会话和工具的 `SessionConfig`
|
||||||
|
|
||||||
|
#### `_get_chat_context(body, __metadata__, __event_call__)`
|
||||||
|
|
||||||
|
- **目的**:使用优先级回退提取 chat_id
|
||||||
|
- **返回**:`{"chat_id": str}`
|
||||||
|
|
||||||
|
### 流式传输
|
||||||
|
|
||||||
|
#### `stream_response(client, session, send_payload, init_message, __event_call__)`
|
||||||
|
|
||||||
|
- **目的**:异步流式事件处理器
|
||||||
|
- **产出**:文本块到 OpenWebUI
|
||||||
|
- **资源**:自动清理客户端和会话
|
||||||
|
|
||||||
|
#### `handler(event)`
|
||||||
|
|
||||||
|
- **目的**:同步事件回调(在 `stream_response` 内)
|
||||||
|
- **操作**:解析事件 → 入队块 → 更新状态
|
||||||
|
|
||||||
|
### 辅助函数
|
||||||
|
|
||||||
|
#### `_emit_debug_log(message, __event_call__)`
|
||||||
|
|
||||||
|
- **目的**:将调试日志发送到前端控制台
|
||||||
|
- **条件**:仅当 `DEBUG=True` 时
|
||||||
|
|
||||||
|
#### `_setup_env(__event_call__)`
|
||||||
|
|
||||||
|
- **目的**:定位 CLI,设置环境变量
|
||||||
|
- **副作用**:修改 `os.environ`
|
||||||
|
|
||||||
|
#### `_extract_system_prompt(body, messages, request_model, real_model_id, __event_call__)`
|
||||||
|
|
||||||
|
- **目的**:多源系统提示词提取
|
||||||
|
- **返回**:`(system_prompt_content, source_name)`
|
||||||
|
|
||||||
|
#### `_process_images(messages, __event_call__)`
|
||||||
|
|
||||||
|
- **目的**:从多模态消息中提取文本和图片
|
||||||
|
- **返回**:`(text_content, attachments_list)`
|
||||||
|
|
||||||
|
#### `_initialize_custom_tools()`
|
||||||
|
|
||||||
|
- **目的**:注册和过滤自定义工具
|
||||||
|
- **返回**:工具函数列表
|
||||||
|
|
||||||
|
### 实用函数
|
||||||
|
|
||||||
|
#### `get_event_type(event) -> str`
|
||||||
|
|
||||||
|
- **目的**:从枚举/字符串提取事件类型字符串
|
||||||
|
- **处理**:`SessionEventType` 枚举 → `.value` 提取
|
||||||
|
|
||||||
|
#### `safe_get_data_attr(event, attr: str, default=None)`
|
||||||
|
|
||||||
|
- **目的**:从 event.data 安全提取属性
|
||||||
|
- **处理**:dict 访问和对象属性访问
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 故障排除指南
|
||||||
|
|
||||||
|
### 启用调试模式
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 在 OpenWebUI Valves UI 中:
|
||||||
|
DEBUG = True
|
||||||
|
SHOW_WORKSPACE_INFO = True
|
||||||
|
LOG_LEVEL = "debug"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调试输出位置
|
||||||
|
|
||||||
|
**前端控制台:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 打开浏览器开发工具 (F12)
|
||||||
|
// 查找前缀为 [Copilot Pipe] 的日志
|
||||||
|
console.debug("[Copilot Pipe] 提取的 ChatID:abc123(来源:__metadata__)")
|
||||||
|
```
|
||||||
|
|
||||||
|
**后端日志:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python 日志输出
|
||||||
|
logger.debug(f"[Copilot Pipe] 会话已恢复:{chat_id}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. 会话未恢复
|
||||||
|
|
||||||
|
**症状**:每次请求都创建新会话
|
||||||
|
**原因**:
|
||||||
|
|
||||||
|
- `chat_id` 提取不正确
|
||||||
|
- Copilot 端会话过期
|
||||||
|
- `INFINITE_SESSION=False`(会话不持久)
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 检查调试日志中的:
|
||||||
|
"提取的 ChatID:<id>(来源:...)"
|
||||||
|
"会话 <id> 未找到(...),正在创建新会话。"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 系统提示词未应用
|
||||||
|
|
||||||
|
**症状**:模型忽略配置的系统提示词
|
||||||
|
**原因**:
|
||||||
|
|
||||||
|
- 在 4 个来源中均未找到
|
||||||
|
- 会话已恢复(系统提示词仅在创建时设置)
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 检查调试日志中的:
|
||||||
|
"从 <source> 提取系统提示词(长度:X)"
|
||||||
|
"配置系统消息(模式:append)"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 工具不可用
|
||||||
|
|
||||||
|
**症状**:模型无法使用自定义工具
|
||||||
|
**原因**:
|
||||||
|
|
||||||
|
- `ENABLE_TOOLS=False`
|
||||||
|
- 工具未在 `_initialize_custom_tools` 中注册
|
||||||
|
- 错误的 `AVAILABLE_TOOLS` 过滤器
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 检查调试日志中的:
|
||||||
|
"已启用 X 个自定义工具:['tool1', 'tool2']"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 模型列表缓存
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 第一次请求:从 API 获取
|
||||||
|
models = await client.list_models()
|
||||||
|
self._model_cache = [...] # 缓存结果
|
||||||
|
|
||||||
|
# 后续请求:使用缓存
|
||||||
|
if self._model_cache:
|
||||||
|
return self._model_cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### 会话持久化
|
||||||
|
|
||||||
|
**影响**:消除每次请求的冗余模型初始化
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 没有会话:
|
||||||
|
# 每次请求:初始化模型 → 加载上下文 → 生成 → 丢弃
|
||||||
|
|
||||||
|
# 有会话(chat_id):
|
||||||
|
# 第一次请求:初始化模型 → 加载上下文 → 生成 → 保存
|
||||||
|
# 后续:恢复 → 生成(即时)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 流式 vs 非流式
|
||||||
|
|
||||||
|
**流式:**
|
||||||
|
|
||||||
|
- 降低感知延迟(首个 token 更快)
|
||||||
|
- 长响应的更好用户体验
|
||||||
|
- 通过生成器退出进行资源清理
|
||||||
|
|
||||||
|
**非流式:**
|
||||||
|
|
||||||
|
- 更简单的错误处理
|
||||||
|
- 原子响应(无部分输出)
|
||||||
|
- 用于短响应
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
### 令牌保护
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ 永远不要记录令牌
|
||||||
|
logger.debug(f"令牌:{self.valves.GH_TOKEN}") # 不要这样做
|
||||||
|
|
||||||
|
# ✅ 屏蔽敏感数据
|
||||||
|
logger.debug(f"令牌已配置:{'*' * 10}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工作区隔离
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 设置 WORKSPACE_DIR 以限制文件访问
|
||||||
|
WORKSPACE_DIR = "/safe/sandbox/path"
|
||||||
|
|
||||||
|
# Copilot CLI 遵守此目录
|
||||||
|
client_config["cwd"] = WORKSPACE_DIR
|
||||||
|
```
|
||||||
|
|
||||||
|
### 输入验证
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 验证 chat_id 格式
|
||||||
|
if chat_id and not re.match(r'^[a-zA-Z0-9_-]+$', chat_id):
|
||||||
|
logger.warning(f"无效的 chat_id 格式:{chat_id}")
|
||||||
|
chat_id = None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 未来增强
|
||||||
|
|
||||||
|
### 计划功能
|
||||||
|
|
||||||
|
1. **多会话管理**:支持每个用户的多个并行会话
|
||||||
|
2. **会话分析**:跟踪 token 使用率、压缩频率
|
||||||
|
3. **工具结果缓存**:避免冗余工具调用
|
||||||
|
4. **自定义事件过滤器**:用户可配置的事件处理
|
||||||
|
5. **工作区模板**:预配置的工作区环境
|
||||||
|
6. **流式中止**:优雅取消长时间运行的请求
|
||||||
|
|
||||||
|
### API 演进
|
||||||
|
|
||||||
|
监控 Copilot SDK 更新:
|
||||||
|
|
||||||
|
- 新事件类型(例如 `assistant.function_call`)
|
||||||
|
- 增强的工具功能
|
||||||
|
- 改进的会话序列化
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- [GitHub Copilot SDK 文档](https://github.com/github/copilot-sdk)
|
||||||
|
- [OpenWebUI Pipe 开发](https://docs.openwebui.com/)
|
||||||
|
- [Awesome OpenWebUI 项目](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**许可证**:MIT
|
||||||
|
**维护者**:Fu-Jie ([@Fu-Jie](https://github.com/Fu-Jie))
|
||||||
124
plugins/debug/legacy_docs/test_capabilities.py
Normal file
124
plugins/debug/legacy_docs/test_capabilities.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from copilot import CopilotClient, define_tool
|
||||||
|
from copilot.types import SessionConfig
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
# Define a simple tool for testing
|
||||||
|
class RandomNumberParams(BaseModel):
|
||||||
|
min: int = Field(description="Minimum value")
|
||||||
|
max: int = Field(description="Maximum value")
|
||||||
|
|
||||||
|
|
||||||
|
@define_tool(description="Generate a random integer within a range.")
|
||||||
|
async def generate_random_number(params: RandomNumberParams) -> str:
|
||||||
|
import random
|
||||||
|
|
||||||
|
return f"Result: {random.randint(params.min, params.max)}"
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print(f"Running tests with Python: {sys.executable}")
|
||||||
|
|
||||||
|
# 1. Setup Client
|
||||||
|
client = CopilotClient({"log_level": "error"})
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("\n=== Test 1: Session Creation & Formatting Injection ===")
|
||||||
|
# Use gpt-4o or similar capable model
|
||||||
|
model_id = "gpt-5-mini"
|
||||||
|
|
||||||
|
system_message_config = {
|
||||||
|
"mode": "append",
|
||||||
|
"content": "You are a test assistant. Always start your response with 'TEST_PREFIX: '.",
|
||||||
|
}
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
model=model_id,
|
||||||
|
system_message=system_message_config,
|
||||||
|
tools=[generate_random_number],
|
||||||
|
)
|
||||||
|
|
||||||
|
session = await client.create_session(config=session_config)
|
||||||
|
session_id = session.session_id
|
||||||
|
print(f"Session Created: {session_id}")
|
||||||
|
|
||||||
|
# Test 1.1: Check system prompt effect
|
||||||
|
resp = await session.send_and_wait(
|
||||||
|
{"prompt": "Say hello.", "mode": "immediate"}
|
||||||
|
)
|
||||||
|
content = resp.data.content
|
||||||
|
print(f"Response 1: {content}")
|
||||||
|
|
||||||
|
if "TEST_PREFIX:" in content:
|
||||||
|
print("✅ System prompt injection active.")
|
||||||
|
else:
|
||||||
|
print("⚠️ System prompt injection NOT detected.")
|
||||||
|
|
||||||
|
print("\n=== Test 2: Tool Execution ===")
|
||||||
|
# Test Tool Usage
|
||||||
|
prompt_with_tool = (
|
||||||
|
"Generate a random number between 100 and 200 using the tool."
|
||||||
|
)
|
||||||
|
print(f"Sending: {prompt_with_tool}")
|
||||||
|
|
||||||
|
# We need to listen to events to verify tool execution,
|
||||||
|
# but send_and_wait handles it internally and returns the final answer.
|
||||||
|
# We check if the final answer mentions the result.
|
||||||
|
|
||||||
|
resp_tool = await session.send_and_wait(
|
||||||
|
{"prompt": prompt_with_tool, "mode": "immediate"}
|
||||||
|
)
|
||||||
|
tool_content = resp_tool.data.content
|
||||||
|
print(f"Response 2: {tool_content}")
|
||||||
|
|
||||||
|
if "Result:" in tool_content or any(char.isdigit() for char in tool_content):
|
||||||
|
print("✅ Tool likely executed (numbers found).")
|
||||||
|
else:
|
||||||
|
print("⚠️ Tool execution uncertain.")
|
||||||
|
|
||||||
|
print("\n=== Test 3: Context Retention (Memory) ===")
|
||||||
|
# Store a fact
|
||||||
|
await session.send_and_wait(
|
||||||
|
{"prompt": "My secret code is 'BLUE-42'. Remember it.", "mode": "immediate"}
|
||||||
|
)
|
||||||
|
print("Fact sent.")
|
||||||
|
|
||||||
|
# Retrieve it
|
||||||
|
resp_mem = await session.send_and_wait(
|
||||||
|
{"prompt": "What is my secret code?", "mode": "immediate"}
|
||||||
|
)
|
||||||
|
mem_content = resp_mem.data.content
|
||||||
|
print(f"Response 3: {mem_content}")
|
||||||
|
|
||||||
|
if "BLUE-42" in mem_content:
|
||||||
|
print("✅ Context retention successful.")
|
||||||
|
else:
|
||||||
|
print("⚠️ Context retention failed.")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
await session.destroy()
|
||||||
|
|
||||||
|
print("\n=== Test 4: Resume Session (Simulation) ===")
|
||||||
|
# Note: Actual resuming depends on backend persistence.
|
||||||
|
# The SDK's client.resume_session(id) tries to find it.
|
||||||
|
# Since we destroyed it above, we expect failure or new session logic in real app.
|
||||||
|
# But let's create a new one to persist, close client, and try to resume if process was same?
|
||||||
|
# Actually persistence usually requires the Copilot Agent/Extension host to keep state or file backed.
|
||||||
|
# The Python SDK defaults to file-based workspace in standard generic usage?
|
||||||
|
# Let's just skip complex resume testing for this simple script as it depends on environment (vscode-chat-session vs file).
|
||||||
|
print("Skipping complex resume test in script.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Test Failed: {e}")
|
||||||
|
finally:
|
||||||
|
await client.stop()
|
||||||
|
print("\nTests Completed.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
94
plugins/debug/legacy_docs/test_injection.py
Normal file
94
plugins/debug/legacy_docs/test_injection.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from copilot import CopilotClient
|
||||||
|
from copilot.types import SessionConfig
|
||||||
|
|
||||||
|
# Define the formatting instruction exactly as in the plugin
|
||||||
|
FORMATTING_INSTRUCTION = (
|
||||||
|
"\n\n[Formatting Guidelines]\n"
|
||||||
|
"When providing explanations or descriptions:\n"
|
||||||
|
"- Use clear paragraph breaks (double line breaks)\n"
|
||||||
|
"- Break long sentences into multiple shorter ones\n"
|
||||||
|
"- Use bullet points or numbered lists for multiple items\n"
|
||||||
|
"- Add headings (##, ###) for major sections\n"
|
||||||
|
"- Ensure proper spacing between different topics"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print(f"Python executable: {sys.executable}")
|
||||||
|
|
||||||
|
# Check for GH_TOKEN
|
||||||
|
token = os.environ.get("GH_TOKEN")
|
||||||
|
if token:
|
||||||
|
print("GH_TOKEN is set.")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"Warning: GH_TOKEN not found in environment variables. Relying on CLI auth."
|
||||||
|
)
|
||||||
|
|
||||||
|
client_config = {"log_level": "debug"}
|
||||||
|
|
||||||
|
client = CopilotClient(client_config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
print("Starting client...")
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
# Test 1: Check available models
|
||||||
|
try:
|
||||||
|
models = await client.list_models()
|
||||||
|
print(f"Connection successful. Found {len(models)} models.")
|
||||||
|
model_id = "gpt-5-mini" # User requested model
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to list models: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\nCreating session with model {model_id} and system injection...")
|
||||||
|
|
||||||
|
system_message_config = {
|
||||||
|
"mode": "append",
|
||||||
|
"content": "You are a helpful assistant." + FORMATTING_INSTRUCTION,
|
||||||
|
}
|
||||||
|
|
||||||
|
session_config = SessionConfig(
|
||||||
|
model=model_id, system_message=system_message_config
|
||||||
|
)
|
||||||
|
|
||||||
|
session = await client.create_session(config=session_config)
|
||||||
|
print(f"Session created: {session.session_id}")
|
||||||
|
|
||||||
|
# Test 2: Ask the model to summarize its instructions
|
||||||
|
prompt = "Please summarize the [Formatting Guidelines] you have been given in a list."
|
||||||
|
|
||||||
|
print(f"\nSending prompt: '{prompt}'")
|
||||||
|
response = await session.send_and_wait({"prompt": prompt, "mode": "immediate"})
|
||||||
|
|
||||||
|
print("\n--- Model Response ---")
|
||||||
|
content = response.data.content if response and response.data else "No content"
|
||||||
|
print(content)
|
||||||
|
print("----------------------")
|
||||||
|
|
||||||
|
required_keywords = ["paragraph", "break", "heading", "spacing", "bullet"]
|
||||||
|
found_keywords = [kw for kw in required_keywords if kw in content.lower()]
|
||||||
|
|
||||||
|
if len(found_keywords) >= 3:
|
||||||
|
print(
|
||||||
|
f"\n✅ SUCCESS: Model summarized the guidelines correctly. Found match for: {found_keywords}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"\n⚠️ UNCERTAIN: Summary might be generic. Found keywords: {found_keywords}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {e}")
|
||||||
|
finally:
|
||||||
|
await client.stop()
|
||||||
|
print("\nClient stopped.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
47
plugins/debug/mcp_test.py
Normal file
47
plugins/debug/mcp_test.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from copilot import CopilotClient
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
token = os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN")
|
||||||
|
if not token:
|
||||||
|
print(
|
||||||
|
"Error: GH_TOKEN (or GITHUB_TOKEN) environment variable not set. Please export GH_TOKEN=... before running."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
client = CopilotClient()
|
||||||
|
await client.start()
|
||||||
|
|
||||||
|
async def on_permission_request(request, _ctx):
|
||||||
|
if request.get("kind") == "mcp":
|
||||||
|
return {"kind": "approved"}
|
||||||
|
return {"kind": "approved"}
|
||||||
|
|
||||||
|
session = await client.create_session(
|
||||||
|
{
|
||||||
|
"model": "gpt-5-mini",
|
||||||
|
"mcp_servers": {
|
||||||
|
"github": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://api.githubcopilot.com/mcp/",
|
||||||
|
"headers": {"Authorization": f"Bearer {token}"},
|
||||||
|
"tools": ["*"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await session.send_and_wait(
|
||||||
|
{
|
||||||
|
"prompt": "Use GitHub MCP tools to find the owner of the 'awesome-openwebui' repository.",
|
||||||
|
},timeout=1000
|
||||||
|
)
|
||||||
|
print(result.data.content)
|
||||||
|
|
||||||
|
await client.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -1,26 +1,13 @@
|
|||||||
# Async Context Compression Filter
|
# Async Context Compression Filter
|
||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.1.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.2.2 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent.
|
This filter reduces token consumption in long conversations through intelligent summarization and message compression while keeping conversations coherent.
|
||||||
|
|
||||||
## What's new in 1.1.3
|
## What's new in 1.2.2
|
||||||
- **Improved Compatibility**: Changed summary injection role from `user` to `assistant` for better compatibility across different LLMs.
|
|
||||||
- **Enhanced Stability**: Fixed a race condition in state management that could cause "inlet state not found" warnings in high-concurrency scenarios.
|
|
||||||
- **Bug Fixes**: Corrected default model handling to prevent misleading logs when no model is specified.
|
|
||||||
|
|
||||||
## What's new in 1.1.2
|
|
||||||
|
|
||||||
- **Open WebUI v0.7.x Compatibility**: Resolved a critical database session binding error affecting Open WebUI v0.7.x users. The plugin now dynamically discovers the database engine and session context, ensuring compatibility across versions.
|
|
||||||
- **Enhanced Error Reporting**: Errors during background summary generation are now reported via both the status bar and browser console.
|
|
||||||
- **Robust Model Handling**: Improved handling of missing or invalid model IDs to prevent crashes.
|
|
||||||
|
|
||||||
## What's new in 1.1.1
|
|
||||||
|
|
||||||
- **Frontend Debugging**: Added `show_debug_log` option to print debug info to the browser console (F12).
|
|
||||||
- **Optimized Compression**: Improved token calculation logic to prevent aggressive truncation of history, ensuring more context is retained.
|
|
||||||
|
|
||||||
|
|
||||||
|
- **Critical Fix**: Resolved `TypeError: 'str' object is not callable` caused by variable name conflict in logging function.
|
||||||
|
- **Compatibility**: Enhanced `params` handling to support Pydantic objects, improving compatibility with different OpenWebUI versions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,6 +18,12 @@ This filter reduces token consumption in long conversations through intelligent
|
|||||||
- ✅ Persistent storage via Open WebUI's shared database connection (PostgreSQL, SQLite, etc.).
|
- ✅ Persistent storage via Open WebUI's shared database connection (PostgreSQL, SQLite, etc.).
|
||||||
- ✅ Flexible retention policy to keep the first and last N messages.
|
- ✅ Flexible retention policy to keep the first and last N messages.
|
||||||
- ✅ Smart injection of historical summaries back into the context.
|
- ✅ Smart injection of historical summaries back into the context.
|
||||||
|
- ✅ Structure-aware trimming that preserves document structure (headers, intro, conclusion).
|
||||||
|
- ✅ Native tool output trimming for cleaner context when using function calling.
|
||||||
|
- ✅ Real-time context usage monitoring with warning notifications (>90%).
|
||||||
|
- ✅ Detailed token logging for precise debugging and optimization.
|
||||||
|
- ✅ **Smart Model Matching**: Automatically inherits configuration from base models for custom presets.
|
||||||
|
- ⚠ **Multimodal Support**: Images are preserved but their tokens are **NOT** calculated. Please adjust thresholds accordingly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -43,11 +36,7 @@ This filter reduces token consumption in long conversations through intelligent
|
|||||||
|
|
||||||
### 2) Filter order
|
### 2) Filter order
|
||||||
|
|
||||||
It is recommended to keep this filter early in the chain so it runs before filters that mutate messages:
|
- Recommended order: pre-filters (<10) → this filter (10) → post-filters (>10).
|
||||||
|
|
||||||
1. Pre-filters (priority < 10) — e.g., system prompt injectors.
|
|
||||||
2. This compression filter (priority = 10).
|
|
||||||
3. Post-filters (priority > 10) — e.g., output formatting.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,14 +50,26 @@ It is recommended to keep this filter early in the chain so it runs before filte
|
|||||||
| `keep_first` | `1` | Always keep the first N messages (protects system prompts). |
|
| `keep_first` | `1` | Always keep the first N messages (protects system prompts). |
|
||||||
| `keep_last` | `6` | Always keep the last N messages to preserve recent context. |
|
| `keep_last` | `6` | Always keep the last N messages to preserve recent context. |
|
||||||
| `summary_model` | `None` | Model for summaries. Strongly recommended to set a fast, economical model (e.g., `gemini-2.5-flash`, `deepseek-v3`). Falls back to the current chat model when empty. |
|
| `summary_model` | `None` | Model for summaries. Strongly recommended to set a fast, economical model (e.g., `gemini-2.5-flash`, `deepseek-v3`). Falls back to the current chat model when empty. |
|
||||||
| `max_summary_tokens` | `4000` | Maximum tokens for the generated summary. |
|
| `summary_model_max_context` | `0` | Max context tokens for the summary model. If 0, falls back to `model_thresholds` or global `max_context_tokens`. |
|
||||||
|
| `max_summary_tokens` | `16384` | Maximum tokens for the generated summary. |
|
||||||
| `summary_temperature` | `0.3` | Randomness for summary generation. Lower is more deterministic. |
|
| `summary_temperature` | `0.3` | Randomness for summary generation. Lower is more deterministic. |
|
||||||
| `model_thresholds` | `{}` | Per-model overrides for `compression_threshold_tokens` and `max_context_tokens` (useful for mixed models). |
|
| `model_thresholds` | `{}` | Per-model overrides for `compression_threshold_tokens` and `max_context_tokens` (useful for mixed models). |
|
||||||
|
| `enable_tool_output_trimming` | `false` | When enabled and `function_calling: "native"` is active, trims verbose tool outputs to extract only the final answer. |
|
||||||
| `debug_mode` | `true` | Log verbose debug info. Set to `false` in production. |
|
| `debug_mode` | `true` | Log verbose debug info. Set to `false` in production. |
|
||||||
| `show_debug_log` | `false` | Print debug logs to browser console (F12). Useful for frontend debugging. |
|
| `show_debug_log` | `false` | Print debug logs to browser console (F12). Useful for frontend debugging. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
|
## Troubleshooting ❓
|
||||||
|
|
||||||
- **Initial system prompt is lost**: Keep `keep_first` greater than 0 to protect the initial message.
|
- **Initial system prompt is lost**: Keep `keep_first` greater than 0 to protect the initial message.
|
||||||
- **Compression effect is weak**: Raise `compression_threshold_tokens` or lower `keep_first` / `keep_last` to allow more aggressive compression.
|
- **Compression effect is weak**: Raise `compression_threshold_tokens` or lower `keep_first` / `keep_last` to allow more aggressive compression.
|
||||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -1,28 +1,15 @@
|
|||||||
# 异步上下文压缩过滤器
|
# 异步上下文压缩过滤器
|
||||||
|
|
||||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.1.3 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.2.2 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
> **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
|
> **重要提示**:为了确保所有过滤器的可维护性和易用性,每个过滤器都应附带清晰、完整的文档,以确保其功能、配置和使用方法得到充分说明。
|
||||||
|
|
||||||
本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。
|
本过滤器通过智能摘要和消息压缩技术,在保持对话连贯性的同时,显著降低长对话的 Token 消耗。
|
||||||
|
|
||||||
## 1.1.3 版本更新
|
## 1.2.2 版本更新
|
||||||
- **兼容性提升**: 将摘要注入角色从 `user` 改为 `assistant`,以提高在不同 LLM 之间的兼容性。
|
|
||||||
- **稳定性增强**: 修复了状态管理中的竞态条件,解决了高并发场景下可能出现的“无法获取 inlet 状态”警告。
|
|
||||||
- **Bug 修复**: 修正了默认模型处理逻辑,防止在未指定模型时产生误导性日志。
|
|
||||||
|
|
||||||
## 1.1.2 版本更新
|
|
||||||
|
|
||||||
- **Open WebUI v0.7.x 兼容性**: 修复了影响 Open WebUI v0.7.x 用户的严重数据库会话绑定错误。插件现在动态发现数据库引擎和会话上下文,确保跨版本兼容性。
|
|
||||||
- **增强错误报告**: 后台摘要生成过程中的错误现在会通过状态栏和浏览器控制台同时报告。
|
|
||||||
- **健壮的模型处理**: 改进了对缺失或无效模型 ID 的处理,防止程序崩溃。
|
|
||||||
|
|
||||||
## 1.1.1 版本更新
|
|
||||||
|
|
||||||
- **前端调试**: 新增 `show_debug_log` 选项,支持在浏览器控制台 (F12) 打印调试信息。
|
|
||||||
- **压缩优化**: 优化 Token 计算逻辑,防止历史记录被过度截断,保留更多上下文。
|
|
||||||
|
|
||||||
|
|
||||||
|
- **严重错误修复**: 解决了因日志函数变量名冲突导致的 `TypeError: 'str' object is not callable` 错误。
|
||||||
|
- **兼容性增强**: 改进了 `params` 处理逻辑以支持 Pydantic 对象,提高了对不同 OpenWebUI 版本的兼容性。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -33,6 +20,12 @@
|
|||||||
- ✅ **持久化存储**: 复用 Open WebUI 共享数据库连接,自动支持 PostgreSQL/SQLite 等。
|
- ✅ **持久化存储**: 复用 Open WebUI 共享数据库连接,自动支持 PostgreSQL/SQLite 等。
|
||||||
- ✅ **灵活保留策略**: 可配置保留对话头部和尾部消息,确保关键信息连贯。
|
- ✅ **灵活保留策略**: 可配置保留对话头部和尾部消息,确保关键信息连贯。
|
||||||
- ✅ **智能注入**: 将历史摘要智能注入到新上下文中。
|
- ✅ **智能注入**: 将历史摘要智能注入到新上下文中。
|
||||||
|
- ✅ **结构感知裁剪**: 智能折叠过长消息,保留文档骨架(标题、首尾)。
|
||||||
|
- ✅ **原生工具输出裁剪**: 支持裁剪冗长的工具调用输出。
|
||||||
|
- ✅ **实时监控**: 实时监控上下文使用情况,超过 90% 发出警告。
|
||||||
|
- ✅ **详细日志**: 提供精确的 Token 统计日志,便于调试。
|
||||||
|
- ✅ **智能模型匹配**: 自定义模型自动继承基础模型的阈值配置。
|
||||||
|
- ⚠ **多模态支持**: 图片内容会被保留,但其 Token **不参与计算**。请相应调整阈值。
|
||||||
|
|
||||||
详细的工作原理和流程请参考 [工作流程指南](WORKFLOW_GUIDE_CN.md)。
|
详细的工作原理和流程请参考 [工作流程指南](WORKFLOW_GUIDE_CN.md)。
|
||||||
|
|
||||||
@@ -47,11 +40,7 @@
|
|||||||
|
|
||||||
### 2. 过滤器顺序
|
### 2. 过滤器顺序
|
||||||
|
|
||||||
建议将此过滤器的优先级设置得相对较高(数值较小),以确保它在其他可能修改消息内容的过滤器之前运行。一个典型的顺序可能是:
|
- 建议顺序:前置过滤器(<10)→ 本过滤器(10)→ 后置过滤器(>10)。
|
||||||
|
|
||||||
1. 前置过滤器 (priority < 10) —— 例如系统提示注入。
|
|
||||||
2. 本压缩过滤器 (priority = 10)。
|
|
||||||
3. 后置过滤器 (priority > 10) —— 例如最终输出格式化。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -74,6 +63,7 @@
|
|||||||
| 参数 | 默认值 | 描述 |
|
| 参数 | 默认值 | 描述 |
|
||||||
| :-------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
| :-------------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `summary_model` | `None` | 用于生成摘要的模型 ID。**强烈建议**配置快速、经济、上下文窗口大的模型(如 `gemini-2.5-flash`、`deepseek-v3`)。留空则尝试复用当前对话模型。 |
|
| `summary_model` | `None` | 用于生成摘要的模型 ID。**强烈建议**配置快速、经济、上下文窗口大的模型(如 `gemini-2.5-flash`、`deepseek-v3`)。留空则尝试复用当前对话模型。 |
|
||||||
|
| `summary_model_max_context` | `0` | 摘要模型的最大上下文 Token 数。如果为 0,则回退到 `model_thresholds` 或全局 `max_context_tokens`。 |
|
||||||
| `max_summary_tokens` | `16384` | 生成摘要时允许的最大 Token 数。 |
|
| `max_summary_tokens` | `16384` | 生成摘要时允许的最大 Token 数。 |
|
||||||
| `summary_temperature` | `0.1` | 控制摘要生成的随机性,较低的值结果更稳定。 |
|
| `summary_temperature` | `0.1` | 控制摘要生成的随机性,较低的值结果更稳定。 |
|
||||||
|
|
||||||
@@ -100,18 +90,25 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `debug_mode`
|
| 参数 | 默认值 | 描述 |
|
||||||
|
| :----------------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
- **默认值**: `true`
|
| `enable_tool_output_trimming` | `false` | 启用时,若 `function_calling: "native"` 激活,将裁剪冗长的工具输出以仅提取最终答案。 |
|
||||||
- **描述**: 是否在 Open WebUI 的控制台日志中打印详细的调试信息(如 Token 计数、压缩进度、数据库操作等)。生产环境建议设为 `false`。
|
| `debug_mode` | `true` | 是否在 Open WebUI 的控制台日志中打印详细的调试信息(如 Token 计数、压缩进度、数据库操作等)。生产环境建议设为 `false`。 |
|
||||||
|
| `show_debug_log` | `false` | 是否在浏览器控制台 (F12) 打印调试日志。便于前端调试。 |
|
||||||
#### `show_debug_log`
|
| `show_token_usage_status` | `true` | 是否在对话结束时显示 Token 使用情况的状态通知。 |
|
||||||
|
|
||||||
- **默认值**: `false`
|
|
||||||
- **描述**: 是否在浏览器控制台 (F12) 打印调试日志。便于前端调试。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ⭐ 支持
|
||||||
|
|
||||||
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
|
## 故障排除 (Troubleshooting) ❓
|
||||||
|
|
||||||
- **初始系统提示丢失**:将 `keep_first` 设置为大于 0。
|
- **初始系统提示丢失**:将 `keep_first` 设置为大于 0。
|
||||||
- **压缩效果不明显**:提高 `compression_threshold_tokens`,或降低 `keep_first` / `keep_last` 以增强压缩力度。
|
- **压缩效果不明显**:提高 `compression_threshold_tokens`,或降低 `keep_first` / `keep_last` 以增强压缩力度。
|
||||||
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
63
plugins/filters/folder-memory/README.md
Normal file
63
plugins/filters/folder-memory/README.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Folder Memory
|
||||||
|
|
||||||
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 0.1.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
|
**Folder Memory** is an intelligent context filter plugin for OpenWebUI. It automatically extracts consistent "Project Rules" from ongoing conversations within a folder and injects them back into the folder's system prompt.
|
||||||
|
|
||||||
|
## 🔥 What's New in v0.1.0
|
||||||
|
|
||||||
|
- **Initial Release**: Automated "Project Rules" management for OpenWebUI folders.
|
||||||
|
- **Folder-Level Persistence**: Automatically updates folder system prompts with extracted rules.
|
||||||
|
- **Optimized Performance**: Runs asynchronously and supports `PRIORITY` configuration for seamless integration with other filters.
|
||||||
|
|
||||||
|
## ✨ Core Features
|
||||||
|
|
||||||
|
- **Automatic Extraction**: Analyzes chat history every N messages to extract project rules.
|
||||||
|
- **Non-destructive Injection**: Updates only the specific "Project Rules" block in the system prompt, preserving other instructions.
|
||||||
|
- **Async Processing**: Runs in the background without blocking the user's chat experience.
|
||||||
|
- **ORM Integration**: Directly updates folder data using OpenWebUI's internal models for reliability.
|
||||||
|
|
||||||
|
## Installation & Configuration
|
||||||
|
|
||||||
|
### 1) Installation
|
||||||
|
|
||||||
|
1. Copy `folder_memory.py` to your OpenWebUI `plugins/filters/` directory (or upload via Admin UI).
|
||||||
|
2. Enable the filter in your **Settings** -> **Filters**.
|
||||||
|
3. **Prerequisite**: Conversations must occur inside a folder (create a folder and start a chat within it).
|
||||||
|
|
||||||
|
### 2) Configuration (Valves)
|
||||||
|
|
||||||
|
| Valve | Default | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `PRIORITY` | `20` | Priority level for the filter operations. |
|
||||||
|
| `MESSAGE_TRIGGER_COUNT` | `10` | The number of messages required to trigger a rule analysis. |
|
||||||
|
| `MODEL_ID` | `""` | The model used to generate rules. If empty, uses the current chat model. |
|
||||||
|
| `RULES_BLOCK_TITLE` | `## 📂 Project Rules` | The title displayed above the injected rules block. |
|
||||||
|
| `SHOW_DEBUG_LOG` | `False` | Show detailed debug logs in the browser console. |
|
||||||
|
| `UPDATE_ROOT_FOLDER` | `False` | If enabled, finds and updates the root folder rules instead of the current subfolder. |
|
||||||
|
|
||||||
|
## ⭐ Support
|
||||||
|
|
||||||
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
|
## 🛠️ How It Works
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **Trigger**: When a conversation reaches `MESSAGE_TRIGGER_COUNT` (e.g., 10, 20 messages).
|
||||||
|
2. **Analysis**: The plugin sends the recent conversation + existing rules to the LLM.
|
||||||
|
3. **Synthesis**: The LLM merges new insights with old rules, removing obsolete ones.
|
||||||
|
4. **Update**: The new rule set replaces the `<!-- OWUI_PROJECT_RULES_START -->` block in the folder's system prompt.
|
||||||
|
|
||||||
|
## ⚠️ Notes
|
||||||
|
|
||||||
|
- This plugin modifies the `system_prompt` of your folders.
|
||||||
|
- It uses a specific marker `<!-- OWUI_PROJECT_RULES_START -->` to locate its content. Do not manually remove these markers if you want the plugin to continue managing that section.
|
||||||
|
|
||||||
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
|
See [ROADMAP.md](./ROADMAP.md) for future plans, including "Project Knowledge" collection.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
65
plugins/filters/folder-memory/README_CN.md
Normal file
65
plugins/filters/folder-memory/README_CN.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 文件夹记忆 (Folder Memory)
|
||||||
|
|
||||||
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 0.1.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
|
**文件夹记忆 (Folder Memory)** 是一个 OpenWebUI 的智能上下文过滤器插件。它能自动从文件夹内的对话中提取一致性的“项目规则”,并将其回写到文件夹的系统提示词中。
|
||||||
|
|
||||||
|
这确保了该文件夹内的所有未来对话都能共享相同的进化上下文和规则,无需手动更新。
|
||||||
|
|
||||||
|
## 🔥 最新更新 v0.1.0
|
||||||
|
|
||||||
|
- **首个版本发布**:专注于自动化的“项目规则”管理。
|
||||||
|
- **文件夹级持久化**:自动将提取的规则回写到文件夹系统提示词中。
|
||||||
|
- **性能优化**:采用异步处理机制,并支持 `PRIORITY` 配置,确保与其他过滤器(如上下文压缩)完美协作。
|
||||||
|
|
||||||
|
## ✨ 核心特性
|
||||||
|
|
||||||
|
- **自动提取**:每隔 N 条消息分析一次聊天记录,提取项目规则。
|
||||||
|
- **无损注入**:仅更新系统提示词中的特定“项目规则”块,保留其他指令。
|
||||||
|
- **异步处理**:在后台运行,不阻塞用户的聊天体验。
|
||||||
|
- **ORM 集成**:直接使用 OpenWebUI 的内部模型更新文件夹数据,确保可靠性。
|
||||||
|
|
||||||
|
## 安装与配置
|
||||||
|
|
||||||
|
### 1. 安装
|
||||||
|
|
||||||
|
1. 将 `folder_memory.py`(或中文版 `folder_memory_cn.py`)复制到 OpenWebUI 的 `plugins/filters/` 目录(或通过管理员 UI 上传)。
|
||||||
|
2. 在 **设置** -> **过滤器** 中启用该插件。
|
||||||
|
3. **前置条件**:对话必须在文件夹内进行(先创建文件夹并在其中开始对话)。
|
||||||
|
|
||||||
|
### 2. 配置 (Valves)
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `PRIORITY` | `20` | 过滤器操作的优先级。 |
|
||||||
|
| `MESSAGE_TRIGGER_COUNT` | `10` | 触发规则分析的消息数量阈值。 |
|
||||||
|
| `MODEL_ID` | `""` | 用于生成规则的模型 ID。若为空,则使用当前对话模型。 |
|
||||||
|
| `RULES_BLOCK_TITLE` | `## 📂 项目规则` | 显示在注入规则块上方的标题。 |
|
||||||
|
| `SHOW_DEBUG_LOG` | `False` | 在浏览器控制台显示详细调试日志。 |
|
||||||
|
| `UPDATE_ROOT_FOLDER` | `False` | 如果启用,将向上查找并更新根文件夹的规则,而不是当前子文件夹。 |
|
||||||
|
|
||||||
|
## ⭐ 支持
|
||||||
|
|
||||||
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
|
## 🛠️ 工作原理
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **触发**:当对话达到 `MESSAGE_TRIGGER_COUNT`(例如 10、20 条消息)时。
|
||||||
|
2. **分析**:插件将最近的对话 + 现有规则发送给 LLM。
|
||||||
|
3. **综合**:LLM 将新见解与旧规则合并,移除过时的规则。
|
||||||
|
4. **更新**:新的规则集替换文件夹系统提示词中的 `<!-- OWUI_PROJECT_RULES_START -->` 块。
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
- 此插件会修改文件夹的 `system_prompt`。
|
||||||
|
- 它使用特定标记 `<!-- OWUI_PROJECT_RULES_START -->` 来定位内容。如果您希望插件继续管理该部分,请勿手动删除这些标记。
|
||||||
|
|
||||||
|
## 🗺️ 路线图
|
||||||
|
|
||||||
|
查看 [ROADMAP.md](./ROADMAP.md) 了解未来计划,包括“项目知识”收集功能。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
10
plugins/filters/folder-memory/ROADMAP.md
Normal file
10
plugins/filters/folder-memory/ROADMAP.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
## Future Features
|
||||||
|
|
||||||
|
### 🧠 Project Knowledge (Planned)
|
||||||
|
In future versions, we plan to introduce "Project Knowledge" collection. Unlike "Rules" which are strict instructions, "Knowledge" will capture reusable information, consensus, and context that helps the LLM understand the project better.
|
||||||
|
|
||||||
|
- **Knowledge Extraction**: Automatically extract reusable knowledge (terminology, style guides, business logic) from conversations.
|
||||||
|
- **Long-term Memory**: Use the entire folder's chat history as a corpus for knowledge generation.
|
||||||
|
- **Context Injection**: Inject summarized knowledge into the system prompt alongside rules.
|
||||||
BIN
plugins/filters/folder-memory/folder-memory-demo.png
Normal file
BIN
plugins/filters/folder-memory/folder-memory-demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 459 KiB |
483
plugins/filters/folder-memory/folder_memory.py
Normal file
483
plugins/filters/folder-memory/folder_memory.py
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
"""
|
||||||
|
title: 📂 Folder Memory
|
||||||
|
author: Fu-Jie
|
||||||
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
|
funding_url: https://github.com/open-webui
|
||||||
|
version: 0.1.0
|
||||||
|
description: Automatically extracts project rules from conversations and injects them into the folder's system prompt.
|
||||||
|
requirements:
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, Dict, List
|
||||||
|
from fastapi import Request
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from open_webui.utils.chat import generate_chat_completion
|
||||||
|
from open_webui.models.users import Users
|
||||||
|
from open_webui.models.folders import Folders, FolderUpdateForm
|
||||||
|
from open_webui.models.chats import Chats
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Markers for rule injection
|
||||||
|
RULES_BLOCK_START = "<!-- OWUI_PROJECT_RULES_START -->"
|
||||||
|
RULES_BLOCK_END = "<!-- OWUI_PROJECT_RULES_END -->"
|
||||||
|
|
||||||
|
# System Prompt for Rule Generation
|
||||||
|
SYSTEM_PROMPT_RULE_GENERATOR = """
|
||||||
|
You are a project rule extractor. Your task is to extract "Project Rules" from the conversation and merge them with existing rules.
|
||||||
|
|
||||||
|
### Input
|
||||||
|
1. **Existing Rules**: Current rules in the folder system prompt.
|
||||||
|
2. **Conversation**: Recent chat history.
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
Synthesize a concise list of rules that apply to this project/folder.
|
||||||
|
- **Remove** rules that are no longer relevant or were one-off instructions.
|
||||||
|
- **Add** new consistent requirements found in the conversation.
|
||||||
|
- **Merge** similar rules.
|
||||||
|
- **Format**: Concise bullet points (Markdown).
|
||||||
|
|
||||||
|
### Output Format
|
||||||
|
ONLY output the rules list as Markdown bullet points. Do not include any intro/outro text.
|
||||||
|
Example:
|
||||||
|
- Always use Python 3.11 for type hinting.
|
||||||
|
- Docstrings must follow Google style.
|
||||||
|
- Commit messages should be in English.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Filter:
|
||||||
|
class Valves(BaseModel):
|
||||||
|
PRIORITY: int = Field(
|
||||||
|
default=20, description="Priority level for the filter operations."
|
||||||
|
)
|
||||||
|
SHOW_DEBUG_LOG: bool = Field(
|
||||||
|
default=False, description="Show debug logs in console."
|
||||||
|
)
|
||||||
|
MESSAGE_TRIGGER_COUNT: int = Field(
|
||||||
|
default=10, description="Analyze rules after every N messages in a chat."
|
||||||
|
)
|
||||||
|
MODEL_ID: str = Field(
|
||||||
|
default="",
|
||||||
|
description="Model used for rule extraction. If empty, uses the current chat model.",
|
||||||
|
)
|
||||||
|
RULES_BLOCK_TITLE: str = Field(
|
||||||
|
default="## 📂 Project Rules",
|
||||||
|
description="Title displayed above the rules block.",
|
||||||
|
)
|
||||||
|
UPDATE_ROOT_FOLDER: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="If enabled, finds and updates the root folder rules instead of the current subfolder.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.valves = self.Valves()
|
||||||
|
|
||||||
|
# ==================== Helper Methods ====================
|
||||||
|
|
||||||
|
def _get_user_context(self, __user__: Optional[dict]) -> Dict[str, str]:
|
||||||
|
"""Safely extracts user context information."""
|
||||||
|
if isinstance(__user__, (list, tuple)):
|
||||||
|
user_data = __user__[0] if __user__ else {}
|
||||||
|
elif isinstance(__user__, dict):
|
||||||
|
user_data = __user__
|
||||||
|
else:
|
||||||
|
user_data = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": user_data.get("id", ""),
|
||||||
|
"user_name": user_data.get("name", "User"),
|
||||||
|
"user_language": user_data.get("language", "en-US"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_chat_context(
|
||||||
|
self, body: dict, __metadata__: Optional[dict] = None
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""Unified extraction of chat context information (chat_id, message_id)."""
|
||||||
|
chat_id = ""
|
||||||
|
message_id = ""
|
||||||
|
|
||||||
|
if isinstance(body, dict):
|
||||||
|
chat_id = body.get("chat_id", "")
|
||||||
|
message_id = body.get("id", "")
|
||||||
|
|
||||||
|
if not chat_id or not message_id:
|
||||||
|
body_metadata = body.get("metadata", {})
|
||||||
|
if isinstance(body_metadata, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = body_metadata.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = body_metadata.get("message_id", "")
|
||||||
|
|
||||||
|
if __metadata__ and isinstance(__metadata__, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = __metadata__.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = __metadata__.get("message_id", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chat_id": str(chat_id).strip(),
|
||||||
|
"message_id": str(message_id).strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _emit_debug_log(self, __event_emitter__, title: str, data: dict):
|
||||||
|
if self.valves.SHOW_DEBUG_LOG and __event_emitter__:
|
||||||
|
try:
|
||||||
|
# Flat log format as requested
|
||||||
|
js_code = f"""
|
||||||
|
console.log("[Folder Memory] {title}", {json.dumps(data, ensure_ascii=False)});
|
||||||
|
"""
|
||||||
|
await __event_emitter__({"type": "execute", "data": {"code": js_code}})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error emitting log: {e}")
|
||||||
|
|
||||||
|
async def _emit_status(
|
||||||
|
self, __event_emitter__, description: str, done: bool = False
|
||||||
|
):
|
||||||
|
if __event_emitter__:
|
||||||
|
await __event_emitter__(
|
||||||
|
{"type": "status", "data": {"description": description, "done": done}}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_folder_id(self, body: dict) -> Optional[str]:
|
||||||
|
# 1. Try retrieving folder_id specifically from metadata
|
||||||
|
if "metadata" in body and isinstance(body["metadata"], dict):
|
||||||
|
if "folder_id" in body["metadata"]:
|
||||||
|
return body["metadata"]["folder_id"]
|
||||||
|
|
||||||
|
# 2. Check regular body chat object if available
|
||||||
|
if "chat" in body and isinstance(body["chat"], dict):
|
||||||
|
if "folder_id" in body["chat"]:
|
||||||
|
return body["chat"]["folder_id"]
|
||||||
|
|
||||||
|
# 3. Try fallback via Chat ID (Most reliable)
|
||||||
|
chat_id = body.get("chat_id")
|
||||||
|
if not chat_id:
|
||||||
|
if "metadata" in body and isinstance(body["metadata"], dict):
|
||||||
|
chat_id = body["metadata"].get("chat_id")
|
||||||
|
|
||||||
|
if chat_id:
|
||||||
|
try:
|
||||||
|
chat = Chats.get_chat_by_id(chat_id)
|
||||||
|
if chat and chat.folder_id:
|
||||||
|
return chat.folder_id
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to fetch chat {chat_id}: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_existing_rules(self, system_prompt: str) -> str:
|
||||||
|
pattern = re.compile(
|
||||||
|
re.escape(RULES_BLOCK_START) + r"([\s\S]*?)" + re.escape(RULES_BLOCK_END)
|
||||||
|
)
|
||||||
|
match = pattern.search(system_prompt)
|
||||||
|
if match:
|
||||||
|
# Remove title if it's inside the block
|
||||||
|
content = match.group(1).strip()
|
||||||
|
# Simple cleanup of the title if user formatted it inside
|
||||||
|
title_pat = re.compile(r"^#+\s+.*$", re.MULTILINE)
|
||||||
|
return title_pat.sub("", content).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _inject_rules(self, system_prompt: str, new_rules: str, title: str) -> str:
|
||||||
|
new_block_content = f"\n{title}\n\n{new_rules}\n"
|
||||||
|
new_block = f"{RULES_BLOCK_START}{new_block_content}{RULES_BLOCK_END}"
|
||||||
|
|
||||||
|
system_prompt = system_prompt or ""
|
||||||
|
pattern = re.compile(
|
||||||
|
re.escape(RULES_BLOCK_START) + r"[\s\S]*?" + re.escape(RULES_BLOCK_END)
|
||||||
|
)
|
||||||
|
|
||||||
|
if pattern.search(system_prompt):
|
||||||
|
return pattern.sub(new_block, system_prompt).strip()
|
||||||
|
else:
|
||||||
|
# Append if not found
|
||||||
|
if system_prompt:
|
||||||
|
return f"{system_prompt}\n\n{new_block}"
|
||||||
|
else:
|
||||||
|
return new_block
|
||||||
|
|
||||||
|
async def _generate_new_rules(
|
||||||
|
self,
|
||||||
|
current_rules: str,
|
||||||
|
messages: List[Dict],
|
||||||
|
user_id: str,
|
||||||
|
__request__: Request,
|
||||||
|
) -> str:
|
||||||
|
# Prepare context
|
||||||
|
conversation_text = "\n".join(
|
||||||
|
[
|
||||||
|
f"{msg['role'].upper()}: {msg['content']}"
|
||||||
|
for msg in messages[-20:] # Analyze last 20 messages context
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Existing Rules:
|
||||||
|
{current_rules if current_rules else "None"}
|
||||||
|
|
||||||
|
Conversation Excerpt:
|
||||||
|
{conversation_text}
|
||||||
|
|
||||||
|
Please output the updated Project Rules:
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": self.valves.MODEL_ID,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": SYSTEM_PROMPT_RULE_GENERATOR},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
"stream": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We need a user object for permission checks in generate_chat_completion
|
||||||
|
user = Users.get_user_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
return current_rules
|
||||||
|
|
||||||
|
completion = await generate_chat_completion(__request__, payload, user)
|
||||||
|
if "choices" in completion and len(completion["choices"]) > 0:
|
||||||
|
content = completion["choices"][0]["message"]["content"].strip()
|
||||||
|
# Basic validation: ensure it looks like a list
|
||||||
|
if (
|
||||||
|
content.startswith("-")
|
||||||
|
or content.startswith("*")
|
||||||
|
or content.startswith("1.")
|
||||||
|
):
|
||||||
|
return content
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Rule generation failed: {e}")
|
||||||
|
|
||||||
|
return current_rules
|
||||||
|
|
||||||
|
async def _process_rules_update(
|
||||||
|
self,
|
||||||
|
folder_id: str,
|
||||||
|
body: dict,
|
||||||
|
user_id: str,
|
||||||
|
__request__: Request,
|
||||||
|
__event_emitter__,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Start Processing",
|
||||||
|
{"step": "start", "initial_folder_id": folder_id, "user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. Fetch Folder Data (ORM)
|
||||||
|
initial_folder = Folders.get_folder_by_id_and_user_id(folder_id, user_id)
|
||||||
|
if not initial_folder:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Error: Initial folder not found",
|
||||||
|
{
|
||||||
|
"step": "fetch_initial_folder",
|
||||||
|
"initial_folder_id": folder_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Subfolder handling logic
|
||||||
|
target_folder = initial_folder
|
||||||
|
if self.valves.UPDATE_ROOT_FOLDER:
|
||||||
|
# Traverse up until a folder with no parent_id is found
|
||||||
|
while target_folder and getattr(target_folder, "parent_id", None):
|
||||||
|
try:
|
||||||
|
parent = Folders.get_folder_by_id_and_user_id(
|
||||||
|
target_folder.parent_id, user_id
|
||||||
|
)
|
||||||
|
if parent:
|
||||||
|
target_folder = parent
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Warning: Failed to traverse parent folder",
|
||||||
|
{"step": "traverse_root", "error": str(e)},
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
target_folder_id = target_folder.id
|
||||||
|
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Target Folder Resolved",
|
||||||
|
{
|
||||||
|
"step": "target_resolved",
|
||||||
|
"target_folder_id": target_folder_id,
|
||||||
|
"target_folder_name": target_folder.name,
|
||||||
|
"is_root_update": target_folder_id != folder_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_data = target_folder.data if target_folder.data else {}
|
||||||
|
existing_sys_prompt = existing_data.get("system_prompt", "")
|
||||||
|
|
||||||
|
# 2. Extract Existing Rules
|
||||||
|
current_rules_content = self._extract_existing_rules(existing_sys_prompt)
|
||||||
|
|
||||||
|
# 3. Generate New Rules
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__, "Analyzing project rules...", done=False
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = body.get("messages", [])
|
||||||
|
new_rules_content = await self._generate_new_rules(
|
||||||
|
current_rules_content, messages, user_id, __request__
|
||||||
|
)
|
||||||
|
|
||||||
|
rules_changed = new_rules_content != current_rules_content
|
||||||
|
|
||||||
|
# 4. If no change, skip
|
||||||
|
if not rules_changed:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"No Changes",
|
||||||
|
{
|
||||||
|
"step": "check_changes",
|
||||||
|
"reason": "content_identical_or_generation_failed",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__,
|
||||||
|
"Rule analysis complete: No new content.",
|
||||||
|
done=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 5. Inject Rules into System Prompt
|
||||||
|
updated_sys_prompt = existing_sys_prompt
|
||||||
|
if rules_changed:
|
||||||
|
updated_sys_prompt = self._inject_rules(
|
||||||
|
updated_sys_prompt,
|
||||||
|
new_rules_content,
|
||||||
|
self.valves.RULES_BLOCK_TITLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Ready to Update DB",
|
||||||
|
{"step": "pre_db_update", "target_folder_id": target_folder_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. Update Folder (ORM) - Only update 'data' field
|
||||||
|
existing_data["system_prompt"] = updated_sys_prompt
|
||||||
|
|
||||||
|
updated_folder = Folders.update_folder_by_id_and_user_id(
|
||||||
|
target_folder_id,
|
||||||
|
user_id,
|
||||||
|
FolderUpdateForm(data=existing_data),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not updated_folder:
|
||||||
|
raise Exception("Update folder failed (ORM returned None)")
|
||||||
|
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__, "Rule analysis complete: Rules updated.", done=True
|
||||||
|
)
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Rule Generation Process & Change Details",
|
||||||
|
{
|
||||||
|
"step": "success",
|
||||||
|
"folder_id": target_folder_id,
|
||||||
|
"target_is_root": target_folder_id != folder_id,
|
||||||
|
"model_used": self.valves.MODEL_ID,
|
||||||
|
"analyzed_messages_count": len(messages),
|
||||||
|
"old_rules_length": len(current_rules_content),
|
||||||
|
"new_rules_length": len(new_rules_content),
|
||||||
|
"changes_digest": {
|
||||||
|
"old_rules_preview": (
|
||||||
|
current_rules_content[:100] + "..."
|
||||||
|
if current_rules_content
|
||||||
|
else "None"
|
||||||
|
),
|
||||||
|
"new_rules_preview": (
|
||||||
|
new_rules_content[:100] + "..."
|
||||||
|
if new_rules_content
|
||||||
|
else "None"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Async rule processing error: {e}")
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__, "Failed to update rules.", done=True
|
||||||
|
)
|
||||||
|
# Emit error to console for debugging
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Execution Error",
|
||||||
|
{"error": str(e), "folder_id": folder_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== Filter Hooks ====================
|
||||||
|
|
||||||
|
async def inlet(
|
||||||
|
self, body: dict, __user__: Optional[dict] = None, __event_emitter__=None
|
||||||
|
) -> dict:
|
||||||
|
return body
|
||||||
|
|
||||||
|
async def outlet(
|
||||||
|
self,
|
||||||
|
body: dict,
|
||||||
|
__user__: Optional[dict] = None,
|
||||||
|
__event_emitter__=None,
|
||||||
|
__request__: Optional[Request] = None,
|
||||||
|
) -> dict:
|
||||||
|
user_ctx = self._get_user_context(__user__)
|
||||||
|
chat_ctx = self._get_chat_context(body)
|
||||||
|
|
||||||
|
messages = body.get("messages", [])
|
||||||
|
if not messages:
|
||||||
|
return body
|
||||||
|
|
||||||
|
# Trigger logic: Message Count threshold
|
||||||
|
if len(messages) % self.valves.MESSAGE_TRIGGER_COUNT != 0:
|
||||||
|
return body
|
||||||
|
|
||||||
|
folder_id = self._get_folder_id(body)
|
||||||
|
if not folder_id:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"Skipping Analysis",
|
||||||
|
{
|
||||||
|
"reason": "Chat does not belong to any folder",
|
||||||
|
"chat_id": chat_ctx.get("chat_id"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return body
|
||||||
|
|
||||||
|
# User Info
|
||||||
|
user_id = user_ctx.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
return body
|
||||||
|
|
||||||
|
# Async Task
|
||||||
|
if self.valves.MODEL_ID == "":
|
||||||
|
self.valves.MODEL_ID = body.get("model", "")
|
||||||
|
|
||||||
|
asyncio.create_task(
|
||||||
|
self._process_rules_update(
|
||||||
|
folder_id, body, user_id, __request__, __event_emitter__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return body
|
||||||
470
plugins/filters/folder-memory/folder_memory_cn.py
Normal file
470
plugins/filters/folder-memory/folder_memory_cn.py
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
"""
|
||||||
|
title: 📂 文件夹记忆 (Folder Memory)
|
||||||
|
author: Fu-Jie
|
||||||
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
|
funding_url: https://github.com/open-webui
|
||||||
|
version: 0.1.0
|
||||||
|
description: 自动从对话中提取项目规则,并将其注入到文件夹的系统提示词中。
|
||||||
|
requirements:
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, Dict, List
|
||||||
|
from fastapi import Request
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from open_webui.utils.chat import generate_chat_completion
|
||||||
|
from open_webui.models.users import Users
|
||||||
|
from open_webui.models.folders import Folders, FolderUpdateForm
|
||||||
|
from open_webui.models.chats import Chats
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 规则注入标记
|
||||||
|
RULES_BLOCK_START = "<!-- OWUI_PROJECT_RULES_START -->"
|
||||||
|
RULES_BLOCK_END = "<!-- OWUI_PROJECT_RULES_END -->"
|
||||||
|
|
||||||
|
# 规则生成系统提示词
|
||||||
|
SYSTEM_PROMPT_RULE_GENERATOR = """
|
||||||
|
你是一个项目规则提取器。你的任务是从对话中提取“项目规则”,并与现有规则合并。
|
||||||
|
|
||||||
|
### 输入
|
||||||
|
1. **现有规则 (Existing Rules)**:当前文件夹系统提示词中的规则。
|
||||||
|
2. **对话片段 (Conversation)**:最近的聊天记录。
|
||||||
|
|
||||||
|
### 目标
|
||||||
|
综合生成一份适用于当前项目/文件夹的简洁规则列表。
|
||||||
|
- **移除** 不再相关或仅是一次性指令的规则。
|
||||||
|
- **添加** 对话中发现的新的、一致性的要求。
|
||||||
|
- **合并** 相似的规则。
|
||||||
|
- **格式**:简洁的 Markdown 项目符号列表。
|
||||||
|
|
||||||
|
### 输出格式
|
||||||
|
仅输出 Markdown 项目符号列表形式的规则。不要包含任何开头或结尾的说明文字。
|
||||||
|
示例:
|
||||||
|
- 始终使用 Python 3.11 进行类型提示。
|
||||||
|
- 文档字符串必须遵循 Google 风格。
|
||||||
|
- 提交信息必须使用英文。
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Filter:
|
||||||
|
class Valves(BaseModel):
|
||||||
|
PRIORITY: int = Field(default=20, description="过滤器操作的优先级。")
|
||||||
|
SHOW_DEBUG_LOG: bool = Field(
|
||||||
|
default=False, description="在控制台显示调试日志。"
|
||||||
|
)
|
||||||
|
MESSAGE_TRIGGER_COUNT: int = Field(
|
||||||
|
default=10, description="每隔 N 条消息分析一次规则。"
|
||||||
|
)
|
||||||
|
MODEL_ID: str = Field(
|
||||||
|
default="", description="用于提取规则的模型 ID。为空则使用当前对话模型。"
|
||||||
|
)
|
||||||
|
RULES_BLOCK_TITLE: str = Field(
|
||||||
|
default="## 📂 项目规则", description="显示在规则块上方的标题。"
|
||||||
|
)
|
||||||
|
UPDATE_ROOT_FOLDER: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="如果启用,将向上查找并更新根文件夹的规则,而不是当前子文件夹。",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.valves = self.Valves()
|
||||||
|
|
||||||
|
# ==================== 辅助方法 ====================
|
||||||
|
|
||||||
|
def _get_user_context(self, __user__: Optional[dict]) -> Dict[str, str]:
|
||||||
|
"""安全提取用户上下文信息。"""
|
||||||
|
if isinstance(__user__, (list, tuple)):
|
||||||
|
user_data = __user__[0] if __user__ else {}
|
||||||
|
elif isinstance(__user__, dict):
|
||||||
|
user_data = __user__
|
||||||
|
else:
|
||||||
|
user_data = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": user_data.get("id", ""),
|
||||||
|
"user_name": user_data.get("name", "User"),
|
||||||
|
"user_language": user_data.get("language", "zh-CN"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_chat_context(
|
||||||
|
self, body: dict, __metadata__: Optional[dict] = None
|
||||||
|
) -> Dict[str, str]:
|
||||||
|
"""统一提取聊天上下文信息 (chat_id, message_id)。"""
|
||||||
|
chat_id = ""
|
||||||
|
message_id = ""
|
||||||
|
|
||||||
|
if isinstance(body, dict):
|
||||||
|
chat_id = body.get("chat_id", "")
|
||||||
|
message_id = body.get("id", "")
|
||||||
|
|
||||||
|
if not chat_id or not message_id:
|
||||||
|
body_metadata = body.get("metadata", {})
|
||||||
|
if isinstance(body_metadata, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = body_metadata.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = body_metadata.get("message_id", "")
|
||||||
|
|
||||||
|
if __metadata__ and isinstance(__metadata__, dict):
|
||||||
|
if not chat_id:
|
||||||
|
chat_id = __metadata__.get("chat_id", "")
|
||||||
|
if not message_id:
|
||||||
|
message_id = __metadata__.get("message_id", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chat_id": str(chat_id).strip(),
|
||||||
|
"message_id": str(message_id).strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _emit_debug_log(self, __event_emitter__, title: str, data: dict):
|
||||||
|
if self.valves.SHOW_DEBUG_LOG and __event_emitter__:
|
||||||
|
try:
|
||||||
|
# 按照用户要求的格式输出展平的日志
|
||||||
|
js_code = f"""
|
||||||
|
console.log("[Folder Memory] {title}", {json.dumps(data, ensure_ascii=False)});
|
||||||
|
"""
|
||||||
|
await __event_emitter__({"type": "execute", "data": {"code": js_code}})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发出日志错误: {e}")
|
||||||
|
|
||||||
|
async def _emit_status(
|
||||||
|
self, __event_emitter__, description: str, done: bool = False
|
||||||
|
):
|
||||||
|
if __event_emitter__:
|
||||||
|
await __event_emitter__(
|
||||||
|
{"type": "status", "data": {"description": description, "done": done}}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_folder_id(self, body: dict) -> Optional[str]:
|
||||||
|
# 1. 尝试从 metadata 获取 folder_id
|
||||||
|
if "metadata" in body and isinstance(body["metadata"], dict):
|
||||||
|
if "folder_id" in body["metadata"]:
|
||||||
|
return body["metadata"]["folder_id"]
|
||||||
|
|
||||||
|
# 2. 检查 chat 对象
|
||||||
|
if "chat" in body and isinstance(body["chat"], dict):
|
||||||
|
if "folder_id" in body["chat"]:
|
||||||
|
return body["chat"]["folder_id"]
|
||||||
|
|
||||||
|
# 3. 尝试通过 Chat ID 查找 (最可靠的方法)
|
||||||
|
chat_id = body.get("chat_id")
|
||||||
|
if not chat_id:
|
||||||
|
if "metadata" in body and isinstance(body["metadata"], dict):
|
||||||
|
chat_id = body["metadata"].get("chat_id")
|
||||||
|
|
||||||
|
if chat_id:
|
||||||
|
try:
|
||||||
|
chat = Chats.get_chat_by_id(chat_id)
|
||||||
|
if chat and chat.folder_id:
|
||||||
|
return chat.folder_id
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取聊天信息失败 chat_id={chat_id}: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _extract_existing_rules(self, system_prompt: str) -> str:
|
||||||
|
pattern = re.compile(
|
||||||
|
re.escape(RULES_BLOCK_START) + r"([\s\S]*?)" + re.escape(RULES_BLOCK_END)
|
||||||
|
)
|
||||||
|
match = pattern.search(system_prompt)
|
||||||
|
if match:
|
||||||
|
# 如果标题在块内,将其移除以便纯净合并
|
||||||
|
content = match.group(1).strip()
|
||||||
|
title_pat = re.compile(r"^#+\s+.*$", re.MULTILINE)
|
||||||
|
return title_pat.sub("", content).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _inject_rules(self, system_prompt: str, new_rules: str, title: str) -> str:
|
||||||
|
new_block_content = f"\n{title}\n\n{new_rules}\n"
|
||||||
|
new_block = f"{RULES_BLOCK_START}{new_block_content}{RULES_BLOCK_END}"
|
||||||
|
|
||||||
|
system_prompt = system_prompt or ""
|
||||||
|
pattern = re.compile(
|
||||||
|
re.escape(RULES_BLOCK_START) + r"[\s\S]*?" + re.escape(RULES_BLOCK_END)
|
||||||
|
)
|
||||||
|
|
||||||
|
if pattern.search(system_prompt):
|
||||||
|
# 替换现有块
|
||||||
|
return pattern.sub(new_block, system_prompt).strip()
|
||||||
|
else:
|
||||||
|
# 追加到末尾
|
||||||
|
if system_prompt:
|
||||||
|
return f"{system_prompt}\n\n{new_block}"
|
||||||
|
else:
|
||||||
|
return new_block
|
||||||
|
|
||||||
|
async def _generate_new_rules(
|
||||||
|
self,
|
||||||
|
current_rules: str,
|
||||||
|
messages: List[Dict],
|
||||||
|
user_id: str,
|
||||||
|
__request__: Request,
|
||||||
|
) -> str:
|
||||||
|
# 准备上下文
|
||||||
|
conversation_text = "\n".join(
|
||||||
|
[
|
||||||
|
f"{msg['role'].upper()}: {msg['content']}"
|
||||||
|
for msg in messages[-20:] # 分析最近 20 条消息上下文
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Existing Rules (现有规则):
|
||||||
|
{current_rules if current_rules else "无"}
|
||||||
|
|
||||||
|
Conversation Excerpt (对话片段):
|
||||||
|
{conversation_text}
|
||||||
|
|
||||||
|
Please output the updated Project Rules (请输出更新后的项目规则):
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": self.valves.MODEL_ID,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": SYSTEM_PROMPT_RULE_GENERATOR},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
"stream": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 需要用户对象进行权限检查
|
||||||
|
user = Users.get_user_by_id(user_id)
|
||||||
|
if not user:
|
||||||
|
return current_rules
|
||||||
|
|
||||||
|
completion = await generate_chat_completion(__request__, payload, user)
|
||||||
|
if "choices" in completion and len(completion["choices"]) > 0:
|
||||||
|
content = completion["choices"][0]["message"]["content"].strip()
|
||||||
|
# 简单验证:确保看起来像个列表
|
||||||
|
if (
|
||||||
|
content.startswith("-")
|
||||||
|
or content.startswith("*")
|
||||||
|
or content.startswith("1.")
|
||||||
|
):
|
||||||
|
return content
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"规则生成失败: {e}")
|
||||||
|
|
||||||
|
return current_rules
|
||||||
|
|
||||||
|
async def _process_rules_update(
|
||||||
|
self,
|
||||||
|
folder_id: str,
|
||||||
|
body: dict,
|
||||||
|
user_id: str,
|
||||||
|
__request__: Request,
|
||||||
|
__event_emitter__,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"开始处理",
|
||||||
|
{"step": "start", "initial_folder_id": folder_id, "user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1. 获取文件夹数据 (ORM)
|
||||||
|
initial_folder = Folders.get_folder_by_id_and_user_id(folder_id, user_id)
|
||||||
|
if not initial_folder:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"错误:未找到初始文件夹",
|
||||||
|
{
|
||||||
|
"step": "fetch_initial_folder",
|
||||||
|
"initial_folder_id": folder_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 处理子文件夹逻辑:决定是更新当前文件夹还是根文件夹
|
||||||
|
target_folder = initial_folder
|
||||||
|
if self.valves.UPDATE_ROOT_FOLDER:
|
||||||
|
# 向上遍历直到找到没有 parent_id 的根文件夹
|
||||||
|
while target_folder and getattr(target_folder, "parent_id", None):
|
||||||
|
try:
|
||||||
|
parent = Folders.get_folder_by_id_and_user_id(
|
||||||
|
target_folder.parent_id, user_id
|
||||||
|
)
|
||||||
|
if parent:
|
||||||
|
target_folder = parent
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"警告:向上查找父文件夹失败",
|
||||||
|
{"step": "traverse_root", "error": str(e)},
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
target_folder_id = target_folder.id
|
||||||
|
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"定目标文件夹",
|
||||||
|
{
|
||||||
|
"step": "target_resolved",
|
||||||
|
"target_folder_id": target_folder_id,
|
||||||
|
"target_folder_name": target_folder.name,
|
||||||
|
"is_root_update": target_folder_id != folder_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_data = target_folder.data if target_folder.data else {}
|
||||||
|
existing_sys_prompt = existing_data.get("system_prompt", "")
|
||||||
|
|
||||||
|
# 2. 提取现有规则
|
||||||
|
current_rules_content = self._extract_existing_rules(existing_sys_prompt)
|
||||||
|
|
||||||
|
# 3. 生成新规则
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__, "正在分析项目规则...", done=False
|
||||||
|
)
|
||||||
|
|
||||||
|
messages = body.get("messages", [])
|
||||||
|
new_rules_content = await self._generate_new_rules(
|
||||||
|
current_rules_content, messages, user_id, __request__
|
||||||
|
)
|
||||||
|
|
||||||
|
rules_changed = new_rules_content != current_rules_content
|
||||||
|
|
||||||
|
# 如果生成结果无变更
|
||||||
|
if not rules_changed:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"无变更",
|
||||||
|
{
|
||||||
|
"step": "check_changes",
|
||||||
|
"reason": "content_identical_or_generation_failed",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__, "规则分析完成:无新增内容。", done=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 5. 注入规则到 System Prompt
|
||||||
|
updated_sys_prompt = existing_sys_prompt
|
||||||
|
if rules_changed:
|
||||||
|
updated_sys_prompt = self._inject_rules(
|
||||||
|
updated_sys_prompt,
|
||||||
|
new_rules_content,
|
||||||
|
self.valves.RULES_BLOCK_TITLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"准备更新数据库",
|
||||||
|
{"step": "pre_db_update", "target_folder_id": target_folder_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. 更新文件夹 (ORM) - 仅更新 'data' 字段
|
||||||
|
existing_data["system_prompt"] = updated_sys_prompt
|
||||||
|
|
||||||
|
updated_folder = Folders.update_folder_by_id_and_user_id(
|
||||||
|
target_folder_id,
|
||||||
|
user_id,
|
||||||
|
FolderUpdateForm(data=existing_data),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not updated_folder:
|
||||||
|
raise Exception("Update folder failed (ORM returned None)")
|
||||||
|
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__, "规则分析完成:规则已更新。", done=True
|
||||||
|
)
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"规则生成过程和变更详情",
|
||||||
|
{
|
||||||
|
"step": "success",
|
||||||
|
"folder_id": target_folder_id,
|
||||||
|
"target_is_root": target_folder_id != folder_id,
|
||||||
|
"model_used": self.valves.MODEL_ID,
|
||||||
|
"analyzed_messages_count": len(messages),
|
||||||
|
"old_rules_length": len(current_rules_content),
|
||||||
|
"new_rules_length": len(new_rules_content),
|
||||||
|
"changes_digest": {
|
||||||
|
"old_rules_preview": (
|
||||||
|
current_rules_content[:100] + "..."
|
||||||
|
if current_rules_content
|
||||||
|
else "None"
|
||||||
|
),
|
||||||
|
"new_rules_preview": (
|
||||||
|
new_rules_content[:100] + "..."
|
||||||
|
if new_rules_content
|
||||||
|
else "None"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"异步规则处理错误: {e}")
|
||||||
|
await self._emit_status(__event_emitter__, "更新规则失败。", done=True)
|
||||||
|
# 在控制台也输出错误信息,方便调试
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__, "执行出错", {"error": str(e), "folder_id": folder_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== Filter Hooks ====================
|
||||||
|
|
||||||
|
async def inlet(
|
||||||
|
self, body: dict, __user__: Optional[dict] = None, __event_emitter__=None
|
||||||
|
) -> dict:
|
||||||
|
return body
|
||||||
|
|
||||||
|
async def outlet(
|
||||||
|
self,
|
||||||
|
body: dict,
|
||||||
|
__user__: Optional[dict] = None,
|
||||||
|
__event_emitter__=None,
|
||||||
|
__request__: Optional[Request] = None,
|
||||||
|
) -> dict:
|
||||||
|
user_ctx = self._get_user_context(__user__)
|
||||||
|
chat_ctx = self._get_chat_context(body)
|
||||||
|
|
||||||
|
messages = body.get("messages", [])
|
||||||
|
if not messages:
|
||||||
|
return body
|
||||||
|
|
||||||
|
# 触发逻辑:消息计数阈值
|
||||||
|
if len(messages) % self.valves.MESSAGE_TRIGGER_COUNT != 0:
|
||||||
|
return body
|
||||||
|
|
||||||
|
folder_id = self._get_folder_id(body)
|
||||||
|
if not folder_id:
|
||||||
|
await self._emit_debug_log(
|
||||||
|
__event_emitter__,
|
||||||
|
"跳过分析",
|
||||||
|
{"reason": "对话不属于任何文件夹", "chat_id": chat_ctx.get("chat_id")},
|
||||||
|
)
|
||||||
|
return body
|
||||||
|
|
||||||
|
# 用户信息
|
||||||
|
user_id = user_ctx.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
return body
|
||||||
|
|
||||||
|
# 异步任务
|
||||||
|
if self.valves.MODEL_ID == "":
|
||||||
|
self.valves.MODEL_ID = body.get("model", "")
|
||||||
|
|
||||||
|
asyncio.create_task(
|
||||||
|
self._process_rules_update(
|
||||||
|
folder_id, body, user_id, __request__, __event_emitter__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return body
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
# Markdown Normalizer Filter
|
# Markdown Normalizer Filter
|
||||||
|
|
||||||
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.2.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **Version:** 1.2.4 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **License:** MIT
|
||||||
|
|
||||||
A content normalizer filter for Open WebUI that fixes common Markdown formatting issues in LLM outputs. It ensures that code blocks, LaTeX formulas, Mermaid diagrams, and other Markdown elements are rendered correctly.
|
A content normalizer filter for Open WebUI that fixes common Markdown formatting issues in LLM outputs. It ensures that code blocks, LaTeX formulas, Mermaid diagrams, and other Markdown elements are rendered correctly.
|
||||||
|
|
||||||
## Features
|
## 🔥 What's New in v1.2.4
|
||||||
|
|
||||||
|
* **Documentation Sync**: Synchronized version numbers across all documentation and code files.
|
||||||
|
|
||||||
|
## ✨ Core Features
|
||||||
|
|
||||||
* **Details Tag Normalization**: Ensures proper spacing for `<details>` tags (used for thought chains). Adds a blank line after `</details>` and ensures a newline after self-closing `<details />` tags to prevent rendering issues.
|
* **Details Tag Normalization**: Ensures proper spacing for `<details>` tags (used for thought chains). Adds a blank line after `</details>` and ensures a newline after self-closing `<details />` tags to prevent rendering issues.
|
||||||
|
* **Emphasis Spacing Fix**: Fixes extra spaces inside emphasis markers (e.g., `** text **` -> `**text**`) which can cause rendering failures. Includes safeguards to protect math expressions (e.g., `2 * 3 * 4`) and list variables.
|
||||||
* **Mermaid Syntax Fix**: Automatically fixes common Mermaid syntax errors, such as unquoted node labels (including multi-line labels and citations) and unclosed subgraphs. **New in v1.1.2**: Comprehensive protection for edge labels (text on connecting lines) across all link types (solid, dotted, thick).
|
* **Mermaid Syntax Fix**: Automatically fixes common Mermaid syntax errors, such as unquoted node labels (including multi-line labels and citations) and unclosed subgraphs. **New in v1.1.2**: Comprehensive protection for edge labels (text on connecting lines) across all link types (solid, dotted, thick).
|
||||||
* **Frontend Console Debugging**: Supports printing structured debug logs directly to the browser console (F12) for easier troubleshooting.
|
* **Frontend Console Debugging**: Supports printing structured debug logs directly to the browser console (F12) for easier troubleshooting.
|
||||||
* **Code Block Formatting**: Fixes broken code block prefixes, suffixes, and indentation.
|
* **Code Block Formatting**: Fixes broken code block prefixes, suffixes, and indentation.
|
||||||
@@ -18,7 +23,7 @@ A content normalizer filter for Open WebUI that fixes common Markdown formatting
|
|||||||
* **Table Fix**: Adds missing closing pipes in tables.
|
* **Table Fix**: Adds missing closing pipes in tables.
|
||||||
* **XML Cleanup**: Removes leftover XML artifacts.
|
* **XML Cleanup**: Removes leftover XML artifacts.
|
||||||
|
|
||||||
## Usage
|
## How to Use 🛠️
|
||||||
|
|
||||||
1. Install the plugin in Open WebUI.
|
1. Install the plugin in Open WebUI.
|
||||||
2. Enable the filter globally or for specific models.
|
2. Enable the filter globally or for specific models.
|
||||||
@@ -27,43 +32,38 @@ A content normalizer filter for Open WebUI that fixes common Markdown formatting
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> As this is an initial version, some "negative fixes" might occur (e.g., breaking valid Markdown). If you encounter issues, please check the console logs, copy the "Original" vs "Normalized" content, and submit an issue.
|
> As this is an initial version, some "negative fixes" might occur (e.g., breaking valid Markdown). If you encounter issues, please check the console logs, copy the "Original" vs "Normalized" content, and submit an issue.
|
||||||
|
|
||||||
## Configuration (Valves)
|
## Configuration (Valves) ⚙️
|
||||||
|
|
||||||
* `priority`: Filter priority (default: 50).
|
| Parameter | Default | Description |
|
||||||
* `enable_escape_fix`: Fix excessive escape characters.
|
| :--- | :--- | :--- |
|
||||||
* `enable_thought_tag_fix`: Normalize thought tags.
|
| `priority` | `50` | Filter priority. Higher runs later (recommended after other filters). |
|
||||||
* `enable_details_tag_fix`: Normalize details tags (default: True).
|
| `enable_escape_fix` | `True` | Fix excessive escape characters (`\n`, `\t`, etc.). |
|
||||||
* `enable_code_block_fix`: Fix code block formatting.
|
| `enable_escape_fix_in_code_blocks` | `False` | Apply escape fix inside code blocks (may affect valid code). |
|
||||||
* `enable_latex_fix`: Normalize LaTeX formulas.
|
| `enable_thought_tag_fix` | `True` | Normalize thought tags (`</thought>`). |
|
||||||
* `enable_list_fix`: Fix list item newlines (Experimental).
|
| `enable_details_tag_fix` | `True` | Normalize `<details>` tags and add safe spacing. |
|
||||||
* `enable_unclosed_block_fix`: Auto-close unclosed code blocks.
|
| `enable_code_block_fix` | `True` | Fix code block formatting (indentation/newlines). |
|
||||||
* `enable_fullwidth_symbol_fix`: Fix full-width symbols in code blocks.
|
| `enable_latex_fix` | `True` | Normalize LaTeX delimiters (`\[` -> `$$`, `\(` -> `$`). |
|
||||||
* `enable_mermaid_fix`: Fix Mermaid syntax errors.
|
| `enable_list_fix` | `False` | Fix list item newlines (experimental). |
|
||||||
* `enable_heading_fix`: Fix missing space in headings.
|
| `enable_unclosed_block_fix` | `True` | Auto-close unclosed code blocks. |
|
||||||
* `enable_table_fix`: Fix missing closing pipe in tables.
|
| `enable_fullwidth_symbol_fix` | `False` | Fix full-width symbols in code blocks. |
|
||||||
* `enable_xml_tag_cleanup`: Cleanup leftover XML tags.
|
| `enable_mermaid_fix` | `True` | Fix common Mermaid syntax errors. |
|
||||||
* `show_status`: Show status notification when fixes are applied.
|
| `enable_heading_fix` | `True` | Fix missing space in headings. |
|
||||||
* `show_debug_log`: Print debug logs to browser console.
|
| `enable_table_fix` | `True` | Fix missing closing pipe in tables. |
|
||||||
|
| `enable_xml_tag_cleanup` | `True` | Cleanup leftover XML tags. |
|
||||||
|
| `enable_emphasis_spacing_fix` | `False` | Fix extra spaces in emphasis. |
|
||||||
|
| `show_status` | `True` | Show status notification when fixes are applied. |
|
||||||
|
| `show_debug_log` | `True` | Print debug logs to browser console (F12). |
|
||||||
|
|
||||||
## Troubleshooting ❓
|
## ⭐ Support
|
||||||
|
|
||||||
- **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
If this plugin has been useful, a star on [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) is a big motivation for me. Thank you for the support.
|
||||||
|
|
||||||
## Changelog
|
## 🧩 Others
|
||||||
|
|
||||||
### v1.2.0
|
### Troubleshooting ❓
|
||||||
* **Details Tag Support**: Added normalization for `<details>` tags.
|
|
||||||
* Ensures a blank line is added after `</details>` closing tags to separate thought content from the main response.
|
|
||||||
* Ensures a newline is added after self-closing `<details ... />` tags to prevent them from interfering with subsequent Markdown headings (e.g., fixing `<details/>#Heading`).
|
|
||||||
* Includes safeguard to prevent modification of `<details>` tags inside code blocks.
|
|
||||||
|
|
||||||
### v1.1.2
|
* **Submit an Issue**: If you encounter any problems, please submit an issue on GitHub: [Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
* **Mermaid Edge Label Protection**: Implemented comprehensive protection for edge labels (text on connecting lines) to prevent them from being incorrectly modified. Now supports all Mermaid link types including solid (`--`), dotted (`-.`), and thick (`==`) lines with or without arrows.
|
|
||||||
* **Bug Fixes**: Fixed an issue where lines without arrows (e.g., `A -- text --- B`) were not correctly protected.
|
|
||||||
|
|
||||||
### v1.1.0
|
### Changelog
|
||||||
* **Mermaid Fix Refinement**: Improved regex to handle nested parentheses in node labels (e.g., `ID("Label (text)")`) and avoided matching connection labels.
|
|
||||||
* **HTML Safeguard Optimization**: Refined `_contains_html` to allow common tags like `<br/>`, `<b>`, `<i>`, etc., ensuring Mermaid diagrams with these tags are still normalized.
|
|
||||||
* **Full-width Symbol Cleanup**: Fixed duplicate keys and incorrect quote mapping in `FULLWIDTH_MAP`.
|
|
||||||
* **Bug Fixes**: Fixed missing `Dict` import in Python files.
|
|
||||||
|
|
||||||
|
See the full history on GitHub: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
# Markdown 格式化过滤器 (Markdown Normalizer)
|
# Markdown 格式化过滤器 (Markdown Normalizer)
|
||||||
|
|
||||||
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.2.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie/awesome-openwebui) | **版本:** 1.2.4 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) | **许可证:** MIT
|
||||||
|
|
||||||
这是一个用于 Open WebUI 的内容格式化过滤器,旨在修复 LLM 输出中常见的 Markdown 格式问题。它能确保代码块、LaTeX 公式、Mermaid 图表和其他 Markdown 元素被正确渲染。
|
这是一个用于 Open WebUI 的内容格式化过滤器,旨在修复 LLM 输出中常见的 Markdown 格式问题。它能确保代码块、LaTeX 公式、Mermaid 图表和其他 Markdown 元素被正确渲染。
|
||||||
|
|
||||||
## 功能特性
|
## 🔥 最新更新 v1.2.4
|
||||||
|
|
||||||
|
* **文档更新**: 同步了所有文档和代码文件的版本号。
|
||||||
|
|
||||||
|
## ✨ 核心特性
|
||||||
|
|
||||||
* **Details 标签规范化**: 确保 `<details>` 标签(常用于思维链)有正确的间距。在 `</details>` 后添加空行,并在自闭合 `<details />` 标签后添加换行,防止渲染问题。
|
* **Details 标签规范化**: 确保 `<details>` 标签(常用于思维链)有正确的间距。在 `</details>` 后添加空行,并在自闭合 `<details />` 标签后添加换行,防止渲染问题。
|
||||||
|
* **强调空格修复**: 修复强调标记内部的多余空格(例如 `** 文本 **` -> `**文本**`),这会导致 Markdown 渲染失败。包含保护机制,防止误修改数学表达式(如 `2 * 3 * 4`)或列表变量。
|
||||||
* **Mermaid 语法修复**: 自动修复常见的 Mermaid 语法错误,如未加引号的节点标签(支持多行标签和引用标记)和未闭合的子图 (Subgraph)。**v1.1.2 新增**: 全面保护各种类型的连线标签(实线、虚线、粗线),防止被误修改。
|
* **Mermaid 语法修复**: 自动修复常见的 Mermaid 语法错误,如未加引号的节点标签(支持多行标签和引用标记)和未闭合的子图 (Subgraph)。**v1.1.2 新增**: 全面保护各种类型的连线标签(实线、虚线、粗线),防止被误修改。
|
||||||
* **前端控制台调试**: 支持将结构化的调试日志直接打印到浏览器控制台 (F12),方便排查问题。
|
* **前端控制台调试**: 支持将结构化的调试日志直接打印到浏览器控制台 (F12),方便排查问题。
|
||||||
* **代码块格式化**: 修复破损的代码块前缀、后缀和缩进问题。
|
* **代码块格式化**: 修复破损的代码块前缀、后缀和缩进问题。
|
||||||
@@ -27,43 +32,38 @@
|
|||||||
> [!WARNING]
|
> [!WARNING]
|
||||||
> 由于这是初版,可能会出现“负向修复”的情况(例如破坏了原本正确的格式)。如果您遇到问题,请务必查看控制台日志,复制“原始 (Original)”与“规范化 (Normalized)”的内容对比,并提交 Issue 反馈。
|
> 由于这是初版,可能会出现“负向修复”的情况(例如破坏了原本正确的格式)。如果您遇到问题,请务必查看控制台日志,复制“原始 (Original)”与“规范化 (Normalized)”的内容对比,并提交 Issue 反馈。
|
||||||
|
|
||||||
## 配置项 (Valves)
|
## 配置参数 (Valves) ⚙️
|
||||||
|
|
||||||
* `priority`: 过滤器优先级 (默认: 50)。
|
| 参数 | 默认值 | 描述 |
|
||||||
* `enable_escape_fix`: 修复过度的转义字符。
|
| :--- | :--- | :--- |
|
||||||
* `enable_thought_tag_fix`: 规范化思维标签。
|
| `priority` | `50` | 过滤器优先级。数值越大越靠后(建议在其他过滤器之后运行)。 |
|
||||||
* `enable_details_tag_fix`: 规范化 Details 标签 (默认: True)。
|
| `enable_escape_fix` | `True` | 修复过度的转义字符(`\n`, `\t` 等)。 |
|
||||||
* `enable_code_block_fix`: 修复代码块格式。
|
| `enable_escape_fix_in_code_blocks` | `False` | 在代码块内应用转义修复(可能影响有效代码)。 |
|
||||||
* `enable_latex_fix`: 规范化 LaTeX 公式。
|
| `enable_thought_tag_fix` | `True` | 规范化思维标签(`</thought>`)。 |
|
||||||
* `enable_list_fix`: 修复列表项换行 (实验性)。
|
| `enable_details_tag_fix` | `True` | 规范化 `<details>` 标签并添加安全间距。 |
|
||||||
* `enable_unclosed_block_fix`: 自动闭合未闭合的代码块。
|
| `enable_code_block_fix` | `True` | 修复代码块格式(缩进/换行)。 |
|
||||||
* `enable_fullwidth_symbol_fix`: 修复代码块中的全角符号。
|
| `enable_latex_fix` | `True` | 规范化 LaTeX 定界符(`\[` -> `$$`, `\(` -> `$`)。 |
|
||||||
* `enable_mermaid_fix`: 修复 Mermaid 语法错误。
|
| `enable_list_fix` | `False` | 修复列表项换行(实验性)。 |
|
||||||
* `enable_heading_fix`: 修复标题中缺失的空格。
|
| `enable_unclosed_block_fix` | `True` | 自动闭合未闭合的代码块。 |
|
||||||
* `enable_table_fix`: 修复表格中缺失的闭合管道符。
|
| `enable_fullwidth_symbol_fix` | `False` | 修复代码块中的全角符号。 |
|
||||||
* `enable_xml_tag_cleanup`: 清理残留的 XML 标签。
|
| `enable_mermaid_fix` | `True` | 修复常见 Mermaid 语法错误。 |
|
||||||
* `show_status`: 应用修复时显示状态通知。
|
| `enable_heading_fix` | `True` | 修复标题中缺失的空格。 |
|
||||||
* `show_debug_log`: 在浏览器控制台打印调试日志。
|
| `enable_table_fix` | `True` | 修复表格中缺失的闭合管道符。 |
|
||||||
|
| `enable_xml_tag_cleanup` | `True` | 清理残留的 XML 标签。 |
|
||||||
|
| `enable_emphasis_spacing_fix` | `False` | 修复强调语法中的多余空格。 |
|
||||||
|
| `show_status` | `True` | 应用修复时显示状态通知。 |
|
||||||
|
| `show_debug_log` | `True` | 在浏览器控制台打印调试日志。 |
|
||||||
|
|
||||||
## 故障排除 (Troubleshooting) ❓
|
## ⭐ 支持
|
||||||
|
|
||||||
- **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
如果这个插件对你有帮助,欢迎到 [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 点个 Star,这将是我持续改进的动力,感谢支持。
|
||||||
|
|
||||||
## 更新日志
|
## 其他
|
||||||
|
|
||||||
### v1.2.0
|
### 故障排除 (Troubleshooting) ❓
|
||||||
* **Details 标签支持**: 新增了对 `<details>` 标签的规范化支持。
|
|
||||||
* 确保在 `</details>` 闭合标签后添加空行,将思维内容与正文分隔开。
|
|
||||||
* 确保在自闭合 `<details ... />` 标签后添加换行,防止其干扰后续的 Markdown 标题(例如修复 `<details/>#标题`)。
|
|
||||||
* 包含保护机制,防止修改代码块内部的 `<details>` 标签。
|
|
||||||
|
|
||||||
### v1.1.2
|
* **提交 Issue**: 如果遇到任何问题,请在 GitHub 上提交 Issue:[Awesome OpenWebUI Issues](https://github.com/Fu-Jie/awesome-openwebui/issues)
|
||||||
* **Mermaid 连线标签保护**: 实现了全面的连线标签保护机制,防止连接线上的文字被误修改。现在支持所有 Mermaid 连线类型,包括实线 (`--`)、虚线 (`-.`) 和粗线 (`==`),无论是否带有箭头。
|
|
||||||
* **Bug 修复**: 修复了无箭头连线(如 `A -- text --- B`)未被正确保护的问题。
|
|
||||||
|
|
||||||
### v1.1.0
|
### 更新日志
|
||||||
* **Mermaid 修复优化**: 改进了正则表达式以处理节点标签中的嵌套括号(如 `ID("标签 (文本)")`),并避免误匹配连接线上的文字。
|
|
||||||
* **HTML 保护机制优化**: 优化了 `_contains_html` 检测,允许 `<br/>`, `<b>`, `<i>` 等常见标签,确保包含这些标签的 Mermaid 图表能被正常规范化。
|
|
||||||
* **全角符号清理**: 修复了 `FULLWIDTH_MAP` 中的重复键名和错误的引号映射。
|
|
||||||
* **Bug 修复**: 修复了 Python 文件中缺失的 `Dict` 类型导入。
|
|
||||||
|
|
||||||
|
完整历史请查看 GitHub 项目: [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: Markdown Normalizer
|
|||||||
author: Fu-Jie
|
author: Fu-Jie
|
||||||
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
author_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
funding_url: https://github.com/open-webui
|
funding_url: https://github.com/open-webui
|
||||||
version: 1.2.0
|
version: 1.2.4
|
||||||
openwebui_id: baaa8732-9348-40b7-8359-7e009660e23c
|
openwebui_id: baaa8732-9348-40b7-8359-7e009660e23c
|
||||||
description: A content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting.
|
description: A content normalizer filter that fixes common Markdown formatting issues in LLM outputs, such as broken code blocks, LaTeX formulas, and list formatting.
|
||||||
"""
|
"""
|
||||||
@@ -43,6 +43,7 @@ class NormalizerConfig:
|
|||||||
)
|
)
|
||||||
enable_table_fix: bool = True # Fix missing closing pipe in tables
|
enable_table_fix: bool = True # Fix missing closing pipe in tables
|
||||||
enable_xml_tag_cleanup: bool = True # Cleanup leftover XML tags
|
enable_xml_tag_cleanup: bool = True # Cleanup leftover XML tags
|
||||||
|
enable_emphasis_spacing_fix: bool = False # Fix spaces inside **emphasis**
|
||||||
|
|
||||||
# Custom cleaner functions (for advanced extension)
|
# Custom cleaner functions (for advanced extension)
|
||||||
custom_cleaners: List[Callable[[str], str]] = field(default_factory=list)
|
custom_cleaners: List[Callable[[str], str]] = field(default_factory=list)
|
||||||
@@ -53,8 +54,8 @@ class ContentNormalizer:
|
|||||||
|
|
||||||
# --- 1. Pre-compiled Regex Patterns (Performance Optimization) ---
|
# --- 1. Pre-compiled Regex Patterns (Performance Optimization) ---
|
||||||
_PATTERNS = {
|
_PATTERNS = {
|
||||||
# Code block prefix: if ``` is not at start of line or file
|
# Code block prefix: if ``` is not at start of line (ignoring whitespace)
|
||||||
"code_block_prefix": re.compile(r"(?<!^)(?<!\n)(```)", re.MULTILINE),
|
"code_block_prefix": re.compile(r"(\S[ \t]*)(```)"),
|
||||||
# Code block suffix: ```lang followed by non-whitespace (no newline)
|
# Code block suffix: ```lang followed by non-whitespace (no newline)
|
||||||
"code_block_suffix": re.compile(r"(```[\w\+\-\.]*)[ \t]+([^\n\r])"),
|
"code_block_suffix": re.compile(r"(```[\w\+\-\.]*)[ \t]+([^\n\r])"),
|
||||||
# Code block indent: whitespace at start of line + ```
|
# Code block indent: whitespace at start of line + ```
|
||||||
@@ -108,6 +109,14 @@ class ContentNormalizer:
|
|||||||
"heading_space": re.compile(r"^(#+)([^ \n#])", re.MULTILINE),
|
"heading_space": re.compile(r"^(#+)([^ \n#])", re.MULTILINE),
|
||||||
# Table: | col1 | col2 -> | col1 | col2 |
|
# Table: | col1 | col2 -> | col1 | col2 |
|
||||||
"table_pipe": re.compile(r"^(\|.*[^|\n])$", re.MULTILINE),
|
"table_pipe": re.compile(r"^(\|.*[^|\n])$", re.MULTILINE),
|
||||||
|
# Emphasis spacing: ** text ** -> **text**, __ text __ -> __text__
|
||||||
|
# Matches emphasis blocks within a single line. We use a recursive approach
|
||||||
|
# in _fix_emphasis_spacing to handle nesting and spaces correctly.
|
||||||
|
# NOTE: We use [^\n] instead of . to prevent cross-line matching.
|
||||||
|
# Supports: * (italic), ** (bold), *** (bold+italic), _ (italic), __ (bold), ___ (bold+italic)
|
||||||
|
"emphasis_spacing": re.compile(
|
||||||
|
r"(?<!\*|_)(\*{1,3}|_{1,3})(?P<inner>[^\n]*?)(\1)(?!\*|_)"
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, config: Optional[NormalizerConfig] = None):
|
def __init__(self, config: Optional[NormalizerConfig] = None):
|
||||||
@@ -207,6 +216,13 @@ class ContentNormalizer:
|
|||||||
if content != original:
|
if content != original:
|
||||||
self.applied_fixes.append("Cleanup XML Tags")
|
self.applied_fixes.append("Cleanup XML Tags")
|
||||||
|
|
||||||
|
# 12. Emphasis spacing fix
|
||||||
|
if self.config.enable_emphasis_spacing_fix:
|
||||||
|
original = content
|
||||||
|
content = self._fix_emphasis_spacing(content)
|
||||||
|
if content != original:
|
||||||
|
self.applied_fixes.append("Fix Emphasis Spacing")
|
||||||
|
|
||||||
# 9. Custom cleaners
|
# 9. Custom cleaners
|
||||||
for cleaner in self.config.custom_cleaners:
|
for cleaner in self.config.custom_cleaners:
|
||||||
original = content
|
original = content
|
||||||
@@ -283,8 +299,6 @@ class ContentNormalizer:
|
|||||||
|
|
||||||
def _fix_code_blocks(self, content: str) -> str:
|
def _fix_code_blocks(self, content: str) -> str:
|
||||||
"""Fix code block formatting (prefixes, suffixes, indentation)"""
|
"""Fix code block formatting (prefixes, suffixes, indentation)"""
|
||||||
# Remove indentation before code blocks
|
|
||||||
content = self._PATTERNS["code_block_indent"].sub(r"\1", content)
|
|
||||||
# Ensure newline before ```
|
# Ensure newline before ```
|
||||||
content = self._PATTERNS["code_block_prefix"].sub(r"\n\1", content)
|
content = self._PATTERNS["code_block_prefix"].sub(r"\n\1", content)
|
||||||
# Ensure newline after ```lang
|
# Ensure newline after ```lang
|
||||||
@@ -443,6 +457,61 @@ class ContentNormalizer:
|
|||||||
"""Remove leftover XML tags"""
|
"""Remove leftover XML tags"""
|
||||||
return self._PATTERNS["xml_artifacts"].sub("", content)
|
return self._PATTERNS["xml_artifacts"].sub("", content)
|
||||||
|
|
||||||
|
def _fix_emphasis_spacing(self, content: str) -> str:
|
||||||
|
"""Fix spaces inside **emphasis** or _emphasis_
|
||||||
|
Example: ** text ** -> **text**, **text ** -> **text**, ** text** -> **text**
|
||||||
|
"""
|
||||||
|
|
||||||
|
def replacer(match):
|
||||||
|
symbol = match.group(1)
|
||||||
|
inner = match.group("inner")
|
||||||
|
|
||||||
|
# Recursive step: Fix emphasis spacing INSIDE the current block first
|
||||||
|
# This ensures that ** _ italic _ ** becomes ** _italic_ ** before we strip outer spaces.
|
||||||
|
inner = self._PATTERNS["emphasis_spacing"].sub(replacer, inner)
|
||||||
|
|
||||||
|
# If no leading/trailing whitespace, nothing to fix at this level
|
||||||
|
stripped_inner = inner.strip()
|
||||||
|
if stripped_inner == inner:
|
||||||
|
return f"{symbol}{inner}{symbol}"
|
||||||
|
|
||||||
|
# Safeguard: If inner content is just whitespace, don't touch it
|
||||||
|
if not stripped_inner:
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
# Safeguard: If it looks like a math expression or list of variables (e.g. " * 3 * " or " _ b _ ")
|
||||||
|
# If the symbol is surrounded by spaces in the original text, it's likely an operator.
|
||||||
|
if inner.startswith(" ") and inner.endswith(" "):
|
||||||
|
# If it's single '*' or '_', and both sides have spaces, it's almost certainly an operator.
|
||||||
|
if symbol in ["*", "_"]:
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
# Safeguard: List marker protection
|
||||||
|
# If symbol is single '*' and inner content starts with whitespace followed by emphasis markers,
|
||||||
|
# this is likely a list item like "* **bold**" - don't merge them.
|
||||||
|
# Pattern: "* **text**" should NOT become "***text**"
|
||||||
|
if symbol == "*" and inner.lstrip().startswith(("*", "_")):
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
# Extended list marker protection:
|
||||||
|
# If symbol is single '*' and inner starts with multiple spaces (list indentation pattern),
|
||||||
|
# this is likely a list item like "* text" - don't strip the spaces.
|
||||||
|
# Pattern: "* U16 forward **Kuang**" should NOT become "*U16 forward **Kuang**"
|
||||||
|
if symbol == "*" and inner.startswith(" "):
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
return f"{symbol}{stripped_inner}{symbol}"
|
||||||
|
|
||||||
|
parts = content.split("```")
|
||||||
|
for i in range(0, len(parts), 2): # Even indices are markdown text
|
||||||
|
# We use a while loop to handle overlapping or multiple occurrences at the top level
|
||||||
|
while True:
|
||||||
|
new_part = self._PATTERNS["emphasis_spacing"].sub(replacer, parts[i])
|
||||||
|
if new_part == parts[i]:
|
||||||
|
break
|
||||||
|
parts[i] = new_part
|
||||||
|
return "```".join(parts)
|
||||||
|
|
||||||
|
|
||||||
class Filter:
|
class Filter:
|
||||||
class Valves(BaseModel):
|
class Valves(BaseModel):
|
||||||
@@ -494,6 +563,10 @@ class Filter:
|
|||||||
enable_xml_tag_cleanup: bool = Field(
|
enable_xml_tag_cleanup: bool = Field(
|
||||||
default=True, description="Cleanup leftover XML tags"
|
default=True, description="Cleanup leftover XML tags"
|
||||||
)
|
)
|
||||||
|
enable_emphasis_spacing_fix: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Fix spaces inside **emphasis** (e.g. ** text ** -> **text**)",
|
||||||
|
)
|
||||||
show_status: bool = Field(
|
show_status: bool = Field(
|
||||||
default=True, description="Show status notification when fixes are applied"
|
default=True, description="Show status notification when fixes are applied"
|
||||||
)
|
)
|
||||||
@@ -622,6 +695,15 @@ class Filter:
|
|||||||
if self._contains_html(content):
|
if self._contains_html(content):
|
||||||
return body
|
return body
|
||||||
|
|
||||||
|
# Skip if content contains tool output markers (native function calling)
|
||||||
|
# Pattern: """...""" or tool_call_id or <details type="tool_calls"...>
|
||||||
|
if (
|
||||||
|
'"""' in content
|
||||||
|
or "tool_call_id" in content
|
||||||
|
or '<details type="tool_calls"' in content
|
||||||
|
):
|
||||||
|
return body
|
||||||
|
|
||||||
# Configure normalizer based on valves
|
# Configure normalizer based on valves
|
||||||
config = NormalizerConfig(
|
config = NormalizerConfig(
|
||||||
enable_escape_fix=self.valves.enable_escape_fix,
|
enable_escape_fix=self.valves.enable_escape_fix,
|
||||||
@@ -637,6 +719,7 @@ class Filter:
|
|||||||
enable_heading_fix=self.valves.enable_heading_fix,
|
enable_heading_fix=self.valves.enable_heading_fix,
|
||||||
enable_table_fix=self.valves.enable_table_fix,
|
enable_table_fix=self.valves.enable_table_fix,
|
||||||
enable_xml_tag_cleanup=self.valves.enable_xml_tag_cleanup,
|
enable_xml_tag_cleanup=self.valves.enable_xml_tag_cleanup,
|
||||||
|
enable_emphasis_spacing_fix=self.valves.enable_emphasis_spacing_fix,
|
||||||
)
|
)
|
||||||
|
|
||||||
normalizer = ContentNormalizer(config)
|
normalizer = ContentNormalizer(config)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user