ozellik: google oauth, gmail-drive araclari ve admin durum kartlarini ekle
This commit is contained in:
@@ -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/
|
||||||
|
|||||||
89
README.md
89
README.md
@@ -8,6 +8,8 @@
|
|||||||
- 🧠 `/tanisalim` ile kalıcı kullanıcı profili ve iletişim tercihleri
|
- 🧠 `/tanisalim` ile kalıcı kullanıcı profili ve iletişim tercihleri
|
||||||
- 🗂️ AnythingLLM tabanlı ikinci beyin sorguları
|
- 🗂️ AnythingLLM tabanlı ikinci beyin sorguları
|
||||||
- 📝 `/notlarima_ekle` ile second brain notu ekleme ve otomatik senkron
|
- 📝 `/notlarima_ekle` ile second brain notu ekleme ve otomatik senkron
|
||||||
|
- 📬 Google OAuth ile Gmail bağlantısı kurup gelen mailleri listeleme, arama ve özetleme
|
||||||
|
- ☁️ Google Drive dosyalarını listeleme, arama ve Telegram'dan gelen dosyaları Drive root'a yükleme
|
||||||
- ⚙️ `/otomasyon_ekle` ile zamanlanmış görev oluşturma
|
- ⚙️ `/otomasyon_ekle` ile zamanlanmış görev oluşturma
|
||||||
- 🌐 Brave Search ile web ve görsel arama
|
- 🌐 Brave Search ile web ve görsel arama
|
||||||
- 🧭 `browser_use` ile gerçek tarayıcıda gezinme
|
- 🧭 `browser_use` ile gerçek tarayıcıda gezinme
|
||||||
@@ -18,6 +20,7 @@
|
|||||||
- `Local (LM Studio)`
|
- `Local (LM Studio)`
|
||||||
- `Z.AI`
|
- `Z.AI`
|
||||||
- 📊 Admin panelden ayarlar, loglar, memory, profiller ve otomasyonları yönetme
|
- 📊 Admin panelden ayarlar, loglar, memory, profiller ve otomasyonları yönetme
|
||||||
|
- 🔗 Admin panelden Google OAuth, AnythingLLM ve LLM bağlantı durumlarını canlı izleme
|
||||||
|
|
||||||
## 🏗️ Mimari
|
## 🏗️ Mimari
|
||||||
|
|
||||||
@@ -49,6 +52,10 @@ Admin panelden aktif sağlayıcı değiştirildiğinde yeni istekler seçili sa
|
|||||||
Gerçek browser otomasyonu
|
Gerçek browser otomasyonu
|
||||||
- `apple_notes`
|
- `apple_notes`
|
||||||
Apple Notes not oluşturma
|
Apple Notes not oluşturma
|
||||||
|
- `gmail`
|
||||||
|
Gmail mesajlarını listeleme ve arama
|
||||||
|
- `google_drive`
|
||||||
|
Google Drive dosyalarını listeleme, arama ve yükleme
|
||||||
- `files`
|
- `files`
|
||||||
Dosya/dizin erişimi
|
Dosya/dizin erişimi
|
||||||
- `terminal`
|
- `terminal`
|
||||||
@@ -56,6 +63,75 @@ Admin panelden aktif sağlayıcı değiştirildiğinde yeni istekler seçili sa
|
|||||||
- `second_brain`
|
- `second_brain`
|
||||||
AnythingLLM workspace context sorgulama
|
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ışı
|
## 🧠 İkinci Beyin Akışı
|
||||||
|
|
||||||
WiseClaw, AnythingLLM'yi ikinci beyin olarak kullanabilir.
|
WiseClaw, AnythingLLM'yi ikinci beyin olarak kullanabilir.
|
||||||
@@ -124,7 +200,10 @@ Admin panelde şunları yönetebilirsin:
|
|||||||
- Runtime settings
|
- Runtime settings
|
||||||
- Model provider
|
- Model provider
|
||||||
- Search provider
|
- Search provider
|
||||||
|
- AnythingLLM canlı durum kartı (`Active / Off`)
|
||||||
|
- Google auth durumu (`Connected / Not connected`)
|
||||||
- Brave / Z.AI / AnythingLLM secret'ları
|
- Brave / Z.AI / AnythingLLM secret'ları
|
||||||
|
- Google OAuth client bilgileri
|
||||||
- Telegram whitelist
|
- Telegram whitelist
|
||||||
- User Profiles
|
- User Profiles
|
||||||
- Automations
|
- Automations
|
||||||
@@ -141,6 +220,8 @@ Admin panelde şunları yönetebilirsin:
|
|||||||
- `/admin/memory`
|
- `/admin/memory`
|
||||||
- `/admin/integrations/llm`
|
- `/admin/integrations/llm`
|
||||||
- `/admin/integrations/telegram`
|
- `/admin/integrations/telegram`
|
||||||
|
- `/admin/integrations/anythingllm`
|
||||||
|
- `/admin/integrations/google`
|
||||||
|
|
||||||
## 🚀 Kurulum
|
## 🚀 Kurulum
|
||||||
|
|
||||||
@@ -175,6 +256,10 @@ npm run dev
|
|||||||
- `WISECLAW_ZAI_MODEL`
|
- `WISECLAW_ZAI_MODEL`
|
||||||
- `WISECLAW_ANYTHINGLLM_BASE_URL`
|
- `WISECLAW_ANYTHINGLLM_BASE_URL`
|
||||||
- `WISECLAW_ANYTHINGLLM_WORKSPACE_SLUG`
|
- `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_TELEGRAM_BOT_TOKEN`
|
||||||
- `WISECLAW_BRAVE_API_KEY`
|
- `WISECLAW_BRAVE_API_KEY`
|
||||||
- `WISECLAW_ZAI_API_KEY`
|
- `WISECLAW_ZAI_API_KEY`
|
||||||
@@ -191,6 +276,8 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔁 Restart
|
## 🔁 Restart
|
||||||
@@ -215,6 +302,8 @@ Bu script:
|
|||||||
- AnythingLLM tarafında görünen workspace adı ile gerçek `slug` farklı olabilir.
|
- 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.
|
- Brave image search sonuçları Telegram'da medya grubu olarak gönderilebilir.
|
||||||
- Bazı browser görevleri captcha/anti-bot nedeniyle manuel müdahale isteyebilir.
|
- 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
|
## 🧭 Geliştirme Notu
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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