docs: sync from backend 8fc2309 — M43/M44 missing FKs + H37 dispute enums
This commit is contained in:
479
DEPLOYMENT.md
Normal file
479
DEPLOYMENT.md
Normal file
@@ -0,0 +1,479 @@
|
||||
# Amanat Assist Mini App - Deployment Guide
|
||||
|
||||
**Codename:** `amanat-assist`
|
||||
**Version:** 1.0
|
||||
**Last Updated:** 2026-06-05
|
||||
**Owner:** Deployment
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
This document describes the deployment architecture for the Amanat Assist Telegram Mini App, using:
|
||||
- **Frontend:** Vite + React (static hosting)
|
||||
- **LLM Edge Function:** Cloudflare Workers (server-side LLM calls)
|
||||
- **Backend:** Amanat API (`dev.amn.gg` or `amn.gg`)
|
||||
|
||||
---
|
||||
|
||||
## 📁 Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Telegram Client │
|
||||
└─────────────────┬─────────────────────────────────┬───────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────┐ ┌─────────────────────────────┐
|
||||
│ Mini App (Static) │ │ LLM Edge Function │
|
||||
│ - React + Vite │ │ - Cloudflare Workers │
|
||||
│ - Hosted on CF Pages │ │ - Route: /api/llm │
|
||||
│ - URL: assist.amn.gg │ │ - Handles auth + LLM calls │
|
||||
└─────────────────────────────┘ └──────────┬────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ LLM Providers │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Mistral │ │ Kimi │ │ DeepSeek │ │ OpenCode │ │
|
||||
│ │ (Primary) │ │ (Fallback) │ │ (Fallback) │ │ (Local) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Amanat Backend │
|
||||
│ - POST /api/auth/telegram (Telegram SSO) │
|
||||
│ - GET /api/marketplace/categories │
|
||||
│ - POST /api/files/upload (File upload) │
|
||||
│ - POST /api/marketplace/purchase-requests (Submit w/ aiGenerated) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Cloudflare Account** with Workers + Pages enabled
|
||||
2. **Amanat Backend** running at `dev.amn.gg` (already deployed ✅)
|
||||
3. **LLM API Keys** (at least one):
|
||||
- Mistral: `sk_...`
|
||||
- Kimi: `sk_...`
|
||||
- DeepSeek: `sk_...`
|
||||
4. **Domain** configured in Cloudflare: `assist.amn.gg` (or subdomain)
|
||||
|
||||
---
|
||||
|
||||
## 📦 Step 1: Deploy Static Frontend (Cloudflare Pages)
|
||||
|
||||
### 1.1 Create Cloudflare Pages Project
|
||||
|
||||
```bash
|
||||
# Navigate to project
|
||||
cd /Users/manwe/CascadeProjects/escrow/amanat-assist
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build production bundle
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 1.2 Cloudflare Dashboard Setup
|
||||
|
||||
1. Go to: [https://dash.cloudflare.com](https://dash.cloudflare.com)
|
||||
2. Select your account → **Workers & Pages** → **Create application** → **Pages**
|
||||
3. **Connect Git repository** (if using Git) OR **Upload files**
|
||||
4. **Project name:** `amanat-assist`
|
||||
5. **Production branch:** `main` (or your deployment branch)
|
||||
6. **Build command:** `npm run build`
|
||||
7. **Build output directory:** `dist`
|
||||
8. **Environment variables:** (see Section 3)
|
||||
|
||||
### 1.3 Configure Custom Domain
|
||||
|
||||
1. In Pages project → **Custom domains** → **Set up custom domain**
|
||||
2. Enter: `assist.amn.gg`
|
||||
3. Cloudflare will issue SSL certificate automatically
|
||||
4. Wait for DNS propagation (~5-10 minutes)
|
||||
|
||||
---
|
||||
|
||||
## ☁️ Step 2: Deploy LLM Edge Function (Cloudflare Workers)
|
||||
|
||||
### 2.1 Create Worker
|
||||
|
||||
1. Go to: [https://dash.cloudflare.com](https://dash.cloudflare.com)
|
||||
2. Select your account → **Workers & Pages** → **Create service** → **Worker**
|
||||
3. **Service name:** `amanat-assist-llm`
|
||||
4. **Starter:** `Fetch handler`
|
||||
|
||||
### 2.2 Worker Code
|
||||
|
||||
Create `index.ts`:
|
||||
|
||||
```typescript
|
||||
// src/index.ts for Cloudflare Worker
|
||||
|
||||
interface LLMRequest {
|
||||
messages: Array<{ role: 'user' | 'assistant'; content: string }>;
|
||||
provider?: 'mistral' | 'kimi' | 'deepseek' | 'opencode';
|
||||
model?: string;
|
||||
}
|
||||
|
||||
interface ProviderConfig {
|
||||
baseUrl: string;
|
||||
apiKeyEnv: string;
|
||||
chatEndpoint: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
const PROVIDERS: Record<string, ProviderConfig> = {
|
||||
mistral: {
|
||||
baseUrl: 'https://api.mistral.ai',
|
||||
apiKeyEnv: 'MISTRAL_API_KEY',
|
||||
chatEndpoint: '/v1/chat/completions',
|
||||
model: 'mistral-large-latest',
|
||||
},
|
||||
kimi: {
|
||||
baseUrl: 'https://api.moonshot.cn',
|
||||
apiKeyEnv: 'KIMI_API_KEY',
|
||||
chatEndpoint: '/v1/chat/completions',
|
||||
model: 'moonshot-v1-8k',
|
||||
},
|
||||
deepseek: {
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
||||
chatEndpoint: '/chat/completions',
|
||||
model: 'deepseek-chat',
|
||||
},
|
||||
opencode: {
|
||||
baseUrl: 'http://127.0.0.1:3456',
|
||||
apiKeyEnv: '',
|
||||
chatEndpoint: '/v1/messages',
|
||||
model: 'claude-3-sonnet',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// Only allow POST
|
||||
if (request.method !== 'POST') {
|
||||
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
||||
status: 405,
|
||||
headers: { 'Content-Type': 'application/json', 'Allow': 'POST' },
|
||||
});
|
||||
}
|
||||
|
||||
// Validate origin (optional - for production)
|
||||
const origin = request.headers.get('origin');
|
||||
const allowedOrigins = [
|
||||
'https://assist.amn.gg',
|
||||
'https://dev.amn.gg',
|
||||
'https://amn.gg',
|
||||
];
|
||||
|
||||
if (origin && !allowedOrigins.some(o => origin.startsWith(o))) {
|
||||
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const body: LLMRequest = await request.json();
|
||||
const provider = body.provider || 'mistral';
|
||||
const config = PROVIDERS[provider];
|
||||
|
||||
if (!config) {
|
||||
return new Response(JSON.stringify({ error: `Unknown provider: ${provider}` }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get API key from environment
|
||||
const apiKey = config.apiKeyEnv ? env[config.apiKeyEnv] : '';
|
||||
|
||||
if (config.apiKeyEnv && !apiKey) {
|
||||
return new Response(JSON.stringify({ error: `API key for ${provider} not configured` }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Build request for the provider
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
const model = body.model || config.model;
|
||||
const messages = body.messages;
|
||||
|
||||
// Format request based on provider
|
||||
let providerBody: any;
|
||||
if (provider === 'opencode') {
|
||||
providerBody = {
|
||||
model,
|
||||
messages: messages.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
max_tokens: 1024,
|
||||
};
|
||||
} else {
|
||||
providerBody = {
|
||||
model,
|
||||
messages,
|
||||
temperature: 0.7,
|
||||
};
|
||||
}
|
||||
|
||||
// Call the LLM provider
|
||||
const providerResponse = await fetch(`${config.baseUrl}${config.chatEndpoint}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(providerBody),
|
||||
});
|
||||
|
||||
if (!providerResponse.ok) {
|
||||
const error = await providerResponse.text();
|
||||
return new Response(JSON.stringify({
|
||||
error: `LLM error: ${providerResponse.status} - ${error}`
|
||||
}), {
|
||||
status: providerResponse.status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const data = await providerResponse.json();
|
||||
|
||||
// Extract content based on provider
|
||||
let content: string;
|
||||
if (provider === 'opencode') {
|
||||
content = data.content?.[0]?.text || data.content || JSON.stringify(data);
|
||||
} else {
|
||||
content = data.choices?.[0]?.message?.content || JSON.stringify(data);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ content, model: data.model || model }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': origin || '*',
|
||||
'Access-Control-Allow-Methods': 'POST',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
return new Response(JSON.stringify({
|
||||
error: `Internal error: ${err instanceof Error ? err.message : String(err)}`
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Type for Cloudflare environment variables
|
||||
export interface Env {
|
||||
MISTRAL_API_KEY?: string;
|
||||
KIMI_API_KEY?: string;
|
||||
DEEPSEEK_API_KEY?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Configure Worker Settings
|
||||
|
||||
1. **Routes:** `assist.amn.gg/api/llm/*` (or `assist.amn.gg/api/llm`)
|
||||
2. **Environment variables:** Add your LLM API keys
|
||||
3. **Enable CORS:** Handled in code
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Step 3: Environment Variables
|
||||
|
||||
### 3.1 Frontend (Cloudflare Pages)
|
||||
|
||||
| Variable | Value | Required | Notes |
|
||||
|----------|-------|----------|-------|
|
||||
| `VITE_AMANAT_API_BASE` | `https://dev.amn.gg` | ✅ | Amanat backend |
|
||||
| `VITE_LLM_PROVIDER` | `mistral` | ✅ | Primary LLM provider |
|
||||
| `VITE_LLM_API_URL` | `https://assist.amn.gg/api/llm` | ✅ | Edge function URL |
|
||||
|
||||
**Optional:**
|
||||
- `VITE_OPENCODE_PROXY_URL` - If using local proxy
|
||||
|
||||
### 3.2 Edge Function (Cloudflare Worker)
|
||||
|
||||
| Variable | Value | Required |
|
||||
|----------|-------|----------|
|
||||
| `MISTRAL_API_KEY` | Your Mistral key | ✅ (if using Mistral) |
|
||||
| `KIMI_API_KEY` | Your Kimi key | ❌ |
|
||||
| `DEEPSEEK_API_KEY` | Your DeepSeek key | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Step 4: Configure Telegram Mini App
|
||||
|
||||
### 4.1 Create Telegram Bot
|
||||
|
||||
1. Open [@BotFather](https://t.me/BotFather) in Telegram
|
||||
2. Send `/newbot`
|
||||
3. Follow prompts to create bot
|
||||
4. **Save the bot token** (needed for backend Telegram webhook)
|
||||
|
||||
### 4.2 Enable Mini App
|
||||
|
||||
1. In [@BotFather](https://t.me/BotFather), send `/mybots`
|
||||
2. Select your bot
|
||||
3. Go to **Bot Settings** → **Mini App**
|
||||
4. Set **URL:** `https://assist.amn.gg`
|
||||
5. Enable **Inline mode** (optional)
|
||||
|
||||
### 4.3 Configure Bot Menu (Optional)
|
||||
|
||||
1. In [@BotFather](https://t.me/BotFather), send `/setcommands`
|
||||
2. Set commands:
|
||||
```
|
||||
start - Open Amanat Assist
|
||||
help - Show help
|
||||
auth - Re-authenticate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Step 5: Test Deployment
|
||||
|
||||
### 5.1 Test Frontend
|
||||
|
||||
```bash
|
||||
# Local test
|
||||
npm run dev
|
||||
# Open: http://localhost:3000
|
||||
|
||||
# Production test
|
||||
# Open: https://assist.amn.gg
|
||||
```
|
||||
|
||||
### 5.2 Test Edge Function
|
||||
|
||||
```bash
|
||||
# Direct test
|
||||
curl -X POST https://assist.amn.gg/api/llm \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"provider": "mistral",
|
||||
"model": "mistral-large-latest"
|
||||
}'
|
||||
```
|
||||
|
||||
### 5.3 Test via Telegram
|
||||
|
||||
1. Open your bot in Telegram
|
||||
2. Click the Mini App button
|
||||
3. Verify:
|
||||
- ✅ Silent auth (no login prompt)
|
||||
- ✅ Greeting message appears
|
||||
- ✅ Chat works
|
||||
- ✅ File upload works
|
||||
- ✅ Submit creates request in Amanat
|
||||
- ✅ AI badge appears in Amanat UI
|
||||
|
||||
---
|
||||
|
||||
## 📊 Monitoring & Logging
|
||||
|
||||
### Cloudflare Workers
|
||||
|
||||
1. Go to: [https://dash.cloudflare.com](https://dash.cloudflare.com)
|
||||
2. Workers & Pages → Your Worker → **Logs**
|
||||
3. View real-time requests and errors
|
||||
|
||||
### Cloudflare Pages
|
||||
|
||||
1. Workers & Pages → Your Pages project → **Deployments**
|
||||
2. View build logs and deployment status
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Security Considerations
|
||||
|
||||
### 1. Origin Validation
|
||||
The edge function validates the `Origin` header to prevent unauthorized access:
|
||||
- Allowed: `https://assist.amn.gg`, `https://dev.amn.gg`, `https://amn.gg`
|
||||
- **Action:** Update the `allowedOrigins` array if adding new domains
|
||||
|
||||
### 2. API Key Protection
|
||||
- **Never** expose LLM API keys in frontend code
|
||||
- All LLM calls go through the edge function
|
||||
- API keys stored only in Worker environment variables
|
||||
|
||||
### 3. Rate Limiting (Recommended)
|
||||
Add to Worker `wrangler.toml`:
|
||||
```toml
|
||||
[triggers]
|
||||
crons = ["*/5 * * * *"] # Optional: cleanup
|
||||
|
||||
# Rate limiting via Cloudflare
|
||||
# Configure in Cloudflare Dashboard → Workers → Rate Limiting
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Update Process
|
||||
|
||||
### Frontend Updates
|
||||
```bash
|
||||
# Make changes
|
||||
npm run build
|
||||
# Push to Git (if using Git integration)
|
||||
git add . && git commit -m "..." && git push
|
||||
# Cloudflare Pages auto-deploys
|
||||
```
|
||||
|
||||
### Worker Updates
|
||||
```bash
|
||||
# Make changes
|
||||
# Deploy via Wrangler or Dashboard
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support & Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| CORS errors | Verify `Access-Control-Allow-Origin` in Worker |
|
||||
| 403 from LLM | Check API key in Worker environment |
|
||||
| 404 on /api/llm | Verify Worker route is configured |
|
||||
| Telegram auth fails | Verify backend `/api/auth/telegram` endpoint |
|
||||
| No AI badge | Verify backend schema changes (PRD §12) |
|
||||
|
||||
### Debug Mode
|
||||
Add to frontend `.env`:
|
||||
```bash
|
||||
VITE_DEBUG=true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 References
|
||||
|
||||
- **PRD:** `/Users/manwe/CascadeProjects/escrow/nick-doc/PRD - AI Request Assistant Mini App.md`
|
||||
- **Backend PRD §12:** Already implemented (commits `6da6e27`, `1ef9b95`)
|
||||
- **Cloudflare Workers Docs:** [https://developers.cloudflare.com/workers/](https://developers.cloudflare.com/workers/)
|
||||
- **Cloudflare Pages Docs:** [https://developers.cloudflare.com/pages/](https://developers.cloudflare.com/pages/)
|
||||
- **Telegram Mini App Docs:** [https://core.telegram.org/bots/webapps](https://core.telegram.org/bots/webapps)
|
||||
|
||||
---
|
||||
|
||||
*Document version: 1.0 — 2026-06-05*
|
||||
*Owner: Deployment Team*
|
||||
Reference in New Issue
Block a user