fix: replace sendData with API POST for Mini App commands
sendData() only works for Mini Apps launched via Keyboard buttons, not Menu Buttons. Replace with POST /miniapp/api/command endpoint that injects commands into the message bus via CommandSender interface. - Add CommandSender interface and POST /miniapp/api/command endpoint - Extract user ID from initData for sender identification - Replace all tg.sendData() calls with fetch POST in JS - Remove deleted /todo from Quick Commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
50f50301a5
commit
5cb276d00e
3 changed files with 98 additions and 7 deletions
|
|
@ -221,7 +221,8 @@ func gatewayCmd() {
|
||||||
|
|
||||||
if webAppURL != "" {
|
if webAppURL != "" {
|
||||||
provider := &agentLoopDataProvider{loop: agentLoop}
|
provider := &agentLoopDataProvider{loop: agentLoop}
|
||||||
handler := miniapp.NewHandler(provider, cfg.Channels.Telegram.Token)
|
sender := &telegramCommandSender{bus: msgBus}
|
||||||
|
handler := miniapp.NewHandler(provider, sender, cfg.Channels.Telegram.Token)
|
||||||
handler.RegisterRoutes(healthServer.Mux())
|
handler.RegisterRoutes(healthServer.Mux())
|
||||||
fmt.Printf("✓ Mini App registered at %s\n", webAppURL)
|
fmt.Printf("✓ Mini App registered at %s\n", webAppURL)
|
||||||
}
|
}
|
||||||
|
|
@ -331,3 +332,20 @@ func (p *agentLoopDataProvider) GetPlanInfo() miniapp.PlanInfo {
|
||||||
func (p *agentLoopDataProvider) GetSessionStats() *stats.Stats {
|
func (p *agentLoopDataProvider) GetSessionStats() *stats.Stats {
|
||||||
return p.loop.GetSessionStats()
|
return p.loop.GetSessionStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// telegramCommandSender injects Mini App commands into the message bus.
|
||||||
|
type telegramCommandSender struct {
|
||||||
|
bus *bus.MessageBus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *telegramCommandSender) SendCommand(senderID, chatID, command string) {
|
||||||
|
s.bus.PublishInbound(bus.InboundMessage{
|
||||||
|
Channel: "telegram",
|
||||||
|
SenderID: senderID,
|
||||||
|
ChatID: chatID,
|
||||||
|
Content: command,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"source": "webapp",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
@ -50,16 +51,23 @@ type DataProvider interface {
|
||||||
GetSessionStats() *stats.Stats
|
GetSessionStats() *stats.Stats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommandSender injects a command into the message bus on behalf of a user.
|
||||||
|
type CommandSender interface {
|
||||||
|
SendCommand(senderID, chatID, command string)
|
||||||
|
}
|
||||||
|
|
||||||
// Handler serves the Mini App HTML and API endpoints.
|
// Handler serves the Mini App HTML and API endpoints.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
provider DataProvider
|
provider DataProvider
|
||||||
|
sender CommandSender
|
||||||
botToken string
|
botToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new Mini App handler.
|
// NewHandler creates a new Mini App handler.
|
||||||
func NewHandler(provider DataProvider, botToken string) *Handler {
|
func NewHandler(provider DataProvider, sender CommandSender, botToken string) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
provider: provider,
|
provider: provider,
|
||||||
|
sender: sender,
|
||||||
botToken: botToken,
|
botToken: botToken,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +78,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("/miniapp/api/skills", h.requireAuth(h.apiSkills))
|
mux.HandleFunc("/miniapp/api/skills", h.requireAuth(h.apiSkills))
|
||||||
mux.HandleFunc("/miniapp/api/plan", h.requireAuth(h.apiPlan))
|
mux.HandleFunc("/miniapp/api/plan", h.requireAuth(h.apiPlan))
|
||||||
mux.HandleFunc("/miniapp/api/session", h.requireAuth(h.apiSession))
|
mux.HandleFunc("/miniapp/api/session", h.requireAuth(h.apiSession))
|
||||||
|
mux.HandleFunc("/miniapp/api/command", h.requireAuth(h.apiCommand))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveIndex(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -116,6 +125,65 @@ func (h *Handler) apiSession(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSON(w, s)
|
writeJSON(w, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) apiCommand(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(io.LimitReader(r.Body, 4096))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":"bad request"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil || req.Command == "" {
|
||||||
|
http.Error(w, `{"error":"missing command"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(req.Command, "/") {
|
||||||
|
http.Error(w, `{"error":"command must start with /"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract user ID from initData to identify the sender
|
||||||
|
initData := r.URL.Query().Get("initData")
|
||||||
|
userID, chatID := extractUserFromInitData(initData)
|
||||||
|
if userID == "" {
|
||||||
|
http.Error(w, `{"error":"cannot identify user"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.sender.SendCommand(userID, chatID, req.Command)
|
||||||
|
writeJSON(w, map[string]string{"status": "ok"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractUserFromInitData parses user.id from the initData query string.
|
||||||
|
// initData contains a "user" param with JSON like {"id":123456,...}.
|
||||||
|
func extractUserFromInitData(initData string) (userID, chatID string) {
|
||||||
|
values, err := url.ParseQuery(initData)
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
userJSON := values.Get("user")
|
||||||
|
if userJSON == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
var user struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(userJSON), &user); err != nil || user.ID == 0 {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
id := fmt.Sprintf("%d", user.ID)
|
||||||
|
// For Mini App commands, chatID = userID (private chat)
|
||||||
|
return id, id
|
||||||
|
}
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, v any) {
|
func writeJSON(w http.ResponseWriter, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(v)
|
json.NewEncoder(w).Encode(v)
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,6 @@
|
||||||
<div class="cmd-chips">
|
<div class="cmd-chips">
|
||||||
<button class="cmd-chip" data-cmd="/session">/session</button>
|
<button class="cmd-chip" data-cmd="/session">/session</button>
|
||||||
<button class="cmd-chip" data-cmd="/skills">/skills</button>
|
<button class="cmd-chip" data-cmd="/skills">/skills</button>
|
||||||
<button class="cmd-chip" data-cmd="/todo">/todo</button>
|
|
||||||
<button class="cmd-chip" data-cmd="/plan status">/plan status</button>
|
<button class="cmd-chip" data-cmd="/plan status">/plan status</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -389,13 +388,19 @@ document.querySelectorAll('.cmd-chip').forEach(chip => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function sendCommand(cmd) {
|
async function sendCommand(cmd) {
|
||||||
if (!cmd.startsWith('/')) return;
|
if (!cmd.startsWith('/')) return;
|
||||||
try {
|
try {
|
||||||
tg.sendData(cmd);
|
const sep = '/miniapp/api/command'.includes('?') ? '&' : '?';
|
||||||
|
const res = await fetch(API_BASE + '/miniapp/api/command' + sep + 'initData=' + encodeURIComponent(initData), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ command: cmd }),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('API error: ' + res.status);
|
||||||
|
tg.showAlert('Sent: ' + cmd);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('sendData:', cmd);
|
tg.showAlert('Failed to send command');
|
||||||
tg.showAlert('Command sent: ' + cmd);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue