Compare commits
3 Commits
37da564a5f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 99eea4632b | |||
| ad847b1cf4 | |||
| 177fd8e1a7 |
@@ -6,7 +6,11 @@ WISECLAW_LOCAL_MODEL=qwen3-vl-8b-instruct-mlx@5bit
|
|||||||
WISECLAW_ZAI_BASE_URL=https://api.z.ai/api/anthropic
|
WISECLAW_ZAI_BASE_URL=https://api.z.ai/api/anthropic
|
||||||
WISECLAW_ZAI_MODEL=glm-5
|
WISECLAW_ZAI_MODEL=glm-5
|
||||||
WISECLAW_ANYTHINGLLM_BASE_URL=http://127.0.0.1:3001
|
WISECLAW_ANYTHINGLLM_BASE_URL=http://127.0.0.1:3001
|
||||||
WISECLAW_ANYTHINGLLM_WORKSPACE_SLUG=wiseclaw
|
WISECLAW_ANYTHINGLLM_WORKSPACE_SLUG=benim-calisma-alanim
|
||||||
|
WISECLAW_GOOGLE_CLIENT_SECRETS_FILE=.google/client_secret.json
|
||||||
|
WISECLAW_GOOGLE_TOKEN_FILE=.google/token.json
|
||||||
|
WISECLAW_GOOGLE_CLIENT_ID=
|
||||||
|
WISECLAW_GOOGLE_CLIENT_SECRET=
|
||||||
WISECLAW_SEARCH_PROVIDER=brave
|
WISECLAW_SEARCH_PROVIDER=brave
|
||||||
WISECLAW_TELEGRAM_BOT_TOKEN=
|
WISECLAW_TELEGRAM_BOT_TOKEN=
|
||||||
WISECLAW_BRAVE_API_KEY=
|
WISECLAW_BRAVE_API_KEY=
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ dist/
|
|||||||
build/
|
build/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
|
.google/
|
||||||
wiseclaw.db
|
wiseclaw.db
|
||||||
.codex/
|
.codex/
|
||||||
.playwright-cli/
|
.playwright-cli/
|
||||||
|
|||||||
308
README.md
308
README.md
@@ -1,28 +1,232 @@
|
|||||||
# WiseClaw
|
# WiseClaw
|
||||||
|
|
||||||
WiseClaw is a local-first personal assistant for macOS. It runs a FastAPI backend, supports either a local LM Studio endpoint or the hosted Z.AI API for LLM access, can augment replies with an AnythingLLM-backed second brain, exposes a Telegram bot, and includes a React admin panel for settings, logs, and memory management.
|
🦉 WiseClaw, macOS üzerinde çalışan yerel-öncelikli bir kişisel asistan altyapısıdır. FastAPI backend, Telegram botu, React admin paneli, çoklu LLM sağlayıcı desteği, tarayıcı otomasyonu, araç çağırma, otomasyonlar ve AnythingLLM tabanlı "ikinci beyin" entegrasyonunu aynı projede bir araya getirir.
|
||||||
|
|
||||||
## Planned capabilities
|
## ✨ Neler Yapabiliyor?
|
||||||
|
|
||||||
- Telegram chat with whitelist support
|
- 🤖 Telegram üzerinden konuşma, komut ve araç kullanımı
|
||||||
- Global model provider switch for `Local (LM Studio)` or `Z.AI`
|
- 🧠 `/tanisalim` ile kalıcı kullanıcı profili ve iletişim tercihleri
|
||||||
- Local LM Studio integration for `qwen3-vl-8b-instruct-mlx@5bit`
|
- 🗂️ AnythingLLM tabanlı ikinci beyin sorguları
|
||||||
- Z.AI integration for `glm-4.7` and `glm-5`
|
- 📝 `/notlarima_ekle` ile second brain notu ekleme ve otomatik senkron
|
||||||
- AnythingLLM second-brain context retrieval via workspace chat
|
- 📬 Google OAuth ile Gmail bağlantısı kurup gelen mailleri listeleme, arama ve özetleme
|
||||||
- Brave or SearXNG-backed web search
|
- ☁️ Google Drive dosyalarını listeleme, arama ve Telegram'dan gelen dosyaları Drive root'a yükleme
|
||||||
- Apple Notes integration via AppleScript
|
- ⚙️ `/otomasyon_ekle` ile zamanlanmış görev oluşturma
|
||||||
- File read/write tools
|
- 🌐 Brave Search ile web ve görsel arama
|
||||||
- Terminal execution with policy modes
|
- 🧭 `browser_use` ile gerçek tarayıcıda gezinme
|
||||||
- SQLite-backed memory, settings, and audit logs
|
- 🍎 Apple Notes üzerinde not oluşturma
|
||||||
- macOS Keychain for secrets
|
- 📁 Dosya okuma/yazma
|
||||||
|
- 🖥️ Politika tabanlı terminal komut çalıştırma
|
||||||
|
- 🔀 Global model sağlayıcı seçimi:
|
||||||
|
- `Local (LM Studio)`
|
||||||
|
- `Z.AI`
|
||||||
|
- 📊 Admin panelden ayarlar, loglar, memory, profiller ve otomasyonları yönetme
|
||||||
|
- 🔗 Admin panelden Google OAuth, AnythingLLM ve LLM bağlantı durumlarını canlı izleme
|
||||||
|
|
||||||
## Repository layout
|
## 🏗️ Mimari
|
||||||
|
|
||||||
- `backend/` FastAPI app and WiseClaw core modules
|
- `backend/`
|
||||||
- `frontend/` React admin panel
|
FastAPI uygulaması, orchestrator, tool'lar, Telegram botu ve scheduler
|
||||||
- `docs/` architecture and rollout notes
|
- `frontend/`
|
||||||
|
React tabanlı admin panel
|
||||||
|
- `docs/`
|
||||||
|
Mimari notlar ve brainstorm kayıtları
|
||||||
|
|
||||||
## Local development
|
## 🧩 LLM Sağlayıcıları
|
||||||
|
|
||||||
|
WiseClaw tek bir global sağlayıcı ile çalışır:
|
||||||
|
|
||||||
|
- 🏠 `Local (LM Studio)`
|
||||||
|
Yerel OpenAI-uyumlu endpoint üzerinden çalışır
|
||||||
|
- ☁️ `Z.AI`
|
||||||
|
Anthropic-uyumlu API üzerinden `glm-4.7` ve `glm-5` modellerini kullanır
|
||||||
|
|
||||||
|
Admin panelden aktif sağlayıcı değiştirildiğinde yeni istekler seçili sağlayıcıya gider.
|
||||||
|
|
||||||
|
## 🛠️ Başlıca Tool'lar
|
||||||
|
|
||||||
|
- `brave_search`
|
||||||
|
Web ve image search
|
||||||
|
- `web_fetch`
|
||||||
|
Tekil URL çekme ve içerik okuma
|
||||||
|
- `browser_use`
|
||||||
|
Gerçek browser otomasyonu
|
||||||
|
- `apple_notes`
|
||||||
|
Apple Notes not oluşturma
|
||||||
|
- `gmail`
|
||||||
|
Gmail mesajlarını listeleme ve arama
|
||||||
|
- `google_drive`
|
||||||
|
Google Drive dosyalarını listeleme, arama ve yükleme
|
||||||
|
- `files`
|
||||||
|
Dosya/dizin erişimi
|
||||||
|
- `terminal`
|
||||||
|
Güvenlik politikasıyla komut çalıştırma
|
||||||
|
- `second_brain`
|
||||||
|
AnythingLLM workspace context sorgulama
|
||||||
|
|
||||||
|
## 📬 Google Entegrasyonları
|
||||||
|
|
||||||
|
WiseClaw artık Google OAuth üzerinden tek bir Google hesabına bağlanabilir.
|
||||||
|
|
||||||
|
### Admin'den Bağlama
|
||||||
|
|
||||||
|
Admin panelde yeni `Google OAuth` kartı bulunur:
|
||||||
|
|
||||||
|
- `Google OAuth client ID`
|
||||||
|
- `Google OAuth client secret`
|
||||||
|
- `Connect Google` / `Reconnect Google`
|
||||||
|
|
||||||
|
Bu bilgiler kaydedildiğinde WiseClaw otomatik olarak şu dosyayı üretir:
|
||||||
|
|
||||||
|
- [backend/.google/client_secret.json](/Users/wisecolt-macmini/Project/wiseclaw/backend/.google/client_secret.json)
|
||||||
|
|
||||||
|
Başarılı bağlantıdan sonra token burada tutulur:
|
||||||
|
|
||||||
|
- [backend/.google/token.json](/Users/wisecolt-macmini/Project/wiseclaw/backend/.google/token.json)
|
||||||
|
|
||||||
|
### Gmail
|
||||||
|
|
||||||
|
Desteklenen ilk sürüm işlemleri:
|
||||||
|
|
||||||
|
- gelen kutusundaki son mailleri listeleme
|
||||||
|
- Gmail arama sorgusuyla filtreleme
|
||||||
|
- kısa özet üretme
|
||||||
|
|
||||||
|
Örnek komutlar:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gmail'de gelen ilk 10 maili özetle
|
||||||
|
Son 5 maili listele
|
||||||
|
OpenAI geçen mailleri bul
|
||||||
|
```
|
||||||
|
|
||||||
|
### Google Drive
|
||||||
|
|
||||||
|
Desteklenen ilk sürüm işlemleri:
|
||||||
|
|
||||||
|
- Drive'daki dosyaları listeleme
|
||||||
|
- dosya adıyla arama
|
||||||
|
- Telegram'dan gelen dosyayı Drive root'a yükleme
|
||||||
|
|
||||||
|
Örnek komutlar:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Drive'da invoice geçen dosyaları ara
|
||||||
|
Google Drive'daki son 10 dosyayı listele
|
||||||
|
```
|
||||||
|
|
||||||
|
Telegram upload akışı:
|
||||||
|
|
||||||
|
1. Telegram'da bir `document` veya `photo` gönder
|
||||||
|
2. O mesaja reply yap
|
||||||
|
3. Şunu yaz:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Bunu Google Drive'a yukle
|
||||||
|
```
|
||||||
|
|
||||||
|
WiseClaw dosyayı geçici alana indirir, Drive root'a yükler ve sana:
|
||||||
|
|
||||||
|
- dosya adı
|
||||||
|
- Drive linki
|
||||||
|
- dosya ID'si
|
||||||
|
|
||||||
|
döner.
|
||||||
|
|
||||||
|
## 🧠 İkinci Beyin Akışı
|
||||||
|
|
||||||
|
WiseClaw, AnythingLLM'yi ikinci beyin olarak kullanabilir.
|
||||||
|
|
||||||
|
### Sorgulama
|
||||||
|
|
||||||
|
Telegram'da örnek:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Notlara bak, serkan ile ne zaman ve nerde buluştum?
|
||||||
|
```
|
||||||
|
|
||||||
|
WiseClaw bu isteği `second_brain` tool'una yönlendirir, AnythingLLM workspace'inden bağlam çeker ve kısa cevap üretir.
|
||||||
|
|
||||||
|
### Not Ekleme
|
||||||
|
|
||||||
|
Telegram akışı:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/notlarima_ekle
|
||||||
|
```
|
||||||
|
|
||||||
|
Ardından gönderilen not:
|
||||||
|
|
||||||
|
1. SQLite veritabanına `second_brain` kaydı olarak yazılır
|
||||||
|
2. [backend/second_brain.md](/Users/wisecolt-macmini/Project/wiseclaw/backend/second_brain.md) dosyası yeniden üretilir
|
||||||
|
3. Eski `second_brain.md` AnythingLLM workspace'inden kaldırılır
|
||||||
|
4. Yeni dosya tekrar upload edilip workspace'e bağlanır
|
||||||
|
|
||||||
|
Bu yaklaşım belge tabanlı RAG akışına daha uygun olduğu için doğrudan DB -> AnythingLLM yazmaktan daha sağlamdır.
|
||||||
|
|
||||||
|
## 💬 Telegram Komutları
|
||||||
|
|
||||||
|
- `/start`
|
||||||
|
- `/tanisalim`
|
||||||
|
- `/profilim`
|
||||||
|
- `/tercihlerim`
|
||||||
|
- `/tanisalim_sifirla`
|
||||||
|
- `/otomasyon_ekle`
|
||||||
|
- `/otomasyonlar`
|
||||||
|
- `/otomasyon_durdur <id>`
|
||||||
|
- `/otomasyon_baslat <id>`
|
||||||
|
- `/otomasyon_sil <id>`
|
||||||
|
- `/notlarima_ekle`
|
||||||
|
- `/clean_chat`
|
||||||
|
|
||||||
|
`/clean_chat` yalnızca Telegram konuşma ekranını temizlemeye çalışır; veritabanındaki memory, audit log, profil veya second brain kayıtlarını silmez.
|
||||||
|
|
||||||
|
## ⏱️ Otomasyonlar
|
||||||
|
|
||||||
|
WiseClaw backend içinde çalışan scheduler ile zamanlanmış görevleri destekler.
|
||||||
|
|
||||||
|
Desteklenen ilk sürüm sıklıkları:
|
||||||
|
|
||||||
|
- günlük
|
||||||
|
- hafta içi
|
||||||
|
- haftalık
|
||||||
|
- saatlik
|
||||||
|
|
||||||
|
Otomasyon sonuçları:
|
||||||
|
|
||||||
|
- Telegram'a gönderilir
|
||||||
|
- audit log'a yazılır
|
||||||
|
|
||||||
|
## 🧪 Admin Panel
|
||||||
|
|
||||||
|
Admin panelde şunları yönetebilirsin:
|
||||||
|
|
||||||
|
- Runtime settings
|
||||||
|
- Model provider
|
||||||
|
- Search provider
|
||||||
|
- AnythingLLM canlı durum kartı (`Active / Off`)
|
||||||
|
- Google auth durumu (`Connected / Not connected`)
|
||||||
|
- Brave / Z.AI / AnythingLLM secret'ları
|
||||||
|
- Google OAuth client bilgileri
|
||||||
|
- Telegram whitelist
|
||||||
|
- User Profiles
|
||||||
|
- Automations
|
||||||
|
- Memory
|
||||||
|
- Recent Logs
|
||||||
|
|
||||||
|
Önemli endpointler:
|
||||||
|
|
||||||
|
- `/admin/dashboard`
|
||||||
|
- `/admin/settings`
|
||||||
|
- `/admin/users`
|
||||||
|
- `/admin/profiles`
|
||||||
|
- `/admin/automations`
|
||||||
|
- `/admin/memory`
|
||||||
|
- `/admin/integrations/llm`
|
||||||
|
- `/admin/integrations/telegram`
|
||||||
|
- `/admin/integrations/anythingllm`
|
||||||
|
- `/admin/integrations/google`
|
||||||
|
|
||||||
|
## 🚀 Kurulum
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
@@ -42,23 +246,73 @@ npm install
|
|||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Smoke checks
|
## 🔐 Ortam Değişkenleri
|
||||||
|
|
||||||
```bash
|
[.env.example](/Users/wisecolt-macmini/Project/wiseclaw/.env.example) dosyasını `.env` olarak kopyalayabilirsin.
|
||||||
cd backend
|
|
||||||
source .venv312/bin/activate
|
|
||||||
uvicorn app.main:app --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Then in another shell:
|
Öne çıkan alanlar:
|
||||||
|
|
||||||
|
- `WISECLAW_MODEL_PROVIDER`
|
||||||
|
- `WISECLAW_LOCAL_BASE_URL`
|
||||||
|
- `WISECLAW_LOCAL_MODEL`
|
||||||
|
- `WISECLAW_ZAI_BASE_URL`
|
||||||
|
- `WISECLAW_ZAI_MODEL`
|
||||||
|
- `WISECLAW_ANYTHINGLLM_BASE_URL`
|
||||||
|
- `WISECLAW_ANYTHINGLLM_WORKSPACE_SLUG`
|
||||||
|
- `WISECLAW_GOOGLE_CLIENT_SECRETS_FILE`
|
||||||
|
- `WISECLAW_GOOGLE_TOKEN_FILE`
|
||||||
|
- `WISECLAW_GOOGLE_CLIENT_ID`
|
||||||
|
- `WISECLAW_GOOGLE_CLIENT_SECRET`
|
||||||
|
- `WISECLAW_TELEGRAM_BOT_TOKEN`
|
||||||
|
- `WISECLAW_BRAVE_API_KEY`
|
||||||
|
- `WISECLAW_ZAI_API_KEY`
|
||||||
|
- `WISECLAW_ANYTHINGLLM_API_KEY`
|
||||||
|
|
||||||
|
Not: secret'lar admin panelden daha sonra da kaydedilebilir.
|
||||||
|
|
||||||
|
## ✅ Hızlı Kontrol
|
||||||
|
|
||||||
|
Backend ayağa kalktıktan sonra:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:8000/health
|
curl http://127.0.0.1:8000/health
|
||||||
curl http://127.0.0.1:8000/bootstrap
|
curl http://127.0.0.1:8000/bootstrap
|
||||||
curl http://127.0.0.1:8000/admin/integrations/llm
|
curl http://127.0.0.1:8000/admin/integrations/llm
|
||||||
curl http://127.0.0.1:8000/admin/integrations/telegram
|
curl http://127.0.0.1:8000/admin/integrations/telegram
|
||||||
|
curl http://127.0.0.1:8000/admin/integrations/anythingllm
|
||||||
|
curl http://127.0.0.1:8000/admin/integrations/google
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment bootstrap
|
## 🔁 Restart
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and fill in only the values you need for the first boot. Secrets such as `WISECLAW_ZAI_API_KEY` and `WISECLAW_ANYTHINGLLM_API_KEY` can also be saved later from the admin panel.
|
Projede tek komutluk restart script'i var:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/wisecolt-macmini/Project/wiseclaw
|
||||||
|
zsh ./restart.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Bu script:
|
||||||
|
|
||||||
|
- eski backend sürecini kapatır
|
||||||
|
- yeni `uvicorn` sürecini başlatır
|
||||||
|
- log'u `.wiseclaw/logs/backend.log` içine yazar
|
||||||
|
- health check ile ayağa kalktığını doğrular
|
||||||
|
|
||||||
|
## 📌 Notlar
|
||||||
|
|
||||||
|
- `LM Studio status: Reachable` görünüp `model is not installed` uyarısı alıyorsan, endpoint açık ama seçili model adı yüklü modellerle birebir eşleşmiyor demektir.
|
||||||
|
- AnythingLLM tarafında görünen workspace adı ile gerçek `slug` farklı olabilir.
|
||||||
|
- Brave image search sonuçları Telegram'da medya grubu olarak gönderilebilir.
|
||||||
|
- Bazı browser görevleri captcha/anti-bot nedeniyle manuel müdahale isteyebilir.
|
||||||
|
- Google Drive upload için OAuth yetkileri değiştiyse Google hesabını yeniden bağlamak gerekebilir.
|
||||||
|
- `/clean_chat` Telegram ekranını temizler ama SQLite kayıtlarını silmez.
|
||||||
|
|
||||||
|
## 🧭 Geliştirme Notu
|
||||||
|
|
||||||
|
Bu repo hızlı iterasyonla büyüdüğü için bazı alanlarda bilinçli teknik borçlar bulunur. Ana yön şu anda şudur:
|
||||||
|
|
||||||
|
- daha sağlam tool routing
|
||||||
|
- daha iyi approval akışları
|
||||||
|
- second brain retrieval kalitesini artırma
|
||||||
|
- admin panel kullanılabilirliğini geliştirme
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.admin.services import AdminService
|
from app.admin.services import AdminService
|
||||||
from app.config import get_settings as get_app_settings
|
from app.config import get_settings as get_app_settings
|
||||||
from app.db import SecretORM, get_session
|
from app.db import SecretORM, get_session
|
||||||
|
from app.google.auth import GoogleAuthError, GoogleAuthManager
|
||||||
from app.llm.ollama_client import OllamaClient
|
from app.llm.ollama_client import OllamaClient
|
||||||
from app.models import AutomationRecord, MemoryRecord, OllamaStatus, RuntimeSettings, TelegramStatus, UserProfileRecord, UserRecord
|
from app.models import (
|
||||||
|
AnythingLLMStatus,
|
||||||
|
AutomationRecord,
|
||||||
|
GoogleIntegrationStatus,
|
||||||
|
MemoryRecord,
|
||||||
|
OllamaStatus,
|
||||||
|
RuntimeSettings,
|
||||||
|
TelegramStatus,
|
||||||
|
UserProfileRecord,
|
||||||
|
UserRecord,
|
||||||
|
)
|
||||||
|
from app.tools.second_brain import SecondBrainTool
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||||
|
|
||||||
@@ -16,10 +31,29 @@ class SecretPayload(BaseModel):
|
|||||||
value: str
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleClientPayload(BaseModel):
|
||||||
|
client_id: str
|
||||||
|
client_secret: str
|
||||||
|
|
||||||
|
|
||||||
def get_admin_service(session: Session = Depends(get_session)) -> AdminService:
|
def get_admin_service(session: Session = Depends(get_session)) -> AdminService:
|
||||||
return AdminService(session)
|
return AdminService(session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_google_auth_manager() -> GoogleAuthManager:
|
||||||
|
return GoogleAuthManager(get_app_settings(), Path(__file__).resolve().parents[2])
|
||||||
|
|
||||||
|
|
||||||
|
def sync_google_client_file(service: AdminService, manager: GoogleAuthManager) -> None:
|
||||||
|
settings = get_app_settings()
|
||||||
|
client_id_record = service.session.get(SecretORM, "google_client_id")
|
||||||
|
client_secret_record = service.session.get(SecretORM, "google_client_secret")
|
||||||
|
client_id = (client_id_record.value if client_id_record else settings.google_client_id).strip()
|
||||||
|
client_secret = (client_secret_record.value if client_secret_record else settings.google_client_secret).strip()
|
||||||
|
if client_id and client_secret:
|
||||||
|
manager.write_client_secrets_file(client_id, client_secret)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/dashboard")
|
@router.get("/dashboard")
|
||||||
def get_dashboard(service: AdminService = Depends(get_admin_service)):
|
def get_dashboard(service: AdminService = Depends(get_admin_service)):
|
||||||
return service.dashboard()
|
return service.dashboard()
|
||||||
@@ -77,6 +111,18 @@ def post_secret(payload: SecretPayload, service: AdminService = Depends(get_admi
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/integrations/google/client")
|
||||||
|
def post_google_client(
|
||||||
|
payload: GoogleClientPayload,
|
||||||
|
service: AdminService = Depends(get_admin_service),
|
||||||
|
manager: GoogleAuthManager = Depends(get_google_auth_manager),
|
||||||
|
):
|
||||||
|
service.save_secret("google_client_id", payload.client_id.strip())
|
||||||
|
service.save_secret("google_client_secret", payload.client_secret.strip())
|
||||||
|
sync_google_client_file(service, manager)
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/integrations/llm", response_model=OllamaStatus)
|
@router.get("/integrations/llm", response_model=OllamaStatus)
|
||||||
@router.get("/integrations/ollama", response_model=OllamaStatus)
|
@router.get("/integrations/ollama", response_model=OllamaStatus)
|
||||||
async def get_llm_status(service: AdminService = Depends(get_admin_service)):
|
async def get_llm_status(service: AdminService = Depends(get_admin_service)):
|
||||||
@@ -94,3 +140,116 @@ async def get_llm_status(service: AdminService = Depends(get_admin_service)):
|
|||||||
@router.get("/integrations/telegram", response_model=TelegramStatus)
|
@router.get("/integrations/telegram", response_model=TelegramStatus)
|
||||||
def get_telegram_status(service: AdminService = Depends(get_admin_service)):
|
def get_telegram_status(service: AdminService = Depends(get_admin_service)):
|
||||||
return service.telegram_status()
|
return service.telegram_status()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/integrations/anythingllm", response_model=AnythingLLMStatus)
|
||||||
|
async def get_anythingllm_status(service: AdminService = Depends(get_admin_service)):
|
||||||
|
runtime = service.get_runtime_settings()
|
||||||
|
settings = get_app_settings()
|
||||||
|
secret = service.session.get(SecretORM, "anythingllm_api_key")
|
||||||
|
tool = SecondBrainTool(
|
||||||
|
base_url=runtime.anythingllm_base_url,
|
||||||
|
workspace_slug=runtime.anythingllm_workspace_slug,
|
||||||
|
api_key=secret.value if secret else settings.anythingllm_api_key,
|
||||||
|
)
|
||||||
|
status = await tool.status()
|
||||||
|
return AnythingLLMStatus(
|
||||||
|
reachable=bool(status.get("reachable")),
|
||||||
|
workspace_found=bool(status.get("workspace_found")),
|
||||||
|
base_url=runtime.anythingllm_base_url,
|
||||||
|
workspace_slug=runtime.anythingllm_workspace_slug,
|
||||||
|
message=str(status.get("message", "")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/integrations/google", response_model=GoogleIntegrationStatus)
|
||||||
|
def get_google_status(
|
||||||
|
request: Request,
|
||||||
|
service: AdminService = Depends(get_admin_service),
|
||||||
|
manager: GoogleAuthManager = Depends(get_google_auth_manager),
|
||||||
|
):
|
||||||
|
sync_google_client_file(service, manager)
|
||||||
|
client_configured, connected, message = manager.oauth_status()
|
||||||
|
connect_url = str(request.url_for("google_oauth_connect"))
|
||||||
|
return GoogleIntegrationStatus(
|
||||||
|
client_configured=client_configured,
|
||||||
|
connected=connected,
|
||||||
|
connect_url=connect_url,
|
||||||
|
message=message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/integrations/google/connect", name="google_oauth_connect")
|
||||||
|
def google_oauth_connect(
|
||||||
|
request: Request,
|
||||||
|
service: AdminService = Depends(get_admin_service),
|
||||||
|
manager: GoogleAuthManager = Depends(get_google_auth_manager),
|
||||||
|
):
|
||||||
|
redirect_uri = str(request.url_for("google_oauth_callback"))
|
||||||
|
try:
|
||||||
|
sync_google_client_file(service, manager)
|
||||||
|
authorization_url = manager.begin_web_oauth(redirect_uri)
|
||||||
|
except GoogleAuthError as exc:
|
||||||
|
return HTMLResponse(
|
||||||
|
(
|
||||||
|
"<html><body style='font-family: sans-serif; padding: 24px;'>"
|
||||||
|
f"<h2>Google connect failed</h2><p>{exc}</p>"
|
||||||
|
"<p>Add your client_secret.json file, then try the connect button again.</p>"
|
||||||
|
"</body></html>"
|
||||||
|
),
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=authorization_url)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/integrations/google/callback", response_class=HTMLResponse, name="google_oauth_callback")
|
||||||
|
def google_oauth_callback(
|
||||||
|
request: Request,
|
||||||
|
state: str | None = None,
|
||||||
|
error: str | None = None,
|
||||||
|
manager: GoogleAuthManager = Depends(get_google_auth_manager),
|
||||||
|
):
|
||||||
|
if error:
|
||||||
|
return HTMLResponse(
|
||||||
|
(
|
||||||
|
"<html><body style='font-family: sans-serif; padding: 24px;'>"
|
||||||
|
f"<h2>Google connect failed</h2><p>{error}</p>"
|
||||||
|
"<p>You can close this tab and try again from the WiseClaw admin panel.</p>"
|
||||||
|
"</body></html>"
|
||||||
|
),
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not state:
|
||||||
|
return HTMLResponse(
|
||||||
|
(
|
||||||
|
"<html><body style='font-family: sans-serif; padding: 24px;'>"
|
||||||
|
"<h2>Google connect failed</h2><p>Missing OAuth state.</p>"
|
||||||
|
"<p>You can close this tab and try again from the WiseClaw admin panel.</p>"
|
||||||
|
"</body></html>"
|
||||||
|
),
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
manager.complete_web_oauth(str(request.url_for("google_oauth_callback")), state, str(request.url))
|
||||||
|
except Exception as exc:
|
||||||
|
return HTMLResponse(
|
||||||
|
(
|
||||||
|
"<html><body style='font-family: sans-serif; padding: 24px;'>"
|
||||||
|
f"<h2>Google connect failed</h2><p>{exc}</p>"
|
||||||
|
"<p>You can close this tab and try again from the WiseClaw admin panel.</p>"
|
||||||
|
"</body></html>"
|
||||||
|
),
|
||||||
|
status_code=400,
|
||||||
|
)
|
||||||
|
|
||||||
|
return HTMLResponse(
|
||||||
|
(
|
||||||
|
"<html><body style='font-family: sans-serif; padding: 24px;'>"
|
||||||
|
"<h2>Google account connected</h2>"
|
||||||
|
"<p>WiseClaw can now use your Gmail and Google Drive tools.</p>"
|
||||||
|
"<p>You can close this tab and refresh the admin panel.</p>"
|
||||||
|
"</body></html>"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -169,7 +169,11 @@ class AdminService:
|
|||||||
|
|
||||||
def get_secret_mask(self, key: str) -> str:
|
def get_secret_mask(self, key: str) -> str:
|
||||||
record = self.session.get(SecretORM, key)
|
record = self.session.get(SecretORM, key)
|
||||||
value = record.value if record else ""
|
if record is not None:
|
||||||
|
value = record.value
|
||||||
|
else:
|
||||||
|
settings = get_settings()
|
||||||
|
value = str(getattr(settings, key, "") or "")
|
||||||
if len(value) < 4:
|
if len(value) < 4:
|
||||||
return ""
|
return ""
|
||||||
return f"{value[:2]}***{value[-2:]}"
|
return f"{value[:2]}***{value[-2:]}"
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ class Settings(BaseSettings):
|
|||||||
zai_base_url: str = "https://api.z.ai/api/anthropic"
|
zai_base_url: str = "https://api.z.ai/api/anthropic"
|
||||||
zai_model: str = "glm-5"
|
zai_model: str = "glm-5"
|
||||||
anythingllm_base_url: str = "http://127.0.0.1:3001"
|
anythingllm_base_url: str = "http://127.0.0.1:3001"
|
||||||
anythingllm_workspace_slug: str = "wiseclaw"
|
anythingllm_workspace_slug: str = "benim-calisma-alanim"
|
||||||
|
google_client_secrets_file: str = ".google/client_secret.json"
|
||||||
|
google_token_file: str = ".google/token.json"
|
||||||
|
google_client_id: str = Field(default="", repr=False)
|
||||||
|
google_client_secret: str = Field(default="", repr=False)
|
||||||
search_provider: str = "brave"
|
search_provider: str = "brave"
|
||||||
telegram_bot_token: str = Field(default="", repr=False)
|
telegram_bot_token: str = Field(default="", repr=False)
|
||||||
brave_api_key: str = Field(default="", repr=False)
|
brave_api_key: str = Field(default="", repr=False)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ DEFAULT_TOOLS = {
|
|||||||
"brave_search": True,
|
"brave_search": True,
|
||||||
"second_brain": True,
|
"second_brain": True,
|
||||||
"browser_use": True,
|
"browser_use": True,
|
||||||
|
"gmail": True,
|
||||||
|
"google_drive": True,
|
||||||
"searxng_search": False,
|
"searxng_search": False,
|
||||||
"web_fetch": True,
|
"web_fetch": True,
|
||||||
"apple_notes": True,
|
"apple_notes": True,
|
||||||
|
|||||||
164
backend/app/google/auth.py
Normal file
164
backend/app/google/auth.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google.auth.transport.requests import Request
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
from google_auth_oauthlib.flow import Flow
|
||||||
|
|
||||||
|
from app.config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
GOOGLE_SCOPES = [
|
||||||
|
"https://www.googleapis.com/auth/gmail.readonly",
|
||||||
|
"https://www.googleapis.com/auth/drive.metadata.readonly",
|
||||||
|
"https://www.googleapis.com/auth/drive.file",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleAuthError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
_OAUTH_STATES: dict[str, float] = {}
|
||||||
|
_OAUTH_STATE_TTL_SECONDS = 900
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleAuthManager:
|
||||||
|
def __init__(self, settings: Settings, workspace_root: Path) -> None:
|
||||||
|
self.settings = settings
|
||||||
|
self.workspace_root = workspace_root.resolve()
|
||||||
|
|
||||||
|
async def get_credentials(self) -> Credentials:
|
||||||
|
return await asyncio.to_thread(self._load_credentials)
|
||||||
|
|
||||||
|
def _load_credentials(self) -> Credentials:
|
||||||
|
token_path = self.token_path
|
||||||
|
client_path = self.client_path
|
||||||
|
|
||||||
|
if not client_path.exists():
|
||||||
|
raise GoogleAuthError(
|
||||||
|
f"Google client secrets file is missing: {client_path}. "
|
||||||
|
"Create a Google OAuth Web Application and place its JSON here."
|
||||||
|
)
|
||||||
|
if not token_path.exists():
|
||||||
|
raise GoogleAuthError(
|
||||||
|
f"Google token file is missing: {token_path}. "
|
||||||
|
"Run the Google OAuth bootstrap step first."
|
||||||
|
)
|
||||||
|
|
||||||
|
credentials = Credentials.from_authorized_user_file(str(token_path), GOOGLE_SCOPES)
|
||||||
|
if credentials.expired and credentials.refresh_token:
|
||||||
|
credentials.refresh(Request())
|
||||||
|
token_path.write_text(credentials.to_json(), encoding="utf-8")
|
||||||
|
|
||||||
|
if not credentials.valid:
|
||||||
|
raise GoogleAuthError(
|
||||||
|
"Google credentials are invalid. Re-run the Google OAuth bootstrap step."
|
||||||
|
)
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_path(self) -> Path:
|
||||||
|
return self._resolve_path(self.settings.google_client_secrets_file)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token_path(self) -> Path:
|
||||||
|
return self._resolve_path(self.settings.google_token_file)
|
||||||
|
|
||||||
|
def write_client_secrets_file(self, client_id: str, client_secret: str) -> Path:
|
||||||
|
client_id = client_id.strip()
|
||||||
|
client_secret = client_secret.strip()
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
raise GoogleAuthError("Google client ID and client secret are required.")
|
||||||
|
|
||||||
|
redirect_uris = [
|
||||||
|
f"http://127.0.0.1:{self.settings.admin_port}/admin/integrations/google/callback",
|
||||||
|
f"http://localhost:{self.settings.admin_port}/admin/integrations/google/callback",
|
||||||
|
]
|
||||||
|
payload = {
|
||||||
|
"web": {
|
||||||
|
"client_id": client_id,
|
||||||
|
"project_id": "wiseclaw-local",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_secret": client_secret,
|
||||||
|
"redirect_uris": redirect_uris,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.client_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.client_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||||
|
return self.client_path
|
||||||
|
|
||||||
|
def begin_web_oauth(self, redirect_uri: str) -> str:
|
||||||
|
if not self.client_path.exists():
|
||||||
|
raise GoogleAuthError(
|
||||||
|
f"Google client secrets file is missing: {self.client_path}. "
|
||||||
|
"Place your Google OAuth client JSON there first."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._configure_local_oauth_transport(redirect_uri)
|
||||||
|
flow = Flow.from_client_secrets_file(str(self.client_path), GOOGLE_SCOPES)
|
||||||
|
flow.redirect_uri = redirect_uri
|
||||||
|
authorization_url, state = flow.authorization_url(
|
||||||
|
access_type="offline",
|
||||||
|
include_granted_scopes="true",
|
||||||
|
prompt="consent",
|
||||||
|
)
|
||||||
|
self._store_state(state)
|
||||||
|
return authorization_url
|
||||||
|
|
||||||
|
def complete_web_oauth(self, redirect_uri: str, state: str, authorization_response: str) -> Credentials:
|
||||||
|
if not self._consume_state(state):
|
||||||
|
raise GoogleAuthError("Google OAuth state is missing or expired. Start the connect flow again.")
|
||||||
|
|
||||||
|
self._configure_local_oauth_transport(redirect_uri)
|
||||||
|
flow = Flow.from_client_secrets_file(str(self.client_path), GOOGLE_SCOPES, state=state)
|
||||||
|
flow.redirect_uri = redirect_uri
|
||||||
|
flow.fetch_token(authorization_response=authorization_response)
|
||||||
|
credentials = flow.credentials
|
||||||
|
self.token_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.token_path.write_text(credentials.to_json(), encoding="utf-8")
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
def oauth_status(self) -> tuple[bool, bool, str]:
|
||||||
|
if not self.client_path.exists():
|
||||||
|
return False, False, (
|
||||||
|
f"Missing Google OAuth client file at {self.client_path}. "
|
||||||
|
"Add client_secret.json first."
|
||||||
|
)
|
||||||
|
if not self.token_path.exists():
|
||||||
|
return False, False, "Google account is not connected yet."
|
||||||
|
try:
|
||||||
|
self._load_credentials()
|
||||||
|
except GoogleAuthError as exc:
|
||||||
|
return True, False, str(exc)
|
||||||
|
return True, True, "Google account is connected."
|
||||||
|
|
||||||
|
def _resolve_path(self, raw_path: str) -> Path:
|
||||||
|
path = Path(raw_path).expanduser()
|
||||||
|
if not path.is_absolute():
|
||||||
|
path = (self.workspace_root / path).resolve()
|
||||||
|
else:
|
||||||
|
path = path.resolve()
|
||||||
|
return path
|
||||||
|
|
||||||
|
def _store_state(self, state: str) -> None:
|
||||||
|
now = time.time()
|
||||||
|
expired = [item for item, created_at in _OAUTH_STATES.items() if now - created_at > _OAUTH_STATE_TTL_SECONDS]
|
||||||
|
for item in expired:
|
||||||
|
_OAUTH_STATES.pop(item, None)
|
||||||
|
_OAUTH_STATES[state] = now
|
||||||
|
|
||||||
|
def _consume_state(self, state: str) -> bool:
|
||||||
|
created_at = _OAUTH_STATES.pop(state, None)
|
||||||
|
if created_at is None:
|
||||||
|
return False
|
||||||
|
return time.time() - created_at <= _OAUTH_STATE_TTL_SECONDS
|
||||||
|
|
||||||
|
def _configure_local_oauth_transport(self, redirect_uri: str) -> None:
|
||||||
|
if redirect_uri.startswith("http://127.0.0.1") or redirect_uri.startswith("http://localhost"):
|
||||||
|
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
||||||
@@ -30,6 +30,8 @@ def build_prompt_context(
|
|||||||
"If the user asks you to create or update files, use the files tool with action `write`.\n"
|
"If the user asks you to create or update files, use the files tool with action `write`.\n"
|
||||||
"If the user asks you to create a note in Apple Notes, use apple_notes with action `create_note`.\n"
|
"If the user asks you to create a note in Apple Notes, use apple_notes with action `create_note`.\n"
|
||||||
"If the user asks about their saved notes, documents, archive, workspace knowledge, or second brain, use second_brain or the injected second-brain context before answering.\n"
|
"If the user asks about their saved notes, documents, archive, workspace knowledge, or second brain, use second_brain or the injected second-brain context before answering.\n"
|
||||||
|
"If the user asks about Gmail or email inbox contents, use the gmail tool before answering.\n"
|
||||||
|
"If the user asks about Google Drive files or documents, use the google_drive tool before answering.\n"
|
||||||
"For a static HTML/CSS/JS app, write the files first, then use the terminal tool to run a local server in the background with a command like `python3 -m http.server 9990 -d <folder>`.\n"
|
"For a static HTML/CSS/JS app, write the files first, then use the terminal tool to run a local server in the background with a command like `python3 -m http.server 9990 -d <folder>`.\n"
|
||||||
"If the user asks you to open, inspect, interact with, or extract information from a website in a real browser, use browser_use.\n"
|
"If the user asks you to open, inspect, interact with, or extract information from a website in a real browser, use browser_use.\n"
|
||||||
"If the user asks you to inspect files, browse the web, or run terminal commands, use the matching tool instead of guessing. "
|
"If the user asks you to inspect files, browse the web, or run terminal commands, use the matching tool instead of guessing. "
|
||||||
|
|||||||
@@ -61,12 +61,14 @@ class RuntimeSettings(BaseModel):
|
|||||||
local_model: str = "qwen3-vl-8b-instruct-mlx@5bit"
|
local_model: str = "qwen3-vl-8b-instruct-mlx@5bit"
|
||||||
zai_model: Literal["glm-4.7", "glm-5"] = "glm-5"
|
zai_model: Literal["glm-4.7", "glm-5"] = "glm-5"
|
||||||
anythingllm_base_url: str = "http://127.0.0.1:3001"
|
anythingllm_base_url: str = "http://127.0.0.1:3001"
|
||||||
anythingllm_workspace_slug: str = "wiseclaw"
|
anythingllm_workspace_slug: str = "benim-calisma-alanim"
|
||||||
tools: list[ToolToggle] = Field(
|
tools: list[ToolToggle] = Field(
|
||||||
default_factory=lambda: [
|
default_factory=lambda: [
|
||||||
ToolToggle(name="brave_search", enabled=True),
|
ToolToggle(name="brave_search", enabled=True),
|
||||||
ToolToggle(name="second_brain", enabled=True),
|
ToolToggle(name="second_brain", enabled=True),
|
||||||
ToolToggle(name="browser_use", enabled=True),
|
ToolToggle(name="browser_use", enabled=True),
|
||||||
|
ToolToggle(name="gmail", enabled=True),
|
||||||
|
ToolToggle(name="google_drive", enabled=True),
|
||||||
ToolToggle(name="searxng_search", enabled=False),
|
ToolToggle(name="searxng_search", enabled=False),
|
||||||
ToolToggle(name="web_fetch", enabled=True),
|
ToolToggle(name="web_fetch", enabled=True),
|
||||||
ToolToggle(name="apple_notes", enabled=True),
|
ToolToggle(name="apple_notes", enabled=True),
|
||||||
@@ -105,6 +107,21 @@ class TelegramStatus(BaseModel):
|
|||||||
message: str
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleIntegrationStatus(BaseModel):
|
||||||
|
client_configured: bool
|
||||||
|
connected: bool
|
||||||
|
connect_url: str
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class AnythingLLMStatus(BaseModel):
|
||||||
|
reachable: bool
|
||||||
|
workspace_found: bool
|
||||||
|
base_url: str
|
||||||
|
workspace_slug: str
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
class AutomationRecord(BaseModel):
|
class AutomationRecord(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
telegram_user_id: int
|
telegram_user_id: int
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from telegram import BotCommand, InputMediaPhoto, Update
|
from telegram import BotCommand, InputMediaPhoto, Update
|
||||||
from telegram.constants import ChatAction
|
from telegram.constants import ChatAction
|
||||||
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
|
from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters
|
||||||
|
|
||||||
|
from app.db import AuditLogORM
|
||||||
from app.orchestrator import WiseClawOrchestrator
|
from app.orchestrator import WiseClawOrchestrator
|
||||||
|
from app.telegram.auth import is_authorized
|
||||||
|
from app.tools.registry import build_tools
|
||||||
|
|
||||||
|
|
||||||
class TelegramBotService:
|
class TelegramBotService:
|
||||||
@@ -75,6 +79,7 @@ class TelegramBotService:
|
|||||||
return
|
return
|
||||||
self.application = Application.builder().token(self.token).build()
|
self.application = Application.builder().token(self.token).build()
|
||||||
self.application.add_handler(CommandHandler("start", self._on_start))
|
self.application.add_handler(CommandHandler("start", self._on_start))
|
||||||
|
self.application.add_handler(CommandHandler("clean_chat", self._on_clean_chat))
|
||||||
self.application.add_handler(CommandHandler("tanisalim", self._on_command_passthrough))
|
self.application.add_handler(CommandHandler("tanisalim", self._on_command_passthrough))
|
||||||
self.application.add_handler(CommandHandler("profilim", self._on_command_passthrough))
|
self.application.add_handler(CommandHandler("profilim", self._on_command_passthrough))
|
||||||
self.application.add_handler(CommandHandler("tercihlerim", self._on_command_passthrough))
|
self.application.add_handler(CommandHandler("tercihlerim", self._on_command_passthrough))
|
||||||
@@ -85,6 +90,7 @@ class TelegramBotService:
|
|||||||
self.application.add_handler(CommandHandler("otomasyon_baslat", self._on_command_passthrough))
|
self.application.add_handler(CommandHandler("otomasyon_baslat", self._on_command_passthrough))
|
||||||
self.application.add_handler(CommandHandler("otomasyon_sil", self._on_command_passthrough))
|
self.application.add_handler(CommandHandler("otomasyon_sil", self._on_command_passthrough))
|
||||||
self.application.add_handler(CommandHandler("notlarima_ekle", self._on_command_passthrough))
|
self.application.add_handler(CommandHandler("notlarima_ekle", self._on_command_passthrough))
|
||||||
|
self.application.add_handler(MessageHandler(filters.Document.ALL | filters.PHOTO, self._on_attachment))
|
||||||
self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self._on_text))
|
self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self._on_text))
|
||||||
await self.application.initialize()
|
await self.application.initialize()
|
||||||
await self.application.bot.set_my_commands(self._telegram_commands())
|
await self.application.bot.set_my_commands(self._telegram_commands())
|
||||||
@@ -110,6 +116,8 @@ class TelegramBotService:
|
|||||||
async def _on_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def _on_text(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
if update.message is None or update.effective_user is None or update.message.text is None:
|
if update.message is None or update.effective_user is None or update.message.text is None:
|
||||||
return
|
return
|
||||||
|
if await self._maybe_handle_drive_upload_from_reply(update, context):
|
||||||
|
return
|
||||||
typing_task = asyncio.create_task(self._send_typing(update.effective_chat.id, context))
|
typing_task = asyncio.create_task(self._send_typing(update.effective_chat.id, context))
|
||||||
try:
|
try:
|
||||||
reply = await self.process_message_payload(update.effective_user.id, update.message.text)
|
reply = await self.process_message_payload(update.effective_user.id, update.message.text)
|
||||||
@@ -127,6 +135,36 @@ class TelegramBotService:
|
|||||||
for chunk in self._chunk_message(text_reply):
|
for chunk in self._chunk_message(text_reply):
|
||||||
await update.message.reply_text(chunk)
|
await update.message.reply_text(chunk)
|
||||||
|
|
||||||
|
async def _on_attachment(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
if update.message is None or update.effective_user is None:
|
||||||
|
return
|
||||||
|
if not self._message_has_supported_attachment(update.message):
|
||||||
|
return
|
||||||
|
if self._looks_like_drive_upload_request(update.message.caption or ""):
|
||||||
|
await self._handle_drive_upload(update, context, update.message)
|
||||||
|
return
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Dosyayi aldim. Google Drive'a yuklemek icin bu mesaja reply yapip `Bunu Google Drive'a yukle` yazabilirsin.",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _on_clean_chat(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
if update.message is None or update.effective_chat is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
chat_id = update.effective_chat.id
|
||||||
|
latest_message_id = update.message.message_id
|
||||||
|
consecutive_failures = 0
|
||||||
|
|
||||||
|
for message_id in range(latest_message_id, 0, -1):
|
||||||
|
try:
|
||||||
|
await context.bot.delete_message(chat_id=chat_id, message_id=message_id)
|
||||||
|
consecutive_failures = 0
|
||||||
|
except Exception:
|
||||||
|
consecutive_failures += 1
|
||||||
|
if consecutive_failures >= 50:
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
|
||||||
async def _on_command_passthrough(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
async def _on_command_passthrough(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
del context
|
del context
|
||||||
if update.message is None or update.effective_user is None or update.message.text is None:
|
if update.message is None or update.effective_user is None or update.message.text is None:
|
||||||
@@ -147,6 +185,119 @@ class TelegramBotService:
|
|||||||
await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
|
await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
|
||||||
await asyncio.sleep(4)
|
await asyncio.sleep(4)
|
||||||
|
|
||||||
|
async def _maybe_handle_drive_upload_from_reply(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
|
||||||
|
if update.message is None or update.effective_user is None or update.message.text is None:
|
||||||
|
return False
|
||||||
|
if not self._looks_like_drive_upload_request(update.message.text):
|
||||||
|
return False
|
||||||
|
reply_target = update.message.reply_to_message
|
||||||
|
if reply_target is None or not self._message_has_supported_attachment(reply_target):
|
||||||
|
await update.message.reply_text(
|
||||||
|
"Google Drive'a yuklemek icin once dosya veya fotograf gonder, sonra o mesaja reply yapip `Bunu Google Drive'a yukle` yaz.",
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
await self._handle_drive_upload(update, context, reply_target)
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _handle_drive_upload(self, update: Update, context: ContextTypes.DEFAULT_TYPE, source_message: Any) -> None:
|
||||||
|
if update.message is None or update.effective_user is None or update.effective_chat is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
with self.orchestrator_factory() as session:
|
||||||
|
if not is_authorized(session, update.effective_user.id):
|
||||||
|
await update.message.reply_text("This Telegram user is not authorized for WiseClaw.")
|
||||||
|
return
|
||||||
|
|
||||||
|
orchestrator = WiseClawOrchestrator(session)
|
||||||
|
runtime = orchestrator.get_runtime_settings()
|
||||||
|
tools = build_tools(runtime, Path(__file__).resolve().parents[2], session)
|
||||||
|
drive_tool = tools.get("google_drive")
|
||||||
|
if drive_tool is None:
|
||||||
|
await update.message.reply_text("Google Drive araci etkin degil.")
|
||||||
|
return
|
||||||
|
|
||||||
|
temp_file = None
|
||||||
|
try:
|
||||||
|
attachment = await self._download_attachment(context, update.effective_chat.id, source_message)
|
||||||
|
if attachment is None:
|
||||||
|
await update.message.reply_text("Bu mesajdan yuklenebilir bir dosya cikarilamadi.")
|
||||||
|
return
|
||||||
|
temp_file = attachment["local_path"]
|
||||||
|
result = await drive_tool.run(
|
||||||
|
{
|
||||||
|
"action": "upload",
|
||||||
|
"local_path": attachment["local_path"],
|
||||||
|
"filename": attachment["filename"],
|
||||||
|
"mime_type": attachment["mime_type"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
AuditLogORM(
|
||||||
|
category="tool",
|
||||||
|
message=f"tool:google_drive:{json.dumps({'action': 'upload', 'filename': attachment['filename']}, ensure_ascii=False)}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if result.get("status") != "ok":
|
||||||
|
message = str(result.get("message", "Google Drive upload failed."))
|
||||||
|
await update.message.reply_text(f"Dosyayi Google Drive'a yukleyemedim: {message}")
|
||||||
|
return
|
||||||
|
|
||||||
|
file_info = result.get("file", {})
|
||||||
|
if not isinstance(file_info, dict):
|
||||||
|
file_info = {}
|
||||||
|
link = str(file_info.get("web_view_link") or file_info.get("web_content_link") or "").strip()
|
||||||
|
file_id = str(file_info.get("id", "")).strip()
|
||||||
|
name = str(file_info.get("name", attachment["filename"])).strip()
|
||||||
|
response_lines = [f"Dosya Google Drive'a yuklendi: {name}"]
|
||||||
|
if link:
|
||||||
|
response_lines.append(f"Link: {link}")
|
||||||
|
if file_id:
|
||||||
|
response_lines.append(f"Dosya ID: {file_id}")
|
||||||
|
await update.message.reply_text("\n".join(response_lines))
|
||||||
|
finally:
|
||||||
|
if temp_file:
|
||||||
|
with suppress(OSError):
|
||||||
|
Path(temp_file).unlink()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
async def _download_attachment(self, context: ContextTypes.DEFAULT_TYPE, chat_id: int, message: Any) -> dict[str, str] | None:
|
||||||
|
if getattr(message, "document", None) is not None:
|
||||||
|
document = message.document
|
||||||
|
tg_file = await context.bot.get_file(document.file_id)
|
||||||
|
filename = document.file_name or f"telegram_document_{message.message_id}"
|
||||||
|
mime_type = document.mime_type or "application/octet-stream"
|
||||||
|
elif getattr(message, "photo", None):
|
||||||
|
photo = message.photo[-1]
|
||||||
|
tg_file = await context.bot.get_file(photo.file_id)
|
||||||
|
filename = f"telegram_photo_{message.message_id}.jpg"
|
||||||
|
mime_type = "image/jpeg"
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
temp_dir = Path(__file__).resolve().parents[2] / "tmp" / "telegram_uploads"
|
||||||
|
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
safe_name = self._sanitize_filename(filename)
|
||||||
|
local_path = temp_dir / f"{chat_id}_{message.message_id}_{safe_name}"
|
||||||
|
await tg_file.download_to_drive(custom_path=str(local_path))
|
||||||
|
return {
|
||||||
|
"local_path": str(local_path),
|
||||||
|
"filename": filename,
|
||||||
|
"mime_type": mime_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _looks_like_drive_upload_request(self, text: str) -> bool:
|
||||||
|
normalized = text.casefold()
|
||||||
|
references_drive = "drive" in normalized or "google drive" in normalized
|
||||||
|
upload_intent = any(term in normalized for term in ("yukle", "yükle", "gonder", "gönder", "upload"))
|
||||||
|
return references_drive and upload_intent
|
||||||
|
|
||||||
|
def _message_has_supported_attachment(self, message: Any) -> bool:
|
||||||
|
return bool(getattr(message, "document", None) is not None or getattr(message, "photo", None))
|
||||||
|
|
||||||
|
def _sanitize_filename(self, filename: str) -> str:
|
||||||
|
cleaned = "".join(char if char.isalnum() or char in {"-", "_", "."} else "_" for char in filename.strip())
|
||||||
|
return cleaned or "attachment.bin"
|
||||||
|
|
||||||
def _chunk_message(self, text: str) -> list[str]:
|
def _chunk_message(self, text: str) -> list[str]:
|
||||||
if len(text) <= self.MAX_MESSAGE_LEN:
|
if len(text) <= self.MAX_MESSAGE_LEN:
|
||||||
return [text]
|
return [text]
|
||||||
@@ -170,6 +321,7 @@ class TelegramBotService:
|
|||||||
BotCommand("profilim", "Kayitli profil ozetimi goster (wc)"),
|
BotCommand("profilim", "Kayitli profil ozetimi goster (wc)"),
|
||||||
BotCommand("tercihlerim", "Kayitli iletisim tercihlerini goster (wc)"),
|
BotCommand("tercihlerim", "Kayitli iletisim tercihlerini goster (wc)"),
|
||||||
BotCommand("tanisalim_sifirla", "Tanisma profilini sifirla (wc)"),
|
BotCommand("tanisalim_sifirla", "Tanisma profilini sifirla (wc)"),
|
||||||
|
BotCommand("clean_chat", "Telegram ekranindaki mesajlari temizle (wc)"),
|
||||||
BotCommand("otomasyon_ekle", "Yeni otomasyon wizard'ini baslat (wc)"),
|
BotCommand("otomasyon_ekle", "Yeni otomasyon wizard'ini baslat (wc)"),
|
||||||
BotCommand("otomasyonlar", "Otomasyon listesini goster (wc)"),
|
BotCommand("otomasyonlar", "Otomasyon listesini goster (wc)"),
|
||||||
BotCommand("otomasyon_durdur", "Bir otomasyonu durdur: /otomasyon_durdur <id> (wc)"),
|
BotCommand("otomasyon_durdur", "Bir otomasyonu durdur: /otomasyon_durdur <id> (wc)"),
|
||||||
|
|||||||
110
backend/app/tools/gmail.py
Normal file
110
backend/app/tools/gmail.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
|
||||||
|
from app.google.auth import GoogleAuthError, GoogleAuthManager
|
||||||
|
from app.tools.base import Tool
|
||||||
|
|
||||||
|
|
||||||
|
class GmailTool(Tool):
|
||||||
|
name = "gmail"
|
||||||
|
description = "List and search Gmail messages for the connected Google account."
|
||||||
|
|
||||||
|
def __init__(self, auth_manager: GoogleAuthManager) -> None:
|
||||||
|
self.auth_manager = auth_manager
|
||||||
|
|
||||||
|
def parameters_schema(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional Gmail search query such as from:someone newer_than:7d.",
|
||||||
|
},
|
||||||
|
"max_results": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum number of messages to return, from 1 to 20.",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
},
|
||||||
|
"label_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Optional Gmail label filters. Defaults to INBOX.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def run(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
query = str(payload.get("query", "")).strip()
|
||||||
|
max_results = max(1, min(20, int(payload.get("max_results", 10) or 10)))
|
||||||
|
label_ids = payload.get("label_ids")
|
||||||
|
if not isinstance(label_ids, list) or not label_ids:
|
||||||
|
label_ids = ["INBOX"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
creds = await self.auth_manager.get_credentials()
|
||||||
|
except GoogleAuthError as exc:
|
||||||
|
return {"tool": self.name, "status": "error", "message": str(exc)}
|
||||||
|
|
||||||
|
return await asyncio.to_thread(
|
||||||
|
self._list_messages,
|
||||||
|
creds,
|
||||||
|
query,
|
||||||
|
max_results,
|
||||||
|
[str(label) for label in label_ids],
|
||||||
|
)
|
||||||
|
|
||||||
|
def _list_messages(
|
||||||
|
self,
|
||||||
|
credentials: Any,
|
||||||
|
query: str,
|
||||||
|
max_results: int,
|
||||||
|
label_ids: list[str],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
service = build("gmail", "v1", credentials=credentials, cache_discovery=False)
|
||||||
|
response = (
|
||||||
|
service.users()
|
||||||
|
.messages()
|
||||||
|
.list(userId="me", q=query or None, labelIds=label_ids, maxResults=max_results)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
message_refs = response.get("messages", [])
|
||||||
|
messages = []
|
||||||
|
for item in message_refs:
|
||||||
|
detail = (
|
||||||
|
service.users()
|
||||||
|
.messages()
|
||||||
|
.get(
|
||||||
|
userId="me",
|
||||||
|
id=item["id"],
|
||||||
|
format="metadata",
|
||||||
|
metadataHeaders=["From", "Subject", "Date"],
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
headers = {
|
||||||
|
header.get("name", "").lower(): header.get("value", "")
|
||||||
|
for header in detail.get("payload", {}).get("headers", [])
|
||||||
|
}
|
||||||
|
messages.append(
|
||||||
|
{
|
||||||
|
"id": detail.get("id", ""),
|
||||||
|
"thread_id": detail.get("threadId", ""),
|
||||||
|
"from": headers.get("from", ""),
|
||||||
|
"subject": headers.get("subject", "(no subject)"),
|
||||||
|
"date": headers.get("date", ""),
|
||||||
|
"snippet": detail.get("snippet", ""),
|
||||||
|
"label_ids": detail.get("labelIds", []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tool": self.name,
|
||||||
|
"status": "ok",
|
||||||
|
"query": query,
|
||||||
|
"count": len(messages),
|
||||||
|
"messages": messages,
|
||||||
|
}
|
||||||
167
backend/app/tools/google_drive.py
Normal file
167
backend/app/tools/google_drive.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from googleapiclient.errors import HttpError
|
||||||
|
from googleapiclient.http import MediaFileUpload
|
||||||
|
|
||||||
|
from app.google.auth import GoogleAuthError, GoogleAuthManager
|
||||||
|
from app.tools.base import Tool
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleDriveTool(Tool):
|
||||||
|
name = "google_drive"
|
||||||
|
description = "List, search, and upload files to the connected Google Drive account."
|
||||||
|
|
||||||
|
def __init__(self, auth_manager: GoogleAuthManager) -> None:
|
||||||
|
self.auth_manager = auth_manager
|
||||||
|
|
||||||
|
def parameters_schema(self) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"action": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["list", "upload"],
|
||||||
|
"description": "Drive action to perform. Defaults to list.",
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional Drive API query or free-text filename search.",
|
||||||
|
},
|
||||||
|
"max_results": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Maximum number of files to return, from 1 to 20.",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
},
|
||||||
|
"local_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Absolute local file path to upload when action is upload.",
|
||||||
|
},
|
||||||
|
"filename": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional destination filename for uploads.",
|
||||||
|
},
|
||||||
|
"mime_type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional MIME type for uploads.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def run(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
action = str(payload.get("action", "list") or "list").strip().lower()
|
||||||
|
query = str(payload.get("query", "")).strip()
|
||||||
|
max_results = max(1, min(20, int(payload.get("max_results", 10) or 10)))
|
||||||
|
local_path = str(payload.get("local_path", "")).strip()
|
||||||
|
filename = str(payload.get("filename", "")).strip()
|
||||||
|
mime_type = str(payload.get("mime_type", "")).strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
creds = await self.auth_manager.get_credentials()
|
||||||
|
except GoogleAuthError as exc:
|
||||||
|
return {"tool": self.name, "status": "error", "message": str(exc)}
|
||||||
|
|
||||||
|
if action == "upload":
|
||||||
|
if not local_path:
|
||||||
|
return {"tool": self.name, "status": "error", "message": "local_path is required for uploads."}
|
||||||
|
try:
|
||||||
|
return await asyncio.to_thread(self._upload_file, creds, local_path, filename, mime_type)
|
||||||
|
except HttpError as exc:
|
||||||
|
return {"tool": self.name, "status": "error", "message": self._format_http_error(exc)}
|
||||||
|
|
||||||
|
return await asyncio.to_thread(self._list_files, creds, query, max_results)
|
||||||
|
|
||||||
|
def _list_files(self, credentials: Any, query: str, max_results: int) -> dict[str, Any]:
|
||||||
|
service = build("drive", "v3", credentials=credentials, cache_discovery=False)
|
||||||
|
api_query = ""
|
||||||
|
if query:
|
||||||
|
if any(token in query for token in ("name contains", "mimeType", "trashed", "modifiedTime")):
|
||||||
|
api_query = query
|
||||||
|
else:
|
||||||
|
escaped = query.replace("'", "\\'")
|
||||||
|
api_query = f"name contains '{escaped}' and trashed = false"
|
||||||
|
else:
|
||||||
|
api_query = "trashed = false"
|
||||||
|
|
||||||
|
response = (
|
||||||
|
service.files()
|
||||||
|
.list(
|
||||||
|
q=api_query,
|
||||||
|
pageSize=max_results,
|
||||||
|
orderBy="modifiedTime desc",
|
||||||
|
fields="files(id,name,mimeType,modifiedTime,webViewLink,owners(displayName))",
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
files = []
|
||||||
|
for item in response.get("files", []):
|
||||||
|
owners = item.get("owners", [])
|
||||||
|
files.append(
|
||||||
|
{
|
||||||
|
"id": item.get("id", ""),
|
||||||
|
"name": item.get("name", ""),
|
||||||
|
"mime_type": item.get("mimeType", ""),
|
||||||
|
"modified_time": item.get("modifiedTime", ""),
|
||||||
|
"web_view_link": item.get("webViewLink", ""),
|
||||||
|
"owners": [owner.get("displayName", "") for owner in owners],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tool": self.name,
|
||||||
|
"status": "ok",
|
||||||
|
"query": query,
|
||||||
|
"count": len(files),
|
||||||
|
"files": files,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _upload_file(self, credentials: Any, local_path: str, filename: str, mime_type: str) -> dict[str, Any]:
|
||||||
|
path = Path(local_path)
|
||||||
|
if not path.exists() or not path.is_file():
|
||||||
|
return {
|
||||||
|
"tool": self.name,
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Upload file was not found: {path}",
|
||||||
|
}
|
||||||
|
|
||||||
|
service = build("drive", "v3", credentials=credentials, cache_discovery=False)
|
||||||
|
final_name = filename or path.name
|
||||||
|
media = MediaFileUpload(str(path), mimetype=mime_type or None, resumable=False)
|
||||||
|
created = (
|
||||||
|
service.files()
|
||||||
|
.create(
|
||||||
|
body={"name": final_name},
|
||||||
|
media_body=media,
|
||||||
|
fields="id,name,mimeType,webViewLink,webContentLink",
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tool": self.name,
|
||||||
|
"status": "ok",
|
||||||
|
"action": "upload",
|
||||||
|
"file": {
|
||||||
|
"id": created.get("id", ""),
|
||||||
|
"name": created.get("name", final_name),
|
||||||
|
"mime_type": created.get("mimeType", mime_type),
|
||||||
|
"web_view_link": created.get("webViewLink", ""),
|
||||||
|
"web_content_link": created.get("webContentLink", ""),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _format_http_error(self, exc: HttpError) -> str:
|
||||||
|
content = getattr(exc, "content", b"")
|
||||||
|
if isinstance(content, bytes):
|
||||||
|
text = content.decode("utf-8", errors="ignore").strip()
|
||||||
|
else:
|
||||||
|
text = str(content).strip()
|
||||||
|
if "insufficientPermissions" in text or "Insufficient Permission" in text:
|
||||||
|
return (
|
||||||
|
"Google Drive upload izni yok. Google'i yeniden baglayip Drive yukleme iznini onaylaman gerekiyor."
|
||||||
|
)
|
||||||
|
return text or str(exc)
|
||||||
@@ -4,11 +4,14 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.db import SecretORM
|
from app.db import SecretORM
|
||||||
|
from app.google.auth import GoogleAuthManager
|
||||||
from app.models import RuntimeSettings
|
from app.models import RuntimeSettings
|
||||||
from app.tools.apple_notes import AppleNotesTool
|
from app.tools.apple_notes import AppleNotesTool
|
||||||
from app.tools.browser_use import BrowserUseTool
|
from app.tools.browser_use import BrowserUseTool
|
||||||
from app.tools.brave_search import BraveSearchTool
|
from app.tools.brave_search import BraveSearchTool
|
||||||
from app.tools.files import FilesTool
|
from app.tools.files import FilesTool
|
||||||
|
from app.tools.gmail import GmailTool
|
||||||
|
from app.tools.google_drive import GoogleDriveTool
|
||||||
from app.tools.second_brain import SecondBrainTool
|
from app.tools.second_brain import SecondBrainTool
|
||||||
from app.tools.terminal import TerminalTool
|
from app.tools.terminal import TerminalTool
|
||||||
from app.tools.web_fetch import WebFetchTool
|
from app.tools.web_fetch import WebFetchTool
|
||||||
@@ -18,6 +21,7 @@ def build_tools(runtime: RuntimeSettings, workspace_root: Path, session: Session
|
|||||||
enabled = {tool.name for tool in runtime.tools if tool.enabled}
|
enabled = {tool.name for tool in runtime.tools if tool.enabled}
|
||||||
tools: dict[str, object] = {}
|
tools: dict[str, object] = {}
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
|
google_auth = GoogleAuthManager(settings, Path(__file__).resolve().parents[2])
|
||||||
|
|
||||||
if "files" in enabled:
|
if "files" in enabled:
|
||||||
tools["files"] = FilesTool(workspace_root)
|
tools["files"] = FilesTool(workspace_root)
|
||||||
@@ -39,6 +43,10 @@ def build_tools(runtime: RuntimeSettings, workspace_root: Path, session: Session
|
|||||||
workspace_slug=runtime.anythingllm_workspace_slug,
|
workspace_slug=runtime.anythingllm_workspace_slug,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
|
if "gmail" in enabled:
|
||||||
|
tools["gmail"] = GmailTool(google_auth)
|
||||||
|
if "google_drive" in enabled:
|
||||||
|
tools["google_drive"] = GoogleDriveTool(google_auth)
|
||||||
if "web_fetch" in enabled:
|
if "web_fetch" in enabled:
|
||||||
tools["web_fetch"] = WebFetchTool()
|
tools["web_fetch"] = WebFetchTool()
|
||||||
if "terminal" in enabled:
|
if "terminal" in enabled:
|
||||||
|
|||||||
@@ -113,6 +113,49 @@ class SecondBrainTool(Tool):
|
|||||||
"raw": data,
|
"raw": data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def status(self) -> dict[str, Any]:
|
||||||
|
if not self.base_url:
|
||||||
|
return {"reachable": False, "workspace_found": False, "message": "AnythingLLM base URL is not configured."}
|
||||||
|
if not self.workspace_slug:
|
||||||
|
return {
|
||||||
|
"reachable": False,
|
||||||
|
"workspace_found": False,
|
||||||
|
"message": "AnythingLLM workspace slug is not configured.",
|
||||||
|
}
|
||||||
|
if not self.api_key:
|
||||||
|
return {"reachable": False, "workspace_found": False, "message": "AnythingLLM API key is not configured."}
|
||||||
|
|
||||||
|
endpoint = f"{self.base_url}/api/v1/workspaces"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||||
|
response = await client.get(endpoint, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
return {"reachable": False, "workspace_found": False, "message": str(exc)}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except ValueError:
|
||||||
|
return {"reachable": False, "workspace_found": False, "message": "AnythingLLM returned invalid JSON."}
|
||||||
|
|
||||||
|
workspaces = self._extract_workspaces(data)
|
||||||
|
slug_match = next((item for item in workspaces if item == self.workspace_slug), None)
|
||||||
|
if slug_match:
|
||||||
|
return {
|
||||||
|
"reachable": True,
|
||||||
|
"workspace_found": True,
|
||||||
|
"message": f"Workspace {self.workspace_slug} is reachable.",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"reachable": True,
|
||||||
|
"workspace_found": False,
|
||||||
|
"message": f"Workspace {self.workspace_slug} was not found.",
|
||||||
|
}
|
||||||
|
|
||||||
def _build_query_prompt(self, query: str, mode: str) -> str:
|
def _build_query_prompt(self, query: str, mode: str) -> str:
|
||||||
if mode == "query":
|
if mode == "query":
|
||||||
return (
|
return (
|
||||||
@@ -128,13 +171,18 @@ class SecondBrainTool(Tool):
|
|||||||
try:
|
try:
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return f"HTTP {response.status_code}"
|
text = response.text.strip()
|
||||||
|
return text or f"HTTP {response.status_code}"
|
||||||
if isinstance(payload, dict):
|
if isinstance(payload, dict):
|
||||||
for key in ("error", "message"):
|
for key in ("error", "message"):
|
||||||
value = payload.get(key)
|
value = payload.get(key)
|
||||||
if isinstance(value, str) and value.strip():
|
if isinstance(value, str) and value.strip():
|
||||||
return value.strip()
|
return value.strip()
|
||||||
return f"HTTP {response.status_code}"
|
detail = payload.get("detail")
|
||||||
|
if isinstance(detail, str) and detail.strip():
|
||||||
|
return detail.strip()
|
||||||
|
text = response.text.strip()
|
||||||
|
return text or f"HTTP {response.status_code}"
|
||||||
|
|
||||||
def _extract_text_response(self, data: Any) -> str:
|
def _extract_text_response(self, data: Any) -> str:
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
@@ -162,3 +210,24 @@ class SecondBrainTool(Tool):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return sources
|
return sources
|
||||||
|
|
||||||
|
def _extract_workspaces(self, data: Any) -> list[str]:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for key in ("workspaces", "data"):
|
||||||
|
value = data.get(key)
|
||||||
|
if isinstance(value, list):
|
||||||
|
return self._workspace_slugs_from_list(value)
|
||||||
|
if isinstance(data, list):
|
||||||
|
return self._workspace_slugs_from_list(data)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _workspace_slugs_from_list(self, items: list[Any]) -> list[str]:
|
||||||
|
slugs: list[str] = []
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
for key in ("slug", "name"):
|
||||||
|
value = item.get(key)
|
||||||
|
if isinstance(value, str) and value.strip():
|
||||||
|
slugs.append(value.strip())
|
||||||
|
break
|
||||||
|
return slugs
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ dependencies = [
|
|||||||
"python-telegram-bot>=22.0,<23.0",
|
"python-telegram-bot>=22.0,<23.0",
|
||||||
"browser-use>=0.12.2,<1.0.0",
|
"browser-use>=0.12.2,<1.0.0",
|
||||||
"anthropic>=0.76.0,<1.0.0",
|
"anthropic>=0.76.0,<1.0.0",
|
||||||
|
"google-api-python-client>=2.181.0,<3.0.0",
|
||||||
|
"google-auth>=2.40.0,<3.0.0",
|
||||||
|
"google-auth-oauthlib>=1.2.2,<2.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
|
|||||||
29
backend/scripts/google_oauth_bootstrap.py
Normal file
29
backend/scripts/google_oauth_bootstrap.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
|
||||||
|
from app.config import get_settings
|
||||||
|
from app.google.auth import GOOGLE_SCOPES
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
settings = get_settings()
|
||||||
|
workspace_root = Path(__file__).resolve().parents[1]
|
||||||
|
client_path = (workspace_root / settings.google_client_secrets_file).resolve()
|
||||||
|
token_path = (workspace_root / settings.google_token_file).resolve()
|
||||||
|
token_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not client_path.exists():
|
||||||
|
raise SystemExit(
|
||||||
|
f"Missing Google OAuth client file: {client_path}\n"
|
||||||
|
"Create a Google OAuth Desktop App and place its JSON there first."
|
||||||
|
)
|
||||||
|
|
||||||
|
flow = InstalledAppFlow.from_client_secrets_file(str(client_path), GOOGLE_SCOPES)
|
||||||
|
creds = flow.run_local_server(port=0, open_browser=True)
|
||||||
|
token_path.write_text(creds.to_json(), encoding="utf-8")
|
||||||
|
print(f"Google token saved to {token_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -2,8 +2,10 @@ import { FormEvent, useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
import type {
|
import type {
|
||||||
|
AnythingLLMStatus,
|
||||||
AutomationRecord,
|
AutomationRecord,
|
||||||
DashboardSnapshot,
|
DashboardSnapshot,
|
||||||
|
GoogleIntegrationStatus,
|
||||||
MemoryRecord,
|
MemoryRecord,
|
||||||
OllamaStatus,
|
OllamaStatus,
|
||||||
RuntimeSettings,
|
RuntimeSettings,
|
||||||
@@ -20,11 +22,13 @@ const defaultSettings: RuntimeSettings = {
|
|||||||
local_model: "qwen3-vl-8b-instruct-mlx@5bit",
|
local_model: "qwen3-vl-8b-instruct-mlx@5bit",
|
||||||
zai_model: "glm-5",
|
zai_model: "glm-5",
|
||||||
anythingllm_base_url: "http://127.0.0.1:3001",
|
anythingllm_base_url: "http://127.0.0.1:3001",
|
||||||
anythingllm_workspace_slug: "wiseclaw",
|
anythingllm_workspace_slug: "benim-calisma-alanim",
|
||||||
tools: [
|
tools: [
|
||||||
{ name: "brave_search", enabled: true },
|
{ name: "brave_search", enabled: true },
|
||||||
{ name: "second_brain", enabled: true },
|
{ name: "second_brain", enabled: true },
|
||||||
{ name: "browser_use", enabled: true },
|
{ name: "browser_use", enabled: true },
|
||||||
|
{ name: "gmail", enabled: true },
|
||||||
|
{ name: "google_drive", enabled: true },
|
||||||
{ name: "searxng_search", enabled: false },
|
{ name: "searxng_search", enabled: false },
|
||||||
{ name: "web_fetch", enabled: true },
|
{ name: "web_fetch", enabled: true },
|
||||||
{ name: "apple_notes", enabled: true },
|
{ name: "apple_notes", enabled: true },
|
||||||
@@ -46,8 +50,14 @@ export function App() {
|
|||||||
const [zaiSecretValue, setZaiSecretValue] = useState("");
|
const [zaiSecretValue, setZaiSecretValue] = useState("");
|
||||||
const [anythingSecretMask, setAnythingSecretMask] = useState("");
|
const [anythingSecretMask, setAnythingSecretMask] = useState("");
|
||||||
const [anythingSecretValue, setAnythingSecretValue] = useState("");
|
const [anythingSecretValue, setAnythingSecretValue] = useState("");
|
||||||
|
const [googleClientIdMask, setGoogleClientIdMask] = useState("");
|
||||||
|
const [googleClientIdValue, setGoogleClientIdValue] = useState("");
|
||||||
|
const [googleClientSecretMask, setGoogleClientSecretMask] = useState("");
|
||||||
|
const [googleClientSecretValue, setGoogleClientSecretValue] = useState("");
|
||||||
const [ollamaStatus, setOllamaStatus] = useState<OllamaStatus | null>(null);
|
const [ollamaStatus, setOllamaStatus] = useState<OllamaStatus | null>(null);
|
||||||
const [telegramStatus, setTelegramStatus] = useState<TelegramStatus | null>(null);
|
const [telegramStatus, setTelegramStatus] = useState<TelegramStatus | null>(null);
|
||||||
|
const [googleStatus, setGoogleStatus] = useState<GoogleIntegrationStatus | null>(null);
|
||||||
|
const [anythingStatus, setAnythingStatus] = useState<AnythingLLMStatus | null>(null);
|
||||||
const [status, setStatus] = useState("Loading WiseClaw admin...");
|
const [status, setStatus] = useState("Loading WiseClaw admin...");
|
||||||
const providerLabel = settings.model_provider === "local" ? "Local (LM Studio)" : "Z.AI";
|
const providerLabel = settings.model_provider === "local" ? "Local (LM Studio)" : "Z.AI";
|
||||||
const searchProviderLabel = settings.search_provider === "brave" ? "Brave" : "SearXNG";
|
const searchProviderLabel = settings.search_provider === "brave" ? "Brave" : "SearXNG";
|
||||||
@@ -63,7 +73,23 @@ export function App() {
|
|||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const [dashboardData, settingsData, userData, profileData, automationData, memoryData, secretData, zaiSecretData, anythingSecretData, ollamaData, telegramData] =
|
const [
|
||||||
|
dashboardData,
|
||||||
|
settingsData,
|
||||||
|
userData,
|
||||||
|
profileData,
|
||||||
|
automationData,
|
||||||
|
memoryData,
|
||||||
|
secretData,
|
||||||
|
zaiSecretData,
|
||||||
|
anythingSecretData,
|
||||||
|
googleClientIdData,
|
||||||
|
googleClientSecretData,
|
||||||
|
ollamaData,
|
||||||
|
telegramData,
|
||||||
|
anythingllmData,
|
||||||
|
googleData,
|
||||||
|
] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
api.getDashboard(),
|
api.getDashboard(),
|
||||||
api.getSettings(),
|
api.getSettings(),
|
||||||
@@ -74,8 +100,12 @@ export function App() {
|
|||||||
api.getSecretMask("brave_api_key"),
|
api.getSecretMask("brave_api_key"),
|
||||||
api.getSecretMask("zai_api_key"),
|
api.getSecretMask("zai_api_key"),
|
||||||
api.getSecretMask("anythingllm_api_key"),
|
api.getSecretMask("anythingllm_api_key"),
|
||||||
|
api.getSecretMask("google_client_id"),
|
||||||
|
api.getSecretMask("google_client_secret"),
|
||||||
api.getOllamaStatus(),
|
api.getOllamaStatus(),
|
||||||
api.getTelegramStatus(),
|
api.getTelegramStatus(),
|
||||||
|
api.getAnythingLLMStatus(),
|
||||||
|
api.getGoogleStatus(),
|
||||||
]);
|
]);
|
||||||
setDashboard(dashboardData);
|
setDashboard(dashboardData);
|
||||||
setSettings(settingsData);
|
setSettings(settingsData);
|
||||||
@@ -86,8 +116,12 @@ export function App() {
|
|||||||
setSecretMask(secretData.masked);
|
setSecretMask(secretData.masked);
|
||||||
setZaiSecretMask(zaiSecretData.masked);
|
setZaiSecretMask(zaiSecretData.masked);
|
||||||
setAnythingSecretMask(anythingSecretData.masked);
|
setAnythingSecretMask(anythingSecretData.masked);
|
||||||
|
setGoogleClientIdMask(googleClientIdData.masked);
|
||||||
|
setGoogleClientSecretMask(googleClientSecretData.masked);
|
||||||
setOllamaStatus(ollamaData);
|
setOllamaStatus(ollamaData);
|
||||||
setTelegramStatus(telegramData);
|
setTelegramStatus(telegramData);
|
||||||
|
setAnythingStatus(anythingllmData);
|
||||||
|
setGoogleStatus(googleData);
|
||||||
setStatus("WiseClaw admin ready.");
|
setStatus("WiseClaw admin ready.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus(error instanceof Error ? error.message : "Failed to load admin data.");
|
setStatus(error instanceof Error ? error.message : "Failed to load admin data.");
|
||||||
@@ -135,6 +169,18 @@ export function App() {
|
|||||||
await load();
|
await load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleGoogleClientSubmit(event: FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!googleClientIdValue.trim() || !googleClientSecretValue.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.saveGoogleClient(googleClientIdValue.trim(), googleClientSecretValue.trim());
|
||||||
|
setGoogleClientIdValue("");
|
||||||
|
setGoogleClientSecretValue("");
|
||||||
|
setStatus("Google OAuth client saved.");
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
async function handleAddUser(event: FormEvent<HTMLFormElement>) {
|
async function handleAddUser(event: FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const form = new FormData(event.currentTarget);
|
const form = new FormData(event.currentTarget);
|
||||||
@@ -197,8 +243,8 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="hero-grid">
|
<div className="hero-grid">
|
||||||
<div>
|
<div>
|
||||||
<span>Terminal mode</span>
|
<span>AnythingLLM</span>
|
||||||
<strong>{settings.terminal_mode}</strong>
|
<strong>{anythingStatus?.reachable && anythingStatus?.workspace_found ? "Active" : "Off"}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Search provider</span>
|
<span>Search provider</span>
|
||||||
@@ -220,6 +266,21 @@ export function App() {
|
|||||||
<strong>{telegramStatus?.configured ? "Configured" : "Missing token"}</strong>
|
<strong>{telegramStatus?.configured ? "Configured" : "Missing token"}</strong>
|
||||||
<p>{telegramStatus?.message || "Checking..."}</p>
|
<p>{telegramStatus?.message || "Checking..."}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="integration-card">
|
||||||
|
<span>Google auth:</span>
|
||||||
|
<strong>{googleStatus?.connected ? "Connected" : "Not connected"}</strong>
|
||||||
|
<p>{googleStatus?.message || "Checking Google OAuth status..."}</p>
|
||||||
|
{googleStatus?.connect_url ? (
|
||||||
|
<a
|
||||||
|
className="button-link"
|
||||||
|
href={googleStatus.connect_url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{googleStatus.connected ? "Reconnect Google" : "Connect Google"}
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -293,7 +354,7 @@ export function App() {
|
|||||||
<input
|
<input
|
||||||
value={settings.anythingllm_workspace_slug}
|
value={settings.anythingllm_workspace_slug}
|
||||||
onChange={(event) => setSettings({ ...settings, anythingllm_workspace_slug: event.target.value })}
|
onChange={(event) => setSettings({ ...settings, anythingllm_workspace_slug: event.target.value })}
|
||||||
placeholder="wiseclaw"
|
placeholder="benim-calisma-alanim"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -406,6 +467,32 @@ export function App() {
|
|||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<form className="panel secret-panel" onSubmit={handleGoogleClientSubmit}>
|
||||||
|
<div className="panel-head">
|
||||||
|
<h3>Google OAuth</h3>
|
||||||
|
<button type="submit">Update</button>
|
||||||
|
</div>
|
||||||
|
<p className="muted">Current client ID: {googleClientIdMask || "not configured"}</p>
|
||||||
|
<p className="muted">Current client secret: {googleClientSecretMask || "not configured"}</p>
|
||||||
|
<label>
|
||||||
|
Google client ID
|
||||||
|
<input
|
||||||
|
value={googleClientIdValue}
|
||||||
|
onChange={(event) => setGoogleClientIdValue(event.target.value)}
|
||||||
|
placeholder="Paste Google OAuth client ID"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Google client secret
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={googleClientSecretValue}
|
||||||
|
onChange={(event) => setGoogleClientSecretValue(event.target.value)}
|
||||||
|
placeholder="Paste Google OAuth client secret"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
|
||||||
<form className="panel" onSubmit={handleAddUser}>
|
<form className="panel" onSubmit={handleAddUser}>
|
||||||
<div className="panel-head">
|
<div className="panel-head">
|
||||||
<h3>Telegram Whitelist</h3>
|
<h3>Telegram Whitelist</h3>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AnythingLLMStatus,
|
||||||
AutomationRecord,
|
AutomationRecord,
|
||||||
DashboardSnapshot,
|
DashboardSnapshot,
|
||||||
|
GoogleIntegrationStatus,
|
||||||
MemoryRecord,
|
MemoryRecord,
|
||||||
OllamaStatus,
|
OllamaStatus,
|
||||||
RuntimeSettings,
|
RuntimeSettings,
|
||||||
@@ -53,6 +55,13 @@ export const api = {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ key, value }),
|
body: JSON.stringify({ key, value }),
|
||||||
}),
|
}),
|
||||||
|
saveGoogleClient: (client_id: string, client_secret: string) =>
|
||||||
|
request<{ status: string }>("/admin/integrations/google/client", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ client_id, client_secret }),
|
||||||
|
}),
|
||||||
getOllamaStatus: () => request<OllamaStatus>("/admin/integrations/llm"),
|
getOllamaStatus: () => request<OllamaStatus>("/admin/integrations/llm"),
|
||||||
getTelegramStatus: () => request<TelegramStatus>("/admin/integrations/telegram"),
|
getTelegramStatus: () => request<TelegramStatus>("/admin/integrations/telegram"),
|
||||||
|
getAnythingLLMStatus: () => request<AnythingLLMStatus>("/admin/integrations/anythingllm"),
|
||||||
|
getGoogleStatus: () => request<GoogleIntegrationStatus>("/admin/integrations/google"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ label {
|
|||||||
|
|
||||||
.integration-grid {
|
.integration-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +192,19 @@ label {
|
|||||||
color: #4f5b57;
|
color: #4f5b57;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
padding: 0.65rem 0.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #1f5c66;
|
||||||
|
color: #f5f1e8;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.grid.two-up {
|
.grid.two-up {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -85,3 +85,18 @@ export type TelegramStatus = {
|
|||||||
polling_active: boolean;
|
polling_active: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GoogleIntegrationStatus = {
|
||||||
|
client_configured: boolean;
|
||||||
|
connected: boolean;
|
||||||
|
connect_url: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AnythingLLMStatus = {
|
||||||
|
reachable: boolean;
|
||||||
|
workspace_found: boolean;
|
||||||
|
base_url: string;
|
||||||
|
workspace_slug: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user