feat: Add OpenWebUI Community API patterns documentation and enhance stats script to accurately count downloadable post types
This commit is contained in:
45
.agent/learnings/openwebui-community-api.md
Normal file
45
.agent/learnings/openwebui-community-api.md
Normal file
@@ -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.
|
||||||
139
ai-tabs.sh
Executable file
139
ai-tabs.sh
Executable file
@@ -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)."
|
||||||
@@ -639,10 +639,9 @@ class OpenWebUIStats:
|
|||||||
stats["total_saves"] += post.get("saveCount", 0)
|
stats["total_saves"] += post.get("saveCount", 0)
|
||||||
stats["total_comments"] += post.get("commentCount", 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:
|
if post_type in self.DOWNLOADABLE_TYPES or post_downloads > 0:
|
||||||
stats["total_views"] += post_views
|
stats["total_views"] += post_views
|
||||||
|
|
||||||
if post_type not in stats["by_type"]:
|
if post_type not in stats["by_type"]:
|
||||||
stats["by_type"][post_type] = 0
|
stats["by_type"][post_type] = 0
|
||||||
stats["by_type"][post_type] += 1
|
stats["by_type"][post_type] += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user