瀏覽代碼

feat: 交互式登录

kotoyuuko 1 周之前
父節點
當前提交
5911418229
共有 7 個文件被更改,包括 113 次插入122 次删除
  1. 21 0
      package-lock.json
  2. 2 0
      package.json
  3. 34 12
      src/command/handlers/login.ts
  4. 45 4
      src/command/index.ts
  5. 0 97
      src/example.ts
  6. 8 7
      src/index.ts
  7. 3 2
      src/scheduler/index.ts

+ 21 - 0
package-lock.json

@@ -8,6 +8,8 @@
 			"name": "cosmoe-bot",
 			"version": "0.0.0",
 			"dependencies": {
+				"@grammyjs/conversations": "^2.1.1",
+				"@grammyjs/storage-cloudflare": "^2.4.2",
 				"grammy": "^1.39.3"
 			},
 			"devDependencies": {
@@ -1144,6 +1146,24 @@
 				"node": ">=18"
 			}
 		},
+		"node_modules/@grammyjs/conversations": {
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/@grammyjs/conversations/-/conversations-2.1.1.tgz",
+			"integrity": "sha512-hoxqwSkaXDeU7mzXulpk3A4Cmd6UZO3HU4aPoITX5ekSHK7ZcUEmMl7RhKKkqw3z6zVbbAShQreJoVV5/dDSLA==",
+			"license": "MIT",
+			"engines": {
+				"node": "^12.20.0 || >=14.13.1"
+			},
+			"peerDependencies": {
+				"grammy": "^1.20.1"
+			}
+		},
+		"node_modules/@grammyjs/storage-cloudflare": {
+			"version": "2.4.2",
+			"resolved": "https://registry.npmjs.org/@grammyjs/storage-cloudflare/-/storage-cloudflare-2.4.2.tgz",
+			"integrity": "sha512-r5lwDEyRRkGN7lQBEy3Qm65BIh8lLHUoDcdOB3N5+hDeuAOBW6q0a2eNFwbmk+9KuwPiRMvlOt/avgMLaoYZ/A==",
+			"license": "MIT"
+		},
 		"node_modules/@grammyjs/types": {
 			"version": "3.23.0",
 			"resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.23.0.tgz",
@@ -2578,6 +2598,7 @@
 			"resolved": "https://registry.npmjs.org/grammy/-/grammy-1.39.3.tgz",
 			"integrity": "sha512-7arRRoOtOh9UwMwANZ475kJrWV6P3/EGNooeHlY0/SwZv4t3ZZ3Uiz9cAXK8Zg9xSdgmm8T21kx6n7SZaWvOcw==",
 			"license": "MIT",
+			"peer": true,
 			"dependencies": {
 				"@grammyjs/types": "3.23.0",
 				"abort-controller": "^3.0.0",

+ 2 - 0
package.json

@@ -16,6 +16,8 @@
 		"wrangler": "^4.59.2"
 	},
 	"dependencies": {
+		"@grammyjs/conversations": "^2.1.1",
+		"@grammyjs/storage-cloudflare": "^2.4.2",
 		"grammy": "^1.39.3"
 	}
 }

+ 34 - 12
src/command/handlers/login.ts

@@ -1,4 +1,5 @@
 import { Context } from "grammy";
