diff --git a/.agent/learnings/openwebui-community-api.md b/.agent/learnings/openwebui-community-api.md new file mode 100644 index 0000000..90b04cc --- /dev/null +++ b/.agent/learnings/openwebui-community-api.md @@ -0,0 +1,45 @@ +# OpenWebUI Community API Patterns + +## Post Data Structure Variations + +When fetching posts from the OpenWebUI Community API (`https://api.openwebui.com/api/v1/posts/...`), the structure of the `data` field varies significantly depending on the `type` of the post. + +### Observed Mappings + +| Post Type | Data Key (under `data`) | Usual Content | +|-----------|-------------------------|---------------| +| `action` | `function` | Plugin code and metadata | +| `filter` | `function` | Filter logic and metadata | +| `pipe` | `function` | Pipe logic and metadata | +| `tool` | `tool` | Tool definition and logic | +| `prompt` | `prompt` | Prompt template strings | +| `model` | `model` | Model configuration | + +### Implementation Workaround + +To robustly extract metadata (like `version` or `description`) regardless of the post type, the following heuristic logic is recommended: + +```python +def _get_plugin_obj(post: dict) -> dict: + data = post.get("data", {}) or {} + post_type = post.get("type") + + # Priority 1: Use specific type key + if post_type in data: + return data[post_type] + + # Priority 2: Fallback to common keys + for k in ["function", "tool", "pipe"]: + if k in data: + return data[k] + + # Priority 3: First available key + if data: + return list(data.values())[0] + + return {} +``` + +### Gotchas +- Some older posts or different categories might not have a `version` field in `manifest`, leading to empty strings or `N/A` in reports. +- `slug` should be used as the unique identifier rather than `title` when tracking stats across history. diff --git a/ai-tabs.sh b/ai-tabs.sh new file mode 100755 index 0000000..152e148 --- /dev/null +++ b/ai-tabs.sh @@ -0,0 +1,139 @@ +#!/bin/bash +# ============================================================================== +# ai-tabs - Ultra Orchestrator +# Version: v1.0.0 +# License: MIT +# Author: Fu-Jie +# Description: Batch-launches and orchestrates multiple AI CLI tools as Tabs. +# ============================================================================== + +# 1. Single-Instance Lock +LOCK_FILE="/tmp/ai_terminal_launch.lock" +# If lock is less than 10 seconds old, another instance is running. Exit. +if [ -f "$LOCK_FILE" ]; then + LOCK_TIME=$(stat -f %m "$LOCK_FILE") + NOW=$(date +%s) + if (( NOW - LOCK_TIME < 10 )); then + echo "⚠️ Another launch in progress. Skipping to prevent duplicates." + exit 0 + fi +fi +touch "$LOCK_FILE" +trap 'rm -f "$LOCK_FILE"' EXIT + +# 2. Configuration & Constants +INIT_DELAY=4.5 +PASTE_DELAY=0.3 +CMD_CREATION_DELAY=0.3 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PARENT_DIR="$(dirname "$SCRIPT_DIR")" + +# Search for .env +if [ -f "${SCRIPT_DIR}/.env" ]; then + ENV_FILE="${SCRIPT_DIR}/.env" +elif [ -f "${PARENT_DIR}/.env" ]; then + ENV_FILE="${PARENT_DIR}/.env" +fi + +# Supported Tools +SUPPORTED_TOOLS=( + "claude:--continue" + "opencode:--continue" + "gemini:--resume latest" + "copilot:--continue" + "iflow:--continue" + "kilo:--continue" +) + +FOUND_TOOLS_NAMES=() +FOUND_CMDS=() + +# 3. Part A: Load Manual Configuration +if [ -f "$ENV_FILE" ]; then + set -a; source "$ENV_FILE"; set +a + for var in $(compgen -v | grep '^TOOL_[0-9]' | sort -V); do + TPATH="${!var}" + if [ -x "$TPATH" ]; then + NAME=$(basename "$TPATH") + FLAG="--continue" + for item in "${SUPPORTED_TOOLS[@]}"; do + [[ "${item%%:*}" == "$NAME" ]] && FLAG="${item#*:}" && break + done + FOUND_TOOLS_NAMES+=("$NAME") + FOUND_CMDS+=("'$TPATH' $FLAG || '$TPATH' || exec \$SHELL") + fi + done +fi + +# 4. Part B: Automatic Tool Discovery +for item in "${SUPPORTED_TOOLS[@]}"; do + NAME="${item%%:*}" + FLAG="${item#*:}" + ALREADY_CONFIGURED=false + for configured in "${FOUND_TOOLS_NAMES[@]}"; do + [[ "$configured" == "$NAME" ]] && ALREADY_CONFIGURED=true && break + done + [[ "$ALREADY_CONFIGURED" == true ]] && continue + TPATH=$(which "$NAME" 2>/dev/null) + if [ -z "$TPATH" ]; then + SEARCH_PATHS=( + "/opt/homebrew/bin/$NAME" + "/usr/local/bin/$NAME" + "$HOME/.local/bin/$NAME" + "$HOME/bin/$NAME" + "$HOME/.$NAME/bin/$NAME" + "$HOME/.nvm/versions/node/*/bin/$NAME" + "$HOME/.npm-global/bin/$NAME" + "$HOME/.cargo/bin/$NAME" + ) + for p in "${SEARCH_PATHS[@]}"; do + for found_p in $p; do [[ -x "$found_p" ]] && TPATH="$found_p" && break 2; done + done + fi + if [ -n "$TPATH" ]; then + FOUND_TOOLS_NAMES+=("$NAME") + FOUND_CMDS+=("'$TPATH' $FLAG || '$TPATH' || exec \$SHELL") + fi +done + +NUM_FOUND=${#FOUND_CMDS[@]} +[[ "$NUM_FOUND" -eq 0 ]] && exit 1 + +# 5. Core Orchestration (Reset + Launch) +# Using Command Palette automation to avoid the need for manual shortcut binding. +AS_SCRIPT="tell application \"System Events\"\n" + +# Phase A: Creation (Using Command Palette to ensure it opens in Editor Area) +for ((i=1; i<=NUM_FOUND; i++)); do + AS_SCRIPT+=" keystroke \"p\" using {command down, shift down}\n" + AS_SCRIPT+=" delay 0.1\n" + # Ensure we are searching for the command. Using clipboard for speed and universal language support. + AS_SCRIPT+=" set the clipboard to \"Terminal: Create New Terminal in Editor Area\"\n" + AS_SCRIPT+=" keystroke \"v\" using {command down}\n" + AS_SCRIPT+=" delay 0.1\n" + AS_SCRIPT+=" keystroke return\n" + AS_SCRIPT+=" delay $CMD_CREATION_DELAY\n" +done + +# Phase B: Warmup +AS_SCRIPT+=" delay $INIT_DELAY\n" + +# Phase C: Command Injection (Reverse) +for ((i=NUM_FOUND-1; i>=0; i--)); do + FULL_CMD="${FOUND_CMDS[$i]}" + CLEAN_CMD=$(echo "$FULL_CMD" | sed 's/"/\\"/g') + AS_SCRIPT+=" set the clipboard to \"$CLEAN_CMD\"\n" + AS_SCRIPT+=" delay 0.1\n" + AS_SCRIPT+=" keystroke \"v\" using {command down}\n" + AS_SCRIPT+=" delay $PASTE_DELAY\n" + AS_SCRIPT+=" keystroke return\n" + if [ $i -gt 0 ]; then + AS_SCRIPT+=" delay 0.5\n" + AS_SCRIPT+=" keystroke \"[\" using {command down, shift down}\n" + fi +done +AS_SCRIPT+="end tell" + +# Execute +echo -e "$AS_SCRIPT" | osascript +echo "✨ Ai tabs initialized successfully ($NUM_FOUND tools found)." \ No newline at end of file diff --git a/scripts/openwebui_stats.py b/scripts/openwebui_stats.py index 530da43..bcb15d4 100644 --- a/scripts/openwebui_stats.py +++ b/scripts/openwebui_stats.py @@ -282,7 +282,7 @@ class OpenWebUIStats: data = post.get("data", {}) or {} if not data: return {} - + # Priority 1: Use post['type'] as the key (standard behavior) post_type = post.get("type") if post_type and post_type in data and data[post_type]: @@ -296,7 +296,7 @@ class OpenWebUIStats: for k in ["tool", "pipe", "action", "filter", "prompt", "model"]: if k in data and data[k]: return data[k] - + # Priority 4: If there's only one key in data, assume that's the one if len(data) == 1: return list(data.values())[0] or {} @@ -639,13 +639,12 @@ class OpenWebUIStats: stats["total_saves"] += post.get("saveCount", 0) stats["total_comments"] += post.get("commentCount", 0) - # Key: total views do not include non-downloadable types (e.g., post, review) + # Key: only count downloadable types in by_type (exclude post, review) if post_type in self.DOWNLOADABLE_TYPES or post_downloads > 0: stats["total_views"] += post_views - - if post_type not in stats["by_type"]: - stats["by_type"][post_type] = 0 - stats["by_type"][post_type] += 1 + if post_type not in stats["by_type"]: + stats["by_type"][post_type] = 0 + stats["by_type"][post_type] += 1 # Individual post information created_at = datetime.fromtimestamp(post.get("createdAt", 0))