From 6f8c87165877da22e8ac2801314e9e687648db19 Mon Sep 17 00:00:00 2001 From: fujie Date: Fri, 20 Mar 2026 03:26:43 +0800 Subject: [PATCH] feat(github-copilot-sdk): bump version to v0.11.0 for performance & stability - Fixed shared client pool bug to eliminate TTFT latency. - Added pure BYOK-only mode support. - Improved cross-user environment isolation for concurrency. - Resolved RichUI infinite vertical sizing loop. - Integrated client.ping() into stall detection. - Automatically hide TODO List widget after completion. - Synced all documentation and release notes. --- .agent/learnings/README.md | 3 + .../github-copilot-sdk-hang-analysis.md | 72 + .../learnings/richui-declarative-priority.md | 27 + .../richui-default-actions-optout.md | 23 + .agent/learnings/richui-interaction-api.md | 89 + .continues-handoff.md | 173 ++ README.md | 12 +- README_CN.md | 12 +- docs/plugins/pipes/github-copilot-sdk.md | 122 +- docs/plugins/pipes/github-copilot-sdk.zh.md | 122 +- docs/plugins/pipes/index.md | 2 +- docs/plugins/pipes/index.zh.md | 2 +- plugins/pipes/github-copilot-sdk/README.md | 109 +- plugins/pipes/github-copilot-sdk/README_CN.md | 109 +- .../github-copilot-sdk/github_copilot_sdk.py | 2062 ++++++++++++++++- plugins/pipes/github-copilot-sdk/v0.11.0.md | 23 + .../pipes/github-copilot-sdk/v0.11.0_CN.md | 23 + 17 files changed, 2862 insertions(+), 123 deletions(-) create mode 100644 .agent/learnings/github-copilot-sdk-hang-analysis.md create mode 100644 .agent/learnings/richui-declarative-priority.md create mode 100644 .agent/learnings/richui-default-actions-optout.md create mode 100644 .agent/learnings/richui-interaction-api.md create mode 100644 .continues-handoff.md create mode 100644 plugins/pipes/github-copilot-sdk/v0.11.0.md create mode 100644 plugins/pipes/github-copilot-sdk/v0.11.0_CN.md diff --git a/.agent/learnings/README.md b/.agent/learnings/README.md index c05a521..8f71306 100644 --- a/.agent/learnings/README.md +++ b/.agent/learnings/README.md @@ -44,3 +44,6 @@ Edge cases or caveats to watch out for. | [openwebui-tool-injection.md](./openwebui-tool-injection.md) | How OpenWebUI injects parameters into Tool functions, and what the Pipe must provide | | [openwebui-mock-request.md](./openwebui-mock-request.md) | How to build a valid Mock Request for calling OpenWebUI-internal APIs from a Pipe | | [copilot-plan-mode-prompt-parity.md](./copilot-plan-mode-prompt-parity.md) | Why Plan Mode prompt logic must be shared between fresh-session and resume-session injection | +| [richui-default-actions-optout.md](./richui-default-actions-optout.md) | How static RichUI widgets opt out of fallback prompt/link action injection | +| [richui-declarative-priority.md](./richui-declarative-priority.md) | How RichUI resolves priority between declarative actions and inline click handlers | +| [richui-interaction-api.md](./richui-interaction-api.md) | Recommended 4-action RichUI interaction contract for chat continuation, prefill, submit, and links | diff --git a/.agent/learnings/github-copilot-sdk-hang-analysis.md b/.agent/learnings/github-copilot-sdk-hang-analysis.md new file mode 100644 index 0000000..87060d7 --- /dev/null +++ b/.agent/learnings/github-copilot-sdk-hang-analysis.md @@ -0,0 +1,72 @@ +# GitHub Copilot SDK 卡顿/悬停问题深度源码分析报告 + +## 📌 问题现象 + +用户反馈在 agent 处理过程中(工具调用、思考、内容输出),`github_copilot_sdk.py` 管道偶尔会卡住(界面转圈不停)。 + +## 🔍 事件流架构(SDK 源码分析) + +通过阅读 `copilot-sdk` 源码 (`jsonrpc.py`, `client.py`, `session.py`),事件流路径如下: + +``` +Copilot CLI (subprocess) + └─ stdout (JSON-RPC over stdio) + └─ JsonRpcClient._read_loop() [daemon thread] + └─ _handle_message() + └─ notification_handler("session.event", params) [线程安全调度到 event loop] + └─ CopilotSession._dispatch_event(event) + └─ plugin handler(event) → queue.put_nowait(chunk) + └─ main loop: await queue.get() → yield chunk +``` + +## 🚨 已确认的三个卡顿根因 + +### 根因 1: Stall 检测豁免盲区 + +原始代码的防卡死检测仅在 `content_sent=False` 且 `thinking_started=False` 且 `running_tool_calls` 为空时才触发。一旦 agent 开始处理(输出内容/调用工具),所有豁免条件为真,防卡死机制永久失效。 + +### 根因 2: 工具调用状态泄漏 + +`running_tool_calls.add(tool_call_id)` 在 `tool.execution_start` 时添加,但如果 SDK 连接断开导致 `tool.execution_complete` 事件丢失,集合永远不为空,直接阻塞 Stall 检测。 + +### 根因 3: `session.abort()` 自身可能卡住(SDK 源码确认) + +**SDK 源码关键证据** (`jsonrpc.py:107-148`): + +```python +async def request(self, method, params=None, timeout=None): + ... + if timeout is not None: + return await asyncio.wait_for(future, timeout=timeout) + return await future # ← 无 timeout,永久等待! +``` + +`session.abort()` 底层调用 `self._client.request("session.abort", ...)` **没有传 timeout**。 +当 CLI 进程挂死但 `_read_loop` 尚未检测到断流(例如 TCP 半开连接),`abort()` RPC 自身会无限等待响应,造成**修复代码自身也卡住**。 + +## ✅ 修复记录 (2026-03-18) + +### 修复 1: `assistant.turn_end` / `session.error` 兜底清理 + +`running_tool_calls.clear()` — 即时清除孤儿工具状态。 + +### 修复 2: 绝对不活跃保护 (Absolute Inactivity Guard) + +当距最后一个事件超过 `min(TIMEOUT, 90) × 2 = 180s` 且无任何新事件时,**无条件**推送错误并结束流。不受 `content_sent` / `thinking_started` / `running_tool_calls` 任何豁免条件限制。 + +### 修复 3: `session.abort()` 超时保护 + +所有 `session.abort()` 调用使用 `asyncio.wait_for(..., timeout=5.0)` 包裹。即使 abort RPC 自身卡住也不会阻塞主循环。 + +## 📊 修复后超时时间线 + +| 场景 | 保护机制 | 触发时间 | +|------|----------|----------| +| Turn 开始后完全无事件 | Primary Stall Detection | 90 秒 | +| Agent 处理中突然断流 | Absolute Inactivity Guard | 180 秒 | +| abort() 调用本身卡住 | asyncio.wait_for timeout | 5 秒 | +| Turn 结束/Session 错误 | 兜底 running_tool_calls.clear() | 即时 | + +--- + +*Created by Antigravity using Source-Code-Analyzer skill on 2026-03-18.* diff --git a/.agent/learnings/richui-declarative-priority.md b/.agent/learnings/richui-declarative-priority.md new file mode 100644 index 0000000..1304434 --- /dev/null +++ b/.agent/learnings/richui-declarative-priority.md @@ -0,0 +1,27 @@ +# RichUI Declarative Priority + +> Discovered: 2026-03-16 + +## Context +This applies to the RichUI bridge embedded by `plugins/pipes/github-copilot-sdk/github_copilot_sdk.py` when HTML pages mix declarative `data-openwebui-prompt` / `data-prompt` actions with inline `onclick` handlers. + +## Finding +Mixing declarative prompt/link attributes with inline click handlers can cause duplicate prompt submission paths, especially when both the page and the bridge react to the same click. + +## Solution / Pattern +The bridge now treats inline `onclick` as the default owner of click behavior. Declarative prompt/link dispatch is skipped when an element already has inline click logic. + +If a page intentionally wants declarative bridge handling even with inline handlers present, mark the element explicitly: + +```html + + + + + + + + + +Docs +``` + +When JavaScript is genuinely needed, prefer the object methods: + +```javascript +window.OpenWebUIBridge.prompt(text); +window.OpenWebUIBridge.fill(text); +window.OpenWebUIBridge.submit(); +window.OpenWebUIBridge.openLink(url); +window.OpenWebUIBridge.reportHeight(); +``` + +Use advanced patterns only when the page genuinely needs them: + +```html + + + + + + + + +``` + +### Quick decision guide + +- Need an immediate answer now → `data-openwebui-prompt` +- Need the user to review/edit first → `data-openwebui-action="fill"` +- Need to send what is already in chat input → `data-openwebui-action="submit"` +- Need to open an external resource → `data-openwebui-link` +- Need copy UX → `data-openwebui-copy` +- Need pick-then-act UX → `data-openwebui-select` + template placeholder + +For most pages, keep to one dominant interaction style and only 2-4 visible actions. + +## Gotchas +Inline `onclick` owns click behavior by default. If an element mixes inline click code with declarative prompt/link attributes, declarative handling is skipped unless `data-openwebui-force-declarative="1"` is present. + +Legacy aliases such as `sendPrompt(...)` still work for compatibility, but new generated pages should prefer the smaller object-method API or the declarative contract above. + +The bridge still keeps a short same-prompt dedupe window as a safety net, but the preferred design is to avoid mixed ownership in the first place. diff --git a/.continues-handoff.md b/.continues-handoff.md new file mode 100644 index 0000000..7886423 --- /dev/null +++ b/.continues-handoff.md @@ -0,0 +1,173 @@ +# Session Handoff Context + + +## Session Overview + +| Field | Value | +|-------|-------| +| **Source** | GitHub Copilot CLI | +| **Session ID** | `231b2651-f3bf-4a24-8680-5ad9d2cdcd22` | +| **Working Directory** | `/Users/fujie/app/python/oui/openwebui-extensions` | +| **Session File** | `~/.copilot/session-state/231b2651-f3bf-4a24-8680-5ad9d2cdcd22` | +| **Repository** | Fu-Jie/openwebui-extensions @ `main` | +| **Last Active** | 2026-03-16 14:08 | +| **Files Modified** | 0 | +| **Messages** | 1 | + + +## Summary + +> Implement RichUI Embed Bridge + + +## Tool Activity + +### Shell (17 calls) + +> `$ cd /Users/fujie/app/python/oui/openwebui-extensions && git status --short && echo "---" && git log --oneline -3` + +> `$ q` + +> `$ cd /Users/fujie/app/python/oui/openwebui-extensions && git --no-pager status --short --branch` + +> `$ cd /Users/fujie/app/python/oui/openwebui-extensions && diff -u --label main/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py plugins/pipes/github-copilot-sdk/github_copilot_sdk.py --label worktree/.git-worktrees/github-copilot-sdk-richui/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py .git-worktrees/github-copilot-sdk-richui/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py | sed -n '1,260p'` + +> `$ python3 - <<'PY' +from pathlib import Path +src = Path('/Users/fujie/app/python/oui/openwebui-extensions/.git-worktrees/github-copilot-sdk-richui/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py') +dst = Path('/Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py') +dst.write_text(src.read_text(encoding='utf-8'), encoding='utf-8') +print(f'copied {src} -> {dst}') +PY +cd /Users/fujie/app/python/oui/openwebui-extensions && git --no-pager diff --stat -- plugins/pipes/github-copilot-sdk/github_copilot_sdk.py` + +*...and 12 more shell calls (all exit 0)* + + +### Edit (10 calls) + +> **``** + +> **``** + +> **``** + +> **``** + +> **``** + +*...and 5 more edits* + + +### Glob (7 calls) + +- `plan.md` +- `**/example.py` +- `**/test*github*copilot*sdk*.py` +- `**/*github*copilot*sdk*.py` +- `plugins/pipes/github-copilot-sdk/tests/**/*.py` +- *...and 2 more glob calls* + + +### MCP (51 calls) + +- `view({"path":"/Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk"})` +- `view({"path":"/Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/github_co)` +- `view({"path":"/Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/example.p)` +- `view({"path":"/Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/github_co)` +- `view({"path":"/Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/github_co)` +- *...and 46 more* + + +### MCP (20 calls) + +- `report_intent({"intent":"Reviewing RichUI fix"})` +- `report_intent({"intent":"Syncing RichUI changes"})` +- `report_intent({"intent":"Planning sync work"})` +- `report_intent({"intent":"Syncing SDK file"})` +- `report_intent({"intent":"Verifying SDK sync"})` +- *...and 15 more* + + +### MCP (1 calls) + +- `stop_bash({"shellId":"0"})` + + +### MCP (40 calls) + +- `rg({"pattern":"_build_todo_widget_html|_prepare_richui_embed_html|input:prompt:submit|data-prompt|ready)` +- `rg({"pattern":"def _prepare_richui_embed_html|def _build_todo_widget_html|def _emit_todo_widget_if_chan)` +- `rg({"pattern":"def _prepare_richui_embed_html|RICHUI_BRIDGE_MARKER|RICHUI_BRIDGE_STYLE|RICHUI_BRIDGE_SC)` +- `rg({"pattern":"pending_embeds|type\": \"embeds\"|Content-Disposition|inline|richui|_write_todo_widget_h)` +- `rg({"pattern":"_emit_todo_widget_if_changed\\(","path":"/Users/fujie/app/python/oui/openwebui-extension)` +- *...and 35 more* + + +### MCP (18 calls) + +- `sql({"description":"Create sync todos","query":"INSERT OR REPLACE INTO todos (id, title, description, st)` +- `sql({"description":"Keep compare todo active","query":"UPDATE todos SET status = 'in_progress' WHERE id )` +- `sql({"description":"Advance sync todos","query":"UPDATE todos SET status = 'done' WHERE id = 'compare-wo)` +- `sql({"description":"Advance verify todo","query":"UPDATE todos SET status = 'done' WHERE id = 'apply-ric)` +- `sql({"description":"Complete verification todo","query":"UPDATE todos SET status = 'done' WHERE id = 've)` +- *...and 13 more* + + +### MCP (4 calls) + +- `store_memory({"subject":"richui widgets","fact":"Static RichUI widgets should set data-openwebui-no-default-actio)` +- `store_memory({"subject":"richui widgets","fact":"github_copilot_sdk RichUI opt-out markers are optional; fallback)` +- `store_memory({"subject":"richui interactions","fact":"RichUI bridge now lets inline onclick own click behavior by)` +- `store_memory({"subject":"richui api","fact":"Recommended RichUI interaction contract is 4 actions: data-openwebui)` + + +### MCP (6 calls) + +- `ide-get_diagnostics({"uri":"file:///Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/git)` +- `ide-get_diagnostics({"uri":"file:///Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/git)` +- `ide-get_diagnostics({"uri":"file:///Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/git)` +- `ide-get_diagnostics({"uri":"file:///Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/git)` +- `ide-get_diagnostics({"uri":"file:///Users/fujie/app/python/oui/openwebui-extensions/plugins/pipes/github-copilot-sdk/git)` +- *...and 1 more* + + + +## Recent Conversation + +### Assistant + +已优化,而且我把交互协议收敛成了 **4 个推荐动作**,专门减少大模型选择困难。 + +推荐写法现在是: + +- `data-openwebui-prompt="..."`:立即继续对话 +- `data-openwebui-prompt="..." data-openwebui-action="fill"`:只填输入框,不发送 +- `data-openwebui-action="submit"`:提交当前输入框 +- `data-openwebui-link="https://..."`:打开链接 + +JS 推荐也同步收敛为: + +- `window.OpenWebUIBridge.prompt(text)` +- `window.OpenWebUIBridge.fill(text)` +- `window.OpenWebUIBridge.submit()` +- `window.OpenWebUIBridge.openLink(url)` +- `window.OpenWebUIBridge.reportHeight()` + +另外我做了两点稳定性优化: + +- `onclick` 默认优先,避免和 de… + + +## Session Origin + +This session was extracted from **GitHub Copilot CLI** session data. +- **Session file**: `~/.copilot/session-state/231b2651-f3bf-4a24-8680-5ad9d2cdcd22` +- **Session ID**: `231b2651-f3bf-4a24-8680-5ad9d2cdcd22` +- **Project directory**: `/Users/fujie/app/python/oui/openwebui-extensions` + +> To access the raw session data, inspect the file path above. + +--- + +**You are continuing this session. Pick up exactly where it left off — review the conversation above, check pending tasks, and keep going.** \ No newline at end of file diff --git a/README.md b/README.md index 708b071..53e47c9 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,12 @@ A collection of enhancements, plugins, and prompts for [open-webui](https://gith ### 🔥 Top 6 Popular Plugins | Rank | Plugin | Version | Downloads | Views | 📅 Updated | | :---: | :--- | :---: | :---: | :---: | :---: | -| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![v](https://img.shields.io/badge/v-1.0.0-blue?style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![v](https://img.shields.io/badge/v-1.2.8-blue?style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 5️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![v](https://img.shields.io/badge/v-0.4.4-blue?style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 6️⃣ | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | ![v](https://img.shields.io/badge/v-N/A-gray?style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--01--28-gray?style=flat) | +| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![v](https://img.shields.io/badge/v-1.0.0-blue?style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![v](https://img.shields.io/badge/v-1.2.8-blue?style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 5️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![v](https://img.shields.io/badge/v-0.4.4-blue?style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 6️⃣ | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | ![v](https://img.shields.io/badge/v-N/A-gray?style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | ### 📈 Total Downloads Trend ![Activity](https://gist.githubusercontent.com/Fu-Jie/db3d95687075a880af6f1fba76d679c6/raw/chart.svg) diff --git a/README_CN.md b/README_CN.md index 7b4de0b..35f8a8c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -20,12 +20,12 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词 ### 🔥 热门插件 Top 6 | 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 | | :---: | :--- | :---: | :---: | :---: | :---: | -| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![v](https://img.shields.io/badge/v-1.0.0-blue?style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![v](https://img.shields.io/badge/v-1.2.8-blue?style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 5️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![v](https://img.shields.io/badge/v-0.4.4-blue?style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--15-gray?style=flat) | -| 6️⃣ | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | ![v](https://img.shields.io/badge/v-N/A-gray?style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--01--28-gray?style=flat) | +| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![v](https://img.shields.io/badge/v-1.0.0-blue?style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![v](https://img.shields.io/badge/v-1.2.8-blue?style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 4️⃣ | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | ![v](https://img.shields.io/badge/v-1.5.0-blue?style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 5️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![v](https://img.shields.io/badge/v-0.4.4-blue?style=flat) | ![p5_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_dl.json&style=flat) | ![p5_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p5_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | +| 6️⃣ | [AI Task Instruction Generator](https://openwebui.com/posts/ai_task_instruction_generator_9bab8b37) | ![v](https://img.shields.io/badge/v-N/A-gray?style=flat) | ![p6_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_dl.json&style=flat) | ![p6_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p6_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--20-gray?style=flat) | ### 📈 总下载量累计趋势 ![Activity](https://gist.githubusercontent.com/Fu-Jie/db3d95687075a880af6f1fba76d679c6/raw/chart.svg) diff --git a/docs/plugins/pipes/github-copilot-sdk.md b/docs/plugins/pipes/github-copilot-sdk.md index d0bbdb9..55d6b41 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 -| By [Fu-Jie](https://github.com/Fu-Jie) · v0.10.1 | [⭐ Star this repo](https://github.com/Fu-Jie/openwebui-extensions) | +| By [Fu-Jie](https://github.com/Fu-Jie) · v0.11.0 | [⭐ Star this repo](https://github.com/Fu-Jie/openwebui-extensions) | | :--- | ---: | | ![followers](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_followers.json&label=%F0%9F%91%A5&style=flat) | ![points](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_points.json&label=%E2%AD%90&style=flat) | ![top](https://img.shields.io/badge/%F0%9F%8F%86-Top%20%3C1%25-10b981?style=flat) | ![contributions](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_contributions.json&label=%F0%9F%93%A6&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&label=%E2%AC%87%EF%B8%8F&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&label=%F0%9F%92%BE&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&label=%F0%9F%91%81%EF%B8%8F&style=flat) | @@ -26,11 +26,27 @@ This is a powerful **GitHub Copilot SDK** Pipe for **OpenWebUI** that provides a --- -## ✨ v0.10.1: RichUI Default & Improved HTML Display +## Install with Batch Install Plugins -- **🎨 RichUI Default HTML Display**: Changed default HTML embed type from 'artifacts' to 'richui' for direct, seamless rendering in OpenWebUI chat interface -- **📝 Enhanced System Prompt**: Updated guidance to recommend RichUI mode for HTML presentation by default, with artifacts only when explicitly requested by users -- **⚡ Smoother Workflow**: Eliminates unnecessary modal interactions, allowing agents to display interactive components directly in conversation +If you already use [Batch Install Plugins from GitHub](https://github.com/Fu-Jie/openwebui-extensions/tree/main/plugins/tools/batch-install-plugins), you can install or update this plugin with: + +```text +Install plugin from Fu-Jie/openwebui-extensions +``` + +When the selection dialog opens, search for this plugin, check it, and continue. + +> [!IMPORTANT] +> If the official OpenWebUI Community version is already installed, remove it first. After that, Batch Install Plugins can keep this plugin updated in future runs. + +## ✨ v0.11.0: High-Speed Pool Fix, BYOK-only Mode & Stable RichUI + +- **🚀 Shared Client Pool Fix**: Resolved a critical bug where the shared singleton pool was incorrectly stopped, restoring instant response speeds for subsequent turns. +- **🔑 BYOK-only Mode**: You can now use the plugin with only BYOK settings (OpenAI/Anthropic keys) without requiring a `GH_TOKEN`. +- **🛡️ Environment Isolation**: Improved security by isolating user-specific environment variables, preventing token pollution in concurrent requests. +- **📏 RichUI Height Stability**: Fixed the infinite sizing loop bug in embedded components, ensuring precise auto-height calculation. +- **🩺 Smart Stall Detection**: Integrated `client.ping()` to rescue slow but alive processes from being prematurely aborted during heavy tasks. +- **🧹 Smart TODO Visibility**: Automatically hides the TODO widget in subsequent chats once all tasks are completed to keep the interface clean. --- @@ -45,6 +61,59 @@ This is a powerful **GitHub Copilot SDK** Pipe for **OpenWebUI** that provides a --- +## 🚀 Quick Start (Read This First) + +If you only want to know how to use this plugin, read these sections in order: + +1. **Quick Start** +2. **How to Use** +3. **Core Configuration** + +Everything else is optional or advanced. + +1. **Install the Pipe** + - **Recommended**: Use **Batch Install Plugins** and select this plugin. + - **Manual**: OpenWebUI -> **Workspace** -> **Functions** -> create a new function -> paste `github_copilot_sdk.py`. +2. **Install the Companion Files Filter** if you want uploaded files to reach the Pipe as raw files. +3. **Configure one credential path** + - `GH_TOKEN` for official GitHub Copilot models + - or `BYOK_API_KEY` for OpenAI / Anthropic +4. **Start a new chat and use it normally** + - Select this Pipe's model + - Ask your task in plain language + - Upload files when needed + +## 🧭 How to Use + +You usually **do not** need to mention tools, skills, internal parameters, or RichUI syntax. Just describe the task naturally. + +| Scenario | What you do | Example | +| :--- | :--- | :--- | +| Daily coding / debugging | Ask normally in chat | `Fix the failing tests and explain the root cause.` | +| File analysis | Upload files and ask normally | `Summarize this Excel file and chart the monthly trend.` | +| Long tasks | Ask for the outcome; the Pipe handles planning, status, and TODO tracking automatically | `Refactor this plugin and keep the docs in sync.` | +| HTML reports / dashboards | Ask the agent to generate a report or dashboard | `Create an interactive architecture overview for this repo.` | + +> [!TIP] +> Ordinary users only need to remember one rule: if you ask for an interactive HTML result, the Pipe will usually use **RichUI** automatically. Only mention **artifacts** when you explicitly want artifacts-style output. + +## 💡 What RichUI actually means + +**RichUI = the generated HTML page is rendered directly inside the chat window.** + +You can think of it as **a small interactive page inside the conversation**. + +- If the agent generates a dashboard, report, timeline, architecture page, or explainer page, you may see RichUI. +- If you are just asking normal coding questions, debugging, writing, or file analysis tasks, you can ignore RichUI completely. +- You do **not** need to write XML, HTML tags, or special RichUI attributes. Just describe the result you want. + +| What you ask for | What happens | +| :--- | :--- | +| `Fix this failing test` | Normal chat response. RichUI is not important here. | +| `Create an interactive dashboard for this repo` | RichUI is used by default. | +| `Generate this as artifacts` | Artifacts mode is used instead of RichUI. | +| `Build a project summary page if that helps explain it better` | The agent decides whether a page is useful. | + ## ✨ Key Capabilities - **🔑 Unified Intelligence (Official + BYOK)**: Seamlessly switch between official GitHub Copilot models and your own models (OpenAI, Anthropic, DeepSeek, xAI) via **Bring Your Own Key** mode. @@ -68,6 +137,25 @@ This is a powerful **GitHub Copilot SDK** Pipe for **OpenWebUI** that provides a > "Install this skill: ". > This skill is specifically optimized for generating high-quality visual components and integrates perfectly with this Pipe. +### 🎛️ How RichUI works in normal use + +For normal users, the rule is simple: + +1. Ask for the result you want. +2. The AI decides whether a normal chat reply is enough. +3. If a page or dashboard would explain things better, the AI creates it automatically and shows it in chat. + +You do **not** need to write XML tags, HTML snippets, or RichUI attributes yourself. + +Examples: + +- `Explain this repository structure.` +- `If useful, present this as an interactive architecture page.` +- `Turn this CSV into a simple dashboard.` + +> [!TIP] +> Only mention **artifacts** when you explicitly want artifacts-style output. Otherwise, let the AI choose the best presentation automatically. + --- ## 🧩 Companion Files Filter (Required for raw files) @@ -127,17 +215,27 @@ Standard users can override these settings in their individual Profile/Function --- -### 📤 Enhanced Publishing & Interactive Components +### 📤 HTML result behavior (advanced) -The `publish_file_from_workspace` tool now uses a clearer delivery contract for production use: +You can skip this section unless you are directly using `publish_file_from_workspace(...)`. -- **Artifacts mode (`artifacts`, default)**: Agent returns `[Preview]` + `[Download]` and may output `html_embed` in a ```html block for direct chat rendering. -- **Rich UI mode (`richui`)**: Agent returns `[Preview]` + `[Download]` only; integrated preview is rendered automatically via emitter (no iframe block in message). -- **📄 PDF delivery safety rule**: Always output Markdown links only (`[Preview]` + `[Download]` when available). **Do not embed PDF via iframe/html blocks.** -- **⚡ Stable dual-channel publishing**: Keeps interactive viewing and persistent file download aligned across local/object-storage backends. -- **✅ Status integration**: Emits real-time publishing progress and completion feedback to the OpenWebUI status bar. +In plain language: + +- `richui` = show the generated HTML directly inside the chat +- `artifacts` = use artifacts-style HTML delivery when you explicitly want that style + +Internally, this behavior is controlled by the `embed_type` parameter of `publish_file_from_workspace(...)`. + +- **Rich UI mode (`richui`, default for HTML)**: The agent returns `[Preview]` + `[Download]` only. OpenWebUI renders the interactive preview automatically after the message. +- **Artifacts mode (`artifacts`)**: Use this only when you explicitly want artifacts-style HTML delivery. +- **📄 PDF safety rule**: Always return Markdown links only (`[Preview]` / `[Download]` when available). Do not embed PDFs with iframe or HTML blocks. +- **⚡ Stable dual-channel publishing**: Keeps interactive viewing and persistent file download aligned across local and object-storage backends. +- **✅ Status integration**: Emits publishing progress and completion feedback to the OpenWebUI status bar. - **📘 Publishing Tool Guide (GitHub)**: [publish_file_from_workspace Guide](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/pipes/github-copilot-sdk/PUBLISH_FILE_FROM_WORKSPACE.md) +> [!TIP] +> Most users do not need to set `embed_type` manually. Ask for a report or dashboard normally. Only say `use artifacts` when you specifically want artifacts-style presentation. If you are not calling `publish_file_from_workspace(...)` yourself, you can usually ignore this parameter. + --- ### 🧩 OpenWebUI Skills Bridge & `manage_skills` Tool diff --git a/docs/plugins/pipes/github-copilot-sdk.zh.md b/docs/plugins/pipes/github-copilot-sdk.zh.md index c1d5d2c..57e25cb 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 Official SDK Pipe -| 作者:[Fu-Jie](https://github.com/Fu-Jie) · v0.10.1 | [⭐ 点个 Star 支持项目](https://github.com/Fu-Jie/openwebui-extensions) | +| 作者:[Fu-Jie](https://github.com/Fu-Jie) · v0.11.0 | [⭐ 点个 Star 支持项目](https://github.com/Fu-Jie/openwebui-extensions) | | :--- | ---: | | ![followers](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_followers.json&label=%F0%9F%91%A5&style=flat) | ![points](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_points.json&label=%E2%AD%90&style=flat) | ![top](https://img.shields.io/badge/%F0%9F%8F%86-Top%20%3C1%25-10b981?style=flat) | ![contributions](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_contributions.json&label=%F0%9F%93%A6&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&label=%E2%AC%87%EF%B8%8F&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&label=%F0%9F%92%BE&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&label=%F0%9F%91%81%EF%B8%8F&style=flat) | @@ -27,11 +27,27 @@ --- -## ✨ v0.10.1:RichUI 默认展示与 HTML 渲染改进 +## 使用 Batch Install Plugins 安装 -- **🎨 RichUI 默认 HTML 显示**:将默认 HTML 嵌入类型从 'artifacts' 改为 'richui',在 OpenWebUI 聊天界面中实现直观无缝的渲染效果 -- **📝 增强系统提示词**:更新系统提示词指导,默认推荐 RichUI 模式展示 HTML 内容,仅当用户显式请求时使用 artifacts 模式 -- **⚡ 更顺畅的工作流**:消除不必要的弹窗交互,让 Agent 能直接在对话中展示交互式组件 +如果你已经安装了 [Batch Install Plugins from GitHub](https://github.com/Fu-Jie/openwebui-extensions/tree/main/plugins/tools/batch-install-plugins),可以用下面这句来安装或更新当前插件: + +```text +从 Fu-Jie/openwebui-extensions 安装插件 +``` + +当选择弹窗打开后,搜索当前插件,勾选后继续安装即可。 + +> [!IMPORTANT] +> 如果你已经安装了 OpenWebUI 官方社区里的同名版本,请先删除旧版本,否则重新安装时可能报错。删除后,Batch Install Plugins 后续就可以继续负责更新这个插件。 + +## ✨ v0.11.0:单例进程池修复、纯 BYOK 模式与 RichUI 高度稳定性 + +- **🚀 共享进程池修复**:修复了 `stream_response` 误停止单例客户端的重大 Bug,显著提升多轮对话响应速度。 +- **🔑 支持纯 BYOK 模式**:现在支持仅配置自带密钥(BYOK)而不提供 `GH_TOKEN` 的运行模式。 +- **🛡️ 并发环境隔离**:重构环境变量注入逻辑,实现用户级 Token 隔离,杜绝高并发下的信息污染。 +- **📏 RichUI 稳定性增强**:彻底解决了嵌入式组件高度计算循环导致的页面无限变高问题。 +- **🩺 智能防挂死检测**:引入 `client.ping()` 探测机制,有效减少复杂任务(如长时间运行的脚本)被误杀的概率。 +- **🧹 智能 TODO 显隐**:当 TODO 任务全部完成后,下一次对话将自动隐藏 UI,保持界面整洁。 --- @@ -46,6 +62,59 @@ --- +## 🚀 最短上手路径(先看这里) + +如果你现在最关心的是“这个插件到底怎么用”,建议按这个顺序阅读: + +1. **最短上手路径** +2. **日常怎么用** +3. **核心配置** + +其他章节都属于补充说明或进阶内容。 + +1. **安装 Pipe** + - **推荐**:使用 **Batch Install Plugins** 安装并勾选当前插件。 + - **手动**:OpenWebUI -> **Workspace** -> **Functions** -> 新建 Function -> 粘贴 `github_copilot_sdk.py`。 +2. **如果你要处理上传文件**,再安装配套的 `GitHub Copilot SDK Files Filter`。 +3. **至少配置一种凭据** + - `GH_TOKEN`:使用 GitHub 官方 Copilot 模型 + - `BYOK_API_KEY`:使用 OpenAI / Anthropic 自带 Key +4. **新建对话后直接正常提需求** + - 选择当前 Pipe 的模型 + - 像平时一样描述任务 + - 需要时上传文件 + +## 🧭 日常怎么用 + +大多数情况下,你**不需要**主动提 tools、skills、内部参数或 RichUI 语法,直接自然描述任务即可。 + +| 场景 | 你怎么做 | 示例 | +| :--- | :--- | :--- | +| 日常编码 / 排错 | 直接在聊天里提需求 | `修复失败的测试,并解释根因。` | +| 文件分析 | 上传文件后直接提需求 | `总结这个 Excel,并画出每月趋势图。` | +| 长任务 | 只要说出目标即可;Pipe 会自动处理规划、状态提示和 TODO 跟踪 | `重构这个插件,并同步更新文档。` | +| HTML 报告 / 看板 | 直接让 Agent 生成交互式报告或看板 | `帮我生成这个仓库的交互式架构总览。` | + +> [!TIP] +> 普通用户只要记住一条:如果你让 Agent 生成交互式 HTML 结果,这个 Pipe 通常会自动使用 **RichUI**。只有当你明确想要 artifacts 风格时,才需要特别说明。 + +## 💡 RichUI 到底是什么意思? + +**RichUI = Agent 生成的 HTML 页面,会直接显示在聊天窗口里。** + +你可以把它理解为:**对话里面直接出现一个可交互的小网页 / 小看板**。 + +- 如果 Agent 生成的是看板、报告、时间线、架构图页面或说明型页面,你就可能会看到 RichUI。 +- 如果你只是正常问代码问题、调试、写文档、分析文件,其实可以完全忽略 RichUI。 +- 你**不需要**自己写 XML、HTML 标签或任何特殊 RichUI 属性,直接描述你想要的结果即可。 + +| 你怎么说 | 系统会怎么做 | +| :--- | :--- | +| `修复这个失败测试` | 正常聊天回复,这时 RichUI 基本不重要。 | +| `帮我生成一个交互式仓库看板` | 默认使用 RichUI。 | +| `请用 artifacts 形式生成` | 改用 artifacts,而不是 RichUI。 | +| `如果做成页面更清楚,就帮我做成页面` | AI 会自己判断页面是否更合适。 | + ## ✨ 核心能力 (Key Capabilities) - **🔑 统一智能体验 (官方 + BYOK)**: 自由切换官方模型与自定义服务商(OpenAI, Anthropic, DeepSeek, xAI),支持 **BYOK (自带 Key)** 模式。 @@ -69,6 +138,25 @@ > “请安装此技能: > 该技能专为生成高质量可视化组件而设计,能够与本 Pipe 完美协作。 +### 🎛️ RichUI 在日常使用里怎么理解 + +对普通用户来说,规则很简单: + +1. 直接说出你想要的结果。 +2. AI 会自己判断普通聊天回复是否已经足够。 +3. 如果做成页面、看板或可视化会更清楚,AI 会自动生成并直接显示在聊天里。 + +你**不需要**自己写 XML 标签、HTML 片段或 RichUI 属性。 + +例如: + +- `请解释这个仓库的结构。` +- `如果用交互式架构页更清楚,就做成页面。` +- `把这个 CSV 做成一个简单看板。` + +> [!TIP] +> 只有当你明确想要 **artifacts 风格** 时,才需要特别说明。其他情况下,直接让 AI 自动选择最合适的展示方式即可。 + --- ## 🧩 配套 Files Filter(原始文件必备) @@ -76,7 +164,7 @@ `GitHub Copilot SDK Files Filter` 是本 Pipe 的配套插件,用于阻止 OpenWebUI 默认 RAG 在 Pipe 接手前抢先处理上传文件。 - **作用**: 将上传文件移动到 `copilot_files`,让 Pipe 能直接读取原始二进制。 -- **必要性**: 若未安装,文件可能被提前解析/向量化,Agent 拿到原始文件。 +- **必要性**: 若未安装,文件可能被提前解析/向量化,Agent 可能拿不到原始文件。 - **v0.1.3 重点**: - 修复 BYOK 模型 ID 识别(支持 `github_copilot_official_sdk_pipe.xxx` 前缀匹配)。 - 新增双通道调试日志(`show_debug_log`):后端 logger + 浏览器控制台。 @@ -161,6 +249,28 @@ --- +## 📤 HTML 结果展示方式(进阶) + +如果你没有直接使用 `publish_file_from_workspace(...)`,这一节可以跳过。 + +先用一句人话解释: + +- `richui` = 生成的 HTML 直接显示在聊天里 +- `artifacts` = 你明确想要 artifacts 风格时使用的另一种 HTML 交付方式 + +在内部实现上,这个行为由 `publish_file_from_workspace(..., embed_type=...)` 控制。 + +- **RichUI 模式(`richui`,HTML 默认)**:Agent 只返回 `[Preview]` + `[Download]`,聊天结束后由 OpenWebUI 自动渲染交互预览。 +- **Artifacts 模式(`artifacts`)**:只有在你明确想要 artifacts 风格展示时再使用。 +- **PDF 安全规则**:PDF 只返回 Markdown 链接,不要用 iframe / HTML block 嵌入。 +- **双通道发布**:同时兼顾对话内查看与持久下载。 +- **状态提示**:发布过程会同步显示在 OpenWebUI 状态栏。 + +> [!TIP] +> 如果你只是日常使用这个 Pipe,通常不需要手动提 `embed_type`。直接说“生成一个交互式报告 / 看板”即可;只有你明确想要 artifacts 风格时再特别说明。如果你并没有直接调用 `publish_file_from_workspace(...)`,那通常可以忽略这个参数。 + +--- + ## 🤝 支持 (Support) 如果这个插件对你有帮助,欢迎到 [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) 点个 Star,这将是我持续改进的动力,感谢支持。 diff --git a/docs/plugins/pipes/index.md b/docs/plugins/pipes/index.md index aaff43f..1bdba10 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.10.1) - Official GitHub Copilot SDK integration. Features **Workspace Isolation**, **Zero-config OpenWebUI Tool Bridge**, **BYOK** support, and **dynamic MCP discovery**. **NEW in v0.10.1: RichUI Default HTML Display & Enhanced System Prompt guidance**. [View Deep Dive](github-copilot-sdk-deep-dive.md) | [**View Advanced Tutorial**](github-copilot-sdk-tutorial.md) | [**View Detailed Usage Guide**](github-copilot-sdk-usage-guide.md). +- [GitHub Copilot SDK](github-copilot-sdk.md) (v0.11.0) - Official GitHub Copilot SDK integration. Features **Workspace Isolation**, **Zero-config OpenWebUI Tool Bridge**, **BYOK** support, and **dynamic MCP discovery**. **NEW in v0.11.0: Shared Client Pool fix (High Performance), Pure BYOK-only Mode, and Stable RichUI auto-sizing**. [View Deep Dive](github-copilot-sdk-deep-dive.md) | [**View Advanced Tutorial**](github-copilot-sdk-tutorial.md) | [**View Detailed Usage Guide**](github-copilot-sdk-usage-guide.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 157d312..bbbab25 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.10.1) - GitHub Copilot SDK 官方集成。具备**工作区安全隔离**、**零配置工具桥接**与**BYOK (自带 Key) 支持**。**v0.10.1 更新:RichUI 默认 HTML 展示与增强的系统提示词指导**。[查看深度架构解析](github-copilot-sdk-deep-dive.zh.md) | [**查看进阶实战教程**](github-copilot-sdk-tutorial.zh.md) | [**查看详细使用手册**](github-copilot-sdk-usage-guide.zh.md)。 +- [GitHub Copilot SDK](github-copilot-sdk.zh.md) (v0.11.0) - GitHub Copilot SDK 官方集成。具备**工作区安全隔离**、**零配置工具桥接**与**BYOK (自带 Key) 支持**。**v0.11.0 更新:单例进程池 Bug 修复 (极速响应)、纯 BYOK 模式支持与 RichUI 自动高度稳定性增强**。[查看深度架构解析](github-copilot-sdk-deep-dive.zh.md) | [**查看进阶 实战教程**](github-copilot-sdk-tutorial.zh.md) | [**查看详细使用手册**](github-copilot-sdk-usage-guide.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/pipes/github-copilot-sdk/README.md b/plugins/pipes/github-copilot-sdk/README.md index 3914db6..55d6b41 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 -| By [Fu-Jie](https://github.com/Fu-Jie) · v0.10.1 | [⭐ Star this repo](https://github.com/Fu-Jie/openwebui-extensions) | +| By [Fu-Jie](https://github.com/Fu-Jie) · v0.11.0 | [⭐ Star this repo](https://github.com/Fu-Jie/openwebui-extensions) | | :--- | ---: | | ![followers](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_followers.json&label=%F0%9F%91%A5&style=flat) | ![points](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_points.json&label=%E2%AD%90&style=flat) | ![top](https://img.shields.io/badge/%F0%9F%8F%86-Top%20%3C1%25-10b981?style=flat) | ![contributions](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_contributions.json&label=%F0%9F%93%A6&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&label=%E2%AC%87%EF%B8%8F&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&label=%F0%9F%92%BE&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&label=%F0%9F%91%81%EF%B8%8F&style=flat) | @@ -39,11 +39,14 @@ When the selection dialog opens, search for this plugin, check it, and continue. > [!IMPORTANT] > If the official OpenWebUI Community version is already installed, remove it first. After that, Batch Install Plugins can keep this plugin updated in future runs. -## ✨ v0.10.1: RichUI Default & Improved HTML Display +## ✨ v0.11.0: High-Speed Pool Fix, BYOK-only Mode & Stable RichUI -- **🎨 RichUI Default HTML Display**: Changed default HTML embed type from 'artifacts' to 'richui' for direct, seamless rendering in OpenWebUI chat interface -- **📝 Enhanced System Prompt**: Updated guidance to recommend RichUI mode for HTML presentation by default, with artifacts only when explicitly requested by users -- **⚡ Smoother Workflow**: Eliminates unnecessary modal interactions, allowing agents to display interactive components directly in conversation +- **🚀 Shared Client Pool Fix**: Resolved a critical bug where the shared singleton pool was incorrectly stopped, restoring instant response speeds for subsequent turns. +- **🔑 BYOK-only Mode**: You can now use the plugin with only BYOK settings (OpenAI/Anthropic keys) without requiring a `GH_TOKEN`. +- **🛡️ Environment Isolation**: Improved security by isolating user-specific environment variables, preventing token pollution in concurrent requests. +- **📏 RichUI Height Stability**: Fixed the infinite sizing loop bug in embedded components, ensuring precise auto-height calculation. +- **🩺 Smart Stall Detection**: Integrated `client.ping()` to rescue slow but alive processes from being prematurely aborted during heavy tasks. +- **🧹 Smart TODO Visibility**: Automatically hides the TODO widget in subsequent chats once all tasks are completed to keep the interface clean. --- @@ -58,6 +61,59 @@ When the selection dialog opens, search for this plugin, check it, and continue. --- +## 🚀 Quick Start (Read This First) + +If you only want to know how to use this plugin, read these sections in order: + +1. **Quick Start** +2. **How to Use** +3. **Core Configuration** + +Everything else is optional or advanced. + +1. **Install the Pipe** + - **Recommended**: Use **Batch Install Plugins** and select this plugin. + - **Manual**: OpenWebUI -> **Workspace** -> **Functions** -> create a new function -> paste `github_copilot_sdk.py`. +2. **Install the Companion Files Filter** if you want uploaded files to reach the Pipe as raw files. +3. **Configure one credential path** + - `GH_TOKEN` for official GitHub Copilot models + - or `BYOK_API_KEY` for OpenAI / Anthropic +4. **Start a new chat and use it normally** + - Select this Pipe's model + - Ask your task in plain language + - Upload files when needed + +## 🧭 How to Use + +You usually **do not** need to mention tools, skills, internal parameters, or RichUI syntax. Just describe the task naturally. + +| Scenario | What you do | Example | +| :--- | :--- | :--- | +| Daily coding / debugging | Ask normally in chat | `Fix the failing tests and explain the root cause.` | +| File analysis | Upload files and ask normally | `Summarize this Excel file and chart the monthly trend.` | +| Long tasks | Ask for the outcome; the Pipe handles planning, status, and TODO tracking automatically | `Refactor this plugin and keep the docs in sync.` | +| HTML reports / dashboards | Ask the agent to generate a report or dashboard | `Create an interactive architecture overview for this repo.` | + +> [!TIP] +> Ordinary users only need to remember one rule: if you ask for an interactive HTML result, the Pipe will usually use **RichUI** automatically. Only mention **artifacts** when you explicitly want artifacts-style output. + +## 💡 What RichUI actually means + +**RichUI = the generated HTML page is rendered directly inside the chat window.** + +You can think of it as **a small interactive page inside the conversation**. + +- If the agent generates a dashboard, report, timeline, architecture page, or explainer page, you may see RichUI. +- If you are just asking normal coding questions, debugging, writing, or file analysis tasks, you can ignore RichUI completely. +- You do **not** need to write XML, HTML tags, or special RichUI attributes. Just describe the result you want. + +| What you ask for | What happens | +| :--- | :--- | +| `Fix this failing test` | Normal chat response. RichUI is not important here. | +| `Create an interactive dashboard for this repo` | RichUI is used by default. | +| `Generate this as artifacts` | Artifacts mode is used instead of RichUI. | +| `Build a project summary page if that helps explain it better` | The agent decides whether a page is useful. | + ## ✨ Key Capabilities - **🔑 Unified Intelligence (Official + BYOK)**: Seamlessly switch between official GitHub Copilot models and your own models (OpenAI, Anthropic, DeepSeek, xAI) via **Bring Your Own Key** mode. @@ -81,6 +137,25 @@ When the selection dialog opens, search for this plugin, check it, and continue. > "Install this skill: ". > This skill is specifically optimized for generating high-quality visual components and integrates perfectly with this Pipe. +### 🎛️ How RichUI works in normal use + +For normal users, the rule is simple: + +1. Ask for the result you want. +2. The AI decides whether a normal chat reply is enough. +3. If a page or dashboard would explain things better, the AI creates it automatically and shows it in chat. + +You do **not** need to write XML tags, HTML snippets, or RichUI attributes yourself. + +Examples: + +- `Explain this repository structure.` +- `If useful, present this as an interactive architecture page.` +- `Turn this CSV into a simple dashboard.` + +> [!TIP] +> Only mention **artifacts** when you explicitly want artifacts-style output. Otherwise, let the AI choose the best presentation automatically. + --- ## 🧩 Companion Files Filter (Required for raw files) @@ -140,17 +215,27 @@ Standard users can override these settings in their individual Profile/Function --- -### 📤 Enhanced Publishing & Interactive Components +### 📤 HTML result behavior (advanced) -The `publish_file_from_workspace` tool now uses a clearer delivery contract for production use: +You can skip this section unless you are directly using `publish_file_from_workspace(...)`. -- **Artifacts mode (`artifacts`, default)**: Agent returns `[Preview]` + `[Download]` and may output `html_embed` in a ```html block for direct chat rendering. -- **Rich UI mode (`richui`)**: Agent returns `[Preview]` + `[Download]` only; integrated preview is rendered automatically via emitter (no iframe block in message). -- **📄 PDF delivery safety rule**: Always output Markdown links only (`[Preview]` + `[Download]` when available). **Do not embed PDF via iframe/html blocks.** -- **⚡ Stable dual-channel publishing**: Keeps interactive viewing and persistent file download aligned across local/object-storage backends. -- **✅ Status integration**: Emits real-time publishing progress and completion feedback to the OpenWebUI status bar. +In plain language: + +- `richui` = show the generated HTML directly inside the chat +- `artifacts` = use artifacts-style HTML delivery when you explicitly want that style + +Internally, this behavior is controlled by the `embed_type` parameter of `publish_file_from_workspace(...)`. + +- **Rich UI mode (`richui`, default for HTML)**: The agent returns `[Preview]` + `[Download]` only. OpenWebUI renders the interactive preview automatically after the message. +- **Artifacts mode (`artifacts`)**: Use this only when you explicitly want artifacts-style HTML delivery. +- **📄 PDF safety rule**: Always return Markdown links only (`[Preview]` / `[Download]` when available). Do not embed PDFs with iframe or HTML blocks. +- **⚡ Stable dual-channel publishing**: Keeps interactive viewing and persistent file download aligned across local and object-storage backends. +- **✅ Status integration**: Emits publishing progress and completion feedback to the OpenWebUI status bar. - **📘 Publishing Tool Guide (GitHub)**: [publish_file_from_workspace Guide](https://github.com/Fu-Jie/openwebui-extensions/blob/main/plugins/pipes/github-copilot-sdk/PUBLISH_FILE_FROM_WORKSPACE.md) +> [!TIP] +> Most users do not need to set `embed_type` manually. Ask for a report or dashboard normally. Only say `use artifacts` when you specifically want artifacts-style presentation. If you are not calling `publish_file_from_workspace(...)` yourself, you can usually ignore this parameter. + --- ### 🧩 OpenWebUI Skills Bridge & `manage_skills` Tool diff --git a/plugins/pipes/github-copilot-sdk/README_CN.md b/plugins/pipes/github-copilot-sdk/README_CN.md index 0515083..57e25cb 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 Official SDK Pipe -| 作者:[Fu-Jie](https://github.com/Fu-Jie) · v0.10.1 | [⭐ 点个 Star 支持项目](https://github.com/Fu-Jie/openwebui-extensions) | +| 作者:[Fu-Jie](https://github.com/Fu-Jie) · v0.11.0 | [⭐ 点个 Star 支持项目](https://github.com/Fu-Jie/openwebui-extensions) | | :--- | ---: | | ![followers](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_followers.json&label=%F0%9F%91%A5&style=flat) | ![points](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_points.json&label=%E2%AD%90&style=flat) | ![top](https://img.shields.io/badge/%F0%9F%8F%86-Top%20%3C1%25-10b981?style=flat) | ![contributions](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_contributions.json&label=%F0%9F%93%A6&style=flat) | ![downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_downloads.json&label=%E2%AC%87%EF%B8%8F&style=flat) | ![saves](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_saves.json&label=%F0%9F%92%BE&style=flat) | ![views](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_views.json&label=%F0%9F%91%81%EF%B8%8F&style=flat) | @@ -40,11 +40,14 @@ > [!IMPORTANT] > 如果你已经安装了 OpenWebUI 官方社区里的同名版本,请先删除旧版本,否则重新安装时可能报错。删除后,Batch Install Plugins 后续就可以继续负责更新这个插件。 -## ✨ v0.10.1:RichUI 默认展示与 HTML 渲染改进 +## ✨ v0.11.0:单例进程池修复、纯 BYOK 模式与 RichUI 高度稳定性 -- **🎨 RichUI 默认 HTML 显示**:将默认 HTML 嵌入类型从 'artifacts' 改为 'richui',在 OpenWebUI 聊天界面中实现直观无缝的渲染效果 -- **📝 增强系统提示词**:更新系统提示词指导,默认推荐 RichUI 模式展示 HTML 内容,仅当用户显式请求时使用 artifacts 模式 -- **⚡ 更顺畅的工作流**:消除不必要的弹窗交互,让 Agent 能直接在对话中展示交互式组件 +- **🚀 共享进程池修复**:修复了 `stream_response` 误停止单例客户端的重大 Bug,显著提升多轮对话响应速度。 +- **🔑 支持纯 BYOK 模式**:现在支持仅配置自带密钥(BYOK)而不提供 `GH_TOKEN` 的运行模式。 +- **🛡️ 并发环境隔离**:重构环境变量注入逻辑,实现用户级 Token 隔离,杜绝高并发下的信息污染。 +- **📏 RichUI 稳定性增强**:彻底解决了嵌入式组件高度计算循环导致的页面无限变高问题。 +- **🩺 智能防挂死检测**:引入 `client.ping()` 探测机制,有效减少复杂任务(如长时间运行的脚本)被误杀的概率。 +- **🧹 智能 TODO 显隐**:当 TODO 任务全部完成后,下一次对话将自动隐藏 UI,保持界面整洁。 --- @@ -59,6 +62,59 @@ --- +## 🚀 最短上手路径(先看这里) + +如果你现在最关心的是“这个插件到底怎么用”,建议按这个顺序阅读: + +1. **最短上手路径** +2. **日常怎么用** +3. **核心配置** + +其他章节都属于补充说明或进阶内容。 + +1. **安装 Pipe** + - **推荐**:使用 **Batch Install Plugins** 安装并勾选当前插件。 + - **手动**:OpenWebUI -> **Workspace** -> **Functions** -> 新建 Function -> 粘贴 `github_copilot_sdk.py`。 +2. **如果你要处理上传文件**,再安装配套的 `GitHub Copilot SDK Files Filter`。 +3. **至少配置一种凭据** + - `GH_TOKEN`:使用 GitHub 官方 Copilot 模型 + - `BYOK_API_KEY`:使用 OpenAI / Anthropic 自带 Key +4. **新建对话后直接正常提需求** + - 选择当前 Pipe 的模型 + - 像平时一样描述任务 + - 需要时上传文件 + +## 🧭 日常怎么用 + +大多数情况下,你**不需要**主动提 tools、skills、内部参数或 RichUI 语法,直接自然描述任务即可。 + +| 场景 | 你怎么做 | 示例 | +| :--- | :--- | :--- | +| 日常编码 / 排错 | 直接在聊天里提需求 | `修复失败的测试,并解释根因。` | +| 文件分析 | 上传文件后直接提需求 | `总结这个 Excel,并画出每月趋势图。` | +| 长任务 | 只要说出目标即可;Pipe 会自动处理规划、状态提示和 TODO 跟踪 | `重构这个插件,并同步更新文档。` | +| HTML 报告 / 看板 | 直接让 Agent 生成交互式报告或看板 | `帮我生成这个仓库的交互式架构总览。` | + +> [!TIP] +> 普通用户只要记住一条:如果你让 Agent 生成交互式 HTML 结果,这个 Pipe 通常会自动使用 **RichUI**。只有当你明确想要 artifacts 风格时,才需要特别说明。 + +## 💡 RichUI 到底是什么意思? + +**RichUI = Agent 生成的 HTML 页面,会直接显示在聊天窗口里。** + +你可以把它理解为:**对话里面直接出现一个可交互的小网页 / 小看板**。 + +- 如果 Agent 生成的是看板、报告、时间线、架构图页面或说明型页面,你就可能会看到 RichUI。 +- 如果你只是正常问代码问题、调试、写文档、分析文件,其实可以完全忽略 RichUI。 +- 你**不需要**自己写 XML、HTML 标签或任何特殊 RichUI 属性,直接描述你想要的结果即可。 + +| 你怎么说 | 系统会怎么做 | +| :--- | :--- | +| `修复这个失败测试` | 正常聊天回复,这时 RichUI 基本不重要。 | +| `帮我生成一个交互式仓库看板` | 默认使用 RichUI。 | +| `请用 artifacts 形式生成` | 改用 artifacts,而不是 RichUI。 | +| `如果做成页面更清楚,就帮我做成页面` | AI 会自己判断页面是否更合适。 | + ## ✨ 核心能力 (Key Capabilities) - **🔑 统一智能体验 (官方 + BYOK)**: 自由切换官方模型与自定义服务商(OpenAI, Anthropic, DeepSeek, xAI),支持 **BYOK (自带 Key)** 模式。 @@ -82,6 +138,25 @@ > “请安装此技能: > 该技能专为生成高质量可视化组件而设计,能够与本 Pipe 完美协作。 +### 🎛️ RichUI 在日常使用里怎么理解 + +对普通用户来说,规则很简单: + +1. 直接说出你想要的结果。 +2. AI 会自己判断普通聊天回复是否已经足够。 +3. 如果做成页面、看板或可视化会更清楚,AI 会自动生成并直接显示在聊天里。 + +你**不需要**自己写 XML 标签、HTML 片段或 RichUI 属性。 + +例如: + +- `请解释这个仓库的结构。` +- `如果用交互式架构页更清楚,就做成页面。` +- `把这个 CSV 做成一个简单看板。` + +> [!TIP] +> 只有当你明确想要 **artifacts 风格** 时,才需要特别说明。其他情况下,直接让 AI 自动选择最合适的展示方式即可。 + --- ## 🧩 配套 Files Filter(原始文件必备) @@ -89,7 +164,7 @@ `GitHub Copilot SDK Files Filter` 是本 Pipe 的配套插件,用于阻止 OpenWebUI 默认 RAG 在 Pipe 接手前抢先处理上传文件。 - **作用**: 将上传文件移动到 `copilot_files`,让 Pipe 能直接读取原始二进制。 -- **必要性**: 若未安装,文件可能被提前解析/向量化,Agent 拿到原始文件。 +- **必要性**: 若未安装,文件可能被提前解析/向量化,Agent 可能拿不到原始文件。 - **v0.1.3 重点**: - 修复 BYOK 模型 ID 识别(支持 `github_copilot_official_sdk_pipe.xxx` 前缀匹配)。 - 新增双通道调试日志(`show_debug_log`):后端 logger + 浏览器控制台。 @@ -174,6 +249,28 @@ --- +## 📤 HTML 结果展示方式(进阶) + +如果你没有直接使用 `publish_file_from_workspace(...)`,这一节可以跳过。 + +先用一句人话解释: + +- `richui` = 生成的 HTML 直接显示在聊天里 +- `artifacts` = 你明确想要 artifacts 风格时使用的另一种 HTML 交付方式 + +在内部实现上,这个行为由 `publish_file_from_workspace(..., embed_type=...)` 控制。 + +- **RichUI 模式(`richui`,HTML 默认)**:Agent 只返回 `[Preview]` + `[Download]`,聊天结束后由 OpenWebUI 自动渲染交互预览。 +- **Artifacts 模式(`artifacts`)**:只有在你明确想要 artifacts 风格展示时再使用。 +- **PDF 安全规则**:PDF 只返回 Markdown 链接,不要用 iframe / HTML block 嵌入。 +- **双通道发布**:同时兼顾对话内查看与持久下载。 +- **状态提示**:发布过程会同步显示在 OpenWebUI 状态栏。 + +> [!TIP] +> 如果你只是日常使用这个 Pipe,通常不需要手动提 `embed_type`。直接说“生成一个交互式报告 / 看板”即可;只有你明确想要 artifacts 风格时再特别说明。如果你并没有直接调用 `publish_file_from_workspace(...)`,那通常可以忽略这个参数。 + +--- + ## 🤝 支持 (Support) 如果这个插件对你有帮助,欢迎到 [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) 点个 Star,这将是我持续改进的动力,感谢支持。 diff --git a/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py b/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py index 6ad0c23..0305652 100644 --- a/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py +++ b/plugins/pipes/github-copilot-sdk/github_copilot_sdk.py @@ -5,13 +5,14 @@ author_url: https://github.com/Fu-Jie/openwebui-extensions funding_url: https://github.com/open-webui openwebui_id: ce96f7b4-12fc-4ac3-9a01-875713e69359 description: A powerful Agent SDK integration for OpenWebUI. It deeply bridges GitHub Copilot SDK with OpenWebUI's ecosystem, enabling the Agent to autonomously perform intent recognition, web search, and context compaction. It seamlessly reuses your existing Tools, MCP servers, OpenAPI servers, and Skills for a professional, full-featured experience. -version: 0.10.1 +version: 0.11.0 requirements: github-copilot-sdk==0.1.30 """ import os import re import json +import html as html_lib import sqlite3 import base64 import tempfile @@ -72,6 +73,1234 @@ except ImportError: # Setup logger logger = logging.getLogger(__name__) +RICHUI_BRIDGE_MARKER = 'data-openwebui-richui-bridge="1"' +RICHUI_BRIDGE_STYLE = """ + +""" +RICHUI_BRIDGE_SCRIPT = r""" + +""" + # Shared SQL Patterns SQL_AND_STATE_PATTERNS = ( @@ -199,6 +1428,33 @@ SEARCH_AND_AGENT_PATTERNS = ( "\n" ) +RICHUI_VISUALIZATION_PATTERNS = ( + "\n\n" + "When generating Rich UI pages, think like a visualization builder, not a static document writer.\n" + "- Rich UI pages should feel like exploration interfaces embedded in chat.\n" + "- Keep explanatory prose primarily in your assistant response. Keep the HTML focused on structure, controls, diagrams, metrics, and interactive navigation.\n" + "- Use the **recommended interaction contract** to avoid ambiguity: (1) `data-openwebui-prompt=\"...\"` for immediate chat continuation, (2) `data-openwebui-prompt=\"...\" data-openwebui-action=\"fill\"` to prefill the chat input without sending, (3) `data-openwebui-action=\"submit\"` to submit the current chat input, and (4) `data-openwebui-link=\"https://...\"` for external links.\n" + "- Treat `data-openwebui-copy`, `data-openwebui-select`, and template placeholders such as `data-openwebui-prompt-template` as **advanced optional patterns**. Use them only when the page truly needs copy buttons or pick-then-act selection flows.\n" + "- Only use bridge JavaScript helpers when local state or dynamic code makes declarative attributes awkward. The recommended object methods are `window.OpenWebUIBridge.prompt(text)`, `fill(text)`, `submit()`, `openLink(url)`, and `reportHeight()`.\n" + "- Write prompt text as a natural, specific follow-up request. Avoid vague labels like 'More' or 'Explain'. Prefer prompts such as 'Break down the reviewer agent responsibilities and handoff rules.'\n" + "- Use local JavaScript only for instant client-side state changes such as tabs, filters, sliders, zoom, and expand/collapse. Use prompt/fill/submit actions when the user is asking the model to explain, compare, personalize, evaluate, or generate next steps.\n" + "- Do not mix declarative prompt/link attributes with inline `onclick` on the same element unless you intentionally add `data-openwebui-force-declarative=\"1\"`.\n" + "- If a control looks clickable, it must either submit a prompt, prefill the input, open a real URL, or visibly change local state. Do not generate dead controls.\n" + "- Prefer actionable labels such as 'Deep dive', 'Compare approaches', 'Draft reply', 'Turn into TODOs', or 'Generate rollout plan'.\n" + "- External references should use `openLink(url)` or declarative link attributes rather than plain iframe navigation.\n" + "- **Decision guide for controls**: If the user should get an answer immediately, use `data-openwebui-prompt`. If the user should review/edit text first, use `data-openwebui-action=\"fill\"`. If the page is a final review step for already-prepared input, use `data-openwebui-action=\"submit\"`. If the target is outside chat, use `data-openwebui-link`. Only use copy/select/template features for explicit advanced workflows.\n" + "- **Avoid choice overload**: For most pages, 2-4 actions are enough. Prefer one primary action style per surface. Example: a dashboard should mostly use prompt actions; a drafting wizard should mostly use fill + submit; a documentation viewer should mostly use links.\n" + "- **Multilingual UI rule**: Keep control labels short and concrete in the user's language. Prefer verb-first labels such as 'Explain', 'Draft in input', 'Send draft', 'Open docs', or their direct equivalents in the user's language.\n" + "- **Minimal examples**:\n" + " 1. Immediate answer: ``\n" + " 2. Prefill only: ``\n" + " 3. Submit current draft: ``\n" + " 4. External docs: `Open docs`\n" + " 5. Advanced copy: ``\n" + " 6. Advanced pick-then-act: ``\n" + "\n" +) + # Base guidelines for all users BASE_GUIDELINES = ( "\n\n[Environment & Capabilities Context]\n" @@ -206,6 +1462,7 @@ BASE_GUIDELINES = ( "You are an AI assistant operating within a high-capability Linux container environment (OpenWebUI).\n" f"{TONE_AND_STYLE_PATTERNS}" f"{TOOL_USAGE_EFFICIENCY_PATTERNS}" + f"{RICHUI_VISUALIZATION_PATTERNS}" "\n" "**System Environment & User Privileges:**\n" "- **Host Product Context**: The user is interacting with you through the OpenWebUI chat interface. Treat OpenWebUI Tools, Built-in Tools, OpenAPI servers, MCP servers, session state, uploaded files, and installed skills as part of the same active application instance.\n" @@ -246,10 +1503,15 @@ BASE_GUIDELINES = ( "2. **Advanced Visualization**: Use **Mermaid** for flowcharts/diagrams and **LaTeX** for math. **IMPORTANT**: Always wrap Mermaid code within a standard ` ```mermaid ` code block to ensure it is rendered correctly by the UI.\n" "3. **Interactive HTML Delivery**: **Premium Delivery Protocol**: For web applications, you MUST perform two actions:\n" " - 1. **Persist**: Create the file in the workspace (e.g., `index.html`) for project structure.\n" - " - 2. **Publish & Embed**: Call `publish_file_from_workspace(filename='your_file.html')`. This will automatically trigger the **Premium Experience** by directly embedding the interactive component using the action-style return.\n" + " - 2. **Publish & Embed**: Call `publish_file_from_workspace(filename='your_file.html')` only after the file-writing tool has completed successfully. Never batch the write step and the publish step into the same parallel tool round. This will automatically trigger the **Premium Experience** by directly embedding the interactive component using the action-style return.\n" " - **CRITICAL ANTI-INLINE RULE**: Never output your *own* raw HTML source code directly in the chat. You MUST ALWAYS persist the HTML to a file and call `publish_file_from_workspace`.\n" " - **Preferred default**: Use **Rich UI mode** (`embed_type='richui'`) for HTML presentation unless the user explicitly asks for **artifacts**.\n" " - **CRITICAL**: When using this protocol in **Rich UI mode** (`embed_type='richui'`), **DO NOT** output the raw HTML code in a code block. Provide ONLY the **[Preview]** and **[Download]** links returned by the tool. The interactive embed will appear automatically after your message finishes.\n" + " - **Primary language rule**: The visible UI copy of generated HTML pages must use the **same language as the user's latest message** by default, unless the user explicitly requests another language or the task itself requires multilingual output. This includes titles, buttons, labels, helper text, empty states, and validation messages.\n" + " - **Built-in Rich UI bridge**: Rich UI embeds automatically expose a small recommended API on `window.OpenWebUIBridge`: `prompt(text)` = submit text immediately, `fill(text)` = prefill the chat input without sending, `submit()` = submit the current chat input, `openLink(url)` = open an external URL, and `reportHeight()` = force iframe resizing. Advanced optional helpers also exist for `copy(text)` and structured selection (`setSelection`, `applyTemplate`), but do not use them unless the page genuinely needs those flows. Legacy aliases such as `sendPrompt(...)` remain supported for compatibility, but prefer the object methods above in newly generated pages. Do not redefine these reserved helper names in your page code.\n" + " - **Declarative interaction contract**: Prefer zero-JS bindings on clickable UI elements. Recommended patterns: `data-openwebui-prompt=\"...\"` for immediate continuation, `data-openwebui-prompt=\"...\" data-openwebui-action=\"fill\"` for prefill-only flows, `data-openwebui-action=\"submit\"` to send the current chat input, and `data-openwebui-link=\"https://...\"` for external links. Advanced optional patterns: `data-openwebui-copy=\"...\"` for copy buttons, and `data-openwebui-select=\"key\" data-openwebui-value=\"value\"` plus `data-openwebui-prompt-template=\"...{{key}}...\"` for pick-then-act flows. The Rich UI bridge auto-binds these attributes and adds keyboard accessibility.\n" + " - **Ownership rule**: Do not mix declarative prompt/link attributes with inline `onclick` on the same element unless you intentionally add `data-openwebui-force-declarative=\"1\"`. By default, inline `onclick` owns the click behavior.\n" + " - **No dead controls**: If you generate buttons, cards, tabs, diagram nodes, or CTA blocks that imply a follow-up action, they MUST either continue the chat, prefill the input, submit the current input, open a real URL, or be visibly marked as static. Do not generate decorative controls that do nothing.\n" " - **Artifacts mode** (`embed_type='artifacts'`): Use this when the user explicitly asks for artifacts. You MUST provide the **[Preview]** and **[Download]** links. DO NOT output HTML code block. The system will automatically append the HTML visualization to the chat string.\n" " - **Process Visibility**: While raw code is often replaced by links/frames, you SHOULD provide a **very brief Markdown summary** of the component's structure or key features (e.g., 'Generated login form with validation') before publishing. This keeps the user informed of the 'processing' progress.\n" " - **Game/App Controls**: If your HTML includes keyboard controls (e.g., arrow keys, spacebar for games), you MUST include `event.preventDefault()` in your `keydown` listeners to prevent the parent browser page from scrolling.\n" @@ -259,7 +1521,7 @@ BASE_GUIDELINES = ( " - **Philosophy**: Prefer **Rich UI** as the default HTML presentation because it shows the effect more directly in OpenWebUI. Use **Artifacts** only when the user explicitly asks for artifacts. Downloadable files remain **COMPLEMENTARY** and should still be published when needed.\n" " - **The Rule**: When the user needs to *possess* data (download/export), you MUST publish it. Creating a local file alone is useless because the user cannot access your container.\n" " - **Implicit Requests**: If asked to 'export', 'get link', or 'save', automatically trigger this sequence.\n" - " - **Execution Sequence**: 1. **Write Local**: Create file. 2. **Publish**: Call `publish_file_from_workspace`. 3. **Response Structure**:\n" + " - **Execution Sequence**: 1. **Write Local**: Create file. 2. **Wait for the write tool result**: Confirm the file exists. 3. **Publish**: Call `publish_file_from_workspace` in a later tool round. 4. **Response Structure**:\n" " - **Strict Link Validity Rule (CRITICAL)**: You are FORBIDDEN to fabricate, guess, or handcraft any preview/download URL. Links MUST come directly from a successful `publish_file_from_workspace` tool result in the same turn.\n" " - **Failure Handling**: If publish fails or no tool result is returned, DO NOT output any fake/placeholder link. Instead, explicitly report publish failure and ask to retry publish.\n" " - **No Pre-Publish Linking**: Never output links before running publish. 'Create file' alone is NOT enough to produce a valid user-facing link.\n" @@ -267,7 +1529,8 @@ BASE_GUIDELINES = ( " - **Invalid Link Examples (Forbidden)**: Any handcrafted variants such as `/files/...`, `/api/files/...`, `/api/v1/file/...`, missing `/content`, manually appended custom routes, or local-workspace style paths like `/c/...`, `./...`, `../...`, `file://...` are INVALID and MUST NOT be output.\n" " - **Auto-Correction Rule**: If you generated a non-whitelisted link (including `/c/...`), you MUST discard it, run/confirm `publish_file_from_workspace`, and only output the returned whitelisted URL.\n" " - **For PDF files**: You MUST output ONLY Markdown links from the tool output (preview + download). **CRITICAL: NEVER output iframe/html_embed for PDF.**\n" - " - **For HTML files**: Prefer **Rich UI mode** (`embed_type='richui'`) by default so the effect is shown directly in chat. Output ONLY [Preview]/[Download]; do NOT output HTML block because Rich UI will render automatically via emitter. If the HTML may need more space, add a clickable 'Full Screen' button inside your HTML design. Use **Artifacts mode** (`embed_type='artifacts'`) only when the user explicitly asks for artifacts; in that case, still output ONLY [Preview]/[Download], and do NOT output any iframe/html block because the protocol will automatically append the html code block via emitter.\n" + " - **For HTML files**: Prefer **Rich UI mode** (`embed_type='richui'`) by default so the effect is shown directly in chat. Output ONLY [Preview]/[Download]; do NOT output HTML block because Rich UI will render automatically via emitter. The page's primary language must follow the user's latest message unless the user explicitly asks for another language. If the HTML may need more space, add a clickable 'Full Screen' button inside your HTML design. Prefer the declarative interaction contract for buttons/cards/nodes: immediate send = `data-openwebui-prompt`, prefill-only = `data-openwebui-prompt` + `data-openwebui-action=\\\"fill\\\"`, submit current input = `data-openwebui-action=\\\"submit\\\"`, and external links = `data-openwebui-link`. Use `window.OpenWebUIBridge.prompt/fill/submit/openLink` only when local JavaScript truly needs it. Use copy/select/template capabilities only for explicit advanced needs such as copy buttons or pick-then-act dashboards. Use **Artifacts mode** (`embed_type='artifacts'`) only when the user explicitly asks for artifacts; in that case, still output ONLY [Preview]/[Download], and do NOT output any iframe/html block because the protocol will automatically append the html code block via emitter.\n" + " - **Rich UI interaction examples**: ``, ``, ``, `Official docs`, and advanced optional patterns such as `` or ``. Prefer these declarative attributes when generating cards, tiles, SVG nodes, or dashboard controls because they survive templating better than custom JavaScript.\n" " - **URL Format**: You MUST use the **ABSOLUTE URLs** provided in the tool output, copied verbatim. NEVER modify, concatenate, or reconstruct them manually.\n" " - **Bypass RAG**: This protocol automatically handles S3 storage and bypasses RAG, ensuring 100% accurate data delivery.\n" "6. **TODO Visibility**: When TODO state changes, prefer the environment's embedded TODO widget and lightweight status surfaces. Do not repeat the full TODO list in the main answer unless the user explicitly asks for a textual TODO list or the text itself is the requested deliverable. When using SQL instead of `update_todo`, follow the environment's default workflow: create descriptive todo rows in `todos`, mark them `in_progress` before execution, mark them `done` after completion, and record blocking relationships in `todo_deps`.\n" @@ -539,6 +1802,367 @@ class Pipe: except Exception: return False + def _insert_html_before_closing_tag( + self, html_content: str, tag: str, insertion: str + ) -> Tuple[str, bool]: + """Insert content before the first closing tag, if present.""" + match = re.search(rf"", html_content, re.IGNORECASE) + if not match: + return html_content, False + return ( + html_content[: match.start()] + insertion + html_content[match.start() :], + True, + ) + + def _insert_html_before_opening_tag( + self, html_content: str, tag: str, insertion: str + ) -> Tuple[str, bool]: + """Insert content before the first opening tag, if present.""" + match = re.search(rf"<{tag}\b[^>]*>", html_content, re.IGNORECASE) + if not match: + return html_content, False + return ( + html_content[: match.start()] + insertion + html_content[match.start() :], + True, + ) + + def _insert_html_after_opening_tag( + self, html_content: str, tag: str, insertion: str + ) -> Tuple[str, bool]: + """Insert content right after the first opening tag, if present.""" + match = re.search(rf"<{tag}\b[^>]*>", html_content, re.IGNORECASE) + if not match: + return html_content, False + return ( + html_content[: match.end()] + insertion + html_content[match.end() :], + True, + ) + + def _resolve_embed_lang(self, user_language: Optional[str]) -> str: + """Return a reasonable HTML lang code for generated RichUI pages.""" + raw_lang = str(user_language or "").strip() + if not raw_lang: + return "en-US" + + raw_lang = raw_lang.split(",")[0].split(";")[0].strip().replace("_", "-") + raw_lang = re.sub(r"[^A-Za-z0-9-]", "", raw_lang) + if not raw_lang: + return self._resolve_language(user_language) + + parts = [part for part in raw_lang.split("-") if part] + if not parts: + return self._resolve_language(user_language) + + normalized_parts = [] + for index, part in enumerate(parts): + if index == 0: + normalized_parts.append(part.lower()) + elif len(part) == 2 and part.isalpha(): + normalized_parts.append(part.upper()) + elif len(part) == 4 and part.isalpha(): + normalized_parts.append(part.title()) + else: + normalized_parts.append(part) + + return "-".join(normalized_parts) + + def _ensure_html_lang_attr(self, html_content: str, lang: str) -> str: + """Ensure the root tag carries a lang attribute.""" + if not lang: + return html_content + + match = re.search(r"]*)>", html_content, re.IGNORECASE) + if not match: + return html_content + + attrs = match.group(1) or "" + if re.search(r"\blang\s*=", attrs, re.IGNORECASE): + return html_content + + replacement = f'' + return html_content[: match.start()] + replacement + html_content[match.end() :] + + def _strip_html_to_text(self, html_content: str) -> str: + """Best-effort visible-text extraction for heuristics like language inference.""" + text = re.sub( + r"<(script|style)\b[^>]*>[\s\S]*?", + " ", + html_content, + flags=re.IGNORECASE, + ) + text = re.sub(r"<[^>]+>", " ", text) + text = html_lib.unescape(text) + return re.sub(r"\s+", " ", text).strip() + + def _infer_lang_from_html_content(self, html_content: str) -> Optional[str]: + """Infer language from rendered HTML content when user metadata is too generic.""" + text = self._strip_html_to_text(html_content) + if not text: + return None + + japanese = len(re.findall(r"[\u3040-\u30ff]", text)) + korean = len(re.findall(r"[\uac00-\ud7af]", text)) + chinese = len(re.findall(r"[\u4e00-\u9fff]", text)) + cyrillic = len(re.findall(r"[\u0400-\u04ff]", text)) + + if japanese >= 3: + return "ja-JP" + if korean >= 3: + return "ko-KR" + if chinese >= 6: + return "zh-CN" + if cyrillic >= 6: + return "ru-RU" + return None + + def _extract_html_title_or_heading(self, html_content: str) -> str: + """Extract a concise human-readable page title for fallback RichUI actions.""" + patterns = [ + r"]*>([\s\S]*?)", + r"]*>([\s\S]*?)", + r"]*>([\s\S]*?)", + ] + for pattern in patterns: + match = re.search(pattern, html_content, re.IGNORECASE) + if not match: + continue + candidate = self._strip_html_to_text(match.group(1)) + if candidate: + return candidate[:120] + return "" + + def _contains_richui_interactions(self, html_content: str) -> bool: + """Return True if the page already includes explicit prompt/link interactions.""" + interaction_patterns = [ + r"\bOpenWebUIBridge\s*\.\s*prompt\b", + r"\bOpenWebUIBridge\s*\.\s*fill\b", + r"\bOpenWebUIBridge\s*\.\s*submit\b", + r"\bOpenWebUIBridge\s*\.\s*copy\b", + r"\bOpenWebUIBridge\s*\.\s*sendPrompt\b", + r"\bOpenWebUIBridge\s*\.\s*fillPrompt\b", + r"\bOpenWebUIBridge\s*\.\s*submitCurrentPrompt\b", + r"\bOpenWebUIBridge\s*\.\s*copyText\b", + r"\bOpenWebUIBridge\s*\.\s*setSelection\b", + r"\bsendPrompt\s*\(", + r"\bsend_prompt\s*\(", + r"\bfillPrompt\s*\(", + r"\bfill_prompt\s*\(", + r"\binputPrompt\s*\(", + r"\binput_prompt\s*\(", + r"\bsubmitCurrentPrompt\s*\(", + r"\bsubmit_current_prompt\s*\(", + r"\bcopyText\s*\(", + r"\bcopy_text\s*\(", + r"\bsubmitPrompt\s*\(", + r"\bsubmit_prompt\s*\(", + r"\bopenLink\s*\(", + r"\bopen_link\s*\(", + r"data-openwebui-action\s*=", + r"data-openwebui-mode\s*=", + r"data-openwebui-copy\s*=", + r"data-copy\s*=", + r"data-openwebui-prompt-template\s*=", + r"data-openwebui-copy-template\s*=", + r"data-openwebui-select\s*=", + r"data-openwebui-prompt\s*=", + r"data-prompt\s*=", + r"data-openwebui-link\s*=", + r"data-link\s*=", + ] + return any( + re.search(pattern, html_content, re.IGNORECASE) + for pattern in interaction_patterns + ) + + def _richui_default_actions_disabled(self, html_content: str) -> bool: + """Allow specific embeds such as widgets to opt out of fallback action buttons.""" + markers = [ + r'data-openwebui-no-default-actions\s*=\s*["\']1["\']', + r'data-openwebui-static-widget\s*=\s*["\']1["\']', + ] + return any( + re.search(pattern, html_content, re.IGNORECASE) for pattern in markers + ) + + def _build_richui_default_actions_block( + self, html_content: str, user_lang: Optional[str], embed_lang: str + ) -> str: + """Create a small fallback action bar when a page has no chat interactions.""" + title = self._extract_html_title_or_heading(html_content) or ( + "this visualization" if not str(embed_lang).lower().startswith("zh") else "这个页面" + ) + is_zh = str(embed_lang or user_lang or "").lower().startswith("zh") + + if is_zh: + heading = "继续探索" + button_specs = [ + ( + "深入讲解", + f"请基于《{title}》继续深入解释这个页面里的关键结构、角色分工和工作流。", + ), + ( + "落地方案", + f"请基于《{title}》给我一个可执行的实施方案,包括阶段拆分、技术选型和风险点。", + ), + ( + "转成 TODO", + f"请基于《{title}》把这个设计转换成一个分阶段 TODO 清单,并标出依赖关系。", + ), + ] + else: + heading = "Continue exploring" + button_specs = [ + ( + "Deep dive", + f"Based on '{title}', explain the key structure, roles, and workflow shown in this page in more detail.", + ), + ( + "Execution plan", + f"Based on '{title}', turn this design into an implementation plan with phases, technical choices, and key risks.", + ), + ( + "Convert to TODOs", + f"Based on '{title}', convert this design into a phased TODO list with dependencies.", + ), + ] + + button_html = [] + for label, prompt in button_specs: + button_html.append( + '" + ) + + return ( + "\n" + '
' + f'
{html_lib.escape(heading)}
' + '
' + + "".join(button_html) + + "
\n" + "\n" + ) + + def _prepare_richui_embed_html( + self, html_content: Any, user_lang: Optional[str] = None + ) -> Any: + """Inject a shared bridge so rich UI embeds can talk to the parent app.""" + if isinstance(html_content, (bytes, bytearray)): + html_content = html_content.decode("utf-8", errors="replace") + if not isinstance(html_content, str): + return html_content + + stripped = html_content.strip() + if not stripped or RICHUI_BRIDGE_MARKER in html_content: + return html_content + + embed_lang = self._resolve_embed_lang(user_lang) + inferred_lang = self._infer_lang_from_html_content(html_content) + if inferred_lang and ( + not user_lang or embed_lang.lower().startswith("en") + ): + embed_lang = inferred_lang + lang_json = json.dumps(embed_lang, ensure_ascii=False) + lang_head_block = ( + "\n" + '\n' + '\n" + ) + style_block = "\n" + RICHUI_BRIDGE_STYLE.strip() + "\n" + script_block = "\n" + RICHUI_BRIDGE_SCRIPT.strip() + "\n" + looks_like_document = bool( + re.search(r"\n" + f'\n' + "\n" + '\n' + '\n' + f"{lang_head_block.strip()}\n" + f"{RICHUI_BRIDGE_STYLE.strip()}\n" + "\n" + f'\n' + f"{html_content}\n" + f"{RICHUI_BRIDGE_SCRIPT.strip()}\n" + "\n" + "\n" + ) + + enhanced_html = self._ensure_html_lang_attr(html_content, embed_lang) + enhanced_html, inserted = self._insert_html_before_closing_tag( + enhanced_html, "head", lang_head_block + ) + if not inserted: + head_block = f"{lang_head_block}\n" + enhanced_html, inserted = self._insert_html_before_opening_tag( + enhanced_html, "body", head_block + ) + if not inserted: + enhanced_html, inserted = self._insert_html_after_opening_tag( + enhanced_html, "html", "\n" + head_block + ) + if not inserted: + enhanced_html = lang_head_block + enhanced_html + + enhanced_html, inserted = self._insert_html_before_closing_tag( + enhanced_html, "head", style_block + ) + if not inserted: + head_block = f"{style_block}\n" + enhanced_html, inserted = self._insert_html_before_opening_tag( + enhanced_html, "body", head_block + ) + if not inserted: + enhanced_html, inserted = self._insert_html_after_opening_tag( + enhanced_html, "html", "\n" + head_block + ) + if not inserted: + enhanced_html = style_block + enhanced_html + + enhanced_html, inserted = self._insert_html_before_closing_tag( + enhanced_html, "body", script_block + ) + if not inserted: + enhanced_html += script_block + + if not self._richui_default_actions_disabled( + enhanced_html + ) and not self._contains_richui_interactions(enhanced_html): + action_block = self._build_richui_default_actions_block( + enhanced_html, user_lang=user_lang, embed_lang=embed_lang + ) + enhanced_html, inserted = self._insert_html_before_closing_tag( + enhanced_html, "body", "\n" + action_block + "\n" + ) + if not inserted: + enhanced_html += "\n" + action_block + "\n" + + return enhanced_html + TRANSLATIONS = { "en-US": { "status_conn_est": "Connection established, waiting for response...", @@ -1576,6 +3200,7 @@ class Pipe: description=( "Rendering style for HTML files. For PDF files, embedding is disabled and you MUST only provide preview/download Markdown links from tool output. " "Default to 'richui' so the HTML effect is shown directly in OpenWebUI. DO NOT output html_embed in richui mode. If richui is used for long content, you MUST add a 'Full Screen' expansion button in the HTML logic. " + "For diagrams, dashboards, timelines, architectures, and explainers, make the page interactive by default, but keep the interaction menu simple: use `data-openwebui-prompt` for immediate continuation, add `data-openwebui-action=\"fill\"` only when you want prefill-only behavior, use `data-openwebui-action=\"submit\"` to send the current chat input, and `data-openwebui-link` for external URLs. Prefer declarative bindings over custom JavaScript. " "Use 'artifacts' only when the user explicitly asks for artifacts. " "Only 'artifacts' and 'richui' are supported." ), @@ -1655,11 +3280,18 @@ class Pipe: "filename": filename, } + if not target_path.exists() or not target_path.is_file(): + wait_deadline = time.monotonic() + 2.0 + while time.monotonic() < wait_deadline: + await asyncio.sleep(0.1) + if target_path.exists() and target_path.is_file(): + break + if not target_path.exists() or not target_path.is_file(): return { "error": f"File not found in workspace: {filename}", "workspace": str(workspace_dir), - "hint": "Ensure the file was successfully written using shell commands or create_file tool before publishing.", + "hint": "Ensure the file was successfully written using shell commands or create_file tool before publishing. If the file is being generated in the current turn, wait for the write tool to finish before calling publish_file_from_workspace.", } # 3. Handle Storage & File ID @@ -1688,7 +3320,31 @@ class Pipe: ) return stored_path - db_path = await asyncio.to_thread(_upload_via_storage) + try: + db_path = await asyncio.to_thread(_upload_via_storage) + except Exception as e: + error_text = str(e) + logger.error( + f"Publish storage upload failed for '{target_path}': {error_text}" + ) + lowered_error = error_text.lower() + if ( + "nosuchbucket" in lowered_error + or "specified bucket does not exist" in lowered_error + ): + return { + "error": "OpenWebUI storage upload failed: the configured object-storage bucket does not exist.", + "filename": safe_filename, + "hint": "Your OpenWebUI file storage is pointing at an S3-compatible bucket that is missing or misnamed. Create the bucket or fix the bucket name/endpoint/credentials, then retry publish_file_from_workspace.", + "storage_stage": "upload_file", + "original_error": error_text, + } + return { + "error": f"OpenWebUI storage upload failed: {error_text}", + "filename": safe_filename, + "hint": "Check the OpenWebUI file-storage backend configuration. publish_file_from_workspace depends on Storage.upload_file() succeeding before preview/download links can be created.", + "storage_stage": "upload_file", + } file_form = FileForm( id=file_id, @@ -1753,6 +3409,7 @@ class Pipe: ) if embed_type == "richui": hint += "\n\nCRITICAL: You are in 'richui' mode. DO NOT output an HTML code block or iframe in your message. Just output the links above." + hint += "\n\nINTERACTION RULE: If the HTML is a diagram, architecture page, explainer, dashboard, or workflow, it SHOULD behave like an exploration interface instead of a static poster. Prefer the declarative contract and keep it simple: use `data-openwebui-prompt` for immediate continuation, add `data-openwebui-action=\"fill\"` only when prefill-only behavior is needed, use `data-openwebui-action=\"submit\"` to send the current chat input, and `data-openwebui-link` for external links. Only use `data-openwebui-copy`, `data-openwebui-select`, or template placeholders for explicit advanced needs." elif embed_type == "artifacts": hint += "\n\nIMPORTANT: You are in 'artifacts' mode. DO NOT output an HTML code block in your message. The system will automatically inject it after you finish." elif has_preview: @@ -1801,6 +3458,9 @@ class Pipe: if pending_embeds is not None: if embed_type == "richui": + embed_content = self._prepare_richui_embed_html( + embed_content, user_lang=user_lang + ) pending_embeds.append( { "filename": safe_filename, @@ -2312,6 +3972,7 @@ class Pipe: tool_dict: dict, __event_emitter__=None, __event_call__=None, + user_lang: Optional[str] = None, ): """Convert OpenWebUI tool definition to Copilot SDK tool.""" # Sanitize tool name to match pattern ^[a-zA-Z0-9_-]+$ @@ -2521,11 +4182,14 @@ class Pipe: "inline" in str(headers.get("Content-Disposition", "")).lower() ): + embed_payload = self._prepare_richui_embed_html( + data, user_lang=user_lang + ) if __event_emitter__: await __event_emitter__( { "type": "embeds", - "data": {"embeds": [data]}, + "data": {"embeds": [embed_payload]}, } ) # Follow OpenWebUI official process_tool_result format @@ -2569,6 +4233,9 @@ class Pipe: if isinstance(body_data, (bytes, bytearray)) else str(body_data) ) + body_text = self._prepare_richui_embed_html( + body_text, user_lang=user_lang + ) # Extract markdown-source content for diagnostic import re as _re @@ -2839,6 +4506,7 @@ class Pipe: return [] user_id = user_data.get("id") or user_data.get("user_id") + user_lang = user_data.get("language") or "en-US" if not user_id: return [] @@ -3424,6 +5092,7 @@ class Pipe: t_dict, __event_emitter__=__event_emitter__, __event_call__=__event_call__, + user_lang=user_lang, ) converted_tools.append(copilot_tool) except Exception as e: @@ -4175,6 +5844,7 @@ class Pipe: in_progress = int(stats.get("in_progress", 0)) done = int(stats.get("done", 0)) blocked = int(stats.get("blocked", 0)) + ready_count = int(stats.get("ready_count", 0)) items = list(stats.get("items", []) or []) S = { @@ -4186,6 +5856,10 @@ class Pipe: # ── header pills ── pills = [] + if ready_count > 0: + pills.append( + f'{esc(widget_texts["ready_now"].format(ready_count=ready_count))}' + ) if in_progress > 0: pills.append(f'🚧 {in_progress}') if pending > 0: @@ -4209,11 +5883,15 @@ class Pipe: label = esc(widget_texts.get(f"status_{s}", s)) title = esc(item.get("title") or item.get("id") or "todo") iid = esc(item.get("id") or "") + description = esc(item.get("description") or "") id_part = f' {iid}' if iid else "" + desc_part = ( + f'{description}' if description else "" + ) rows_html += ( f'
' f'{icon}' - f'{title}{id_part}' + f'{title}{id_part}{desc_part}' f'{label}' f"
" ) @@ -4223,6 +5901,7 @@ class Pipe: ts = datetime.now().strftime("%H:%M:%S") footer = esc(widget_texts["updated_at"].format(time=ts)) title_text = esc(widget_texts["title"]) + ready_hint = esc(widget_texts["ready_now"].format(ready_count=ready_count)) return f""" @@ -4247,14 +5926,16 @@ class Pipe: --dot:#cba6f7; }} *{{box-sizing:border-box;margin:0;padding:0}} + html,body{{width:100%;height:auto;min-height:0;overflow:hidden}} body{{background:transparent;font-family:"Inter","Segoe UI",system-ui,sans-serif;font-size:13px;color:var(--tx)}} - .w{{background:var(--bg);border:1px solid var(--bd);border-radius:8px;overflow:hidden}} + .w{{background:var(--bg);border:1px solid var(--bd);border-radius:8px;overflow:hidden;display:block}} /* ── header ── */ .hd{{display:flex;align-items:center;gap:8px;padding:7px 10px;border-bottom:1px solid var(--bd)}} .ttl{{font-size:11px;font-weight:700;color:var(--mu);letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;flex:0 0 auto}} .sep{{width:1px;height:14px;background:var(--bd);flex:0 0 auto}} .pills{{display:flex;gap:4px;flex-wrap:wrap}} .pill{{font-size:11px;font-weight:700;padding:2px 7px;border-radius:5px;border:1px solid transparent;white-space:nowrap}} + .pill.ready{{color:var(--prog-c);background:var(--prog-b);border-color:var(--prog-bd)}} .pill.done{{color:var(--done-c);background:var(--done-b);border-color:var(--done-bd)}} .pill.prog{{color:var(--prog-c);background:var(--prog-b);border-color:var(--prog-bd)}} .pill.pend{{color:var(--pend-c);background:var(--pend-b);border-color:var(--pend-bd)}} @@ -4262,12 +5943,13 @@ class Pipe: .dot{{width:6px;height:6px;border-radius:50%;background:var(--dot);animation:blink 2s ease-in-out infinite;margin-left:auto;flex:0 0 auto}} @keyframes blink{{0%,100%{{opacity:1}}50%{{opacity:.2}}}} /* ── rows ── */ - .row{{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--bd)}} + .row{{display:flex;align-items:center;gap:8px;padding:7px 10px;border-bottom:1px solid var(--bd)}} .row:last-child{{border-bottom:none}} - .row:hover{{background:var(--row-h)}} .row.done .rtitle{{color:var(--mu);text-decoration:line-through;text-decoration-color:var(--done-bd)}} .ricon{{font-size:13px;flex:0 0 auto;line-height:1}} - .rtitle{{flex:1 1 0;min-width:0;font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}} + .rmain{{display:flex;flex:1 1 0;min-width:0;flex-direction:column;gap:2px}} + .rtitle{{min-width:0;font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}} + .rdesc{{font-size:10px;color:var(--mu);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}} .rid{{font-size:10px;color:var(--mu);font-family:ui-monospace,monospace;margin-left:5px}} .chip{{font-size:10px;font-weight:700;padding:1px 6px;border-radius:4px;flex:0 0 auto;border:1px solid transparent;white-space:nowrap}} .chip.done{{color:var(--done-c);background:var(--done-b);border-color:var(--done-bd)}} @@ -4275,7 +5957,8 @@ class Pipe: .chip.pend{{color:var(--pend-c);background:var(--pend-b);border-color:var(--pend-bd)}} .chip.blk {{color:var(--blk-c); background:var(--blk-b); border-color:var(--blk-bd)}} .empty{{padding:10px;font-size:12px;color:var(--mu)}} - .foot{{padding:4px 10px 5px;font-size:10px;color:var(--mu);text-align:right;border-top:1px solid var(--bd)}} + .foot{{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:5px 10px;font-size:10px;color:var(--mu);border-top:1px solid var(--bd)}} + .foot .hint{{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}} @@ -4289,33 +5972,11 @@ class Pipe:
{rows_html}
-
{footer}
+
+ {ready_hint} + {footer} +
- """ @@ -4339,6 +6000,20 @@ class Pipe: if stats is not None else self._read_todo_status_from_session_db(chat_id) ) + + # If there are tasks but they are all done, do not emit the widget. + # We also clear the previous hash so if a new task is added later, it will re-trigger. + if current_stats: + total_tasks = int(current_stats.get("total", 0)) + done_tasks = int(current_stats.get("done", 0)) + if total_tasks > 0 and done_tasks == total_tasks: + self._write_todo_widget_hash(chat_id, "") # Reset hash + return { + "emitted": False, + "changed": False, + "reason": "all_tasks_done", + } + snapshot_hash = self._compute_todo_widget_hash(current_stats) previous_hash = self._read_todo_widget_hash(chat_id) changed = force or snapshot_hash != previous_hash @@ -4349,7 +6024,10 @@ class Pipe: "reason": "unchanged", } - html_doc = self._build_todo_widget_html(lang, current_stats) + html_doc = self._prepare_richui_embed_html( + self._build_todo_widget_html(lang, current_stats), + user_lang=lang, + ) try: await emitter({"type": "embeds", "data": {"embeds": [html_doc]}}) self._write_todo_widget_hash(chat_id, snapshot_hash) @@ -4924,28 +6602,28 @@ class Pipe: except Exception as e: logger.warning(f"[Session Tracking] Failed to persist mapping: {e}") - def _build_client_config(self, user_id: str = None, chat_id: str = None) -> dict: + def _build_client_config(self, user_id: str = None, chat_id: str = None, token: str = None) -> dict: """Build CopilotClient config from valves and request body.""" cwd = self._get_workspace_dir(user_id=user_id, chat_id=chat_id) config_dir = self._get_copilot_config_dir() - # Set environment variable for SDK/CLI to pick up the new config location - os.environ["COPILOTSDK_CONFIG_DIR"] = config_dir - client_config = {} if os.environ.get("COPILOT_CLI_PATH"): client_config["cli_path"] = os.environ["COPILOT_CLI_PATH"] client_config["cwd"] = cwd client_config["config_dir"] = config_dir - if self.valves.LOG_LEVEL: - client_config["log_level"] = self.valves.LOG_LEVEL - if self.valves.LOG_LEVEL: client_config["log_level"] = self.valves.LOG_LEVEL # Setup persistent CLI tool installation directories agent_env = dict(os.environ) + + # Safely inject token and config dir into the local agent environment + agent_env["COPILOTSDK_CONFIG_DIR"] = config_dir + if token: + agent_env["GH_TOKEN"] = token + agent_env["GITHUB_TOKEN"] = token if os.path.exists("/app/backend/data"): tools_dir = "/app/backend/data/.copilot_tools" npm_dir = f"{tools_dir}/npm" @@ -5217,6 +6895,74 @@ class Pipe: return SessionConfig(**session_params) + async def _abort_stale_resumed_turn_if_needed( + self, + session, + __event_call__=None, + debug_enabled: bool = False, + ) -> bool: + """ + Abort unfinished assistant work left in a resumed session before a new send. + + Without this guard, a resumed chat can continue stale in-flight work from a + previous interrupted request, which may cause unrelated extra model turns. + """ + try: + history = await session.get_messages() + except Exception as e: + await self._emit_debug_log( + f"Failed to inspect resumed session history: {e}", + __event_call__, + debug_enabled=debug_enabled, + ) + return False + + if not history: + return False + + last_user_index = -1 + for idx in range(len(history) - 1, -1, -1): + if getattr(history[idx], "type", "") == "user.message": + last_user_index = idx + break + + if last_user_index < 0: + return False + + pending_turn_depth = 0 + for event in history[last_user_index + 1 :]: + event_type = getattr(event, "type", "") + if event_type == "assistant.turn_start": + pending_turn_depth += 1 + elif event_type == "assistant.turn_end": + pending_turn_depth = max(0, pending_turn_depth - 1) + elif event_type in {"session.idle", "session.error"}: + pending_turn_depth = 0 + + if pending_turn_depth <= 0: + return False + + await self._emit_debug_log( + ( + "Detected unfinished assistant work in resumed session; " + "aborting stale turn before sending the new prompt." + ), + __event_call__, + debug_enabled=debug_enabled, + ) + + try: + await session.abort() + await asyncio.sleep(0.1) + return True + except Exception as e: + await self._emit_debug_log( + f"Failed to abort stale resumed turn: {e}", + __event_call__, + debug_enabled=debug_enabled, + ) + return False + def _build_session_hooks(self, cwd: str, __event_call__=None): """ Build session lifecycle hooks. @@ -5567,11 +7313,10 @@ class Pipe: async def _get_client(self, token: str) -> Any: """Get or create the persistent CopilotClient from the pool based on token.""" - if not token: - raise ValueError("GitHub Token is required to initialize CopilotClient") - - # Use an MD5 hash of the token as the key for the client pool - token_hash = hashlib.md5(token.encode()).hexdigest() + # Use an MD5 hash of the token as the key for the client pool. + # If no token is provided (BYOK-only mode), use a dedicated cache key. + safe_token = token or "byok_only_mode" + token_hash = hashlib.md5(safe_token.encode()).hexdigest() async with self.__class__._shared_client_lock: # Check if client exists for this token and is healthy @@ -5595,7 +7340,7 @@ class Pipe: self._setup_env(token=token) # Build configuration and start persistent client - client_config = self._build_client_config(user_id=None, chat_id=None) + client_config = self._build_client_config(user_id=None, chat_id=None, token=token) client_config["github_token"] = token client_config["auto_start"] = True @@ -5697,10 +7442,10 @@ class Pipe: ): """Setup environment variables and resolve the deterministic Copilot CLI path.""" - # 1. Real-time Token Injection (Always updates on each call) + # 1. Real-time Token Injection + # Note: We no longer set os.environ["GH_TOKEN"] globally here to prevent cross-user token pollution. + # It is handled securely via CopilotClient configuration and local agent_env. effective_token = token or self.valves.GH_TOKEN - if effective_token: - os.environ["GH_TOKEN"] = os.environ["GITHUB_TOKEN"] = effective_token if self._env_setup_done: return @@ -6120,13 +7865,18 @@ class Pipe: live_todo_stats = self._read_todo_status_from_session_db(chat_id or "") if live_todo_stats: - await self._emit_todo_widget( - chat_id=chat_id or "", - lang=user_lang, - emitter=__event_emitter__, - stats=live_todo_stats, - force=True, - ) + total_tasks = int(live_todo_stats.get("total", 0)) + done_tasks = int(live_todo_stats.get("done", 0)) + + # Only show the widget if there are tasks and not ALL of them are done. + if total_tasks > 0 and done_tasks < total_tasks: + await self._emit_todo_widget( + chat_id=chat_id or "", + lang=user_lang, + emitter=__event_emitter__, + stats=live_todo_stats, + force=True, + ) is_streaming = body.get("stream", False) await self._emit_debug_log( @@ -6427,6 +8177,11 @@ class Pipe: f"Successfully resumed session {chat_id} with model {real_model_id}", __event_call__, ) + await self._abort_stale_resumed_turn_if_needed( + session, + __event_call__=__event_call__, + debug_enabled=effective_debug, + ) except Exception as e: await self._emit_debug_log( f"Session {chat_id} not found or failed to resume ({str(e)}), creating new.", @@ -6591,6 +8346,7 @@ class Pipe: queue = asyncio.Queue() done = asyncio.Event() SENTINEL = object() + stream_start_ts = time.monotonic() # Use local state to handle concurrency and tracking state = { "thinking_started": False, @@ -6598,14 +8354,17 @@ class Pipe: "last_status_desc": None, "idle_reached": False, "session_finalized": False, + "turn_started": False, + "turn_started_ts": None, + "last_event_ts": stream_start_ts, "final_status_desc": self._get_translation( user_lang, "status_task_completed" ), } has_content = False # Track if any content has been yielded active_tools = {} # Map tool_call_id to tool_name + running_tool_calls = set() skill_invoked_in_turn = False - stream_start_ts = time.monotonic() last_wait_status_ts = 0.0 wait_status_interval = 15.0 @@ -6646,6 +8405,7 @@ class Pipe: Processes streaming deltas, reasoning, tool events, and session state. """ event_type = get_event_type(event) + state["last_event_ts"] = time.monotonic() # --- Status Emission Helper --- async def _emit_status_helper(description: str, is_done: bool = False): @@ -6719,6 +8479,8 @@ class Pipe: # === Turn Management Events === if event_type == "assistant.turn_start": + state["turn_started"] = True + state["turn_started_ts"] = state["last_event_ts"] self._emit_debug_log_sync( "Assistant Turn Started", __event_call__, @@ -6927,6 +8689,7 @@ class Pipe: "arguments": tool_args, "status_text": tool_status_text if __event_emitter__ else None, } + running_tool_calls.add(tool_call_id) # Close thinking tag if open before showing tool if state["thinking_started"]: @@ -6944,6 +8707,8 @@ class Pipe: elif event_type == "tool.execution_complete": tool_call_id = safe_get_data_attr(event, "tool_call_id", "") + if tool_call_id: + running_tool_calls.discard(tool_call_id) tool_info = active_tools.get(tool_call_id) tool_name = "tool" @@ -7121,7 +8886,13 @@ class Pipe: escape_html_attr(args_json_str) if args_json_str else "{}" ) # Use "Success" if result_content is empty to ensure card renders - result_for_attr = escape_html_attr(result_content or "Success") + # Truncate long results to prevent massive HTML attributes that cause + # page height explosion in OpenWebUI (e.g. large crawl/scrape outputs) + _max_result_display = 2000 + _result_raw = result_content or "Success" + if len(_result_raw) > _max_result_display: + _result_raw = _result_raw[:_max_result_display] + f"\n... ({len(result_content)} chars total, truncated)" + result_for_attr = escape_html_attr(_result_raw) # Emit the unified native tool_calls block: # OpenWebUI 0.8.3 frontend regex explicitly expects: name="xxx" arguments="..." result="..." done="true" @@ -7243,6 +9014,15 @@ class Pipe: ) elif event_type == "assistant.turn_end": + # Fallback: clear orphaned tool call tracking to unblock stall detection + if running_tool_calls: + self._emit_debug_log_sync( + f"Turn ended with {len(running_tool_calls)} orphaned tool call(s), clearing", + __event_call__, + debug_enabled=debug_enabled, + ) + running_tool_calls.clear() + self._emit_debug_log_sync( "Assistant Turn Ended", __event_call__, @@ -7291,13 +9071,18 @@ class Pipe: elif event_type == "session.idle": # Session finished processing - signal to the generator loop to finalize state["idle_reached"] = True + state["turn_started"] = False try: queue.put_nowait(IDLE_SENTINEL) except: pass elif event_type == "session.error": + state["turn_started"] = False + # Fallback: clear orphaned tool call tracking on session error + running_tool_calls.clear() error_msg = safe_get_data_attr(event, "message", "Unknown Error") + state["last_error_msg"] = error_msg emit_status( self._get_translation( user_lang, "status_session_error", error=error_msg @@ -7345,6 +9130,31 @@ class Pipe: # Use asyncio.create_task used to prevent session.send from blocking the stream reading # if the SDK implementation waits for completion. send_task = asyncio.create_task(session.send(send_payload)) + + def _handle_send_task_done(task: asyncio.Task): + if done.is_set(): + return + try: + exc = task.exception() + except asyncio.CancelledError: + return + except Exception as callback_err: + exc = callback_err + if not exc: + return + error_msg = f"Copilot send failed: {exc}" + self._emit_debug_log_sync( + error_msg, + __event_call__, + debug_enabled=debug_enabled, + ) + try: + queue.put_nowait(f"\n[Error: {error_msg}]") + queue.put_nowait(ERROR_SENTINEL) + except Exception: + pass + + send_task.add_done_callback(_handle_send_task_done) self._emit_debug_log_sync( f"Prompt sent (async task started)", __event_call__, @@ -7569,13 +9379,14 @@ class Pipe: if chunk is ERROR_SENTINEL: # Extract error message if possible or use default + error_desc = state.get("last_error_msg", "Error during processing") if __event_emitter__: try: await __event_emitter__( { "type": "status", "data": { - "description": "Error during processing", + "description": error_desc, "done": True, }, } @@ -7602,6 +9413,109 @@ class Pipe: break now_ts = time.monotonic() + no_progress_timeout = min(float(self.valves.TIMEOUT), 90.0) + time_since_last_event = now_ts - state.get("last_event_ts", stream_start_ts) + + # --- Primary Stall Detection: no content started yet --- + if ( + state.get("turn_started") + and not state["content_sent"] + and not state["thinking_started"] + and not running_tool_calls + and time_since_last_event >= no_progress_timeout + ): + # Attempt to rescue via Ping before aborting + try: + await asyncio.wait_for(client.ping(), timeout=5.0) + state["last_event_ts"] = time.monotonic() + self._emit_debug_log_sync( + "Stall detection threshold reached, but client is alive (Ping successful). Extended wait time.", + __event_call__, + debug_enabled=debug_enabled, + ) + continue + except Exception as ping_err: + stall_msg = ( + f"Copilot stalled after assistant.turn_start. Ping failed ({ping_err}). The request was aborted." + ) + self._emit_debug_log_sync( + stall_msg, + __event_call__, + debug_enabled=debug_enabled, + ) + try: + await asyncio.wait_for(session.abort(), timeout=5.0) + except asyncio.TimeoutError: + self._emit_debug_log_sync( + "session.abort() itself timed out (5s) — connection likely dead", + __event_call__, + debug_enabled=debug_enabled, + ) + except Exception as abort_err: + self._emit_debug_log_sync( + f"Failed to abort stalled session: {abort_err}", + __event_call__, + debug_enabled=debug_enabled, + ) + try: + queue.put_nowait(f"\n[Error: {stall_msg}]") + queue.put_nowait(ERROR_SENTINEL) + except Exception: + pass + continue + + # --- Secondary Absolute Inactivity Guard --- + # If tools are actively running (e.g. long bash crawl task), + # allow the full TIMEOUT before force-aborting. + # Otherwise use a shorter limit for faster recovery. + if running_tool_calls: + absolute_inactivity_limit = float(self.valves.TIMEOUT) + else: + absolute_inactivity_limit = no_progress_timeout * 2.0 + if time_since_last_event >= absolute_inactivity_limit: + # Attempt to rescue via Ping before aborting + try: + await asyncio.wait_for(client.ping(), timeout=5.0) + state["last_event_ts"] = time.monotonic() + self._emit_debug_log_sync( + f"Inactivity limit ({absolute_inactivity_limit}s) reached, but client is alive (Ping successful). Extended wait time.", + __event_call__, + debug_enabled=debug_enabled, + ) + continue + except Exception as ping_err: + stall_msg = ( + f"Copilot session inactive for {int(time_since_last_event)}s " + f"(limit: {int(absolute_inactivity_limit)}s). Ping failed ({ping_err}). " + f"Force-aborting to prevent permanent hang." + ) + self._emit_debug_log_sync( + stall_msg, + __event_call__, + debug_enabled=debug_enabled, + ) + running_tool_calls.clear() + try: + await asyncio.wait_for(session.abort(), timeout=5.0) + except asyncio.TimeoutError: + self._emit_debug_log_sync( + "session.abort() itself timed out (5s) — connection likely dead", + __event_call__, + debug_enabled=debug_enabled, + ) + except Exception as abort_err: + self._emit_debug_log_sync( + f"Failed to abort inactive session: {abort_err}", + __event_call__, + debug_enabled=debug_enabled, + ) + try: + queue.put_nowait(f"\n[Error: {stall_msg}]") + queue.put_nowait(ERROR_SENTINEL) + except Exception: + pass + continue + if __event_emitter__ and ( now_ts - last_wait_status_ts >= wait_status_interval ): @@ -7680,6 +9594,11 @@ class Pipe: except: pass # Connection already closed finally: + try: + if not send_task.done(): + send_task.cancel() + except Exception: + pass # Final Status Cleanup: Emergency mark all as done if not already if __event_emitter__: try: @@ -7730,13 +9649,10 @@ class Pipe: pass unsubscribe() - # Cleanup client and session - try: - # We do not destroy session here to allow persistence, - # but we must stop the client. - await client.stop() - except Exception: - pass + # In the current architecture, CopilotClient is a persistent singleton + # shared across requests to eliminate the 1-2s startup latency. + # Therefore, we NEVER stop the client here. + # The session remains alive in the SDK for follow-up turns. # Triggering release after CI fix diff --git a/plugins/pipes/github-copilot-sdk/v0.11.0.md b/plugins/pipes/github-copilot-sdk/v0.11.0.md new file mode 100644 index 0000000..4b208d3 --- /dev/null +++ b/plugins/pipes/github-copilot-sdk/v0.11.0.md @@ -0,0 +1,23 @@ +[![](https://img.shields.io/badge/OpenWebUI%20Community-Get%20Plugin-blue?style=for-the-badge)](https://openwebui.com/posts/ce96f7b4-12fc-4ac3-9a01-875713e69359) + +## Overview + +This release brings significant performance optimizations and stability enhancements. We fixed a critical bug in the client management logic, eliminating the 1-2s process startup latency between turns and significantly improving Time to First Token (TTFT). Additionally, the plugin now supports a pure BYOK mode and features improved environment isolation for concurrent user requests. + +[**View Plugin Source & Documentation**](https://github.com/Fu-Jie/openwebui-extensions/tree/main/plugins/pipes/github-copilot-sdk) + +## New Features + +- **🔑 Pure BYOK Mode**: Allows the plugin to operate without a `GH_TOKEN` by using custom API keys (OpenAI/Anthropic) via BYOK Valves. +- **🩺 Smart Stall Detection**: Integrated `client.ping()` into the timeout logic. The system now "pokes" the underlying process before aborting, preventing the termination of slow but healthy long-running tasks. +- **🧹 Smart TODO Visibility**: The TODO List widget is now automatically hidden in subsequent chats once all tasks are marked as completed, keeping the UI clean. + +## Bug Fixes + +- **🚀 Major Performance Optimization**: Fixed a regression where the shared singleton client pool was incorrectly terminated after each response. Restored 1-2s startup speed for all follow-up messages. +- **🛡️ Cross-user Isolation**: Redesigned environment variable injection to prevent token pollution during concurrent requests from different users. +- **📏 RichUI Height Stability**: Fixed the infamous infinite vertical growth bug in embedded HTML components by refining the height measurement and breaking the ResizeObserver feedback loop. + +## Migration Notes + +- If you previously relied on `GH_TOKEN` for standard Copilot models but want to switch to a pure BYOK setup, you can now safely clear the `GH_TOKEN` Valve and only configure `BYOK_BASE_URL` and `BYOK_API_KEY`. diff --git a/plugins/pipes/github-copilot-sdk/v0.11.0_CN.md b/plugins/pipes/github-copilot-sdk/v0.11.0_CN.md new file mode 100644 index 0000000..e026825 --- /dev/null +++ b/plugins/pipes/github-copilot-sdk/v0.11.0_CN.md @@ -0,0 +1,23 @@ +[![](https://img.shields.io/badge/OpenWebUI%20%E7%A4%BE%E5%8C%BA-%E8%8E%B7%E5%8F%96%E6%8F%92%E4%BB%B6-blue?style=for-the-badge)](https://openwebui.com/posts/ce96f7b4-12fc-4ac3-9a01-875713e69359) + +## Overview + +本次更新带来了重大的性能优化与稳定性提升。我们修复了客户端管理逻辑中的关键 Bug,消除了多轮对话中由于进程频繁重启导致的 1-2 秒冷启动延迟,显著优化了首字响应速度(TTFT)。此外,插件现在支持纯 BYOK 运行模式,并针对多用户并发请求强化了环境隔离机制。 + +[**查看插件源码与文档**](https://github.com/Fu-Jie/openwebui-extensions/tree/main/plugins/pipes/github-copilot-sdk) + +## 新功能 + +- **🔑 纯 BYOK 模式支持**:解除对 `GH_TOKEN` 的强制依赖。现在您可以仅通过配置 BYOK 选项(如 OpenAI/Anthropic 密钥)来完整运行插件。 +- **🩺 智能防挂死探测**:在超时逻辑中集成了 `client.ping()`。系统会在强行中断前先探测底层进程存活状态,有效避免误杀正在处理复杂长任务的健康进程。 +- **🧹 智能 TODO 显隐**:优化了 TODO List 小组件显示策略。当所有子任务标记为完成后,下一次聊天将自动隐藏该组件,保持界面清爽。 + +## 问题修复 + +- **🚀 核心性能修复**:修复了一个导致共享单例客户端池在每次响应后被误停止的 Bug。恢复了后续对话中 1-2 秒的极速启动能力。 +- **🛡️ 增强并发安全性**:重构了环境变量注入逻辑,实现了严格的用户级环境隔离,彻底杜绝了高并发场景下多用户 Token 互相污染的风险。 +- **📏 RichUI 稳定性增强**:通过改进高度测量算法并打破 ResizeObserver 递归反馈链,彻底解决了嵌入式 HTML 组件高度无限增长的问题。 + +## 迁移指南 + +- 如果您之前为了兼容性同时配置了 `GH_TOKEN` 和 BYOK,现在您可以安全地移除 `GH_TOKEN`,仅保留 BYOK 相关配置。