diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 5587a06..82b531f 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -478,7 +478,117 @@ async def get_user_language(self):
**注意**: 即使插件有 `Valves` 配置,也应优先尝试自动探测,提升用户体验。
-### 8. 智能代理文件交付规范 (Agent File Delivery Standards)
+### 8. 国际化 (i18n) 适配规范 (Internationalization Standards)
+
+开发供全球用户使用的插件时,必须预置多语言支持(如中文、英文等)。
+
+#### i18n 字典定义
+
+在文件顶部定义 `TRANSLATIONS` 字典存储多语言字符串:
+
+```python
+TRANSLATIONS = {
+ "en-US": {
+ "status_starting": "Smart Mind Map is starting...",
+ },
+ "zh-CN": {
+ "status_starting": "智能思维导图正在启动...",
+ },
+ # ... 其他语言
+}
+
+# 语言回退映射 (Fallback Map)
+FALLBACK_MAP = {
+ "zh": "zh-CN",
+ "zh-TW": "zh-CN",
+ "zh-HK": "zh-CN",
+ "en": "en-US",
+ "en-GB": "en-US"
+}
+```
+
+#### 获取当前用户真实语言 (Robust Language Detection)
+
+Open WebUI 的前端(localStorage)并未自动同步语言设置到后端数据库或通过标准 API 参数传递。为了获取精准的用户偏好语言,**必须**使用多层级回退机制(Multi-level Fallback):
+`JS 动态探测 (localStorage)` > `HTTP 浏览器头 (Accept-Language)` > `用户 Profile 默认设置` > `en-US`
+
+> **注意!防卡死指南 (Anti-Deadlock Guide)**
+> 在通过 `__event_call__` 执行前端 JS 脚本时,如果前端脚本不慎抛出异常 (`Exception`) 会导致回调函数 `cb()` 永不执行,这会让后端的 `asyncio` 永远阻塞并卡死整个请求队列!
+> **必须**做两重防护:
+> 1. JS 内部包裹 `try...catch` 保证必须有 `return`。
+> 2. 后端使用 `asyncio.wait_for` 设置强制超时(建议 2 秒)。
+
+```python
+import asyncio
+from fastapi import Request
+
+async def _get_user_context(
+ self,
+ __user__: Optional[dict],
+ __event_call__: Optional[callable] = None,
+ __request__: Optional[Request] = None,
+) -> dict:
+ user_language = __user__.get("language", "en-US") if __user__ else "en-US"
+
+ # 1st Fallback: HTTP Accept-Language header
+ if __request__ and hasattr(__request__, "headers") and "accept-language" in __request__.headers:
+ raw_lang = __request__.headers.get("accept-language", "")
+ if raw_lang:
+ user_language = raw_lang.split(",")[0].split(";")[0]
+
+ # 2nd Fallback (Best): Execute JS in frontend to read localStorage
+ if __event_call__:
+ try:
+ js_code = """
+ try {
+ return (
+ document.documentElement.lang ||
+ localStorage.getItem('locale') ||
+ navigator.language ||
+ 'en-US'
+ );
+ } catch (e) {
+ return 'en-US';
+ }
+ """
+ # 【致命!】必须设置 wait_for 防止前端无响应卡死后端
+ frontend_lang = await asyncio.wait_for(
+ __event_call__({"type": "execute", "data": {"code": js_code}}),
+ timeout=2.0
+ )
+ if frontend_lang and isinstance(frontend_lang, str):
+ user_language = frontend_lang
+ except Exception as e:
+ pass # fallback to accept-language or en-US
+
+ return {
+ "user_language": user_language,
+ # ... user_name, user_id etc.
+ }
+```
+
+#### 实际使用 (Usage in Action/Filter)
+
+在 Action 或者 Filter 执行时引用这套上下文获取机制,然后传入映射器获取最终翻译:
+
+```python
+async def action(
+ self,
+ body: dict,
+ __user__: Optional[dict] = None,
+ __event_call__: Optional[callable] = None,
+ __request__: Optional[Request] = None,
+ **kwargs
+) -> Optional[dict]:
+
+ user_ctx = await self._get_user_context(__user__, __event_call__, __request__)
+ user_lang = user_ctx["user_language"]
+
+ # 获取多语言文本 (通过你的 translation.get() 扩展)
+ # start_msg = self._get_translation(user_lang, "status_starting")
+```
+
+### 9. 智能代理文件交付规范 (Agent File Delivery Standards)
在开发具备文件生成能力的智能代理插件(如 GitHub Copilot SDK 集成)时,必须遵循以下标准流程,以确保文件在不同存储后端(本地/S3)下的可用性并绕过不必要的 RAG 处理。
@@ -498,7 +608,7 @@ async def get_user_language(self):
- 代理应始终将“当前目录”视为其受保护所在的私有工作空间。
- `publish_file_from_workspace` 的参数 `filename` 仅需传入相对于当前目录的文件名。
-### 9. Copilot SDK 插件工具定义规范 (Copilot SDK Tool Definition Standards)
+### 10. Copilot SDK 插件工具定义规范 (Copilot SDK Tool Definition Standards)
在为 GitHub Copilot SDK 开发自定义工具时,为了确保大模型能正确识别参数(避免生成空的 `properties` Schema),必须遵循以下定义模式:
@@ -532,6 +642,63 @@ my_tool = define_tool(
2. **Field 描述**: 在 `BaseModel` 中使用 `Field(..., description="...")` 为每个参数提供详细的描述信息。
3. **Required vs Optional**: 明确标注必填项(无默认值)和可选项(带 `default`)。
+### 11. Copilot SDK 流式渲染与工具卡片规范 (Streaming & Tool Card Standards)
+
+在处理大模型的思维链(Reasoning)输出和工具调用(Tool Calls)时,为了确保能完美兼容 OpenWebUI 0.8.x 前端的 Markdown 解析器及原生折叠 UI 组件,必须遵循以下极度严格的输出格式规范。
+
+#### 思维链流式渲染 (Reasoning Streaming)
+
+为了让前端能够正确显示“Thinking...”的折叠框和 Spinner 动画,**必须**使用原生的 `` 标签。
+
+- **正确的标签包裹**:
+ ```html
+
+ 这里是思考过程...
+
+ ```
+- **关键细节**:
+ - **标签闭合检测**: 必须在代码内部维护状态(如 `state["thinking_started"]`)。当(1)正文内容即将开始输出,或(2)工具调用触发 (`tool.execution_start`) 时,**必须优先输出 `\n\n` 强制闭合标签**。如果不闭合,后续的正文或工具面板会被全部吞进思考框内,导致页面完全崩坏!
+ - **不要手动拼装**: 严禁通过手动输出 `` 等大段 HTML 来模拟思考过程,这种方式极易在流式片段发送中破坏前端 DOM 树并导致错位。
+
+#### 工具调用原生卡片 (Native Tool Calls Block)
+
+为了在对话界面中生成标准、原生的下拉折叠“工具调用”卡片,当 `event_type == "tool.execution_complete"` 时,必须向队列输出如下严格格式的 HTML:
+
+```python
+# 必须转义属性中的双引号为 "
+args_for_attr = args_json_str.replace('"', """)
+result_for_attr = result_content.replace('"', """)
+
+tool_block = (
+ f'\\n\\n'
+ f"Tool Executed
\\n"
+ f" \\n\\n"
+)
+queue.put_nowait(tool_block)
+```
+
+- **致命避坑点 (Critical Pitfalls)**:
+ 1. **属性转义 (Extremely Important)**: `` 内的 `arguments` 和 `result` 属性**必须**将内部的所有双引号 `"` 替换为 `"`。因为 OpenWebUI 前端提取这些数据的 Regex 是严格的 `="([^"]*)"`,一旦内容中出现原生双引号,就会被瞬间截断,导致参数被渲染为空并引发解析错误!
+ 2. **换行符要求**: `` 尖括号闭合后紧接着的内容**必须换行**(即 `>\\n`),否则 Markdown 扩展引擎无法将其识别为独立的 UI Block。
+ 3. **去除冗余通知**: 不要在 `tool.execution_start` 事件中提前向对话流输出普通的 `🔧 Executing...` 纯文本块,这会导致最终页面上同时出现两块工具提示(一个文本,一个折叠卡片)。
+
+#### Debug 信息的解耦 (Decoupling Debug Logs)
+
+对于连接建立、运行环境、缓存加载等属于 *脚本自身运行状态* 的 Debug 信息:
+- **禁止**: 不要将这些内容 yield 到最终的回答数据流(或塞进 `` 标签内),这会污染回答的纯粹性。
+- **推荐**: 统一使用 OpenWebUI 顶部的原生状态反馈气泡(Status Events):
+ ```python
+ await __event_emitter__({
+ "type": "status",
+ "data": {"description": "连接建立,正在等待响应...", "done": True}
+ })
+ ```
+
---
## ⚡ Action 插件规范 (Action Plugin Standards)
diff --git a/docs/plugins/pipes/github-copilot-sdk.md b/docs/plugins/pipes/github-copilot-sdk.md
index 77ffeaf..6cf091e 100644
--- a/docs/plugins/pipes/github-copilot-sdk.md
+++ b/docs/plugins/pipes/github-copilot-sdk.md
@@ -1,6 +1,6 @@
# GitHub Copilot SDK Pipe for OpenWebUI
-**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.6.2 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
+**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.7.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/open-webui) that integrates the official [GitHub Copilot SDK](https://github.com/github/copilot-sdk). It enables you to use **GitHub Copilot models** (e.g., `gpt-5.2-codex`, `claude-sonnet-4.5`,`gemini-3-pro`, `gpt-5-mini`) **AND** your own models via **BYOK** (OpenAI, Anthropic) directly within OpenWebUI, providing a unified agentic experience with **strict User & Chat-level Workspace Isolation**.
@@ -14,12 +14,13 @@ This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/
---
-## ✨ v0.6.2 Updates (What's New)
+## ✨ v0.7.0 Updates (What's New)
-- **🛠️ New Workspace Artifacts Tool**: Introduced `publish_file_from_workspace`. Agents can now generate files (e.g., Python-generated Excel/CSV) and provide direct download links for the user to click and save.
-- **⚙️ Workflow Optimization**: Improved reliability of the internal agentic workspace management.
-- **🛡️ Enhanced Security**: Refined access control for system resources within the isolated environment.
-- **🔧 Performance Tuning**: Optimized stream processing for larger context windows.
+- **🚀 Integrated CLI Management**: The Copilot CLI is now automatically managed and bundled via the `github-copilot-sdk` pip package. (v0.7.0)
+- **🧠 Native Tool Call UI**: Full adaptation to **OpenWebUI's native tool call UI** and thinking process visualization. (v0.7.0)
+- **🏠 OpenWebUI v0.8.0+ Fix**: Resolved "Error getting file content" download failure by switching to absolute path registration for published files. (v0.7.0)
+- **🌐 Comprehensive Multi-language Support**: Native localization for status messages in 11 languages (EN, ZH, JA, KO, FR, DE, ES, IT, RU, VI, ID). (v0.7.0)
+- **🧹 Architecture Cleanup**: Refactored core setup and optimized reasoning status display for a leaner experience. (v0.7.0)
---
@@ -31,8 +32,8 @@ This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/
- **♾️ Infinite Session Management**: Smart context window management with automatic compaction for indefinite conversation capability.
- **🧠 Deep Database Integration**: Real-time persistence of TOD·O lists for long-running workflows.
- **🌊 Advanced Streaming**: Full support for thinking process/Chain of Thought visualization.
-- **🖼️ Intelligent Multimodal**: Vision capabilities and raw file analysis support.
-- **⚡ Full-Lifecycle File Agent**: Supports receiving uploaded files for raw bypass analysis and publishing results (Excel/reports) as downloadable links.
+- **🖼️ Intelligent Multimodal**: Vision capabilities and raw file analysis support (bypasses RAG for direct binary access).
+- **📤 Workspace Artifacts (`publish_file_from_workspace`)**: Agents can generate files (Excel, CSV, HTML reports, etc.) and provide **persistent download links** directly in the chat.
- **🖼️ Interactive Artifacts**: Automatically renders HTML/JS apps generated by the agent directly in the chat interface.
---
@@ -110,7 +111,7 @@ If this plugin has been useful, a **Star** on [OpenWebUI Extensions](https://git
- **Agent ignores files?**: Ensure the Files Filter is enabled, otherwise RAG will interfere with raw binaries.
- **No progress bar?**: The bar only appears when the Agent uses the `update_todo` tool.
-- **Dependencies**: This Pipe automatically installs `github-copilot-sdk` (Python) and `github-copilot-cli` (Binary).
+- **Dependencies**: This Pipe automatically manages `github-copilot-sdk` (Python) and utilizes the bundled binary CLI. No manual install required.
---
diff --git a/docs/plugins/pipes/github-copilot-sdk.zh.md b/docs/plugins/pipes/github-copilot-sdk.zh.md
index 1ab2e2a..5f620be 100644
--- a/docs/plugins/pipes/github-copilot-sdk.zh.md
+++ b/docs/plugins/pipes/github-copilot-sdk.zh.md
@@ -1,6 +1,6 @@
# GitHub Copilot SDK 官方管道
-**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.6.2 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
+**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 0.7.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
这是一个用于 [OpenWebUI](https://github.com/open-webui/open-webui) 的高级 Pipe 函数,深度集成了 **GitHub Copilot SDK**。它不仅支持 **GitHub Copilot 官方模型**(如 `gpt-5.2-codex`, `claude-sonnet-4.5`, `gemini-3-pro`, `gpt-5-mini`),还支持 **BYOK (自带 Key)** 模式对接自定义服务商(OpenAI, Anthropic),并具备**严格的用户与会话级工作区隔离**能力,提供统一且安全的 Agent 交互体验。
@@ -14,12 +14,13 @@
---
-## ✨ 0.6.2 更新内容 (What's New)
+## ✨ 0.7.0 更新内容 (What's New)
-- **🛠️ 新增工作区产物工具**: 引入 `publish_file_from_workspace`。Agent 现在可以生成物理文件(如使用 Python 生成的 Excel/CSV 报表),并直接在聊天界面提供点击下载链接。
-- **⚙️ 工作流优化**: 提升了内部 Agent 物理工作区管理的可靠性与原子性。
-- **🛡️ 安全增强**: 精细化了隔离环境下系统资源的访问控制策略。
-- **🔧 性能微调**: 针对大上下文窗口优化了流式数据处理性能。
+- **🚀 CLI 免维护集成**: Copilot CLI 现在通过 `github-copilot-sdk` pip 包自动同步管理,彻底告别手动 `curl | bash` 安装问题。(v0.7.0)
+- **🧠 原生工具调用 UI**: 全面适配 **OpenWebUI 原生工具调用 UI** 与模型思考过程(思维链)展示。(v0.7.0)
+- **🏠 OpenWebUI v0.8.0+ 兼容性修复**: 通过切换为绝对路径注册发布文件,彻底解决了“Error getting file content”无法下载到本地的问题。(v0.7.0)
+- **🌐 全面的多语言支持**: 针对状态消息进行了 11 国语言的原生本地化 (中/英/日/韩/法/德/西/意/俄/越/印尼)。(v0.7.0)
+- **🧹 架构精简**: 重构了初始化逻辑并优化了推理状态显示,提供更轻量稳健的体验。(v0.7.0)
---
@@ -31,8 +32,8 @@
- **♾️ 无限会话管理**: 智能上下文窗口管理与自动压缩算法,支持无限时长的对话交互。
- **🧠 深度数据库集成**: 实时持久化 TOD·O 列表到 UI 进度条。
- **🌊 深度推理展示**: 完整支持模型思考过程 (Thinking Process) 的流式渲染。
-- **🖼️ 智能多模态**: 完整支持图像识别与附件上传分析。
-- **⚡ 全生命周期文件 Agent**: 支持接收上传文件进行绕过 RAG 的深度分析,并将处理结果(如 Excel/报告)发布为下载链接实现闭环。
+- **🖼️ 智能多模态**: 完整支持图像识别与附件上传分析(绕过 RAG 直接访问原始二进制内容)。
+- **📤 工作区产物工具 (`publish_file_from_workspace`)**: Agent 可生成文件(Excel、CSV、HTML 报告等)并直接提供**持久化下载链接**。管理员还可额外获得通过 `/content/html` 接口的**聊天内 HTML 预览**链接。
- **🖼️ 交互式伪影 (Artifacts)**: 自动渲染 Agent 生成的 HTML/JS 应用程序,直接在聊天界面交互。
---
@@ -95,7 +96,7 @@
### 1) 导入函数
1. 打开 OpenWebUI,前往 **工作区** -> **函数**。
-2. 点击 **+** (创建函数),完整粘贴 `github_copilot_sdk_cn.py` 的内容。
+2. 点击 **+** (创建函数),完整粘贴 `github_copilot_sdk.py` 的内容。
3. 点击保存并确保已启用。
### 2) 获取 Token (Get Token)
@@ -110,7 +111,7 @@
- **Agent 无法识别文件?**: 请确保已安装并启用了 Files Filter 插件,否则原始文件会被 RAG 干扰。
- **看不到 TODO 进度条?**: 进度条仅在 Agent 使用 `update_todo` 工具(通常是处理复杂任务)时出现。
-- **依赖安装**: 本管道会自动尝试安装 `github-copilot-sdk` (Python 包) 和 `github-copilot-cli` (官方二进制)。
+- **依赖安装**: 本管道会自动管理 `github-copilot-sdk` (Python 包) 并优先直接使用内置的二进制 CLI,无需手动干预。
---
diff --git a/docs/plugins/pipes/index.md b/docs/plugins/pipes/index.md
index 7318fce..3630b7d 100644
--- a/docs/plugins/pipes/index.md
+++ b/docs/plugins/pipes/index.md
@@ -15,7 +15,7 @@ Pipes allow you to:
## Available Pipe Plugins
-- [GitHub Copilot SDK](github-copilot-sdk.md) (v0.6.2) - Official GitHub Copilot SDK integration. Features **Workspace Isolation**, **Database Persistence**, **Zero-config OpenWebUI Tool Bridge**, **BYOK** support, and **dynamic MCP discovery**. Supports streaming, multimodal, and infinite sessions. [View Deep Dive](github-copilot-sdk-deep-dive.md) | [**View Advanced Tutorial**](github-copilot-sdk-tutorial.md).
+- [GitHub Copilot SDK](github-copilot-sdk.md) (v0.7.0) - Official GitHub Copilot SDK integration. Features **Workspace Isolation**, **Database Persistence**, **Zero-config OpenWebUI Tool Bridge**, **BYOK** support, and **dynamic MCP discovery**. Supports streaming, multimodal, and infinite sessions. [View Deep Dive](github-copilot-sdk-deep-dive.md) | [**View Advanced Tutorial**](github-copilot-sdk-tutorial.md).
- **[Case Study: GitHub 100 Star Growth Analysis](star-prediction-example.md)** - Learn how to use the GitHub Copilot SDK Pipe with Minimax 2.1 to automatically analyze CSV data and generate project growth reports.
- **[Case Study: High-Quality Video to GIF Conversion](video-processing-example.md)** - See how the model uses system-level FFmpeg to accelerate, scale, and optimize colors for screen recordings.
diff --git a/docs/plugins/pipes/index.zh.md b/docs/plugins/pipes/index.zh.md
index 4b6a5d6..587ebba 100644
--- a/docs/plugins/pipes/index.zh.md
+++ b/docs/plugins/pipes/index.zh.md
@@ -15,7 +15,7 @@ Pipes 可以用于:
## 可用的 Pipe 插件
-- [GitHub Copilot SDK](github-copilot-sdk.zh.md) (v0.6.2) - GitHub Copilot SDK 官方集成。具备**工作区安全隔离**、**数据库持久化**、**零配置工具桥接**与**BYOK (自带 Key) 支持**。支持流式输出、打字机思考过程及无限会话。[查看深度架构解析](github-copilot-sdk-deep-dive.zh.md) | [**查看进阶实战教程**](github-copilot-sdk-tutorial.zh.md)。
+- [GitHub Copilot SDK](github-copilot-sdk.zh.md) (v0.7.0) - GitHub Copilot SDK 官方集成。具备**工作区安全隔离**、**数据库持久化**、**零配置工具桥接**与**BYOK (自带 Key) 支持**。支持流式输出、打字机思考过程及无限会话。[查看深度架构解析](github-copilot-sdk-deep-dive.zh.md) | [**查看进阶实战教程**](github-copilot-sdk-tutorial.zh.md)。
- **[实战案例:GitHub 100 Star 增长预测](star-prediction-example.zh.md)** - 展示如何使用 GitHub Copilot SDK Pipe 结合 Minimax 2.1 模型,自动编写脚本分析 CSV 数据并生成详细的项目增长报告。
- **[实战案例:视频高质量 GIF 转换与加速](video-processing-example.zh.md)** - 演示模型如何通过底层 FFmpeg 工具对录屏进行加速、缩放及双阶段色彩优化处理。
diff --git a/plugins/actions/smart-mind-map/smart_mind_map.py b/plugins/actions/smart-mind-map/smart_mind_map.py
index 12a822b..9dfa969 100644
--- a/plugins/actions/smart-mind-map/smart_mind_map.py
+++ b/plugins/actions/smart-mind-map/smart_mind_map.py
@@ -9,6 +9,7 @@ icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAw
description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.
"""
+import asyncio
import logging
import os
import re
@@ -1514,6 +1515,7 @@ class Action:
self,
__user__: Optional[Dict[str, Any]],
__event_call__: Optional[Callable[[Any], Awaitable[None]]] = None,
+ __request__: Optional[Request] = None,
) -> Dict[str, str]:
"""Extract basic user context with safe fallbacks."""
if isinstance(__user__, (list, tuple)):
@@ -1528,20 +1530,36 @@ class Action:
# Default from profile
user_language = user_data.get("language", "en-US")
- # Priority: Document Lang > LocalStorage (Frontend) > Browser > Profile (Default)
+ # Level 1 Fallback: Accept-Language from __request__ headers
+ if (
+ __request__
+ and hasattr(__request__, "headers")
+ and "accept-language" in __request__.headers
+ ):
+ raw_lang = __request__.headers.get("accept-language", "")
+ if raw_lang:
+ user_language = raw_lang.split(",")[0].split(";")[0]
+
+ # Priority: Document Lang > LocalStorage (Frontend) > Browser > Request Header > Profile
if __event_call__:
try:
js_code = """
- return (
- document.documentElement.lang ||
- localStorage.getItem('locale') ||
- localStorage.getItem('language') ||
- navigator.language ||
- 'en-US'
- );
+ try {
+ return (
+ document.documentElement.lang ||
+ localStorage.getItem('locale') ||
+ localStorage.getItem('language') ||
+ navigator.language ||
+ 'en-US'
+ );
+ } catch (e) {
+ return 'en-US';
+ }
"""
- frontend_lang = await __event_call__(
- {"type": "execute", "data": {"code": js_code}}
+ # Use asyncio.wait_for to prevent hanging if frontend fails to callback
+ frontend_lang = await asyncio.wait_for(
+ __event_call__({"type": "execute", "data": {"code": js_code}}),
+ timeout=2.0,
)
if frontend_lang and isinstance(frontend_lang, str):
user_language = frontend_lang
@@ -2387,7 +2405,7 @@ class Action:
__request__: Optional[Request] = None,
) -> Optional[dict]:
logger.info("Action: Smart Mind Map (v1.0.0) started")
- user_ctx = await self._get_user_context(__user__, __event_call__)
+ user_ctx = await self._get_user_context(__user__, __event_call__, __request__)
user_language = user_ctx["user_language"]
user_name = user_ctx["user_name"]
user_id = user_ctx["user_id"]
diff --git a/plugins/pipes/github-copilot-sdk/README.md b/plugins/pipes/github-copilot-sdk/README.md
index 77ffeaf..60e9548 100644
--- a/plugins/pipes/github-copilot-sdk/README.md
+++ b/plugins/pipes/github-copilot-sdk/README.md
@@ -1,6 +1,6 @@
# GitHub Copilot SDK Pipe for OpenWebUI
-**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.6.2 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
+**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.7.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT
This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/open-webui) that integrates the official [GitHub Copilot SDK](https://github.com/github/copilot-sdk). It enables you to use **GitHub Copilot models** (e.g., `gpt-5.2-codex`, `claude-sonnet-4.5`,`gemini-3-pro`, `gpt-5-mini`) **AND** your own models via **BYOK** (OpenAI, Anthropic) directly within OpenWebUI, providing a unified agentic experience with **strict User & Chat-level Workspace Isolation**.
@@ -14,12 +14,13 @@ This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/
---
-## ✨ v0.6.2 Updates (What's New)
+## ✨ v0.7.0 Updates (What's New)
-- **🛠️ New Workspace Artifacts Tool**: Introduced `publish_file_from_workspace`. Agents can now generate files (e.g., Python-generated Excel/CSV) and provide direct download links for the user to click and save.
-- **⚙️ Workflow Optimization**: Improved reliability of the internal agentic workspace management.
-- **🛡️ Enhanced Security**: Refined access control for system resources within the isolated environment.
-- **🔧 Performance Tuning**: Optimized stream processing for larger context windows.
+- **🚀 Integrated CLI Management**: The Copilot CLI is now automatically managed and bundled via the `github-copilot-sdk` pip package. No more manual `curl | bash` installation or version mismatches. (v0.7.0)
+- **🧠 Native Tool Call UI**: Full adaptation to **OpenWebUI's native tool call UI** and thinking process visualization. (v0.7.0)
+- **🏠 OpenWebUI v0.8.0+ Fix**: Resolved "Error getting file content" download failure by switching to absolute path registration for published files. (v0.7.0)
+- **🌐 Comprehensive Multi-language Support**: Native localization for status messages in 11 languages (EN, ZH, JA, KO, FR, DE, ES, IT, RU, VI, ID). (v0.7.0)
+- **🧹 Architecture Cleanup**: Refactored core setup and optimized reasoning status display for a leaner experience. (v0.7.0)
---
@@ -31,8 +32,8 @@ This is an advanced Pipe function for [OpenWebUI](https://github.com/open-webui/
- **♾️ Infinite Session Management**: Smart context window management with automatic compaction for indefinite conversation capability.
- **🧠 Deep Database Integration**: Real-time persistence of TOD·O lists for long-running workflows.
- **🌊 Advanced Streaming**: Full support for thinking process/Chain of Thought visualization.
-- **🖼️ Intelligent Multimodal**: Vision capabilities and raw file analysis support.
-- **⚡ Full-Lifecycle File Agent**: Supports receiving uploaded files for raw bypass analysis and publishing results (Excel/reports) as downloadable links.
+- **🖼️ Intelligent Multimodal**: Vision capabilities and raw file analysis support (bypasses RAG for direct binary access).
+- **📤 Workspace Artifacts (`publish_file_from_workspace`)**: Agents can generate files (Excel, CSV, HTML reports, etc.) and provide **persistent download links** directly in the chat.
- **🖼️ Interactive Artifacts**: Automatically renders HTML/JS apps generated by the agent directly in the chat interface.
---
@@ -110,7 +111,7 @@ If this plugin has been useful, a **Star** on [OpenWebUI Extensions](https://git
- **Agent ignores files?**: Ensure the Files Filter is enabled, otherwise RAG will interfere with raw binaries.
- **No progress bar?**: The bar only appears when the Agent uses the `update_todo` tool.
-- **Dependencies**: This Pipe automatically installs `github-copilot-sdk` (Python) and `github-copilot-cli` (Binary).
+- **Dependencies**: This Pipe automatically manages `github-copilot-sdk` (Python) and utilizes the bundled binary CLI. No manual install required.
---
diff --git a/plugins/pipes/github-copilot-sdk/README_CN.md b/plugins/pipes/github-copilot-sdk/README_CN.md
index 8a213bc..d07f135 100644
--- a/plugins/pipes/github-copilot-sdk/README_CN.md
+++ b/plugins/pipes/github-copilot-sdk/README_CN.md
@@ -1,6 +1,6 @@
# GitHub Copilot SDK 官方管道
-**作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 0.6.2 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
+**作者:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **版本:** 0.7.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT
这是一个用于 [OpenWebUI](https://github.com/open-webui/open-webui) 的高级 Pipe 函数,深度集成了 **GitHub Copilot SDK**。它不仅支持 **GitHub Copilot 官方模型**(如 `gpt-5.2-codex`, `claude-sonnet-4.5`, `gemini-3-pro`, `gpt-5-mini`),还支持 **BYOK (自带 Key)** 模式对接自定义服务商(OpenAI, Anthropic),并具备**严格的用户与会话级工作区隔离**能力,提供统一且安全的 Agent 交互体验。
@@ -14,12 +14,13 @@
---
-## ✨ 0.6.2 更新内容 (What's New)
+## ✨ 0.7.0 更新内容 (What's New)
-- **🛠️ 新增工作区产物工具**: 引入 `publish_file_from_workspace`。Agent 现在可以生成物理文件(如使用 Python 生成的 Excel/CSV 报表),并直接在聊天界面提供点击下载链接。
-- **⚙️ 工作流优化**: 提升了内部 Agent 物理工作区管理的可靠性与原子性。
-- **🛡️ 安全增强**: 精细化了隔离环境下系统资源的访问控制策略。
-- **🔧 性能微调**: 针对大上下文窗口优化了流式数据处理性能。
+- **🚀 CLI 免维护集成**: Copilot CLI 现在通过 `github-copilot-sdk` pip 包自动同步管理,彻底告别手动 `curl | bash` 安装及版本不匹配问题。(v0.7.0)
+- **🧠 原生工具调用 UI**: 全面适配 **OpenWebUI 原生工具调用 UI** 与模型思考过程(思维链)展示。(v0.7.0)
+- **🏠 OpenWebUI v0.8.0+ 兼容性修复**: 通过切换为绝对路径注册发布文件,彻底解决了“Error getting file content”无法下载到本地的问题。(v0.7.0)
+- **🌐 全面的多语言支持**: 针对状态消息进行了 11 国语言的原生本地化 (中/英/日/韩/法/德/西/意/俄/越/印尼)。(v0.7.0)
+- **🧹 架构精简**: 重构了初始化逻辑并优化了推理状态显示,提供更轻量稳健的体验。(v0.7.0)
---
@@ -31,8 +32,8 @@
- **♾️ 无限会话管理**: 智能上下文窗口管理与自动压缩算法,支持无限时长的对话交互。
- **🧠 深度数据库集成**: 实时持久化 TOD·O 列表到 UI 进度条。
- **🌊 深度推理展示**: 完整支持模型思考过程 (Thinking Process) 的流式渲染。
-- **🖼️ 智能多模态**: 完整支持图像识别与附件上传分析。
-- **⚡ 全生命周期文件 Agent**: 支持接收上传文件进行绕过 RAG 的深度分析,并将处理结果(如 Excel/报告)发布为下载链接。
+- **🖼️ 智能多模态**: 完整支持图像识别与附件上传分析(绕过 RAG 直接访问原始二进制内容)。
+- **📤 工作区产物工具 (`publish_file_from_workspace`)**: Agent 可生成文件(Excel、CSV、HTML 报告等)并直接在聊天中提供**持久化下载链接**。
- **🖼️ 交互式伪影 (Artifacts)**: 自动渲染 Agent 生成的 HTML/JS 应用程序,直接在聊天界面交互。
---
@@ -95,7 +96,7 @@
### 1) 导入函数
1. 打开 OpenWebUI,前往 **工作区** -> **函数**。
-2. 点击 **+** (创建函数),完整粘贴 `github_copilot_sdk_cn.py` 的内容。
+2. 点击 **+** (创建函数),完整粘贴 `github_copilot_sdk.py` 的内容。
3. 点击保存并确保已启用。
### 2) 获取 Token (Get Token)
@@ -114,7 +115,7 @@
- **Agent 无法识别文件?**: 请确保已安装并启用了 Files Filter 插件,否则原始文件会被 RAG 干扰。
- **看不到 TODO 进度条?**: 进度条仅在 Agent 使用 `update_todo` 工具(通常是处理复杂任务)时出现。
-- **依赖安装**: 本管道会自动尝试安装 `github-copilot-sdk` (Python 包) 和 `github-copilot-cli` (官方二进制)。
+- **依赖安装**: 本管道会自动管理 `github-copilot-sdk` (Python 包) 并优先直接使用内置的二进制 CLI,无需手动干预。
---
diff --git a/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py b/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py
index da6d9e2..b2f4650 100644
--- a/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py
+++ b/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py
@@ -1,12 +1,12 @@
"""
title: GitHub Copilot Official SDK Pipe
author: Fu-Jie
-author_url: https://github.com/Fu-Jie/awesome-openwebui
+author_url: https://github.com/Fu-Jie/openwebui-extensions
funding_url: https://github.com/open-webui
openwebui_id: ce96f7b4-12fc-4ac3-9a01-875713e69359
description: Integrate GitHub Copilot SDK. Supports dynamic models, multi-turn conversation, streaming, multimodal input, infinite sessions, and frontend debug logging.
-version: 0.6.2
-requirements: github-copilot-sdk==0.1.23
+version: 0.7.0
+requirements: github-copilot-sdk==0.1.25
"""
import os
@@ -226,10 +226,7 @@ class Pipe:
default=300,
description="Timeout for each stream chunk (seconds)",
)
- COPILOT_CLI_VERSION: str = Field(
- default="0.0.406",
- description="Specific Copilot CLI version to install/enforce (e.g. '0.0.406'). Leave empty for latest.",
- )
+
EXCLUDE_KEYWORDS: str = Field(
default="",
description="Exclude models containing these keywords (comma separated, e.g.: codex, haiku)",
@@ -360,6 +357,116 @@ class Pipe:
_env_setup_done = False # Track if env setup has been completed
_last_update_check = 0 # Timestamp of last CLI update check
+ TRANSLATIONS = {
+ "en-US": {
+ "status_conn_est": "Connection established, waiting for response...",
+ "status_reasoning_inj": "Reasoning Effort injected: {effort}",
+ "debug_agent_working_in": "Agent working in: {path}",
+ "debug_mcp_servers": "🔌 Connected MCP Servers: {servers}",
+ "publish_success": "File published successfully.",
+ "publish_hint_html": "Link: [View {filename}]({view_url}) | [Download]({download_url})",
+ "publish_hint_default": "Link: [Download {filename}]({download_url})",
+ },
+ "zh-CN": {
+ "status_conn_est": "已建立连接,等待响应...",
+ "status_reasoning_inj": "已注入推理级别:{effort}",
+ "debug_agent_working_in": "Agent 工作目录: {path}",
+ "debug_mcp_servers": "🔌 已连接 MCP 服务器: {servers}",
+ "publish_success": "文件发布成功。",
+ "publish_hint_html": "链接: [查看 {filename}]({view_url}) | [下载]({download_url})",
+ "publish_hint_default": "链接: [下载 {filename}]({download_url})",
+ },
+ "zh-HK": {
+ "status_conn_est": "已建立連接,等待響應...",
+ "status_reasoning_inj": "已注入推理級別:{effort}",
+ "debug_agent_working_in": "Agent 工作目錄: {path}",
+ "debug_mcp_servers": "🔌 已連接 MCP 伺服器: {servers}",
+ "publish_success": "文件發布成功。",
+ "publish_hint_html": "連結: [查看 {filename}]({view_url}) | [下載]({download_url})",
+ "publish_hint_default": "連結: [下載 {filename}]({download_url})",
+ },
+ "zh-TW": {
+ "status_conn_est": "已建立連接,等待響應...",
+ "status_reasoning_inj": "已注入推理級別:{effort}",
+ "debug_agent_working_in": "Agent 工作目錄: {path}",
+ "debug_mcp_servers": "🔌 已連接 MCP 伺服器: {servers}",
+ "publish_success": "文件發布成功。",
+ "publish_hint_html": "連結: [查看 {filename}]({view_url}) | [下載]({download_url})",
+ "publish_hint_default": "連結: [下載 {filename}]({download_url})",
+ },
+ "ja-JP": {
+ "status_conn_est": "接続が確立されました。応答を待っています...",
+ "status_reasoning_inj": "推論レベルが注入されました:{effort}",
+ "debug_agent_working_in": "Agent 作業ディレクトリ: {path}",
+ "debug_mcp_servers": "🔌 接続済み MCP サーバー: {servers}",
+ },
+ "ko-KR": {
+ "status_conn_est": "연결이 설정되었습니다. 응답을 기다리는 중...",
+ "status_reasoning_inj": "추론 수준 설정됨: {effort}",
+ "debug_agent_working_in": "Agent 작업 디렉토리: {path}",
+ "debug_mcp_servers": "🔌 연결된 MCP 서버: {servers}",
+ },
+ "fr-FR": {
+ "status_conn_est": "Connexion établie, en attente de réponse...",
+ "status_reasoning_inj": "Effort de raisonnement injecté : {effort}",
+ "debug_agent_working_in": "Répertoire de travail de l'Agent : {path}",
+ "debug_mcp_servers": "🔌 Serveurs MCP connectés : {servers}",
+ },
+ "de-DE": {
+ "status_conn_est": "Verbindung hergestellt, warte auf Antwort...",
+ "status_reasoning_inj": "Argumentationsaufwand injiziert: {effort}",
+ "debug_agent_working_in": "Agent-Arbeitsverzeichnis: {path}",
+ "debug_mcp_servers": "🔌 Verbundene MCP-Server: {servers}",
+ },
+ "es-ES": {
+ "status_conn_est": "Conexión establecida, esperando respuesta...",
+ "status_reasoning_inj": "Nivel de razonamiento inyectado: {effort}",
+ "debug_agent_working_in": "Directorio de trabajo del Agente: {path}",
+ "debug_mcp_servers": "🔌 Servidores MCP conectados: {servers}",
+ },
+ "it-IT": {
+ "status_conn_est": "Connessione stabilita, in attesa di risposta...",
+ "status_reasoning_inj": "Livello di ragionamento iniettato: {effort}",
+ "debug_agent_working_in": "Directory di lavoro dell'Agente: {path}",
+ "debug_mcp_servers": "🔌 Server MCP connessi: {servers}",
+ },
+ "ru-RU": {
+ "status_conn_est": "Соединение установлено, ожидание ответа...",
+ "status_reasoning_inj": "Уровень рассуждения внедрен: {effort}",
+ "debug_agent_working_in": "Рабочий каталог Агента: {path}",
+ "debug_mcp_servers": "🔌 Подключенные серверы MCP: {servers}",
+ },
+ "vi-VN": {
+ "status_conn_est": "Đã thiết lập kết nối, đang chờ phản hồi...",
+ "status_reasoning_inj": "Cấp độ suy luận đã được áp dụng: {effort}",
+ "debug_agent_working_in": "Thư mục làm việc của Agent: {path}",
+ "debug_mcp_servers": "🔌 Các máy chủ MCP đã kết nối: {servers}",
+ },
+ "id-ID": {
+ "status_conn_est": "Koneksi terjalin, menunggu respons...",
+ "status_reasoning_inj": "Tingkat penalaran diterapkan: {effort}",
+ "debug_agent_working_in": "Direktori kerja Agent: {path}",
+ "debug_mcp_servers": "🔌 Server MCP yang terhubung: {servers}",
+ },
+ }
+
+ FALLBACK_MAP = {
+ "zh": "zh-CN",
+ "zh-TW": "zh-TW",
+ "zh-HK": "zh-HK",
+ "en": "en-US",
+ "en-GB": "en-US",
+ "ja": "ja-JP",
+ "ko": "ko-KR",
+ "fr": "fr-FR",
+ "de": "de-DE",
+ "es": "es-ES",
+ "it": "it-IT",
+ "ru": "ru-RU",
+ "vi": "vi-VN",
+ "id": "id-ID",
+ }
+
def __init__(self):
self.type = "pipe"
self.id = "github_copilot_sdk"
@@ -390,6 +497,83 @@ class Pipe:
except Exception as e:
logger.error(f"[Database] ❌ Initialization failed: {str(e)}")
+ def _resolve_language(self, user_language: str) -> str:
+ """Normalize user language code to a supported translation key."""
+ if not user_language:
+ return "en-US"
+ if user_language in self.TRANSLATIONS:
+ return user_language
+ lang_base = user_language.split("-")[0]
+ if user_language in self.FALLBACK_MAP:
+ return self.FALLBACK_MAP[user_language]
+ if lang_base in self.FALLBACK_MAP:
+ return self.FALLBACK_MAP[lang_base]
+ return "en-US"
+
+ def _get_translation(self, lang: str, key: str, **kwargs) -> str:
+ """Helper function to get translated string for a key."""
+ lang_key = self._resolve_language(lang)
+ trans_map = self.TRANSLATIONS.get(lang_key, self.TRANSLATIONS["en-US"])
+ text = trans_map.get(key, self.TRANSLATIONS["en-US"].get(key, key))
+ if kwargs:
+ try:
+ text = text.format(**kwargs)
+ except Exception as e:
+ logger.warning(f"Translation formatting failed for {key}: {e}")
+ return text
+
+ async def _get_user_context(self, __user__, __event_call__=None, __request__=None):
+ """Extract basic user context with safe fallbacks including JS localStorage."""
+ if isinstance(__user__, (list, tuple)):
+ user_data = __user__[0] if __user__ else {}
+ elif isinstance(__user__, dict):
+ user_data = __user__
+ else:
+ 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 (
+ __request__
+ and hasattr(__request__, "headers")
+ and "accept-language" in __request__.headers
+ ):
+ raw_lang = __request__.headers.get("accept-language", "")
+ if raw_lang:
+ user_language = raw_lang.split(",")[0].split(";")[0]
+
+ if __event_call__:
+ try:
+ js_code = """
+ try {
+ return (
+ document.documentElement.lang ||
+ localStorage.getItem('locale') ||
+ localStorage.getItem('language') ||
+ navigator.language ||
+ 'en-US'
+ );
+ } catch (e) {
+ return 'en-US';
+ }
+ """
+ frontend_lang = await asyncio.wait_for(
+ __event_call__({"type": "execute", "data": {"code": js_code}}),
+ timeout=2.0,
+ )
+ if frontend_lang and isinstance(frontend_lang, str):
+ user_language = frontend_lang
+ except Exception as e:
+ pass
+
+ return {
+ "user_id": user_id,
+ "user_name": user_name,
+ "user_language": user_language,
+ }
+
@contextlib.contextmanager
def _db_session(self):
"""Yield a database session using Open WebUI helpers with graceful fallbacks."""
@@ -611,6 +795,8 @@ class Pipe:
user_data = {}
user_id = user_data.get("id") or user_data.get("user_id")
+ user_lang = user_data.get("language") or "en-US"
+ is_admin = user_data.get("role") == "admin"
if not user_id:
return None
@@ -746,10 +932,7 @@ class Pipe:
dest_path = Path(UPLOAD_DIR) / f"{file_id}_{safe_filename}"
await asyncio.to_thread(shutil.copy2, target_path, dest_path)
- try:
- db_path = str(os.path.relpath(dest_path, DATA_DIR))
- except:
- db_path = str(dest_path)
+ db_path = str(dest_path)
file_form = FileForm(
id=file_id,
@@ -769,12 +952,37 @@ class Pipe:
# 5. Result
download_url = f"/api/v1/files/{file_id}/content"
+ view_url = download_url
+ is_html = safe_filename.lower().endswith(".html")
+
+ # For HTML files, if user is admin, provide a direct view link (/content/html)
+ if is_html and is_admin:
+ view_url = f"{download_url}/html"
+
+ # Localized output
+ msg = self._get_translation(user_lang, "publish_success")
+ if is_html and is_admin:
+ hint = self._get_translation(
+ user_lang,
+ "publish_hint_html",
+ filename=safe_filename,
+ view_url=view_url,
+ download_url=download_url,
+ )
+ else:
+ hint = self._get_translation(
+ user_lang,
+ "publish_hint_default",
+ filename=safe_filename,
+ download_url=download_url,
+ )
+
return {
"file_id": file_id,
"filename": safe_filename,
"download_url": download_url,
- "message": "File published successfully.",
- "hint": f"Link: [Download {safe_filename}]({download_url})",
+ "message": msg,
+ "hint": hint,
}
except Exception as e:
return {"error": str(e)}
@@ -1921,10 +2129,6 @@ class Pipe:
"on_post_tool_use": on_post_tool_use,
}
- def _get_user_context(self):
- """Helper to get user context (placeholder for future use)."""
- return {}
-
def _get_chat_context(
self,
body: dict,
@@ -2327,25 +2531,11 @@ class Pipe:
token: str = None,
enable_mcp: bool = True,
enable_cache: bool = True,
- skip_cli_install: bool = False,
+ skip_cli_install: bool = False, # Kept for call-site compatibility, no longer used
__event_emitter__=None,
+ user_lang: str = "en-US",
):
- """Setup environment variables and verify Copilot CLI. Dynamic Token Injection."""
- def emit_status_sync(description: str, done: bool = False):
- if not __event_emitter__:
- return
- try:
- loop = asyncio.get_running_loop()
- loop.create_task(
- __event_emitter__(
- {
- "type": "status",
- "data": {"description": description, "done": done},
- }
- )
- )
- except Exception:
- pass
+ """Setup environment variables and resolve Copilot CLI path from SDK bundle."""
# 1. Real-time Token Injection (Always updates on each call)
effective_token = token or self.valves.GH_TOKEN
@@ -2353,8 +2543,6 @@ class Pipe:
os.environ["GH_TOKEN"] = os.environ["GITHUB_TOKEN"] = effective_token
if self._env_setup_done:
- # If done, we only sync MCP if called explicitly or in debug mode
- # To improve speed, we avoid redundant file I/O here for regular requests
if debug_enabled:
self._sync_mcp_config(
__event_call__,
@@ -2365,186 +2553,46 @@ class Pipe:
return
os.environ["COPILOT_AUTO_UPDATE"] = "false"
- self._emit_debug_log_sync(
- "Disabled CLI auto-update (COPILOT_AUTO_UPDATE=false)",
- __event_call__,
- debug_enabled=debug_enabled,
- )
- # 2. CLI Path Discovery
- cli_path = "/usr/local/bin/copilot"
- if os.environ.get("COPILOT_CLI_PATH"):
- cli_path = os.environ["COPILOT_CLI_PATH"]
-
- target_version = self.valves.COPILOT_CLI_VERSION.strip()
- found = False
- current_version = None
-
- def get_cli_version(path):
- try:
- output = (
- subprocess.check_output(
- [path, "--version"], stderr=subprocess.STDOUT
- )
- .decode()
- .strip()
- )
- import re
-
- match = re.search(r"(\d+\.\d+\.\d+)", output)
- return match.group(1) if match else output
- except Exception:
- return None
-
- # Check existing version
- if os.path.exists(cli_path):
- found = True
- current_version = get_cli_version(cli_path)
+ # 2. CLI Path Discovery (priority: env var > PATH > SDK bundle)
+ cli_path = os.environ.get("COPILOT_CLI_PATH", "")
+ found = bool(cli_path and os.path.exists(cli_path))
if not found:
sys_path = shutil.which("copilot")
if sys_path:
cli_path = sys_path
found = True
- current_version = get_cli_version(cli_path)
if not found:
- pkg_path = os.path.join(os.path.dirname(__file__), "bin", "copilot")
- if os.path.exists(pkg_path):
- cli_path = pkg_path
- found = True
- current_version = get_cli_version(cli_path)
-
- # 3. Installation/Update Logic
- should_install = not found
- install_reason = "CLI not found"
- if found and target_version:
- norm_target = target_version.lstrip("v")
- norm_current = current_version.lstrip("v") if current_version else ""
-
- # Only install if target version is GREATER than current version
try:
- from packaging.version import parse as parse_version
+ from copilot.client import _get_bundled_cli_path
- if parse_version(norm_target) > parse_version(norm_current):
- should_install = True
- install_reason = (
- f"Upgrade needed ({current_version} -> {target_version})"
- )
- elif parse_version(norm_target) < parse_version(norm_current):
- self._emit_debug_log_sync(
- f"Current version ({current_version}) is newer than specified ({target_version}). Skipping downgrade.",
- __event_call__,
- debug_enabled=debug_enabled,
- )
- except Exception as e:
- # Fallback to string comparison if packaging is not available
- if norm_target != norm_current:
- should_install = True
- install_reason = (
- f"Version mismatch ({current_version} != {target_version})"
- )
+ bundled_path = _get_bundled_cli_path()
+ if bundled_path and os.path.exists(bundled_path):
+ cli_path = bundled_path
+ found = True
+ except ImportError:
+ pass
- if should_install and not skip_cli_install:
- self._emit_debug_log_sync(
- f"Installing/Updating Copilot CLI: {install_reason}...",
- __event_call__,
- debug_enabled=debug_enabled,
- )
- emit_status_sync(
- "🔧 正在安装/更新 Copilot CLI(首次可能需要 1-3 分钟)...",
- done=False,
- )
- try:
- env = os.environ.copy()
- if target_version:
- env["VERSION"] = target_version
- proc = subprocess.Popen(
- "curl -fsSL https://gh.io/copilot-install | bash",
- shell=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- bufsize=1,
- env=env,
- )
-
- progress_percent = -1
- line_count = 0
- while True:
- raw_line = proc.stdout.readline() if proc.stdout else ""
- if raw_line == "" and proc.poll() is not None:
- break
-
- line = (raw_line or "").strip()
- if not line:
- continue
-
- line_count += 1
- percent_match = re.search(r"(\d{1,3})%", line)
- if percent_match:
- try:
- pct = int(percent_match.group(1))
- if pct >= progress_percent + 5:
- progress_percent = pct
- emit_status_sync(
- f"📦 Copilot CLI 安装中:{pct}%", done=False
- )
- except Exception:
- pass
- elif line_count % 20 == 0:
- emit_status_sync(
- f"📦 Copilot CLI 安装中:{line[:120]}", done=False
- )
-
- return_code = proc.wait()
- if return_code != 0:
- raise subprocess.CalledProcessError(
- return_code,
- "curl -fsSL https://gh.io/copilot-install | bash",
- )
-
- # Re-verify
- current_version = get_cli_version(cli_path)
- emit_status_sync(
- f"✅ Copilot CLI 安装完成(v{current_version or target_version or 'latest'})",
- done=False,
- )
- except Exception as e:
- self._emit_debug_log_sync(
- f"CLI installation failed: {e}",
- __event_call__,
- debug_enabled=debug_enabled,
- )
- emit_status_sync(
- f"❌ Copilot CLI 安装失败:{str(e)[:120]}",
- done=True,
- )
- elif should_install and skip_cli_install:
- self._emit_debug_log_sync(
- f"Skipping CLI install during model listing: {install_reason}",
- __event_call__,
- debug_enabled=debug_enabled,
- )
-
- # 4. Finalize
- cli_ready = bool(cli_path and os.path.exists(cli_path))
+ # 3. Finalize
+ cli_ready = found
if cli_ready:
os.environ["COPILOT_CLI_PATH"] = cli_path
+ # Add the CLI's parent directory to PATH so subprocesses can invoke `copilot` directly
+ cli_bin_dir = os.path.dirname(cli_path)
+ current_path = os.environ.get("PATH", "")
+ if cli_bin_dir and cli_bin_dir not in current_path.split(os.pathsep):
+ os.environ["PATH"] = cli_bin_dir + os.pathsep + current_path
self.__class__._env_setup_done = cli_ready
self.__class__._last_update_check = datetime.now().timestamp()
self._emit_debug_log_sync(
- f"Environment setup complete. CLI ready={cli_ready}. Path: {cli_path} (v{current_version})",
+ f"Environment setup complete. CLI ready={cli_ready}. Path: {cli_path}",
__event_call__,
debug_enabled=debug_enabled,
)
- if not skip_cli_install:
- if cli_ready:
- emit_status_sync("✅ Copilot CLI 已就绪", done=True)
- else:
- emit_status_sync("⚠️ Copilot CLI 尚未就绪,请稍后重试。", done=True)
def _process_attachments(
self,
@@ -2822,6 +2870,9 @@ class Pipe:
effective_mcp = user_valves.ENABLE_MCP_SERVER
effective_cache = user_valves.ENABLE_TOOL_CACHE
+ user_ctx = await self._get_user_context(__user__, __event_call__, __request__)
+ user_lang = user_ctx["user_language"]
+
# 2. Setup environment with effective settings
self._setup_env(
__event_call__,
@@ -2830,11 +2881,12 @@ class Pipe:
enable_mcp=effective_mcp,
enable_cache=effective_cache,
__event_emitter__=__event_emitter__,
+ user_lang=user_lang,
)
cwd = self._get_workspace_dir(user_id=user_id, chat_id=chat_id)
await self._emit_debug_log(
- f"Agent working in: {cwd} (Admin: {is_admin}, MCP: {effective_mcp})",
+ f"{self._get_translation(user_lang, 'debug_agent_working_in', path=cwd)} (Admin: {is_admin}, MCP: {effective_mcp})",
__event_call__,
debug_enabled=effective_debug,
)
@@ -3269,9 +3321,9 @@ class Pipe:
if body.get("stream", False):
init_msg = ""
if effective_debug:
- init_msg = f"> [Debug] Agent working in: {self._get_workspace_dir(user_id=user_id, chat_id=chat_id)}\n"
+ init_msg = f"> [Debug] {self._get_translation(user_lang, 'debug_agent_working_in', path=self._get_workspace_dir(user_id=user_id, chat_id=chat_id))}\n"
if mcp_server_names:
- init_msg += f"> [Debug] 🔌 Connected MCP Servers: {', '.join(mcp_server_names)}\n"
+ init_msg += f"> [Debug] {self._get_translation(user_lang, 'debug_mcp_servers', servers=', '.join(mcp_server_names))}\n"
# Transfer client ownership to stream_response
should_stop_client = False
@@ -3284,9 +3336,14 @@ class Pipe:
init_message=init_msg,
__event_call__=__event_call__,
__event_emitter__=__event_emitter__,
- reasoning_effort=effective_reasoning_effort,
+ reasoning_effort=(
+ effective_reasoning_effort
+ if (is_reasoning and not is_byok_model)
+ else "off"
+ ),
show_thinking=show_thinking,
debug_enabled=effective_debug,
+ user_lang=user_lang,
)
else:
try:
@@ -3332,6 +3389,7 @@ class Pipe:
reasoning_effort: str = "",
show_thinking: bool = True,
debug_enabled: bool = False,
+ user_lang: str = "en-US",
) -> AsyncGenerator:
"""
Stream response from Copilot SDK, handling various event types.
@@ -3476,14 +3534,8 @@ class Pipe:
queue.put_nowait("\n\n")
state["thinking_started"] = False
- # Display tool call with improved formatting
- if tool_args:
- tool_args_json = json.dumps(tool_args, indent=2, ensure_ascii=False)
- tool_display = f"\n\n\n🔧 Executing Tool: {tool_name}
\n\n**Parameters:**\n\n```json\n{tool_args_json}\n```\n\n \n\n"
- else:
- tool_display = f"\n\n\n🔧 Executing Tool: {tool_name}
\n\n*No parameters*\n\n \n\n"
-
- queue.put_nowait(tool_display)
+ # Note: We do NOT emit a done="false" card here to avoid card duplication
+ # (unless we have a way to update text which SSE content stream doesn't)
self._emit_debug_log_sync(
f"Tool Start: {tool_name}",
@@ -3600,31 +3652,55 @@ class Pipe:
)
# ------------------------
- # Try to detect content type for better formatting
- is_json = False
- try:
- json_obj = (
- json.loads(result_content)
- if isinstance(result_content, str)
- else result_content
+ # --- Build native OpenWebUI 0.8.3 tool_calls block ---
+ # Serialize input args (from execution_start)
+ tool_args_for_block = {}
+ if tool_call_id and tool_call_id in active_tools:
+ tool_args_for_block = active_tools[tool_call_id].get(
+ "arguments", {}
)
- if isinstance(json_obj, (dict, list)):
- result_content = json.dumps(
- json_obj, indent=2, ensure_ascii=False
- )
- is_json = True
- except:
- pass
- # Format based on content type
- if is_json:
- # JSON content: use code block with syntax highlighting
- result_display = f"\n\n{status_icon} Tool Result: {tool_name}
\n\n```json\n{result_content}\n```\n\n \n\n"
- else:
- # Plain text: use text code block to preserve formatting and add line breaks
- result_display = f"\n\n{status_icon} Tool Result: {tool_name}
\n\n```text\n{result_content}\n```\n\n \n\n"
+ try:
+ args_json_str = json.dumps(
+ tool_args_for_block, ensure_ascii=False
+ )
+ except Exception:
+ args_json_str = "{}"
- queue.put_nowait(result_display)
+ def escape_html_attr(s: str) -> str:
+ if not isinstance(s, str):
+ return ""
+ return (
+ str(s)
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace('"', """)
+ .replace("\n", "
")
+ .replace("\r", "
")
+ )
+
+ # MUST escape both arguments and result with " and
to satisfy OpenWebUI's strict regex /="([^"]*)"/
+ # OpenWebUI `marked` extension does not match multiline attributes properly without
+ args_for_attr = (
+ escape_html_attr(args_json_str) if args_json_str else "{}"
+ )
+ result_for_attr = escape_html_attr(result_content)
+
+ # Emit the unified native tool_calls block:
+ # OpenWebUI 0.8.3 frontend regex explicitly expects: name="xxx" arguments="..." result="..." done="true"
+ # CRITICAL: tag MUST be followed immediately by \n for the frontend Markdown extension to parse it!
+ tool_block = (
+ f'\n\n'
+ f"Tool Executed
\n"
+ f" \n\n"
+ )
+ queue.put_nowait(tool_block)
elif event_type == "tool.execution_progress":
# Tool execution progress update (for long-running tools)
@@ -3725,20 +3801,42 @@ class Pipe:
# Safe initial yield with error handling
try:
- if debug_enabled and show_thinking:
- yield "\n"
+ if debug_enabled and __event_emitter__:
+ # Emit debug info as UI status rather than reasoning block
+ async def _emit_status(key: str, desc: str = None, **kwargs):
+ try:
+ final_desc = (
+ desc
+ if desc
+ else self._get_translation(user_lang, key, **kwargs)
+ )
+ await __event_emitter__(
+ {
+ "type": "status",
+ "data": {"description": final_desc, "done": True},
+ }
+ )
+ except:
+ pass
+
if init_message:
- yield init_message
+ for line in init_message.split("\n"):
+ if line.strip():
+ clean_msg = line.replace("> [Debug] ", "").strip()
+ asyncio.create_task(_emit_status("custom", desc=clean_msg))
if reasoning_effort and reasoning_effort != "off":
- yield f"> [Debug] Reasoning Effort injected: {reasoning_effort.upper()}\n"
+ asyncio.create_task(
+ _emit_status(
+ "status_reasoning_inj", effort=reasoning_effort.upper()
+ )
+ )
- yield "> [Debug] Connection established, waiting for response...\n"
- state["thinking_started"] = True
+ asyncio.create_task(_emit_status("status_conn_est"))
except Exception as e:
# If initial yield fails, log but continue processing
self._emit_debug_log_sync(
- f"Initial yield warning: {e}",
+ f"Initial status warning: {e}",
__event_call__,
debug_enabled=debug_enabled,
)
@@ -3766,12 +3864,21 @@ class Pipe:
except asyncio.TimeoutError:
if done.is_set():
break
- if state["thinking_started"]:
+ if __event_emitter__ and debug_enabled:
try:
- yield f"> [Debug] Waiting for response ({self.valves.TIMEOUT}s exceeded)...\n"
+ asyncio.create_task(
+ __event_emitter__(
+ {
+ "type": "status",
+ "data": {
+ "description": f"Waiting for response ({self.valves.TIMEOUT}s exceeded)...",
+ "done": True,
+ },
+ }
+ )
+ )
except:
- # If yield fails during timeout, connection is gone
- break
+ pass
continue
while not queue.empty():
diff --git a/plugins/pipes/github-copilot-sdk/github_copilot_sdk_cn.py b/plugins/pipes/github-copilot-sdk/github_copilot_sdk_cn.py
deleted file mode 100644
index e7e7aa8..0000000
--- a/plugins/pipes/github-copilot-sdk/github_copilot_sdk_cn.py
+++ /dev/null
@@ -1,1736 +0,0 @@
-"""
-title: GitHub Copilot SDK 官方管道
-author: Fu-Jie
-author_url: https://github.com/Fu-Jie/openwebui-extensions
-funding_url: https://github.com/open-webui
-description: 集成 GitHub Copilot SDK。支持动态模型、多选提供商、流式输出、多模态 input、无限会话及前端调试日志。
-version: 0.6.2
-requirements: github-copilot-sdk==0.1.23
-"""
-
-import os
-import re
-import json
-import base64
-import tempfile
-import asyncio
-import logging
-import shutil
-import subprocess
-import hashlib
-import aiohttp
-from pathlib import Path
-from typing import Optional, Union, AsyncGenerator, List, Any, Dict, Literal, Tuple
-from types import SimpleNamespace
-from pydantic import BaseModel, Field, create_model
-
-# 导入 Copilot SDK 模块
-from copilot import CopilotClient, define_tool
-
-# 导入 Tool Server Connections 和 Tool System (从 OpenWebUI 配置)
-from open_webui.config import (
- PERSISTENT_CONFIG_REGISTRY,
- TOOL_SERVER_CONNECTIONS,
-)
-from open_webui.utils.tools import get_tools as get_openwebui_tools, get_builtin_tools
-from open_webui.models.tools import Tools
-from open_webui.models.users import Users
-from open_webui.models.files import Files, FileForm
-from open_webui.config import UPLOAD_DIR, DATA_DIR
-import mimetypes
-import uuid
-import shutil
-
-# Setup logger
-logger = logging.getLogger(__name__)
-
-FORMATTING_GUIDELINES = (
- "\n\n[环境与能力上下文]\n"
- "你是一个在特定高性能环境中运行的 AI 助手。了解你的上下文对于做出最佳决策至关重要。\n"
- "\n"
- "**系统环境:**\n"
- "- **平台**:你在 **OpenWebUI** 托管的 Linux 容器化环境中运行。\n"
- "- **核心引擎**:你由 **GitHub Copilot SDK** 驱动,并通过 **GitHub Copilot CLI** 进行交互。\n"
- "- **访问权限**:你可以直接访问 **OpenWebUI 源代码**。你可以通过文件操作或工具读取、分析和参考你正在运行的平台的内部实现。\n"
- "- **文件系统访问**:你以 **root** 身份运行。你对 **整个容器文件系统** 拥有读取权限。但是,你应仅写入工作区目录。\n"
- "- **原生 Python 环境**:你运行在一个丰富的 Python 环境中,已经包含了 OpenWebUI 的所有依赖库。\n"
- "\n"
- "**界面能力 (OpenWebUI):**\n"
- "- **视觉渲染**:你可以并且应该使用高级视觉元素(如 Mermaid 图表、交互式 HTML)来清晰地解释概念。\n"
- "- **内置工具**:OpenWebUI 提供了与内部服务直接交互的原生工具(如笔记、记忆管理)。\n"
- "\n"
- "**格式化与呈现指令:**\n"
- "1. **Markdown & 多媒体**:自由使用粗体、斜体、表格和列表。\n"
- "2. **Mermaid 图表**:请务必使用标准的 ```mermaid 代码块。\n"
- "3. **交互式 HTML/JS**:你可以输出完整的 ```html 代码块(含 CSS/JS),将在 iframe 中渲染。\n"
- "4. **文件交付与发布协议 (双渠道交付)**:\n"
- " - **核心理念**:视觉产物 (HTML/Mermaid) 与可下载文件是**互补**的。应始终追求双重交付:既在聊天中提供直观的视觉洞察,又提供持久的文件供用户保存。\n"
- " - **基本原则**:当用户需要“拥有”数据(下载、离线编辑)时,你必须发布文件。仅在本地生成文件是无用的,因为用户无法访问你的容器。\n"
- " - **隐式动作**:若用户说“导出”、“保存”或“给我链接”,自动执行三步曲。\n"
- " - **执行序列**:1. **本地写入**:将文件写入当前目录 (`.`)。2. **发布文件**:调用 `publish_file_from_workspace(filename='name.ext')`。3. **呈现链接**:展示返回的 `download_url` 链接。\n"
- " - **RAG 绕过**:此流程会自动适配 S3 存储映射并绕过 RAG,确保数据交付 100% 准确。\n"
- "7. **主动与自主**: 你是专家工程师。对于显而易见的步骤,**不要**请求许可。**不要**停下来问“我通过吗?”或“是否继续?”。\n"
- " - **行为模式**: 分析用户请求 -> 制定计划 -> **立即执行**计划。\n"
- " - **澄清**: 仅当请求模棱两可或具有高风险(例如破坏性操作)时才提出问题。\n"
- " - **目标**: 最小化用户摩擦。交付结果,而不是问题。\n"
- "8. **大文件输出管理**: 如果工具执行输出被截断或保存到临时文件 (例如 `/tmp/...`),**不要**担心。系统会自动将其移动到你的工作区并通知你新的文件名。然后你可以直接读取它。\n"
-)
-
-
-class Pipe:
- class Valves(BaseModel):
- GH_TOKEN: str = Field(
- default="",
- description="GitHub Access Token (PAT 或 OAuth Token)。用于聊天。",
- )
- ENABLE_OPENWEBUI_TOOLS: bool = Field(
- default=True,
- description="启用 OpenWebUI 工具 (包括自定义工具和工具服务器工具)。",
- )
- ENABLE_MCP_SERVER: bool = Field(
- default=True,
- description="启用直接 MCP 客户端连接 (建议)。",
- )
- ENABLE_TOOL_CACHE: bool = Field(
- default=True,
- description="缓存配置以优化性能。",
- )
- REASONING_EFFORT: Literal["low", "medium", "high", "xhigh"] = Field(
- default="medium",
- description="推理强度级别 (low, medium, high, xhigh)。仅影响标准模型。",
- )
- SHOW_THINKING: bool = Field(
- default=True,
- description="显示模型推理/思考过程",
- )
-
- INFINITE_SESSION: bool = Field(
- default=True,
- description="启用无限会话(自动上下文压缩)",
- )
- DEBUG: bool = Field(
- default=False,
- description="启用技术调试日志(输出到浏览器控制台)",
- )
- LOG_LEVEL: str = Field(
- default="error",
- description="Copilot CLI 日志级别:none, error, warning, info, debug, all",
- )
- TIMEOUT: int = Field(
- default=300,
- description="每个流式分块超时(秒)",
- )
- WORKSPACE_DIR: str = Field(
- default="",
- description="文件操作受限目录。为空则使用默认路径。",
- )
- COPILOT_CLI_VERSION: str = Field(
- default="0.0.406",
- description="指定强制使用的 Copilot CLI 版本 (例如 '0.0.406')。",
- )
- PROVIDERS: str = Field(
- default="OpenAI, Anthropic, Google",
- description="允许使用的模型提供商 (逗号分隔)。留空则显示所有。",
- )
- EXCLUDE_KEYWORDS: str = Field(
- default="",
- description="排除包含这些关键词的模型(逗号分隔,如:codex, haiku)",
- )
- MAX_MULTIPLIER: float = Field(
- default=1.0,
- description="标准模型允许的最大计费倍率。0 表示仅显示免费模型。",
- )
- COMPACTION_THRESHOLD: float = Field(
- default=0.8,
- description="后台压缩阈值 (0.0-1.0)",
- )
- BUFFER_THRESHOLD: float = Field(
- default=0.95,
- description="缓冲区耗尽阈值 (0.0-1.0)",
- )
- CUSTOM_ENV_VARS: str = Field(
- default="",
- description="自定义环境变量(JSON 格式)",
- )
-
- BYOK_TYPE: Literal["openai", "anthropic"] = Field(
- default="openai",
- description="BYOK 供应商类型:openai, anthropic",
- )
- BYOK_BASE_URL: str = Field(
- default="",
- description="BYOK 基础 URL (例如 https://api.openai.com/v1)",
- )
- BYOK_API_KEY: str = Field(
- default="",
- description="BYOK API 密钥 (全局设置)",
- )
- BYOK_BEARER_TOKEN: str = Field(
- default="",
- description="BYOK Bearer 令牌 (优先级高于 API Key)",
- )
- BYOK_MODELS: str = Field(
- default="",
- description="BYOK 模型列表 (逗号分隔)。",
- )
- BYOK_WIRE_API: Literal["completions", "responses"] = Field(
- default="completions",
- description="BYOK 通信协议:completions, responses",
- )
-
- class UserValves(BaseModel):
- GH_TOKEN: str = Field(
- default="",
- description="个人 GitHub Token (覆盖全局设置)",
- )
- REASONING_EFFORT: Literal["", "low", "medium", "high", "xhigh"] = Field(
- default="",
- description="推理强度级别覆盖。",
- )
- SHOW_THINKING: bool = Field(
- default=True,
- description="显示模型推理/思考过程",
- )
- DEBUG: bool = Field(
- default=False,
- description="启用技术调试日志",
- )
- MAX_MULTIPLIER: Optional[float] = Field(
- default=None,
- description="计费倍率覆盖。",
- )
- PROVIDERS: str = Field(
- default="",
- description="允许的提供商覆盖 (逗号分隔)。",
- )
- EXCLUDE_KEYWORDS: str = Field(
- default="",
- description="排除关键词 (支持个人覆盖)。",
- )
- ENABLE_OPENWEBUI_TOOLS: bool = Field(
- default=True,
- description="启用 OpenWebUI 工具。",
- )
- ENABLE_MCP_SERVER: bool = Field(
- default=True,
- description="启用动态 MCP 服务器加载。",
- )
- ENABLE_TOOL_CACHE: bool = Field(
- default=True,
- description="启用配置缓存。",
- )
- COMPACTION_THRESHOLD: Optional[float] = Field(
- default=None,
- description="压缩阈值覆盖。",
- )
- BUFFER_THRESHOLD: Optional[float] = Field(
- default=None,
- description="缓冲区阈值覆盖。",
- )
-
- # BYOK 覆盖
- BYOK_API_KEY: str = Field(default="", description="BYOK API 密钥覆盖")
- BYOK_TYPE: Literal["", "openai", "anthropic"] = Field(
- default="", description="BYOK 类型覆盖"
- )
- BYOK_BASE_URL: str = Field(default="", description="BYOK URL 覆盖")
- BYOK_BEARER_TOKEN: str = Field(default="", description="BYOK Token 覆盖")
- BYOK_MODELS: str = Field(default="", description="BYOK 模型列表覆盖")
- BYOK_WIRE_API: Literal["", "completions", "responses"] = Field(
- default="", description="协议覆盖"
- )
-
- _model_cache: List[dict] = []
- _last_byok_config_hash: str = "" # 跟踪配置状态以失效缓存
- _standard_model_ids: set = set()
- _tool_cache = None
- _mcp_server_cache = None
- _env_setup_done = False
- _last_update_check = 0
-
- def __init__(self):
- self.type = "pipe"
- self.id = "copilot"
- self.name = "copilotsdk"
- self.valves = self.Valves()
- self.temp_dir = tempfile.mkdtemp(prefix="copilot_images_")
-
- def __del__(self):
- try:
- shutil.rmtree(self.temp_dir)
- except:
- pass
-
- async def pipe(
- self,
- body: dict,
- __metadata__=None,
- __user__=None,
- __event_emitter__=None,
- __event_call__=None,
- __request__=None,
- ) -> Union[str, AsyncGenerator]:
- return await self._pipe_impl(
- body,
- __metadata__=__metadata__,
- __user__=__user__,
- __event_emitter__=__event_emitter__,
- __event_call__=__event_call__,
- __request__=__request__,
- )
-
- async def _initialize_custom_tools(
- self,
- body: dict = None,
- __user__=None,
- __event_call__=None,
- __request__=None,
- __metadata__=None,
- ):
- """基于配置初始化自定义工具"""
- # 1. 确定有效设置 (用户覆盖 > 全局)
- uv = self._get_user_valves(__user__)
- enable_tools = uv.ENABLE_OPENWEBUI_TOOLS
- enable_openapi = uv.ENABLE_OPENAPI_SERVER
- enable_cache = uv.ENABLE_TOOL_CACHE
-
- # 2. 如果所有工具类型都已禁用,立即返回空
- if not enable_tools and not enable_openapi:
- return []
-
- # 提取 Chat ID 以对齐工作空间
- chat_ctx = self._get_chat_context(body, __metadata__)
- chat_id = chat_ctx.get("chat_id")
-
- # 3. 检查缓存
- if enable_cache and self._tool_cache is not None:
- await self._emit_debug_log("ℹ️ 使用缓存的 OpenWebUI 工具。", __event_call__)
- tools = list(self._tool_cache)
- # 注入文件发布工具
- file_tool = self._get_publish_file_tool(__user__, chat_id, __request__)
- if file_tool:
- tools.append(file_tool)
- return tools
-
- # 动态加载 OpenWebUI 工具
- openwebui_tools = await self._load_openwebui_tools(
- __user__=__user__,
- __event_call__=__event_call__,
- body=body,
- enable_tools=enable_tools,
- enable_openapi=enable_openapi,
- )
-
- # 更新缓存
- if enable_cache:
- self._tool_cache = openwebui_tools
- await self._emit_debug_log(
- "✅ OpenWebUI 工具已缓存,供后续请求使用。", __event_call__
- )
-
- final_tools = list(openwebui_tools)
- # 注入文件发布工具
- file_tool = self._get_publish_file_tool(__user__, chat_id, __request__)
- if file_tool:
- final_tools.append(file_tool)
-
- return final_tools
-
- def _get_publish_file_tool(self, __user__, chat_id, __request__=None):
- """创建发布工作区文件为下载链接的工具"""
- if isinstance(__user__, (list, tuple)):
- user_data = __user__[0] if __user__ else {}
- elif isinstance(__user__, dict):
- user_data = __user__
- else:
- user_data = {}
-
- user_id = user_data.get("id") or user_data.get("user_id")
- if not user_id:
- return None
-
- # 锁定当前聊天的隔离工作空间
- workspace_dir = Path(self._get_workspace_dir(user_id=user_id, chat_id=chat_id))
-
- # 为 SDK 定义参数 Schema
- class PublishFileParams(BaseModel):
- filename: str = Field(
- ...,
- description="你在当前目录创建的文件的确切名称(如 'report.csv')。必填。",
- )
-
- async def publish_file_from_workspace(filename: Any) -> dict:
- """将本地聊天工作区的文件发布为可下载的 URL。"""
- try:
- # 1. 参数鲁棒提取
- if hasattr(filename, "model_dump"): # Pydantic v2
- filename = filename.model_dump().get("filename")
- elif hasattr(filename, "dict"): # Pydantic v1
- filename = filename.dict().get("filename")
-
- if isinstance(filename, dict):
- filename = (
- filename.get("filename")
- or filename.get("file")
- or filename.get("file_path")
- )
-
- if isinstance(filename, str):
- filename = filename.strip()
- if filename.startswith("{"):
- try:
- import json
-
- data = json.loads(filename)
- if isinstance(data, dict):
- filename = (
- data.get("filename") or data.get("file") or filename
- )
- except:
- pass
-
- if (
- not filename
- or not isinstance(filename, str)
- or filename.strip() in ("", "{}", "None", "null")
- ):
- return {
- "error": "缺少必填参数: 'filename'。",
- "hint": "请以字符串形式提供文件名,例如 'report.md'。",
- }
-
- filename = filename.strip()
-
- # 2. 路径解析(锁定当前聊天工作区)
- target_path = workspace_dir / filename
- try:
- target_path = target_path.resolve()
- if not str(target_path).startswith(str(workspace_dir.resolve())):
- return {"error": "拒绝访问:文件必须位于当前聊天工作区内。"}
- except Exception as e:
- return {"error": f"路径校验失败: {e}"}
-
- if not target_path.exists() or not target_path.is_file():
- return {
- "error": f"在聊天工作区未找到文件 '{filename}'。请确保你已将其保存到当前目录 (.)。"
- }
-
- # 3. 通过 API 上传 (兼容 S3)
- api_success = False
- file_id = None
- safe_filename = filename
-
- token = None
- if __request__:
- auth_header = __request__.headers.get("Authorization")
- if auth_header and auth_header.startswith("Bearer "):
- token = auth_header.split(" ")[1]
- if not token and "token" in __request__.cookies:
- token = __request__.cookies.get("token")
-
- if token:
- try:
- import aiohttp
-
- base_url = str(__request__.base_url).rstrip("/")
- upload_url = f"{base_url}/api/v1/files/"
-
- async with aiohttp.ClientSession() as session:
- with open(target_path, "rb") as f:
- data = aiohttp.FormData()
- data.add_field("file", f, filename=target_path.name)
- import json
-
- data.add_field(
- "metadata",
- json.dumps(
- {
- "source": "copilot_workspace_publish",
- "skip_rag": True,
- }
- ),
- )
-
- async with session.post(
- upload_url,
- data=data,
- headers={"Authorization": f"Bearer {token}"},
- ) as resp:
- if resp.status == 200:
- api_res = await resp.json()
- file_id = api_res.get("id")
- safe_filename = api_res.get(
- "filename", target_path.name
- )
- api_success = True
- except Exception as e:
- logger.error(f"API 上传失败: {e}")
-
- # 4. 兜底:手动插入数据库 (仅限本地存储)
- if not api_success:
- file_id = str(uuid.uuid4())
- safe_filename = target_path.name
- dest_path = Path(UPLOAD_DIR) / f"{file_id}_{safe_filename}"
- await asyncio.to_thread(shutil.copy2, target_path, dest_path)
-
- try:
- db_path = str(os.path.relpath(dest_path, DATA_DIR))
- except:
- db_path = str(dest_path)
-
- file_form = FileForm(
- id=file_id,
- filename=safe_filename,
- path=db_path,
- data={"status": "completed", "skip_rag": True},
- meta={
- "name": safe_filename,
- "content_type": mimetypes.guess_type(safe_filename)[0]
- or "text/plain",
- "size": os.path.getsize(dest_path),
- "source": "copilot_workspace_publish",
- "skip_rag": True,
- },
- )
- await asyncio.to_thread(Files.insert_new_file, user_id, file_form)
-
- # 5. 返回结果
- download_url = f"/api/v1/files/{file_id}/content"
- return {
- "file_id": file_id,
- "filename": safe_filename,
- "download_url": download_url,
- "message": "文件发布成功。",
- "hint": f"链接: [下载 {safe_filename}]({download_url})",
- }
- except Exception as e:
- return {"error": str(e)}
-
- return define_tool(
- name="publish_file_from_workspace",
- description="将你在本地工作区创建的文件转换为可下载的 URL。请在完成文件写入当前目录后再使用此工具。",
- params_type=PublishFileParams,
- )(publish_file_from_workspace)
-
- def _json_schema_to_python_type(self, schema: dict) -> Any:
- if not isinstance(schema, dict):
- return Any
- e = schema.get("enum")
- if e and isinstance(e, list):
- return Literal[tuple(e)]
- t = schema.get("type")
- if isinstance(t, list):
- t = next((x for x in t if x != "null"), t[0])
- if t == "string":
- return str
- if t == "integer":
- return int
- if t == "number":
- return float
- if t == "boolean":
- return bool
- if t == "object":
- return Dict[str, Any]
- if t == "array":
- return List[self._json_schema_to_python_type(schema.get("items", {}))]
- return Any
-
- def _convert_openwebui_tool(self, n, d, __event_call__=None):
- sn = re.sub(r"[^a-zA-Z0-9_-]", "_", n)
- if not sn or re.match(r"^[_.-]+$", sn):
- sn = f"tool_{hashlib.md5(n.encode()).hexdigest()[:8]}"
- spec = d.get("spec", {})
- props = spec.get("parameters", {}).get("properties", {})
- req = spec.get("parameters", {}).get("required", [])
- fields = {}
- for pn, ps in props.items():
- pt = self._json_schema_to_python_type(ps)
- fields[pn] = (
- pt if pn in req else Optional[pt],
- Field(
- default=ps.get("default") if pn not in req else ...,
- description=ps.get("description", ""),
- ),
- )
-
- async def _tool(p):
- payload = (
- p.model_dump(exclude_unset=True) if hasattr(p, "model_dump") else {}
- )
- return await d.get("callable")(**payload)
-
- _tool.__name__, _tool.__doc__ = sn, spec.get("description", "") or spec.get(
- "summary", ""
- )
- return define_tool(
- name=sn,
- description=_tool.__doc__,
- params_type=create_model(f"{sn}_Params", **fields),
- )(_tool)
-
- def _build_openwebui_request(self, user=None, token: str = None):
- cfg = SimpleNamespace()
- for i in PERSISTENT_CONFIG_REGISTRY:
- val = i.value
- if hasattr(val, "value"):
- val = val.value
- setattr(cfg, i.env_name, val)
-
- if not hasattr(cfg, "TOOL_SERVER_CONNECTIONS"):
- if hasattr(TOOL_SERVER_CONNECTIONS, "value"):
- cfg.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS.value
- else:
- cfg.TOOL_SERVER_CONNECTIONS = TOOL_SERVER_CONNECTIONS
-
- app_state = SimpleNamespace(
- config=cfg,
- TOOLS={},
- TOOL_CONTENTS={},
- FUNCTIONS={},
- FUNCTION_CONTENTS={},
- MODELS={},
- redis=None,
- TOOL_SERVERS=[],
- )
-
- def url_path_for(name: str, **path_params):
- if name == "get_file_content_by_id":
- return f"/api/v1/files/{path_params.get('id')}/content"
- return f"/mock/{name}"
-
- req_headers = {
- "user-agent": "Copilot-Pipe",
- "host": "localhost:8080",
- "accept": "*/*",
- }
- if token:
- req_headers["Authorization"] = f"Bearer {token}"
-
- return SimpleNamespace(
- app=SimpleNamespace(state=app_state, url_path_for=url_path_for),
- url=SimpleNamespace(
- path="/api/chat/completions",
- base_url="http://localhost:8080",
- __str__=lambda s: "http://localhost:8080/api/chat/completions",
- ),
- base_url="http://localhost:8080",
- headers=req_headers,
- method="POST",
- cookies={},
- state=SimpleNamespace(
- token=SimpleNamespace(credentials=token if token else ""),
- user=user or {},
- ),
- )
-
- async def _load_openwebui_tools(
- self,
- __user__=None,
- __event_call__=None,
- body: dict = None,
- enable_tools: bool = True,
- enable_openapi: bool = True,
- ):
- ud = __user__[0] if isinstance(__user__, (list, tuple)) else (__user__ or {})
- uid = ud.get("id") or ud.get("user_id")
- if not uid:
- return []
- u = Users.get_user_by_id(uid)
- if not u:
- return []
- tids = []
- # 1. 获取用户自定义工具 (Python 脚本)
- if enable_tools:
- tool_items = Tools.get_tools_by_user_id(uid, permission="read")
- if tool_items:
- tids.extend([tool.id for tool in tool_items])
-
- # 2. 获取 OpenAPI 工具服务器工具
- if enable_openapi:
- if hasattr(TOOL_SERVER_CONNECTIONS, "value"):
- tids.extend(
- [
- f"server:{s.get('id')}"
- for s in TOOL_SERVER_CONNECTIONS.value
- if (
- s.get("type", "openapi") == "openapi"
- or s.get("type") is None
- )
- and s.get("id")
- ]
- )
-
- token = None
- if isinstance(body, dict):
- token = body.get("token")
-
- req = self._build_openwebui_request(ud, token)
- td = {}
-
- if tids:
- td = await get_openwebui_tools(
- req,
- tids,
- u,
- {
- "__request__": req,
- "__user__": ud,
- "__event_emitter__": None,
- "__event_call__": __event_call__,
- "__chat_id__": None,
- "__message_id__": None,
- "__model_knowledge__": [],
- "__oauth_token__": {"access_token": token} if token else None,
- },
- )
-
- # 3. 获取内建工具 (网页搜索、内存等)
- if enable_tools:
- try:
- bi = get_builtin_tools(
- req,
- {
- "__user__": ud,
- "__chat_id__": None,
- "__message_id__": None,
- },
- model={
- "info": {
- "meta": {
- "capabilities": {
- "web_search": True,
- "image_generation": True,
- }
- }
- }
- },
- )
- if bi:
- td.update(bi)
- except:
- pass
- return [
- self._convert_openwebui_tool(n, d, __event_call__=__event_call__)
- for n, d in td.items()
- ]
-
- def _get_user_valves(self, __user__: Optional[dict]) -> "Pipe.UserValves":
- """从 __user__ 上下文中稳健地提取 UserValves。"""
- if not __user__:
- return self.UserValves()
-
- # 处理列表/元组包装
- user_data = __user__[0] if isinstance(__user__, (list, tuple)) else __user__
- if not isinstance(user_data, dict):
- return self.UserValves()
-
- raw_valves = user_data.get("valves")
- if isinstance(raw_valves, self.UserValves):
- return raw_valves
- if isinstance(raw_valves, dict):
- try:
- return self.UserValves(**raw_valves)
- except Exception as e:
- logger.warning(f"[Copilot] 解析 UserValves 失败: {e}")
- return self.UserValves()
-
- def _parse_mcp_servers(self, __event_call__=None) -> Optional[dict]:
- if not self.valves.ENABLE_MCP_SERVER:
- return None
- if self.valves.ENABLE_TOOL_CACHE and self._mcp_server_cache is not None:
- return self._mcp_server_cache
- mcp = {}
- conns = (
- getattr(TOOL_SERVER_CONNECTIONS, "value", [])
- if hasattr(TOOL_SERVER_CONNECTIONS, "value")
- else (
- TOOL_SERVER_CONNECTIONS
- if isinstance(TOOL_SERVER_CONNECTIONS, list)
- else []
- )
- )
- for c in conns:
- if not isinstance(c, dict) or c.get("type") != "mcp":
- continue
- info = c.get("info", {})
- rid = info.get("id") or c.get("id") or f"mcp-{len(mcp)}"
- sid = re.sub(r"[^a-zA-Z0-9-]", "-", str(rid)).lower().strip("-")
- url = c.get("url")
- if not url:
- continue
- mtype = "http"
- if "/sse" in url.lower() or "sse" in str(c.get("config", {})).lower():
- mtype = "sse"
- h = c.get("headers", {})
- at, key = str(c.get("auth_type", "bearer")).lower(), c.get("key", "")
- if key and "Authorization" not in h:
- if at == "bearer":
- h["Authorization"] = f"Bearer {key}"
- elif at == "basic":
- h["Authorization"] = (
- f"Basic {base64.b64encode(key.encode()).decode()}"
- )
- elif at in ["api_key", "apikey"]:
- h["X-API-Key"] = key
- ff = c.get("config", {}).get("function_name_filter_list", "")
- allowed = [f.strip() for f in ff.split(",") if f.strip()] if ff else ["*"]
- self._emit_debug_log_sync(
- f"🔌 发现 MCP 节点: {sid} ({mtype.upper()}) | URL: {url}"
- )
- mcp[sid] = {"type": mtype, "url": url, "headers": h, "tools": allowed}
- if self.valves.ENABLE_TOOL_CACHE:
- self._mcp_server_cache = mcp
- return mcp if mcp else None
-
- async def _emit_debug_log(
- self, message: str, __event_call__=None, debug_enabled: Optional[bool] = None
- ):
- is_debug = (
- debug_enabled
- if debug_enabled is not None
- else getattr(self.valves, "DEBUG", False)
- )
- log_msg = f"[Copilot SDK] {message}"
- if is_debug:
- logger.info(log_msg)
- else:
- logger.debug(log_msg)
- if is_debug and __event_call__:
- try:
- js = f"console.debug('%c[Copilot SDK] ' + {json.dumps(message, ensure_ascii=False)}, 'color: #3b82f6;');"
- await __event_call__({"type": "execute", "data": {"code": js}})
- except:
- pass
-
- def _emit_debug_log_sync(
- self, message: str, __event_call__=None, debug_enabled: Optional[bool] = None
- ):
- is_debug = (
- debug_enabled
- if debug_enabled is not None
- else getattr(self.valves, "DEBUG", False)
- )
- log_msg = f"[Copilot SDK] {message}"
- if is_debug:
- logger.info(log_msg)
- else:
- logger.debug(log_msg)
- if is_debug and __event_call__:
- try:
- asyncio.get_running_loop().create_task(
- self._emit_debug_log(message, __event_call__, True)
- )
- except:
- pass
-
- def _get_provider_name(self, mi: Any) -> str:
- mid = getattr(mi, "id", str(mi)).lower()
- if any(k in mid for k in ["gpt", "codex"]):
- return "OpenAI"
- if "claude" in mid:
- return "Anthropic"
- if "gemini" in mid:
- return "Google"
- p = getattr(mi, "policy", None)
- if p:
- t = str(getattr(p, "terms", "")).lower()
- if "openai" in t:
- return "OpenAI"
- if "anthropic" in t:
- return "Anthropic"
- if "google" in t:
- return "Google"
- return "Unknown"
-
- def _clean_model_id(self, mid: str) -> str:
- if "." in mid:
- mid = mid.split(".", 1)[-1]
- for p in ["copilot-", "copilot - "]:
- if mid.startswith(p):
- mid = mid[len(p) :]
- return mid
-
- def _setup_env(self, __event_call__=None, debug_enabled: bool = False):
- if self.__class__._env_setup_done:
- # 即使已完成环境配置,在调试模式下仍同步一次 MCP。
- if debug_enabled:
- self._sync_mcp_config(__event_call__, debug_enabled)
- return
-
- os.environ["COPILOT_AUTO_UPDATE"] = "false"
- cp = os.environ.get("COPILOT_CLI_PATH", "/usr/local/bin/copilot")
- target = self.valves.COPILOT_CLI_VERSION.strip()
-
- # 记录检查时间
- from datetime import datetime
-
- self.__class__._last_update_check = datetime.now().timestamp()
-
- def gv(p):
- try:
- return re.search(
- r"(\d+\.\d+\.\d+)",
- subprocess.check_output(
- [p, "--version"], stderr=subprocess.STDOUT
- ).decode(),
- ).group(1)
- except:
- return None
-
- cv = gv(cp)
- if not cv:
- cp = shutil.which("copilot") or os.path.join(
- os.path.dirname(__file__), "bin", "copilot"
- )
- cv = gv(cp)
- if not cv or (target and target.lstrip("v") > (cv or "")):
- self._emit_debug_log_sync(
- f"正在更新 Copilot CLI 至 {target}...", __event_call__, debug_enabled
- )
- try:
- ev = os.environ.copy()
- if target:
- ev["VERSION"] = target
- subprocess.run(
- "curl -fsSL https://gh.io/copilot-install | bash",
- shell=True,
- check=True,
- env=ev,
- )
- cp, cv = "/usr/local/bin/copilot", gv("/usr/local/bin/copilot")
- except:
- pass
- os.environ["COPILOT_CLI_PATH"] = cp
- self.__class__._env_setup_done = True
- self._sync_mcp_config(__event_call__, debug_enabled)
-
- def _sync_mcp_config(self, __event_call__=None, debug_enabled: bool = False):
- if not self.valves.ENABLE_MCP_SERVER:
- return
- mcp = self._parse_mcp_servers(__event_call__)
- if not mcp:
- return
- try:
- path = os.path.expanduser("~/.copilot/config.json")
- os.makedirs(os.path.dirname(path), exist_ok=True)
- data = {}
- if os.path.exists(path):
- try:
- with open(path, "r") as f:
- data = json.load(f)
- except:
- pass
- if json.dumps(data.get("mcp_servers"), sort_keys=True) != json.dumps(
- mcp, sort_keys=True
- ):
- data["mcp_servers"] = mcp
- with open(path, "w") as f:
- json.dump(data, f, indent=4)
- self._emit_debug_log_sync(
- f"已将 {len(mcp)} 个 MCP 节点同步至配置文件",
- __event_call__,
- debug_enabled,
- )
- except:
- pass
-
- def _get_workspace_dir(self, user_id: str = None, chat_id: str = None) -> str:
- """获取具有用户和聊天隔离的有效工作区目录"""
- d = self.valves.WORKSPACE_DIR
- base_cwd = (
- d
- if d
- else (
- "/app/backend/data/copilot_workspace"
- if os.path.exists("/app/backend/data")
- else os.path.join(os.getcwd(), "copilot_workspace")
- )
- )
-
- cwd = base_cwd
- if user_id:
- safe_user_id = re.sub(r"[^a-zA-Z0-9_-]", "_", str(user_id))
- cwd = os.path.join(cwd, safe_user_id)
- if chat_id:
- safe_chat_id = re.sub(r"[^a-zA-Z0-9_-]", "_", str(chat_id))
- cwd = os.path.join(cwd, safe_chat_id)
-
- try:
- os.makedirs(cwd, exist_ok=True)
- return cwd
- except Exception as e:
- return base_cwd
-
- def _process_images(
- self, messages, __event_call__=None, debug_enabled: bool = False
- ):
- if not messages:
- return "", []
- last = messages[-1].get("content", "")
- if not isinstance(last, list):
- return str(last), []
- text, att = "", []
- for item in last:
- if item.get("type") == "text":
- text += item.get("text", "")
- elif item.get("type") == "image_url":
- url = item.get("image_url", {}).get("url", "")
- if url.startswith("data:image"):
- try:
- h, e = url.split(",", 1)
- ext = h.split(";")[0].split("/")[-1]
- path = os.path.join(self.temp_dir, f"img_{len(att)}.{ext}")
- with open(path, "wb") as f:
- f.write(base64.b64decode(e))
- att.append(
- {
- "type": "file",
- "path": path,
- "display_name": f"img_{len(att)}",
- }
- )
- except:
- pass
- return text, att
-
- async def _fetch_byok_models(self, uv: "Pipe.UserValves" = None) -> List[dict]:
- """从配置的提供商获取 BYOK 模型。"""
- model_list = []
-
- # 确定有效配置 (用户 > 全局)
- effective_base_url = (
- uv.BYOK_BASE_URL if uv else ""
- ) or self.valves.BYOK_BASE_URL
- effective_type = (uv.BYOK_TYPE if uv else "") or self.valves.BYOK_TYPE
- effective_api_key = (uv.BYOK_API_KEY if uv else "") or self.valves.BYOK_API_KEY
- effective_bearer_token = (
- uv.BYOK_BEARER_TOKEN if uv else ""
- ) or self.valves.BYOK_BEARER_TOKEN
- effective_models = (uv.BYOK_MODELS if uv else "") or self.valves.BYOK_MODELS
-
- if effective_base_url:
- try:
- base_url = effective_base_url.rstrip("/")
- url = f"{base_url}/models"
- headers = {}
- provider_type = effective_type.lower()
-
- if provider_type == "anthropic":
- if effective_api_key:
- headers["x-api-key"] = effective_api_key
- headers["anthropic-version"] = "2023-06-01"
- else:
- if effective_bearer_token:
- headers["Authorization"] = f"Bearer {effective_bearer_token}"
- elif effective_api_key:
- headers["Authorization"] = f"Bearer {effective_api_key}"
-
- timeout = aiohttp.ClientTimeout(total=60)
- async with aiohttp.ClientSession(timeout=timeout) as session:
- for attempt in range(3):
- try:
- async with session.get(url, headers=headers) as resp:
- if resp.status == 200:
- data = await resp.json()
- if (
- isinstance(data, dict)
- and "data" in data
- and isinstance(data["data"], list)
- ):
- for item in data["data"]:
- if isinstance(item, dict) and "id" in item:
- model_list.append(item["id"])
- elif isinstance(data, list):
- for item in data:
- if isinstance(item, dict) and "id" in item:
- model_list.append(item["id"])
-
- await self._emit_debug_log(
- f"BYOK: 从 {url} 获取了 {len(model_list)} 个模型"
- )
- break
- else:
- await self._emit_debug_log(
- f"BYOK: 获取模型失败 {url} (尝试 {attempt+1}/3). 状态码: {resp.status}"
- )
- except Exception as e:
- await self._emit_debug_log(
- f"BYOK: 模型获取错误 (尝试 {attempt+1}/3): {e}"
- )
-
- if attempt < 2:
- await asyncio.sleep(1)
-
- except Exception as e:
- await self._emit_debug_log(f"BYOK: 设置错误: {e}")
-
- # 如果自动获取失败,回退到手动配置列表
- if not model_list:
- if effective_models.strip():
- model_list = [
- m.strip() for m in effective_models.split(",") if m.strip()
- ]
- await self._emit_debug_log(
- f"BYOK: 使用用户手动配置的 BYOK_MODELS ({len(model_list)} 个模型)."
- )
-
- return [
- {
- "id": m,
- "name": f"-{self._clean_model_id(m)}",
- "source": "byok",
- "provider": effective_type.capitalize(),
- "raw_id": m,
- }
- for m in model_list
- ]
-
- def _build_session_config(
- self,
- cid,
- rmid,
- tools,
- sysp,
- ist,
- prov=None,
- eff="medium",
- isr=False,
- cthr=None,
- bthr=None,
- ec=None,
- uid=None,
- ):
- from copilot.types import SessionConfig, InfiniteSessionConfig
-
- inf = (
- InfiniteSessionConfig(
- enabled=True,
- background_compaction_threshold=cthr
- or self.valves.COMPACTION_THRESHOLD,
- buffer_exhaustion_threshold=bthr or self.valves.BUFFER_THRESHOLD,
- )
- if self.valves.INFINITE_SESSION
- else None
- )
- p = {
- "session_id": cid,
- "model": rmid,
- "streaming": ist,
- "tools": tools,
- "system_message": {
- "content": (sysp.strip() + "\n" if sysp else "")
- + FORMATTING_GUIDELINES
- + (
- f"\n[会话上下文]\n"
- f"- **您的隔离工作区**: `{self._get_workspace_dir(uid, cid)}`\n"
- f"- **活跃会话 ID**: `{cid}`\n"
- "**关键指令**: 所有文件操作必须在这个上述工作区进行。\n"
- "- **不要**在 `/tmp` 或其他系统目录创建文件。\n"
- "- 始终将“当前目录”理解为您的隔离工作区。"
- ),
- "mode": "replace",
- },
- "infinite_sessions": inf,
- "working_directory": self._get_workspace_dir(uid, cid),
- }
- if isr and eff:
- m = next((x for x in self._model_cache if x.get("raw_id") == rmid), None)
- supp = (
- m.get("meta", {})
- .get("capabilities", {})
- .get("supported_reasoning_efforts", [])
- if m
- else []
- )
- p["reasoning_effort"] = (
- (eff if eff in supp else ("high" if "high" in supp else "medium"))
- if supp
- else eff
- )
- if self.valves.ENABLE_MCP_SERVER:
- mcp = self._parse_mcp_servers(ec)
- if mcp:
- p["mcp_servers"], p["available_tools"] = mcp, None
- else:
- p["available_tools"] = [t.name for t in tools] if tools else None
- if prov:
- p["provider"] = prov
-
- # 注入自动大文件处理钩子
- wd = p.get("working_directory", "")
- p["hooks"] = self._build_session_hooks(cwd=wd, __event_call__=ec)
-
- return SessionConfig(**p)
-
- def _build_session_hooks(self, cwd: str, __event_call__=None):
- """
- 构建会话生命周期钩子。
- 当前实现:
- - on_post_tool_use: 自动将 /tmp 中的大文件复制到工作区
- """
-
- async def on_post_tool_use(input_data, invocation):
- result = input_data.get("result", "")
-
- # 检测并移动 /tmp 中保存的大文件
- # 模式: Saved to: /tmp/copilot_result_xxxx.txt
- import re
- import shutil
-
- # 搜索输出中潜在的 /tmp 文件路径
- # 常见 CLI 模式: "Saved to: /tmp/..." 或仅 "/tmp/..."
- match = re.search(r"(/tmp/[\w\-\.]+)", str(result))
- if match:
- tmp_path = match.group(1)
- if os.path.exists(tmp_path):
- try:
- filename = os.path.basename(tmp_path)
- target_path = os.path.join(cwd, f"auto_output_{filename}")
- shutil.copy2(tmp_path, target_path)
-
- self._emit_debug_log_sync(
- f"Hook [on_post_tool_use]: 自动将大文件输出从 {tmp_path} 移动到 {target_path}",
- __event_call__,
- )
-
- return {
- "additionalContext": (
- f"\n[系统自动管理] 输出内容过大,最初保存在 {tmp_path}。\n"
- f"我已经自动将其移动到您的工作区,文件名为: `{os.path.basename(target_path)}`。\n"
- f"您现在应该对该文件使用 `read_file` 或 `grep` 来访问内容。"
- )
- }
- except Exception as e:
- self._emit_debug_log_sync(
- f"Hook [on_post_tool_use] 移动文件错误: {e}",
- __event_call__,
- )
-
- return {}
-
- return {
- "on_post_tool_use": on_post_tool_use,
- }
-
- async def _pipe_impl(
- self,
- body,
- __metadata__=None,
- __user__=None,
- __event_emitter__=None,
- __event_call__=None,
- __request__=None,
- ) -> Union[str, AsyncGenerator]:
- ud = __user__[0] if isinstance(__user__, (list, tuple)) else (__user__ or {})
- uid = ud.get("id") or ud.get("user_id") or "default_user"
- uv = self.UserValves(**(__user__.get("valves", {}) if __user__ else {}))
- debug = self.valves.DEBUG or uv.DEBUG
- self._setup_env(__event_call__, debug)
- byok_active = bool(
- self.valves.BYOK_BASE_URL
- and (
- uv.BYOK_API_KEY
- or self.valves.BYOK_API_KEY
- or self.valves.BYOK_BEARER_TOKEN
- )
- )
- if not self.valves.GH_TOKEN and not byok_active:
- return "Error: 配置缺失。"
- rid = (
- __metadata__.get("base_model_id")
- if __metadata__ and __metadata__.get("base_model_id")
- else body.get("model", "")
- )
- rmid = self._clean_model_id(rid)
- mi = next(
- (x for x in (self._model_cache or []) if x.get("raw_id") == rmid), None
- )
- isr = (
- mi.get("meta", {}).get("capabilities", {}).get("reasoning", False)
- if mi
- else any(k in rmid.lower() for k in ["gpt", "codex"])
- )
- isb = (
- mi.get("source") == "byok"
- if mi
- else (
- not bool(re.search(r"[\((]\d+(?:\.\d+)?x[\))]", rid)) and byok_active
- )
- )
- cid = str(
- (__metadata__ or {}).get("chat_id")
- or body.get("chat_id")
- or body.get("metadata", {}).get("chat_id", "")
- ).strip()
- sysp, _ = await self._extract_system_prompt(
- body,
- body.get("messages", []),
- body.get("model", ""),
- rmid,
- __event_call__,
- debug,
- )
- text, att = self._process_images(
- body.get("messages", []), __event_call__, debug
- )
- client = CopilotClient(self._build_client_config(body, uid, cid))
- try:
- await client.start()
- # 同步更新工具初始化参数
- tools = await self._initialize_custom_tools(
- body=body,
- __user__=__user__,
- __event_call__=__event_call__,
- __request__=__request__,
- __metadata__=__metadata__,
- )
- prov = (
- {
- "type": (uv.BYOK_TYPE or self.valves.BYOK_TYPE).lower() or "openai",
- "wire_api": (uv.BYOK_WIRE_API or self.valves.BYOK_WIRE_API),
- "base_url": uv.BYOK_BASE_URL or self.valves.BYOK_BASE_URL,
- }
- if isb
- else None
- )
- if prov:
- if uv.BYOK_API_KEY or self.valves.BYOK_API_KEY:
- prov["api_key"] = uv.BYOK_API_KEY or self.valves.BYOK_API_KEY
- if self.valves.BYOK_BEARER_TOKEN:
- prov["bearer_token"] = self.valves.BYOK_BEARER_TOKEN
- session = None
- if cid:
- try:
- rp = {
- "model": rmid,
- "streaming": body.get("stream", False),
- "tools": tools,
- "system_message": {
- "mode": "replace",
- "content": (sysp.strip() + "\n" if sysp else "")
- + FORMATTING_GUIDELINES,
- },
- }
- if self.valves.ENABLE_MCP_SERVER:
- mcp = self._parse_mcp_servers(__event_call__)
- if mcp:
- rp["mcp_servers"], rp["available_tools"] = mcp, None
- else:
- rp["available_tools"] = (
- [t.name for t in tools] if tools else None
- )
- if isr:
- eff = uv.REASONING_EFFORT or self.valves.REASONING_EFFORT
- supp = (
- mi.get("meta", {})
- .get("capabilities", {})
- .get("supported_reasoning_efforts", [])
- if mi
- else []
- )
- rp["reasoning_effort"] = (
- (
- eff
- if eff in supp
- else ("high" if "high" in supp else "medium")
- )
- if supp
- else eff
- )
- if prov:
- rp["provider"] = prov
- session = await client.resume_session(cid, rp)
- except:
- pass
- if not session:
- session = await client.create_session(
- config=self._build_session_config(
- cid,
- rmid,
- tools,
- sysp,
- body.get("stream", False),
- prov,
- uv.REASONING_EFFORT or self.valves.REASONING_EFFORT,
- isr,
- uv.COMPACTION_THRESHOLD,
- uv.BUFFER_THRESHOLD,
- __event_call__,
- uid,
- )
- )
- if body.get("stream", False):
- return self.stream_response(
- client,
- session,
- {"prompt": text, "mode": "immediate", "attachments": att},
- "",
- __event_call__,
- uv.REASONING_EFFORT or self.valves.REASONING_EFFORT,
- uv.SHOW_THINKING,
- debug,
- )
- else:
- r = await session.send_and_wait(
- {"prompt": text, "mode": "immediate", "attachments": att}
- )
- return r.data.content if r else "空响应。"
- except Exception as e:
- return f"错误: {e}"
- finally:
- if not body.get("stream"):
- await client.stop()
-
- async def pipes(self, __user__: Optional[dict] = None) -> List[dict]:
- # 获取用户配置
- uv = self._get_user_valves(__user__)
- token = uv.GH_TOKEN or self.valves.GH_TOKEN
-
- # 环境初始化 (带有 24 小时冷却时间)
- from datetime import datetime
-
- now = datetime.now().timestamp()
- if not self.__class__._env_setup_done or (
- now - self.__class__._last_update_check > 86400
- ):
- self._setup_env(debug_enabled=uv.DEBUG or self.valves.DEBUG, token=token)
- elif token:
- os.environ["GH_TOKEN"] = os.environ["GITHUB_TOKEN"] = token
-
- # 确定倍率限制
- eff_max = self.valves.MAX_MULTIPLIER
- if uv.MAX_MULTIPLIER is not None:
- eff_max = uv.MAX_MULTIPLIER
-
- # 确定关键词和提供商过滤
- ex_kw = [
- k.strip().lower()
- for k in (self.valves.EXCLUDE_KEYWORDS + "," + uv.EXCLUDE_KEYWORDS).split(
- ","
- )
- if k.strip()
- ]
- allowed_p = [
- p.strip().lower()
- for p in (uv.PROVIDERS if uv.PROVIDERS else self.valves.PROVIDERS).split(
- ","
- )
- if p.strip()
- ]
-
- # --- 新增:配置感知缓存刷新 ---
- # 计算当前配置指纹以检测变化
- current_config_str = f"{token}|{(uv.BYOK_BASE_URL if uv else '') or self.valves.BYOK_BASE_URL}|{(uv.BYOK_API_KEY if uv else '') or self.valves.BYOK_API_KEY}|{(uv.BYOK_BEARER_TOKEN if uv else '') or self.valves.BYOK_BEARER_TOKEN}"
- import hashlib
-
- current_config_hash = hashlib.md5(current_config_str.encode()).hexdigest()
-
- if (
- self._model_cache
- and self.__class__._last_byok_config_hash != current_config_hash
- ):
- self.__class__._model_cache = []
- self.__class__._last_byok_config_hash = current_config_hash
-
- # 如果缓存为空,刷新模型列表
- if not self._model_cache:
- self.__class__._last_byok_config_hash = current_config_hash
- byok_models = []
- standard_models = []
-
- # 1. 获取 BYOK 模型 (优先使用个人设置)
- if ((uv.BYOK_BASE_URL if uv else "") or self.valves.BYOK_BASE_URL) and (
- (uv.BYOK_API_KEY if uv else "")
- or self.valves.BYOK_API_KEY
- or (uv.BYOK_BEARER_TOKEN if uv else "")
- or self.valves.BYOK_BEARER_TOKEN
- ):
- byok_models = await self._fetch_byok_models(uv=uv)
-
- # 2. 获取标准 Copilot 模型
- if token:
- c = await self._get_client()
- try:
- raw_models = await c.list_models()
- raw = raw_models if isinstance(raw_models, list) else []
- processed = []
-
- for m in raw:
- try:
- m_is_dict = isinstance(m, dict)
- mid = m.get("id") if m_is_dict else getattr(m, "id", str(m))
- bill = (
- m.get("billing")
- if m_is_dict
- else getattr(m, "billing", None)
- )
- if bill and not isinstance(bill, dict):
- bill = (
- bill.to_dict()
- if hasattr(bill, "to_dict")
- else vars(bill)
- )
-
- pol = (
- m.get("policy")
- if m_is_dict
- else getattr(m, "policy", None)
- )
- if pol and not isinstance(pol, dict):
- pol = (
- pol.to_dict()
- if hasattr(pol, "to_dict")
- else vars(pol)
- )
-
- if (pol or {}).get("state") == "disabled":
- continue
-
- cap = (
- m.get("capabilities")
- if m_is_dict
- else getattr(m, "capabilities", None)
- )
- vis, reas, ctx, supp = False, False, None, []
- if cap:
- if not isinstance(cap, dict):
- cap = (
- cap.to_dict()
- if hasattr(cap, "to_dict")
- else vars(cap)
- )
- s = cap.get("supports", {})
- vis, reas = s.get("vision", False), s.get(
- "reasoning_effort", False
- )
- l = cap.get("limits", {})
- ctx = l.get("max_context_window_tokens")
-
- raw_eff = (
- m.get("supported_reasoning_efforts")
- if m_is_dict
- else getattr(m, "supported_reasoning_efforts", [])
- ) or []
- supp = [str(e).lower() for e in raw_eff if e]
- mult = (bill or {}).get("multiplier", 1)
- cid = self._clean_model_id(mid)
- processed.append(
- {
- "id": f"{self.id}-{mid}",
- "name": (
- f"-{cid} ({mult}x)"
- if mult > 0
- else f"-🔥 {cid} (0x)"
- ),
- "multiplier": mult,
- "raw_id": mid,
- "source": "copilot",
- "provider": self._get_provider_name(m),
- "meta": {
- "capabilities": {
- "vision": vis,
- "reasoning": reas,
- "supported_reasoning_efforts": supp,
- },
- "context_length": ctx,
- },
- }
- )
- except:
- continue
-
- processed.sort(key=lambda x: (x["multiplier"], x["raw_id"]))
- standard_models = processed
- self._standard_model_ids = {m["raw_id"] for m in processed}
- except:
- pass
- finally:
- await c.stop()
-
- self._model_cache = standard_models + byok_models
-
- if not self._model_cache:
- return [
- {"id": "error", "name": "未找到任何模型。请检查 Token 或 BYOK 配置。"}
- ]
-
- # 3. 实时过滤结果
- res = []
- for m in self._model_cache:
- # 提供商过滤
- if allowed_p and m.get("provider", "Unknown").lower() not in allowed_p:
- continue
-
- mid, mname = (m.get("raw_id") or m.get("id", "")).lower(), m.get(
- "name", ""
- ).lower()
- # 关键词过滤
- if any(kw in mid or kw in mname for kw in ex_kw):
- continue
-
- # 倍率限制 (仅限 Copilot 官方模型)
- if m.get("source") == "copilot":
- if float(m.get("multiplier", 1)) > (float(eff_max) + 0.0001):
- continue
-
- res.append(m)
-
- return res if res else [{"id": "none", "name": "没有匹配当前过滤条件的模型"}]
-
- async def stream_response(
- self,
- client,
- session,
- payload,
- init_msg,
- __event_call__,
- effort="",
- show_thinking=True,
- debug=False,
- ) -> AsyncGenerator:
- queue, done, sentinel = asyncio.Queue(), asyncio.Event(), object()
- state = {"thinking_started": False, "content_sent": False}
- has_content = False
-
- def handler(event):
- etype = (
- event.type.value if hasattr(event.type, "value") else str(event.type)
- )
-
- def get_attr(a):
- if not hasattr(event, "data") or event.data is None:
- return None
- return (
- event.data.get(a)
- if isinstance(event.data, dict)
- else getattr(event.data, a, None)
- )
-
- if etype == "assistant.message_delta":
- delta = (
- get_attr("delta_content")
- or get_attr("deltaContent")
- or get_attr("content")
- )
- if delta:
- state["content_sent"] = True
- if state["thinking_started"]:
- queue.put_nowait("\n\n")
- state["thinking_started"] = False
- queue.put_nowait(delta)
- elif etype == "assistant.reasoning_delta":
- delta = (
- get_attr("reasoning_text")
- or get_attr("reasoningText")
- or get_attr("delta_content")
- )
- if delta and not state["content_sent"] and show_thinking:
- if not state["thinking_started"]:
- queue.put_nowait("\n")
- state["thinking_started"] = True
- queue.put_nowait(delta)
- elif etype == "assistant.usage":
- queue.put_nowait(
- {
- "choices": [{"delta": {}, "finish_reason": "stop", "index": 0}],
- "usage": {
- "prompt_tokens": get_attr("input_tokens") or 0,
- "completion_tokens": get_attr("output_tokens") or 0,
- "total_tokens": get_attr("total_tokens") or 0,
- },
- }
- )
- elif etype == "session.idle":
- done.set()
- queue.put_nowait(sentinel)
- elif etype == "session.error":
- queue.put_nowait(f"\n[错误: {get_attr('message')}]")
- done.set()
- queue.put_nowait(sentinel)
-
- unsubscribe = session.on(handler)
- asyncio.create_task(session.send(payload))
- try:
- while not done.is_set() or not queue.empty():
- try:
- chunk = await asyncio.wait_for(
- queue.get(), timeout=float(self.valves.TIMEOUT)
- )
- if chunk is sentinel:
- break
- if chunk:
- has_content = True
- yield chunk
- except asyncio.TimeoutError:
- if done.is_set():
- break
- continue
- if state["thinking_started"]:
- yield "\n\n"
- if not has_content:
- yield "⚠️ 未返回内容。"
- except Exception as e:
- yield f"\n[流错误: {e}]"
- finally:
- unsubscribe()
- await client.stop()
-
- async def _extract_system_prompt(
- self,
- body,
- messages,
- request_model,
- real_model_id,
- __event_call__=None,
- debug_enabled=False,
- ):
- sysp, src = None, ""
- if body.get("system_prompt"):
- sysp, src = body.get("system_prompt"), "body_explicit"
- if not sysp:
- meta = body.get("metadata", {}).get("model", {}).get("params", {})
- if meta.get("system"):
- sysp, src = meta.get("system"), "metadata_params"
- if not sysp:
- try:
- from open_webui.models.models import Models
-
- for mid in [request_model, real_model_id]:
- m_rec = Models.get_model_by_id(mid)
- if (
- m_rec
- and hasattr(m_rec, "params")
- and isinstance(m_rec.params, dict)
- and m_rec.params.get("system")
- ):
- sysp, src = m_rec.params.get("system"), f"模型库:{mid}"
- break
- except:
- pass
- if not sysp:
- for msg in messages:
- if msg.get("role") == "system":
- sysp, src = msg.get("content", ""), "消息历史"
- break
- if sysp:
- await self._emit_debug_log(
- f"系统提示词来源: {src} ({len(sysp)} 字符)",
- __event_call__,
- debug_enabled,
- )
- return sysp, src
-
- def _build_client_config(self, body, user_id=None, chat_id=None):
- c = {
- "cli_path": os.environ.get("COPILOT_CLI_PATH"),
- "cwd": self._get_workspace_dir(user_id, chat_id),
- }
- if self.valves.LOG_LEVEL:
- c["log_level"] = self.valves.LOG_LEVEL
- if self.valves.CUSTOM_ENV_VARS:
- try:
- e = json.loads(self.valves.CUSTOM_ENV_VARS)
- c.update({"env": e}) if isinstance(e, dict) else None
- except:
- pass
- return c
diff --git a/plugins/pipes/github-copilot-sdk/v0.7.0.md b/plugins/pipes/github-copilot-sdk/v0.7.0.md
new file mode 100644
index 0000000..590b07d
--- /dev/null
+++ b/plugins/pipes/github-copilot-sdk/v0.7.0.md
@@ -0,0 +1,82 @@
+# GitHub Copilot SDK Pipe v0.7.0
+
+**GitHub Copilot SDK Pipe v0.7.0** — A major infrastructure and UX upgrade. This release eliminates manual CLI management, fully embraces OpenWebUI's native tool calling interface, and ensures seamless compatibility with the latest OpenWebUI versions.
+
+---
+
+## 📦 Quick Installation
+
+- **GitHub Copilot SDK (Pipe)**: [Install v0.7.0](https://openwebui.com/posts/ce96f7b4-12fc-4ac3-9a01-875713e69359)
+- **GitHub Copilot SDK (Filter)**: [Install v0.1.2](https://openwebui.com/posts/403a62ee-a596-45e7-be65-fab9cc249dd6)
+
+---
+
+## 🚀 What's New in v0.7.0
+
+### 1. Zero-Maintenance CLI Integration
+
+The most significant infrastructure change: you no longer need to worry about CLI versions or background downloads.
+
+| Before (v0.6.x) | After (v0.7.0) |
+| :--- | :--- |
+| CLI installed via background `curl \| bash` | CLI bundled inside the `github-copilot-sdk` pip package |
+| Version mismatches between SDK and CLI | Versions are always in sync automatically |
+| Fails in restricted networks | Works everywhere `pip install` works |
+
+**How it works**: When you install `github-copilot-sdk==0.1.25`, the matching `copilot-cli v0.0.411` is included. The plugin auto-discovers the path and injects it into the environment—zero configuration required.
+
+### 2. Native OpenWebUI Tool Call UI
+
+Tool calls from Copilot agents now render using **OpenWebUI's built-in tool call UI**.
+
+- Tool execution status is displayed natively in the chat interface.
+- Thinking processes (Chain of Thought) are visualized with the standard collapsible UI.
+- Improved visual consistency and integration with the main OpenWebUI interface.
+
+### 3. OpenWebUI v0.8.0+ Compatibility Fix (Bug Fix)
+
+Resolved the **"Error getting file content"** failure that affected users on OpenWebUI v0.8.0 and later.
+
+- **The Issue**: Relative path registration for published files was rejected by the latest OpenWebUI versions.
+- **The Fix**: Switched to **absolute path registration**, restoring the ability to download generated artifacts to your local machine.
+
+### 4. Comprehensive Multi-language Support (i18n)
+
+Native localization for status messages and UI hints in **11 languages**:
+*English, Chinese (Simp/Trad/HK/TW), Japanese, Korean, French, German, Spanish, Italian, Russian, Vietnamese, and Indonesian.*
+
+### 5. Reasoning Status & UX Optimizations
+
+- **Intelligent Status Display**: `Reasoning Effort injected` status is now only shown for native Copilot reasoning models.
+- **Clean UI**: Removed redundant debug/status noise for BYOK and standard models.
+- **Architecture Cleanup**: Refactored core setup and removed legacy installation code for a robust "one-click" experience.
+
+---
+
+## 🛠️ Key Capabilities
+
+| Feature | Description |
+| :--- | :--- |
+| **Universal Tool Protocol** | Native support for **MCP**, **OpenAPI**, and **OpenWebUI built-in tools**. |
+| **Native Tool Call UI** | Adapted to OpenWebUI's built-in tool call rendering. |
+| **Workspace Isolation** | Strict sandboxing for per-session data privacy and security. |
+| **Workspace Artifacts** | Agents generate files (Excel/CSV/HTML) with persistent download links via `publish_file_from_workspace`. |
+| **Tool Execution** | Direct access to system binaries (Python, FFmpeg, Git, etc.). |
+| **11-Language Localization** | Auto-detected, native status messages for global users. |
+| **OpenWebUI v0.8.0+ Support** | Robust file handling for the latest OpenWebUI platform versions. |
+
+---
+
+## 📥 Import Chat Templates
+
+- [📥 Star Prediction Chat log](https://fu-jie.github.io/awesome-openwebui/plugins/pipes/star-prediction-chat.json)
+- [📥 Video Processing Chat log](https://fu-jie.github.io/awesome-openwebui/plugins/pipes/video-processing-chat.json)
+
+*Settings -> Data -> Import Chats.*
+
+---
+
+## 🔗 Resources
+
+- **GitHub Repository**: [openwebui-extensions](https://github.com/Fu-Jie/openwebui-extensions)
+- **Full Changelog**: [README.md](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/pipes/github-copilot-sdk/README.md)