ozellik: google oauth, gmail-drive araclari ve admin durum kartlarini ekle

This commit is contained in:
2026-03-22 18:50:06 +03:00
parent 177fd8e1a7
commit ad847b1cf4
20 changed files with 970 additions and 14 deletions

View File

@@ -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
View File

@@ -12,6 +12,7 @@ dist/
build/ build/
.DS_Store .DS_Store
.env .env
.google/
wiseclaw.db wiseclaw.db
.codex/ .codex/
.playwright-cli/ .playwright-cli/

View File

@@ -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

View File

@@ -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>"
)
)

View File

@@ -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:]}"

View File

@@ -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)

View File

@@ -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
View 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"

View File

@@ -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. "

View File

@@ -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
View 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,
}

View 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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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]

View 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()

View File

@@ -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>

View File

@@ -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"),
}; };

View File

@@ -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));

View File

@@ -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;
};