+import { Conversation, ConversationFlavor } from "@grammyjs/conversations";
 import { CosmoeClient } from "../../client/cosmoe";
 
 export interface Env {
@@ -8,16 +9,36 @@ export interface Env {
   COSMOE_STORAGE: KVNamespace;
 }
 
-export async function handleLoginCommand(ctx: Context, env: Env): Promise<void> {
+// Interactive conversation-based login handler
+export async function handleInteractiveLogin(
+  conversation: Conversation<Context>,
+  ctx: Context,
+  env: Env
+) {
   try {
-    const args = ctx.match?.toString().split(' ') || [];
-    if (args.length < 2) {
-      await ctx.reply("Please provide your username and password. Usage: /login username password");
+    // Ask for username
+    await ctx.reply("请输入用户名");
+    
+    // Wait for user's username
+    const usernameCtx = await conversation.wait();
+    const username = usernameCtx.message?.text;
+    
+    if (!username) {
+      await ctx.reply("用户名未提供,登录已取消");
       return;
     }
     
-    const username = args[0];
-    const password = args[1];
+    // Ask for password
+    await ctx.reply("请输入密码");
+    
+    // Wait for user's password (with privacy protection)
+    const passwordCtx = await conversation.wait();
+    const password = passwordCtx.message?.text;
+    
+    if (!password) {
+      await ctx.reply("密码未提供,登录已取消");
+      return;
+    }
     
     // Initialize Cosmoe API client
     const cosmoeClient = new CosmoeClient();
@@ -29,7 +50,7 @@ export async function handleLoginCommand(ctx: Context, env: Env): Promise<void>
       // Store credentials in KV namespace associated with this Telegram user
       const telegramUserId = ctx.from?.id.toString();
       if (!telegramUserId) {
-        await ctx.reply("Could not identify your Telegram user ID.");
+        await ctx.reply("无法获取你的 Telegram 用户身份");
         return;
       }
       
@@ -41,12 +62,13 @@ export async function handleLoginCommand(ctx: Context, env: Env): Promise<void>
       
       await env.COSMOE_CREDENTIALS.put(telegramUserId, JSON.stringify(credentials));
       
-      await ctx.reply(`Successfully logged in as ${username}. Your credentials are now stored securely.`);
+      await ctx.reply(`登录成功,${username},你的凭证现在已安全存储`);
+      
     } else {
-      await ctx.reply(`Login failed: ${authResult.msg || 'Invalid credentials'}`);
+      await ctx.reply(`登录失败:${authResult.msg || '用户名或密码错误'}`);
     }
   } catch (error) {
-    console.error("Error during login:", error);
-    await ctx.reply("An error occurred during login. Please try again.");
+    console.error("Error during interactive login:", error);
+    await ctx.reply("出错了,请稍后重试");
   }
-}
+}

+ 45 - 4
src/command/index.ts

@@ -1,6 +1,8 @@
 import { Bot, Context } from "grammy";
+import { conversations, createConversation, ConversationFlavor, VersionedStateStorage } from "@grammyjs/conversations";
+import { KvAdapter } from "@grammyjs/storage-cloudflare";
 import { handleStartCommand } from "./handlers/start";
-import { handleLoginCommand } from "./handlers/login";
+import { handleInteractiveLogin } from "./handlers/login";
 import { handleEventsCommand } from "./handlers/events";
 import { handleEventDetails } from "./handlers/eventDetails";
 import { handleBookEvent } from "./handlers/bookEvent";
@@ -15,13 +17,52 @@ export interface Env {
   COSMOE_STORAGE: KVNamespace;
 }
 
-export function setupCommands(bot: Bot, env: Env) {
+export function setupCommands(bot: Bot<ConversationFlavor<Context>>, env: Env) {
+  // Create a KV adapter for conversation storage using COSMOE_STORAGE namespace
+  const kvAdapter = new KvAdapter(env.COSMOE_STORAGE);
+  
+  // Define conversation storage using the KV adapter
+  const conversationStorage = {
+    read: async (key: string) => {
+      try {
+        const value = await kvAdapter.read(key);
+        return value ? JSON.parse(value as string) : undefined;
+      } catch (error) {
+        console.error('Error reading conversation from KV:', error);
+        return undefined;
+      }
+    },
+    write: async (key: string, value: any) => {
+      try {
+        await kvAdapter.write(key, JSON.stringify(value));
+      } catch (error) {
+        console.error('Error writing conversation to KV:', error);
+      }
+    },
+    delete: async (key: string) => {
+      try {
+        await kvAdapter.delete(key);
+      } catch (error) {
+        console.error('Error deleting conversation from KV:', error);
+      }
+    },
+  };
+  
+  // Install the conversations plugin with KV storage
+  bot.use(conversations({ storage: conversationStorage }));
+  
+  // Create the login conversation, with environment bound to the handler
+  bot.use(createConversation(async (conversation, ctx) => {
+    await handleInteractiveLogin(conversation, ctx, env);
+  }, "login"));
+
   bot.command("start", async (ctx: Context) => {
     await handleStartCommand(ctx);
   });
 
-  bot.command("login", async (ctx: Context) => {
-    await handleLoginCommand(ctx, env);
+  // Use the interactive conversation for login
+  bot.command("login", async (ctx) => {
+    await ctx.conversation.enter("login");
   });
 
   bot.command("events", async (ctx: Context) => {

+ 0 - 97
src/example.ts

@@ -1,97 +0,0 @@
-/**
- * Example usage of the CosmoeClient
- * Demonstrates how to use all the API endpoints
- */
-
-import { CosmoeClient } from './client/cosmoe';
-
-async function example() {
-  const client = new CosmoeClient();
-  
-  try {
-    // 1. Get authentication token
-    console.log('Getting authentication token...');
-    const tokenResult = await client.getToken('your_username', 'your_password');
-    if (tokenResult.code === 200) {
-      console.log('Successfully authenticated:', tokenResult.data);
-    } else {
-      console.error('Authentication failed:', tokenResult.msg);
-      return;
-    }
-    
-    // 2. Get events list
-    console.log('\nGetting events list...');
-    const eventsResult = await client.getEvents();
-    if (eventsResult.code === 200) {
-      console.log('Available events:', eventsResult.data);
-      
-      // Get details of the first event if any exist
-      if (eventsResult.data.length > 0) {
-        const firstEvent = eventsResult.data[0];
-        console.log(`\nGetting details for event: ${firstEvent.name} on ${firstEvent.event_date}`);
-        
-        const eventDetailResult = await client.getEventDetail(Number(firstEvent.id));
-        if (eventDetailResult.code === 200) {
-          console.log('Event details:', eventDetailResult.data);
-                  
-          // 3. Book an event (example with first available time slot)
-          const firstTimeSlot = eventDetailResult.data.slots[0];
-          if (firstTimeSlot && firstTimeSlot.remaining > 0) {
-            console.log(`\nBooking event ${firstEvent.id} at ${firstTimeSlot.range}...`);
-                      
-            const bookingResult = await client.bookEvent({
-              event_id: Number(firstEvent.id),
-              time_slot: firstTimeSlot.range,
-              user_note: 'Booking via CosmoeBot'
-            });
-            
-            if (bookingResult.code === 200) {
-              console.log('Booking successful:', bookingResult.msg);
-            } else {
-              console.error('Booking failed:', bookingResult.msg);
-            }
-          }
-        } else {
-          console.error('Failed to get event details:', eventDetailResult.msg);
-        }
-      }
-    } else {
-      console.error('Failed to get events:', eventsResult.msg);
-    }
-    
-    // 4. Get user profile and statistics
-    console.log('\nGetting user profile...');
-    const profileResult = await client.getProfile();
-    if (profileResult.code === 200) {
-      console.log('User profile:', profileResult.data);
-    } else {
-      console.error('Failed to get profile:', profileResult.msg);
-    }
-    
-    // 5. Get user's booking history
-    console.log('\nGetting booking history...');
-    const bookingsResult = await client.getMyBookings();
-    if (bookingsResult.code === 200) {
-      console.log('Booking history:', bookingsResult.data);
-    } else {
-      console.error('Failed to get booking history:', bookingsResult.msg);
-    }
-    
-    // 6. Change password example (uncomment to use)
-    /*
-    console.log('\nChanging password...');
-    const passwordChangeResult = await client.changePassword('current_password', 'new_password');
-    if (passwordChangeResult.code === 200) {
-      console.log('Password changed successfully:', passwordChangeResult.msg);
-    } else {
-      console.error('Password change failed:', passwordChangeResult.msg);
-    }
-    */
-    
-  } catch (error) {
-    console.error('Error occurred:', error);
-  }
-}
-
-// Run the example
-example().catch(console.error);

+ 8 - 7
src/index.ts

@@ -1,4 +1,5 @@
 import { Bot, Context, webhookCallback } from "grammy";
+import { ConversationFlavor } from "@grammyjs/conversations";
 import { setupCommands } from "./command";
 import { handleScheduledEvent } from "./scheduler";
 
@@ -15,17 +16,17 @@ export default {
     env: Env,
     ctx: ExecutionContext,
   ): Promise<Response> {
-    const bot = new Bot(env.BOT_TOKEN, { botInfo: JSON.parse(env.BOT_INFO) });
+    const bot = new Bot<ConversationFlavor<Context>>(env.BOT_TOKEN, { botInfo: JSON.parse(env.BOT_INFO) });
     
     setupCommands(bot, env);
     
     // Set up command menu
     bot.api.setMyCommands([
-      { command: "start", description: "Start the bot and get welcome message" },
-      { command: "login", description: "Log in to your CosMoe account" },
-      { command: "logout", description: "Log out and clear your account information" },
-      { command: "events", description: "List upcoming events" },
-      { command: "history", description: "Show your booking history" },
+      { command: "start", description: "LINK START" },
+      { command: "login", description: "登录" },
+      { command: "logout", description: "退出登录并清除账户信息" },
+      { command: "events", description: "最近的摄影活动" },
+      { command: "history", description: "预约记录" },
     ]).catch(error => {
       console.error('Error setting bot commands:', error);
     });
@@ -38,7 +39,7 @@ export default {
     env: Env,
     ctx: ExecutionContext
   ): Promise<void> {
-    const bot = new Bot(env.BOT_TOKEN, { botInfo: JSON.parse(env.BOT_INFO) });
+    const bot = new Bot<ConversationFlavor<Context>>(env.BOT_TOKEN, { botInfo: JSON.parse(env.BOT_INFO) });
 
     await handleScheduledEvent(bot, controller, env, ctx);
   },

+ 3 - 2
src/scheduler/index.ts

@@ -1,4 +1,5 @@
-import { Bot } from "grammy";
+import { Bot, Context } from "grammy";
+import { ConversationFlavor } from "@grammyjs/conversations";
 import { CosmoeClient } from "../client/cosmoe";
 
 export interface Env {
@@ -9,7 +10,7 @@ export interface Env {
 }
 
 export async function handleScheduledEvent(
-  bot: Bot,
+  bot: Bot<ConversationFlavor<Context>>,
   controller: ScheduledController,
   env: Env,
   ctx: ExecutionContext