MCP驱动的AI角色扮演游戏

项目概述

我开发了一个基于MCP(Model Context Protocol)的AI智能角色扮演游戏,通过将游戏逻辑与AI决策分离,创造了一个动态、开放的游戏世界。项目已开源在github上传,欢迎体验和贡献。

核心架构设计

MCP服务端:游戏逻辑引擎

MCP服务端作为游戏的核心引擎,为AI提供了丰富的游戏操作接口(游戏系统中的各个抽象类已省略):

TypeScript 复制代码
expressApp.get('/initialize', async (req, res) => {
  try {
    game.initialize()
    game.add_world_info("奇点侦测站","init",defaultConfig.main_world);
    game.add_world_info("齿轮","init",defaultConfig.world1);
    game.add_world_info("源法","init",defaultConfig.world2);
    game.add_world_info("混元","init",defaultConfig.world3);
    game.add_world_info("黯蚀","init",defaultConfig.world4);
    game.add_world_info("终焉","init",defaultConfig.final_world);
    res.status(200).json("游戏重置成功")
  } catch (error) {
    console.error("初始化错误:", error);
    res.status(500).json({ error: "无法重置游戏" });
  }
});
expressApp.post('/initialize_name', async (req, res) => {
  try {
    const { name } = req.body;
    game.player_name = name;
    game.player.name = name;
    res.status(200).json("初始化主角姓名成功")
  } catch (error) {
    console.error("初始化错误:", error);
    res.status(500).json({ error: "无法给角色命名" });
  }
});

expressApp.get('/save_slots', async (req, res) => {
  try {
    const slots: SaveSlot[] = [];
    
    // 并行获取所有槽位信息
    for (let i = 1; i <= 6; i++) {
      const slotInfo = await getSaveSlotInfo(i);
      slots.push(slotInfo);
    }
    
    res.json(slots);
  } catch (error) {
    console.error("获取存档槽位错误:", error);
    res.status(500).json({ error: "无法获取存档槽位信息" });
  }
});

expressApp.post('/save', async (req, res) => {
  try {
    const { slot } = req.body;
    console.log(typeof(slot))
    console.log(`slot:${slot}`)
    
    const filename = getSaveFileName(slot);
    await game.save_to_file(filename);
    
    res.status(200).json({ message: "存档成功" });
  } catch (error) {
    console.error("存档错误:", error);
    res.status(500).json({ error: "存档失败" });
  }
});

expressApp.post('/load', async (req, res) => {
  try {
    const { slot } = req.body;
    
    if (typeof slot !== 'number' || slot < 1 || slot > 6) {
      return res.status(400).json({ error: "无效的存档槽位" });
    }
    
    const filename = getSaveFileName(slot);
    
    // 检查文件是否存在
    if (!fs.existsSync(filename)) {
      return res.status(404).json({ error: "存档不存在" });
    }
    
    await game.load_from_file(filename);
    
    res.status(200).json({ message: "读档成功" });
  } catch (error) {
    console.error("读档错误:", error);
    res.status(500).json({ error: "读档失败" });
  }
});

expressApp.post('/delete', async (req, res) => {
  try {
    const { slot } = req.body;
    
    if (typeof slot !== 'number' || slot < 1 || slot > 6) {
      return res.status(400).json({ error: "无效的存档槽位" });
    }
    
    const filename = getSaveFileName(slot);
    
    // 检查文件是否存在
    if (!fs.existsSync(filename)) {
      return res.status(404).json({ error: "存档不存在" });
    }
    
    // 删除文件
    fs.unlinkSync(filename);
    
    res.status(200).json({ message: "删除存档成功" });
  } catch (error) {
    console.error("删除存档错误:", error);
    res.status(500).json({ error: "删除存档失败" });
  }
});

//GameInfo&Functions
expressApp.get('/game/status',(req: Request, res: Response) =>{
  try{
    let result = game.toJson();
    result.current_worldview = game.worldViews.get(game.current_world)?.toPromptString();
    res.status(200).json(result);
  }catch(error){
    console.log(error);
  }
})

expressApp.post('/game/chageSkill',async(req:Request,res:Response)=>{
  try{
    const { name, skill_name, skill_description, skill_dependent } = req.body;
    const skill  = new Skill(skill_name,skill_description,skill_dependent);
    if(name == "self"){
      game.player.boundCombatAttribute(skill);
    }else{
      game.player.partyMembers.get(name)?.boundCombatAttribute(skill);
    }
    res.status(200).json("已成功绑定");
  }catch(error){
    console.log(error);
  }
})

expressApp.post('/game/addSkill',async(req:Request,res:Response)=>{
  try{
    const { name, skill_name, skill_description, skill_dependent } = req.body;
    const skill  = new Skill(skill_name,skill_description,skill_dependent);
    if(name == "self"){
      game.player.addSkill( skill_name, skill_description, skill_dependent);
    }else{
      game.player.partyMembers.get(name)?.addSkill( skill_name, skill_description, skill_dependent);
    }
    res.status(200).json("已成功绑定");
  }catch(error){
    console.log(error);
  }
})


expressApp.post('/game/getprompt',async(req:Request,res:Response)=>{
  try{
    const { name, context } = req.body;
    let prompt = [];
    if(game.player.partyMembers.has(name)){
      prompt = game.player.partyMembers.get(name)?.buildPrompt(context,[game.player.name??'玩家'])??[];
    }else{
      prompt = game.NPCs.get(name)?.buildPrompt(context,[game.player.name??'玩家'])??[];
    }
    res.status(200).json(prompt);
  }catch(error){
    console.log(error);
  }
})

expressApp.post('/game/addPrompt',async(req:Request,res:Response)=>{
  try{
    const { name, speaker, message, mode } = req.body;
    const isRespond: boolean = name===speaker
    if(game.player.partyMembers.has(name)){
      game.player.partyMembers.get(name)?.addConversationMessage(speaker,message,isRespond,mode);
    }else{
      game.NPCs.get(name)?.addConversationMessage(speaker,message,isRespond,mode);
    }
    res.status(200).json(`为${name}添加${speaker}发言信息成功`);
  }catch(error){
    console.log(error);
    res.status(500);
  }
})

expressApp.get('/game/set_built_Prompt_false',async(req:Request,res:Response)=>{
  try{
    game.built_Prompt_for_NPCs = false
    res.status(200).json();
  }catch(error){
    console.log(error);
    res.status(500);
  }
})

expressApp.get('/game/built_Prompt',async(req:Request,res:Response)=>{
  try{
    res.status(200).json({isBuilt:game.built_Prompt_for_NPCs});
  }catch(error){
    console.log(error);
    res.status(500);
  }
})

expressApp.get('/game/checkBetrayal',(req: Request, res: Response) =>{
  try{
    let result = game.betrayal();
    res.status(200).json(result);
  }catch(error){
    console.log(error);
  }
})

expressApp.get('/game/BattleBtn',(req: Request, res: Response) =>{
  try{
    res.status(200).json(game.battle())
  }catch(error){
    console.log(error);
    res.status(500);
  }
})

expressApp.post('/game/getProfile',(req: Request, res: Response) =>{
  try{
    const{name} = req.body;
    if(game.player.partyMembers.has(name)){
      res.status(200).json(game.player.partyMembers.get(name)?.profile)
    }else{
      res.status(200).json(game.NPCs.get(name)?.profile)
    }
  }catch(error){
    console.log(error);
    res.status(500);
  }
})

expressApp.post('/game/setProfile',(req: Request, res: Response) =>{
  try{
    const{name,profile} = req.body;
    if(name === 'self'){
      game.player.profile = profile
    }
    else if(game.player.partyMembers.has(name)){
      game.player.partyMembers.get(name)!.profile = profile
    }else{
      game.NPCs.get(name)!.profile = profile
    }
    res.status(200).json(`为${name}设置头像成功`);
  }catch(error){
    console.log(`设置头像失败!${error}`);
    res.status(500);
  }
})

expressApp.post('/game/setEnemyProfile',(req: Request, res: Response) =>{
  try{
    const{name,profile} = req.body;
    for(const enemy of game.enemy){
      if(enemy.name===name){
        enemy.profile = profile
      }
    }
    res.status(200).json(`为${name}设置头像成功`);
  }catch(error){
    console.log(error);
    console.log(`设置头像失败!${error}`);
  }
})

expressApp.post('/add_to_party', async (req, res) => {
  try {
    const { name } = req.body;
    game.add_partyMembers(name)
    res.status(200).json("加入队友成功")
  } catch (error) {
    console.error("加入队友错误:", error);
    res.status(500).json({ error: "无法加入队友" });
  }
});

//Main MCP
expressApp.post('/mcp', async (req: Request, res: Response) => {
  // In stateless mode, create a new instance of transport and server for each request
  // to ensure complete isolation. A single instance would cause request ID collisions
  // when multiple clients connect concurrently.
  
  try {
    const server = new McpServer({
      name: "GM",
      version: "1.0.0"
    });
    const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
    });
    res.on('close', () => {
      console.log('Request closed');
      transport.close();
      server.close();
    });

    server.registerTool("add_world_info",
      {
        title: "更新世界观",
        description: defaultConfig.add_world_info,
        inputSchema: { world:z.string(),info:z.string() }
      },
      async ({ world,info }) => 
        {
          const success_info = `成功为世界${world}增加世界观:${info}`;
          const faild_info = `失败动作:为世界${world}增加世界观:${info}`;
          try {
            game.add_world_info(world,"dynamic",info);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("update_relation",
      {
        title: "更新世界观中的势力关系",
        description: defaultConfig.update_relation,
        inputSchema: { world:z.string(),
                      factionA:z.string(),
                      factionB:z.string(),
                      attitude:z.enum(["hostile", "neutral","friendly","allied"]),
                      eventDescription:z.string(),
                      tensionDelta:z.number()
                    }
      },
      async ({ world,factionA,factionB,attitude,eventDescription,tensionDelta }) => 
        {
          const success_info = `成功为世界${world}增加派系冲突:${factionA}-${factionB}-${attitude}-${eventDescription}-${tensionDelta}`;
          const faild_info = `失败动作:为世界${world}增加派系冲突:${factionA}-${factionB}-${attitude}-${eventDescription}-${tensionDelta}`;
          try {
            game.update_relation(
              world,factionA,factionB,attitude,eventDescription,tensionDelta
            );
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("change_world",
      {
        title: "穿越世界",
        description: defaultConfig.change_world,
        inputSchema: { world:z.string(),summary:z.string() }
      },
      async ({ world,summary }) => 
        {
          const success_info = `成功穿越到世界${world}`;
          const faild_info = `失败动作:穿越到世界${world}`;
          try {
            game.change_world(world,summary);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("add_NPC",
      {
        title: "添加NPC",
        description: defaultConfig.add_NPC,
        inputSchema: { 
          name:z.string(),
          CON:z.number(),
          DEX:z.number(),
          INT:z.number(),
          level:z.number(),
          character_design:z.string(),
          skill:z.string(),
          skill_desc:z.string(),
          dependent:z.string(),
          item:z.string(),
          item_description:z.string()
         }
      },
      async ({
        name,
        CON,
        DEX,
        INT,
        level,
        character_design,
        skill,
        skill_desc,
        dependent,
        item,
        item_description
       }) => 
        {
          const success_info = `成功添加NPC${name}`;
          const faild_info = `失败动作:添加NPC${name}-${CON}-${DEX}-${INT}-${level}-${character_design}-${skill}-${skill_desc}-${dependent}-${item}-${item_description}`;
          try {
            game.add_NPC(
              name,
              CON,
              DEX,
              INT,
              level,
              character_design,
              skill,
              skill_desc,
              dependent,
              item,
              item_description
            );
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("battle",
      {
        title: "战斗",
        description: defaultConfig.battle,
        inputSchema: { npc_names:z.array(z.string()) }
      },
      async ({ npc_names }) => 
        {
          const success_info = `成功触发和以下NPC的战斗:${npc_names}`;
          const faild_info = `失败动作:触发战斗:${npc_names}`;
          try {
            game.battle(npc_names);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("changeStatus",
      {
        title: "改变NPC或自己的状态",
        description: defaultConfig.changeStatus,
        inputSchema: { npc:z.string(),name: z.string(), value: z.number() }
      },
      async ({ npc,name,value }) => 
        {
          const success_info = `成功触发状态改变:${npc}-${name}-${value}`;
          const faild_info = `失败动作:触发状态改变:${npc}-${name}-${value}`;
          try {
            game.changeStatus(npc,name,value);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("addSkill",
      {
        title: "增加技能",
        description: defaultConfig.addSkill,
        inputSchema: { npc:z.string(),
                      name:z.string(),
                      description:z.string(),
                      dependent:z.string() }
      },
      async ({ npc,name,description,dependent }) => 
        {
          const success_info = `成功触发技能添加:${npc}-${name}-${description}-${dependent}`;
          const faild_info = `失败动作:触发技能添加:${npc}-${name}-${description}-${dependent}`;
          try {
            game.addSkill(npc,name,description,dependent);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("gainItem",
      {
        title: "获得物品",
        description: defaultConfig.gainItem,
        inputSchema: { npc:z.string(),name:z.string(),description:z.string() }
      },
      async ({ npc,name,description }) => 
        {
          const success_info = `成功触发获得物品事件:${npc}-${name}-${description}`;
          const faild_info = `失败动作:触发获得物品事件:${npc}-${name}-${description}`;
          try {
            game.gainItem(npc,name,description);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("minusItem",
      {
        title: "失去物品",
        description: defaultConfig.minusItem,
        inputSchema: { npc:z.string(),name:z.string(),description:z.string() }
      },
      async ({ npc,name,description }) => 
        {
          const success_info = `成功触发失去物品事件:${npc}-${name}-${description}`;
          const faild_info = `失败动作:触发失去物品事件:${npc}-${name}-${description}`;
          try {
            game.minusItem(npc,name,description);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("add_quest",
      {
        title: "刷新任务信息",
        description: defaultConfig.add_quest,
        inputSchema: { s:z.string() }
      },
      async ({ s }) => 
        {
          const success_info = `成功刷新任务信息${s}`;
          const faild_info = `失败动作:刷新任务信息${s}`;
          try {
            game.refresh_quest(s);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("buildPrompt",
      {
        title: "构建给到NPC的提示词",
        description: defaultConfig.buildPrompt,
        inputSchema: { s:z.string() }
      },
      async ({ s }) => 
        {
          const success_info = `成功构建给到NPC的提示词${s}`;
          const faild_info = `失败动作:构建给到NPC的提示词${s}`;
          try {
            game.buildPrompt(s,true);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("end",
      {
        title: "结局",
        description: defaultConfig.end,
        inputSchema: { }
      },
      async ({ }) => 
        {
          const success_info = `成功构建结局`;
          const faild_info = `失败:构建结局`;
          try {
            game.end(defaultConfig.ending_with_high_EGO_and_high_morality,
              defaultConfig.ending_with_low_EGO_and_low_morality,
              defaultConfig.ending_with_high_EGO_and_low_morality,
              defaultConfig.ending_with_low_EGO_and_high_morality,
              defaultConfig.morality_threshold,
              defaultConfig.EGO_threshold);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error('Error handling MCP request:', error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: '2.0',
        error: {
          code: -32603,
          message: 'Internal server error',
        },
        id: null,
      });
    }
  }
});

// SSE notifications not supported in stateless mode
expressApp.get('/mcp', async (req: Request, res: Response) => {
  console.log('Received GET MCP request');
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Method not allowed."
    },
    id: null
  }));
});

// Session termination not needed in stateless mode
expressApp.delete('/mcp', async (req: Request, res: Response) => {
  console.log('Received DELETE MCP request');
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Method not allowed."
    },
    id: null
  }));
});

//MCP for NPC
expressApp.post('/NPCMCP', async (req: Request, res: Response) => {
  // In stateless mode, create a new instance of transport and server for each request
  // to ensure complete isolation. A single instance would cause request ID collisions
  // when multiple clients connect concurrently.
  
  try {
    const server = new McpServer({
      name: "NPC",
      version: "1.0.0"
    });
    const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
    });
    res.on('close', () => {
      console.log('Request closed');
      transport.close();
      server.close();
    });

    server.registerTool("add_character_design",
      {
        title: "更新人设",
        description: defaultConfig.add_character_design,
        inputSchema: { name:z.string(),new_info:z.string() }
      },
      async ({ name,new_info }) => 
        {
          const success_info = `成功为NPC${name}增加人设:${new_info}`;
          const faild_info = `失败动作:为NPC${name}增加人设:${new_info}`;
          try {
            game.NPCs.get(name)?.add_character_design(new_info);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("Betrayal",
      {
        title: "叛变",
        description: defaultConfig.Betrayal,
        inputSchema: { name:z.string() }
      },
      async ({ name }) => 
        {
          const success_info = `成功:NPC${name}叛变`;
          const faild_info = `失败动作:NPC${name}叛变`;
          try {
            game.NPCs.get(name)?.Betrayal();
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("add_to_potential_member",
      {
        title: "设为潜在队友",
        description: defaultConfig.add_to_potential_member,
        inputSchema: { name:z.string() }
      },
      async ({ name }) => 
        {
          const success_info = `成功:NPC${name}被设为潜在队友`;
          const faild_info = `失败动作:NPC${name}被设为潜在队友`;
          try {
            game.NPCs.get(name)?.add_to_potential_member();
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("summary",
      {
        title: "概括当前世界经历",
        description: defaultConfig.summary,
        inputSchema: { name:z.string(),summary:z.string() }
      },
      async ({ name,summary }) => 
        {
          const success_info = `成功:NPC${name}更新提示词-${summary}`;
          const faild_info = `失败动作:NPC${name}更新提示词-${summary}`;
          try {
            game.NPCs.get(name)?.summary(summary);
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    server.registerTool("summary_to_Player",
      {
        title: "NPC的重要信息给到主对话",
        description: defaultConfig.summary_to_Player,
        inputSchema: { name:z.string(),summary:z.string() }
      },
      async ({ name,summary }) => 
        {
          const success_info = `成功:NPC${name}加入重要信息-${summary}`;
          const faild_info = `失败动作:NPC${name}加入重要信息-${summary}`;
          try {
            game.summaryLog_NPC.push(`${name}:${summary}`)
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );
	
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error('Error handling MCP request:', error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: '2.0',
        error: {
          code: -32603,
          message: 'Internal server error',
        },
        id: null,
      });
    }
  }
});

// SSE notifications not supported in stateless mode
expressApp.get('/NPCMCP', async (req: Request, res: Response) => {
  console.log('Received GET MCP request');
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Method not allowed."
    },
    id: null
  }));
});

// Session termination not needed in stateless mode
expressApp.delete('/NPCMCP', async (req: Request, res: Response) => {
  console.log('Received DELETE MCP request');
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Method not allowed."
    },
    id: null
  }));
});


//MCP for tiny model
expressApp.post('/TinyMCP', async (req: Request, res: Response) => {
  // In stateless mode, create a new instance of transport and server for each request
  // to ensure complete isolation. A single instance would cause request ID collisions
  // when multiple clients connect concurrently.
  
  try {
    const server = new McpServer({
      name: "tiny",
      version: "1.0.0"
    });
    const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
    });
    res.on('close', () => {
      console.log('Request closed');
      transport.close();
      server.close();
    });

    server.registerTool("refresh_enemys",
      {
        title: "刷新敌人",
        description: defaultConfig.refresh_enemys,
        inputSchema: { 
          regular:z.string(),
          regular_item_names:z.array(z.string()),
          regular_item_description:z.array(z.string()),
          regular_skill:z.array(z.string()),

          elite:z.string(),
          elite_item_names:z.array(z.string()),
          elite_item_description:z.array(z.string()),
          elite_skill:z.array(z.string()),

          badass:z.string(),
          badass_item_names:z.array(z.string()),
          badass_item_description:z.array(z.string()),
          badass_skill:z.array(z.string())
         }
      },
      async ({ 
        regular,
        regular_item_names,
        regular_item_description,
        regular_skill,

        elite,
        elite_item_names,
        elite_item_description,
        elite_skill,

        badass,
        badass_item_names,
        badass_item_description,
        badass_skill
       }) => 
        {
          const success_info:string = `成功刷新敌人列表:${regular}-${regular_item_names}-${regular_item_description}-${regular_skill}-${elite}-${elite_item_names}-${elite_item_description}-${elite_skill}-${badass}-${badass_item_names}-${badass_item_description}-${badass_skill}`;
          const faild_info = `失败动作:刷新敌人列表:${regular}-${regular_item_names}-${regular_item_description}-${regular_skill}-${elite}-${elite_item_names}-${elite_item_description}-${elite_skill}-${badass}-${badass_item_names}-${badass_item_description}-${badass_skill}`;
          try {
            game.refresh_enemys(
              regular,regular_item_names,regular_item_description,regular_skill,
              elite,elite_item_names,elite_item_description,elite_skill,
              badass,badass_item_names,badass_item_description,badass_skill)
            console.log(success_info);
            return {content: [{ type: "text", text: success_info }]} 
          } catch (error) {
            console.log(error)
            return {content: [{ type: "text", text: faild_info }]} 
          }
        }
    );

    // server.registerTool("text2Img",
    //   {
    //     title: "文生图",
    //     description: defaultConfig.text2Img,
    //     inputSchema: { 
    //       prompt:z.string()
    //      }
    //   },
    //   async ({ 
    //     prompt
    //    }) => 
    //     {
    //       const success_info = `成功触发文生图:${prompt}`;
    //       const faild_info = `失败动作:触发文生图:${prompt}`;
    //       try {
    //         game.text2Img(prompt);
    //         console.log(success_info);
    //         return {content: [{ type: "text", text: success_info }]} 
    //       } catch (error) {
    //         console.log(error)
    //         return {content: [{ type: "text", text: faild_info }]} 
    //       }
    //     }
    // );

    // server.registerTool("decide_to_talk",
    //   {
    //     title: "决定现在是否要开口",
    //     description: defaultConfig.decide_to_talk,
    //     inputSchema: { name:z.string(),talk:z.boolean() }
    //   },
    //   async ({ name,talk }) => 
    //     {
    //       const success_info = `成功:NPC${name}决定是否开口-${talk}`;
    //       const faild_info = `失败动作:NPC${name}决定是否开口-${talk}`;
    //       try {
    //         game.NPCs.get(name)?.decide_to_talk(talk);
    //         console.log(success_info);
    //         return {content: [{ type: "text", text: success_info }]} 
    //       } catch (error) {
    //         console.log(error)
    //         return {content: [{ type: "text", text: faild_info }]} 
    //       }
    //     }
    // );
	
    await server.connect(transport);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error('Error handling MCP request:', error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: '2.0',
        error: {
          code: -32603,
          message: 'Internal server error',
        },
        id: null,
      });
    }
  }
});

// SSE notifications not supported in stateless mode
expressApp.get('/TinyMCP', async (req: Request, res: Response) => {
  console.log('Received GET MCP request');
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Method not allowed."
    },
    id: null
  }));
});

// Session termination not needed in stateless mode
expressApp.delete('/TinyMCP', async (req: Request, res: Response) => {
  console.log('Received DELETE MCP request');
  res.writeHead(405).end(JSON.stringify({
    jsonrpc: "2.0",
    error: {
      code: -32000,
      message: "Method not allowed."
    },
    id: null
  }));
});

const config = loadConfig();
const PORT = config.server.port;
const HOST = config.server.host;

// Start the server
expressApp.listen(PORT,HOST, (error) => {
  if (error) {
    console.error('Failed to start server:', error);
    process.exit(1);
  }
  console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
});

AI客户端:智能决策层

AI客户端负责处理与大型语言模型的交互,并且连接MCP进行函数调用,将自然语言指令转换为具体的游戏操作:(其他的抽象类以及建立服务代码省去)

TypeScript 复制代码
export interface NPC_card{
  profile: string
  hp: number
  name: string
  type: string
}


export class Dialogue {

  constructor(
	  private settings: SharedState,
    private openai:AsyncOpenAI|undefined = undefined,
    private openai_mini:AsyncOpenAI|undefined = undefined,
    private mcpServer_Main:MCPServerStreamableHttp|undefined = undefined,
    private mcpServer_NPC:MCPServerStreamableHttp|undefined = undefined,
    private mcpServer_tiny:MCPServerStreamableHttp|undefined = undefined,
    private agent:Agent|undefined = undefined,
    private agent_mini:Agent|undefined = undefined,
    private agent_NPC:Agent|undefined = undefined, //暂时没用,因为和NPC对话的模型与正常指令模式的模型是一样的
    private agent_tiny:Agent|undefined = undefined,
    private stream = true
  ){
    this.openai = new AsyncOpenAI(
      {
          apiKey: this.settings.apikey,
          baseURL: this.settings.baseurl,
          dangerouslyAllowBrowser: true
      }
    )

    this.openai_mini = new AsyncOpenAI(
    {
        apiKey: this.settings.mini_model_apikey,
        baseURL: this.settings.mini_model_baseurl,
        dangerouslyAllowBrowser: true
    }
    )

    this.mcpServer_Main = new MCPServerStreamableHttp({
      url: `http://${this.settings.MCP_Server}:${this.settings.port}/mcp`,
      name: '游戏主系统',
    })


    this.mcpServer_NPC = new MCPServerStreamableHttp({
      url: `http://${this.settings.MCP_Server}:${this.settings.port}/NPCMCP`,
      name: 'NPC系统',
    })

    this.mcpServer_tiny = new MCPServerStreamableHttp({
      url: `http://${this.settings.MCP_Server}:${this.settings.port}/TinyMCP`,
      name: '小模型MCP',
    })

    setTracingDisabled(true)
    setDefaultOpenAIClient(this.openai)
    setOpenAIAPI('chat_completions')
    this.mcpServer_Main.connect()
    this.mcpServer_NPC.connect()
    this.mcpServer_tiny.connect()
  }

  private async setBuiltPromptFalse(){
    try{
      await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/set_built_Prompt_false`)
    } catch(error){
      console.log(`清空广播状态失败:${error}`)
    }
  }

  private async getBuiltPrompt(){
    try{
      const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/built_Prompt`)
      const res = await response.json()
      const isBuilt:boolean = res.isBuilt
      return isBuilt
    } catch(error){
      console.log(`清空广播状态失败:${error}`)
      return false
    }
  }

  //指令模式
  public async Command_dialogue(ipt: string,initialize_Prompt:boolean = false) {
    try {
      await this.setBuiltPromptFalse()
      const res:AgentInputItem[]=[];
      (await this.buildPrompt(ipt,initialize_Prompt)).forEach(value => {
        if(value.role == "system"){
          const s:SystemMessageItem = {role:"system",content:value.content};
          res.push(s)
        }else if(value.role == "user"){
          const s:UserMessageItem = {role:"user",content:value.content};
          res.push(s);
        }else{
          const s:AssistantMessageItem={role:"assistant",status:"completed",content:[{type:"output_text",text:value.content}]};
          res.push(s);
        }
      })
      if(this.mcpServer_Main===undefined){
        return
      }
      console.log('开始')
      await this.mcpServer_Main.connect()
      console.log('链接MCP完成')
      this.agent = new Agent({
        name: 'Agent',
        instructions:
          '你不再是一个LLM Assistant,而是一个智能的文字冒险游戏系统,现在请你智能地调用MCP中的函数。',
        mcpServers: [this.mcpServer_Main],
        model: this.settings.modelName
      })
      const res_msg = await this.get_LLM_result(this.agent,res);
      const res_Message = new Message("assistant",res_msg??"")
      const ipt_Message = new Message("user",ipt??"")
      if(!initialize_Prompt){
        this.settings.history.push(ipt_Message)
      }
      if(!initialize_Prompt){
        this.settings.display_history.push(ipt)
      }
      this.settings.history.push(res_Message)
      this.settings.display_history.push(res_msg??"...")
      await this.mcpServer_Main.close()
      // 处理对话后的其他信息(广播给NPC(已使用NPC MCP中的函数)->是否有Betrayal)
      const isBuiltPrompt = await this.getBuiltPrompt()
      // let still_need_respond = false
      // let maxixmum_iteration = 3
      if(!initialize_Prompt && isBuiltPrompt){
        await this.NPCs_respond()
        // still_need_respond = true
      }
      // 想要文生图的情形:
      if(this.settings.img_generation){
        const s:AssistantMessageItem={role:"assistant",status:"completed",content:[{type:"output_text",text:res_msg}]};
        res.push(s)
        const prompt_for_img = await this.get_prompt_for_img(res)
        if(prompt_for_img != undefined && prompt_for_img.length > 0){
          console.log(`触发文生图! Prompt: ${prompt_for_img}`)
          const res_img = await this.get_img(prompt_for_img)
          this.settings.display_history.push(res_img)
        }
      }
      await this.setBuiltPromptFalse()
      // 暂时不考虑"玩家杀死NPC1后NPC2又因此倒戈的情况"
      // while(still_need_respond && maxixmum_iteration>=0){
      //   still_need_respond = await this.check_battle();
      //   maxixmum_iteration -= 1
      // }
    }catch(error){
      console.log(error)
      await this.setBuiltPromptFalse()
    }
  }

  public async set_player_profile(ipt: string){
    let res = ""
    if(this.settings.img_generation){
      res = await this.get_img(ipt)
    }
    if(res == ''){
      const profile_idx = Math.floor(Math.random() * (28)); //0~27内随便选
      res = img_dict.img_base64.get(`img_${profile_idx}_left`) ?? img_dict.img_base64.get(`img_0_left`)!
    }
    const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/setProfile`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({name:'self',profile:res})
    })
  }

  private async get_LLM_result(agent: Agent<unknown, "text">, his){
    let fullText = '';
    const response_before = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
    const jsonData_before = await response_before.json()
    const world_before_cmd = jsonData_before.current_world
    if(this.stream){
        const result = await run(agent, his,
        {stream:true});
        const stream = result.toTextStream({compatibleWithNodeStreams: true,})
        await new Promise((resolve, reject) => {
          stream.on('data', (chunk) => {
            fullText += chunk;
          });
          stream.on('end', resolve);
          stream.on('error', reject);
        });
      }else{
        const result = await run(agent, his);
        fullText = result.finalOutput??"";
      }
    const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
    const jsonData = await response.json()
    if(jsonData.current_world != world_before_cmd){
      this.settings.history.length=0
    }
    return fullText;
  }

  private async buildPrompt(ipt: string,initialize_Prompt:boolean = false) {
    const res = [...this.settings.history]
    const prompt = await this.build_Prompt_from_status(initialize_Prompt);
    res.push(
        new Message('user', ipt),
        new Message('system', prompt)
    )
    return res;
  }

  private async build_Prompt_from_status(initialize:boolean = false) {
      try {
        let res = defaultConfig.systemPrompt + '\n'
        const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
        const jsonData = await response.json()
        const quests: string = jsonData.quests
        const ego = jsonData.player.EGO
        //const partys = new Map(jsonData.player._partys)
        // 平均状态 hp/等级/三维/金钱
        try{
          let level = jsonData.player.level
          let CON = jsonData.player.CON
          let DEX = jsonData.player.DEX
          let INT = jsonData.player.INT
          const gold = jsonData.player.gold
          let denominator = 1

          
          jsonData.player._partys.forEach(x => {
            //hp += x.hp;
            level += x.level;
            CON += x.CON;
            DEX += x.DEX;
            INT += x.INT;
            denominator += 1;
          });

        //hp = Math.ceil(hp/denominator*100)/100;
        level = Math.ceil(level/denominator*100)/100;
        CON = Math.ceil(CON/denominator*100)/100;
        DEX = Math.ceil(DEX/denominator*100)/100;
        INT = Math.ceil(INT/denominator*100)/100;

        res += `\n**玩家队伍状态**:\n`
        res += `平均等级:${level}\n平均体质:${CON}\n平均感知:${DEX}\n平均智力:${INT}\n金钱:${gold}\n\n`
        } catch (error){
          console.log("获取玩家队伍状态失败:")
          console.log(error)
        }
        
        try{
          if (quests.length > 0) {
            res += `**任务信息**:\n`
            res += `现在玩家的任务有:${quests}\n\n`
          }
        } catch (error){
          console.log("获取任务状态失败:")
          console.log(error)
        }
        
        try{
          if (jsonData.NPCs.length > 0) {
            const npc_names:string[] = []
            jsonData.NPCs.forEach(x => npc_names.push(x.name))
            res += `**NPC信息**:\n`
            res += `当前已经生成了以下NPC:${npc_names}\n\n`
          }
        } catch (error){
          console.log("获取NPC信息失败:")
          console.log(error)
        }
        
        try{
          if (jsonData.player._partys.length > 0) {
            res += `**队友信息**:\n`
            res += `现在的玩家有以下队友:${jsonData.player._partys.map(p => p.name).join(",")}\n\n`
          }
        } catch (error){
          console.log("获取队友信息失败:")
          console.log(error)
        }

        try{
          if(jsonData.player._skills.length>0)
          {
            res += `**技能信息**:\n`
            res += `现在玩家有以下技能:`
            jsonData.player._skills.forEach((s: { name: string; description: string; dependent: string; })=>{
              res+=s.name+','
            });
            res = res.slice(0,res.length-1)+"\n\n"
          }
          if(jsonData.player._partys.length>0){
            res += `队友技能信息:\n`
          }
          for(let i=0;i<jsonData.player._partys.length;i++){
            res += jsonData.player._partys[i].name + ":"
            for (const s of jsonData.player._partys[i]._skills) {
              res += s.name + ','
            }
            res = res.slice(0,res.length-1)+"\n\n"
          }
        } catch (error){
          console.log("获取技能信息失败:")
          console.log(error)
        }
        try{
          res += `**世界观信息**:\n`
          res += `现在玩家位于的世界的世界观如下:\n${jsonData.current_worldview} \n`

          if(jsonData.summaryLogs.length>0){
            res += `**重要历史信息**:\n${Array.from(jsonData.summaryLogs)}\n`
          }
        } catch (error){
          console.log("获取世界观信息失败:")
          console.log(error)
        }
        
        try{
          if(jsonData.summaryLog_NPC.length>0){
            res += `**NPC相关重要历史信息**:\n${Array.from(jsonData.summaryLog_NPC)}`
          }
          if(jsonData.current_world!='奇点侦测站'){
            res += `**"自我值"信息**:\n`
            if (ego >= 70) {
              res += defaultConfig.Prompt_with_Ego
            } else if (ego >= 30) {
              res += defaultConfig.Prompt_with_Less_Ego
            } else {
              res += defaultConfig.Prompt_without_Ego
            }
          }

          if(initialize){
            res+="\n"+defaultConfig.initialize_Prompt+"\n"
          }
        } catch (error){
          console.log("获取历史信息失败:")
          console.log(error)
        }

        console.log(res)
        return res
      } catch (error) {
        console.error(error)
        return ""
      }
    }

  //对话相关
  public async public_talk(ipt:string){
    try {
      // 公共喊话
      const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
      const jsonData = await response.json()
      const npcs:string[] = [];
      for(const npc of jsonData.player._partys){
        npcs.push(npc.name)
      }
      for(const npc of jsonData.npcs){
        npcs.push(npc.name)
      }
      if(npcs.length>0){
        this.settings.display_history.push(`${jsonData.player.name}:${ipt}`)
        const npcPromises = npcs.map(npc => this.Talk_To_NPC(npc,ipt));
        const results = await Promise.all(npcPromises);

        // 首先加入历史
        for(const npc of npcs) {
          const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({name:npc,speaker:jsonData.player.name, message:ipt, mode: "public"})
          })
          const add_result = await response_add.json()
          // if(!add_result.ok){
          //   console.log(`失败:为${npc}加入玩家对话历史`)
          // }

          await this.NPCs_broadcast(npc,npcs,results)

        }
        // Betrayal处理
        let still_need_respond = await this.check_battle();
        let maxixmum_iteration = 3
        while(still_need_respond && maxixmum_iteration>=0){
          still_need_respond = await this.check_battle();
          maxixmum_iteration -= 1
        }
      }
    }catch(error){
      console.log(`公共聊天出错!${error}`)
    }
  }

  public async private_talk(npc:string,ipt:string){
    try{
      // 私聊
      const result = await this.Talk_To_NPC(npc,ipt,'private')

      const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
      const jsonData = await response.json()

      // 首先加入历史
      const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({name:npc,speaker:jsonData.player.name, message:ipt, mode: "private"})
      })
      const add_result = await response_add.json()

      const response_npc = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({name:npc,speaker:npc, message:result, mode: "private"})
      })
      console.log(result)
      return result
    }catch(error){
      console.log(`私人聊天出错!${error}`)
      return null
    }
  }

  private async check_battle(){
    try
      {const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/checkBetrayal`)
      const jsonData = await response.json()
      if(jsonData === null){
        return false
      }else{
        // 进入Battle页面(待定)
      //   const changePage = inject('change_page') as (page: string) => void;
      //   const update_battleProcess = inject('update_battleProcess') as (jsonDataString: string) => void;
      //   changePage('battle');
      //   update_battleProcess(jsonData)
        // respond
        await this.NPCs_respond()
        return true
      }}catch(error){
        console.log(`检查战斗出错!${error}`)
        return false
      }
  }

  private async NPCs_broadcast(npc:string,npcs:string[],results:string[]){
    try{
      let my_response = ""
      if(npcs.length != results.length) return
      for (let idx=0;idx<npcs.length;idx++){
        const responser = npcs[idx]
        if(responser == npc){
          my_response = results[idx] ?? ""
        }else{
          const response_npc = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({name:npc,speaker:responser, message:results[idx], mode: "public"})
          })
          const npc_result = await response_npc.json()
          // if(!npc_result.ok){
          //   console.log(`失败:为${npc}加入${responser}对话历史`)
          // }
        }
      }
      //最后将自己的回复加入作为assistance
      await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/addPrompt`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({name:npc,speaker:npc, message:my_response, mode: "public"})
      })
      // if(!npc_result.ok){
      // console.log(`失败:为${npc}加入自己的回复`)
      // }
      this.settings.display_history.push(`<NPC_respond>${npc}:${my_response}</NPC_respond>`)
    }catch(error){
      console.log(`广播出错!${error}`)
    }
  }

  private async NPCs_respond(){
    // 各个NPC对于玩家行为的回复
    const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
    const jsonData = await response.json()
    const npcs:string[] = [];
    for(const npc of jsonData.player._partys){
      npcs.push(npc.name)
    }
    for(const npc of jsonData.NPCs){
      npcs.push(npc.name)
    }
    if(npcs.length>0){
      const npcPromises = npcs.map(npc => this.Talk_To_NPC(npc,''))
      const results = await Promise.all(npcPromises);
      // 将npc的话广播给其他npc
      for (const npc of npcs) {
        await this.NPCs_broadcast(npc,npcs,results)
      }
    }
  }

  private async get_history(name,mode):Promise<AgentInputItem[]>{
    const prompt = await this.get_NPC_prompt(name,mode);
    const input_item:AgentInputItem[] = [];
    prompt.forEach((value: { role: string; content; }) => {
      if(value.role == "system"){
        const s:SystemMessageItem = {role:"system",content:value.content};
        input_item.push(s)
      }else if(value.role == "user"){
        const s:UserMessageItem = {role:"user",content:value.content};
        input_item.push(s);
      }else{
        const s:AssistantMessageItem={role:"assistant",status:"completed",content:[{type:"output_text",text:value.content}]};
        input_item.push(s);
      }
    });
    return input_item
  }

  public async Talk_To_NPC(name:string ,ipt:string, mode:string = "public"){
    try{
      const input_item:AgentInputItem[] = await this.get_history(name,mode)
      const s:UserMessageItem = {role:"user",content:ipt};
      if(ipt.length>0){
        input_item.push(s); //为空时代表处理"其他行为后是否有连锁反应"
      }
      let is_talk:boolean = true
      if(mode == "public"){
        is_talk = await this.decide_to_talk(input_item);
      }
      if(is_talk && this.mcpServer_NPC){
        try{
          this.mcpServer_NPC.connect()
          this.agent = new Agent({
            name: 'Agent',
            instructions:
              '你不再是一个LLM Assistant,而是一个智能的文字冒险游戏中的NPC。',
            mcpServers: [this.mcpServer_NPC],
            model: this.settings.modelName
          })
          const res_msg =await this.get_LLM_result(this.agent,input_item);
          await this.mcpServer_NPC.close()
          return res_msg
        }
        catch(error){
          console.log(error)
          return ""
        }
      }else{
        return ""
      }
    }catch(error){
      console.log(`对话出错!${error}`)
      return ""
    }
  }

  private async decide_to_talk(his:AgentInputItem[]){
    const DECIDE_TO_TALK:SystemMessageItem = {role:"system",content:defaultConfig.DECIDE_TO_TALK_prompt}
    const tmp_his = [...his]
    tmp_his.push(DECIDE_TO_TALK)
    this.agent_mini = new Agent({
      name: 'Agent',
      instructions:
      '你不再是一个LLM Assistant,而是一个智能的文字冒险游戏系统。',
      //mcpServers: [this.mcpServer_tiny],
      model: this.settings.mini_model
    })
    const res_msg =await this.get_LLM_result(this.agent_mini,tmp_his);
    if(res_msg == "是"){
      return true
    }else{
      return false
    }
  }

  private async get_NPC_prompt(name:string,mode:string = "public"){
    const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/getprompt`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({name, mode})
    })
    const jsonData = await response.json()
    return jsonData
  }

  //图像
  private async get_img(prompt:string){
    const res = await textToImage(this.settings, prompt,500,500)
    return res
  }

  private async get_prompt_for_img(his:AgentInputItem[]){
    const DECIDE_TO_img:SystemMessageItem = {role:"system",content:defaultConfig.DECIDE_TO_img_prompt}
    his.push(DECIDE_TO_img)
    this.agent_mini = new Agent({
      name: 'Agent',
      instructions:
      '你不再是一个LLM Assistant,而是一个游戏场景分析代理。',
      model: this.settings.mini_model
    })
    const res_msg =await this.get_LLM_result(this.agent_mini,his);
    if(res_msg.length > 0){
      return res_msg
    }else{
      return ''
    }
  }

  private async NPC_profiles(name:string, character_design:string){
    try {
      console.log('开始设置头像')
      const new_profile = await this.set_profile(character_design)
      const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/setProfile`, {
          method: 'POST',
          headers: {
              'Content-Type': 'application/json'
          },
          body: JSON.stringify({ name: name, profile: new_profile })
      })
      const result = await response.json()
  } catch (error) {
      console.log(`为 ${name} 设置头像时发生错误:${error}`)
  }
  }

  public async set_NPC_profile(){
    try{
      const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
      const jsonData = await response.json()
      // NPC 不考虑一次多个NPC没头像造成此处耗费大量时间的场景
      // 不能使用Promise.all,因为文生图有rate limit
      for(const npc of jsonData.NPCs){
        if(npc.profile==""){
          await this.NPC_profiles(npc.name,npc.character_design)
        }
      }
    }catch(error){
      console.log(`设置头像失败!${error}`)
    }
  }

  // public async get_NPC(){
  //   try{
  //     const res:NPC_card[] = []
  //     const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
  //     const jsonData = await response.json()
  //     // player
  //     res.push(
  //       {
  //         profile: jsonData.player.profile,
  //         hp: jsonData.player.hp,
  //         name: jsonData.player.name,
  //         type: "player"
  //       }
  //     )
  //     // NPC 现在只考虑"NPC还没有头像"的情形,不考虑一次多个NPC没头像造成此处耗费大量时间的场景
  //     for(const npc of jsonData.NPCs){
  //       let profile = ""
  //       if(npc.profile==""){
  //         const new_profile = await this.set_profile(npc.character_design)
  //         console.log(new_profile)
  //         const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/setProfile`, {
  //           method: 'POST',
  //           headers: {
  //             'Content-Type': 'application/json'
  //           },
  //           body: JSON.stringify({name:npc.name,profile:new_profile})
  //         })
  //         const set_result = await response_add.json()
  //         profile = new_profile
  //       }else{
  //         profile = npc.profile
  //       }
  //         res.push(
  //         {
  //           profile: profile,
  //           hp: npc.hp,
  //           name: npc.name,
  //           type: "npc"
  //         }
  //       )
  //     }
  //     // 队友
  //     for(const party of jsonData.player._partys){
  //         res.push(
  //         {
  //           profile: party.profile,
  //           hp: party.hp,
  //           name: party.name,
  //           type: "party"
  //         }
  //       )
  //     }

  //     return res
  //   }catch(error){
  //     console.log(`获取队友信息失败!${error}`)
  //     return []
  //   }
  // }

  public async set_profile(character_design:string){
    try{
      if(this.settings.img_generation){
        const given_prompt = await this.get_profile_img_prompt(character_design) ?? ""
        if(given_prompt != undefined && given_prompt.length > 0){
          console.log(`触发文生图! Prompt: ${given_prompt}`)
          const res_img = await this.get_img(given_prompt)
          console.log(res_img.length)
          if(res_img.length>0){
            return res_img
            }
          }
        }
      const profile_idx = Math.floor(Math.random() * (28)); //0~27内随便选
      return img_dict.img_base64.get(`img_${profile_idx}_left`) ?? img_dict.img_base64.get(`img_0_left`)!
    }catch(error){
      console.log(`设置形象出错!${error}`)
      return ""
    }
  }

  public async refresh_enemy(){
    try{
      if(this.mcpServer_tiny === undefined) return
      const his:AgentInputItem[] = []
      this.settings.history.forEach(value => {
        if(value.role == "system"){
          const s:SystemMessageItem = {role:"system",content:value.content};
          his.push(s)
        }else if(value.role == "user"){
          const s:UserMessageItem = {role:"user",content:value.content};
          his.push(s);
        }else{
          const s:AssistantMessageItem={role:"assistant",status:"completed",content:[{type:"output_text",text:value.content}]};
          his.push(s);
        }
      })
      const refresh_enemy:SystemMessageItem = {role:"system",content:defaultConfig.prompt_for_refresh_enemy}
      his.push(refresh_enemy)
      await this.mcpServer_tiny.connect()
      this.agent_mini = new Agent({
        name: 'Agent',
        instructions:
        '你不再是一个LLM Assistant,而是一个游戏场景分析代理。',
        model: this.settings.mini_model,
        mcpServers: [this.mcpServer_tiny]
      })
      await this.get_LLM_result(this.agent_mini,his); //输出无意义
      let use_img_generation = false
      if(this.settings.img_generation){
        his.pop()
        use_img_generation = true
        try{
          await this.profile_enemy(his)
        }catch(error){
          console.log("敌人文生图失败!")
          use_img_generation = false
        }
      }

      if(!use_img_generation){
        await this.set_enemy_profile_random()
      }
    }catch(error){
      console.log(`刷新敌人出错!${error}`)
    }
  }

  private async profile_enemy(his:AgentInputItem[]){
    const type_dict = new Map([['regular','杂鱼'],['elite','精英'],['badass','传奇']])
    const painters :Promise<string>[] = []
    const enemy_name:string[] = []
    const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
    const jsonData = await response.json()
    const enemys = jsonData.enemy
    for(const enemy of enemys){
      const prompt = defaultConfig.Img_prompt_for_enemy + `\n现在请你为${type_dict.get(enemy.type) ?? "精英"}敌人:${enemy.name}设计提示词`
      const DECIDE_TO_img:SystemMessageItem = {role:"system",content:prompt}
      this.agent_mini = new Agent({
        name: 'Agent',
        instructions:
        '你不再是一个LLM Assistant,而是一个游戏场景分析代理。',
        //mcpServers: [this.mcpServer_tiny],
        model: this.settings.mini_model
      })
      his.push(DECIDE_TO_img)
      const res_msg = this.get_LLM_result(this.agent_mini,his);
      painters.push(res_msg)
      enemy_name.push(enemy.name)
    }
    const all_prompts = await Promise.all(painters)
    // 不能使用Promise.all,因为文生图有rate limit
    const workers:Promise<string>[] = []
    for(let i=0;i<enemy_name.length;i++){
      const given_prompt = all_prompts[i]
      if(given_prompt != undefined){
          console.log(`触发文生图! Prompt: ${given_prompt}`)
          let res = await this.get_img(given_prompt)
          if(res == ''){
            const profile_idx = Math.floor(Math.random() * (28)); //0~27内随便选
            res = img_dict.img_base64.get(`img_${profile_idx}_left`) ?? img_dict.img_base64.get(`img_0_left`)!
          }
          await this.set_enemy_profile(enemy_name[i]??"",res??"")
      }
    }
  }

  private async set_enemy_profile_random(){
    const response = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/status`)
    const jsonData = await response.json()
    const enemys = jsonData.enemy
    for(const enemy of enemys){
      const profile_idx = Math.floor(Math.random() * (28)); //0~27内随便选
      const img = img_dict.img_base64.get(`img_${profile_idx}_left`) ?? img_dict.img_base64.get(`img_0_left`)!
      await this.set_enemy_profile(enemy.name,img)
    }
  }

  private async set_enemy_profile(name:string, profile:string){
    const response_add = await fetch(`http://${this.settings.MCP_Server}:${this.settings.port}/game/setEnemyProfile`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({name:name,profile:profile})
    })
    const set_result = await response_add.json()
    // if(!set_result.ok){
    //   console.log(`设置头像失败!`)
    // }
  }

  private async get_profile_img_prompt(character_design: string){
    if(this.mcpServer_tiny === undefined)return
    let Img_prompt = defaultConfig.Img_prompt_for_profile
    Img_prompt += `\n 人物设定如下:\n${character_design}`
    const DECIDE_TO_img:SystemMessageItem = {role:"system",content:Img_prompt}
    const his = [DECIDE_TO_img]
    this.agent_mini = new Agent({
      name: 'Agent',
      instructions:
      '你不再是一个LLM Assistant,而是一个游戏场景分析代理。',
      //mcpServers: [this.mcpServer_tiny],
      model: this.settings.mini_model
    })
    const res_msg =await this.get_LLM_result(this.agent_mini,his);
    if(res_msg.length > 0){
      return res_msg
    }else{
      return ''
    }
  }
}

Electron客户端:玩家交互界面

使用Electron构建桌面应用(此处展示的是Vue中的部分代码,整个项目可以参考我的git链接)

TypeScript 复制代码
const current_page: Ref<string, string> = ref('cover')
const battleProcess = ref<BattleLog[]>([])
const OurSide_Meta = ref<battle_player_constructor[]>([])
const EnemySide_Meta = ref<battle_player_constructor[]>([])

const teammates = ref<CharacterData[]>([])
const npcs = ref<CharacterData[]>([])
const enemys = ref<CharacterData[]>([])

const EGO = ref(100)
const battleKey = ref(0)
const quests = ref('')

const change_current_page = (newPage: string) => {
  current_page.value = newPage
}

const update_battleProcess = (s: string) => {
  const Battle_from_Server = JSON.parse(s)
  const OurSide = parse_Battle_Meta(Battle_from_Server[0])
  const EnemySide = parse_Battle_Meta(Battle_from_Server[1])
  const ThisBattle = parse_Battle_Log(Battle_from_Server.slice(2))

  OurSide_Meta.value = OurSide
  EnemySide_Meta.value = EnemySide
  battleProcess.value = ThisBattle

  battleKey.value++
  change_current_page('battle')
}

const parse_Battle_Meta = (ipt) => {
  const res: battle_player_constructor[] = []
  for (const i of Array(ipt.hp.length).keys()) {
    const thisPerson = {
      image: ipt.profile[i],
      hp: ipt.hp[i],
      name: ipt.name[i],
      skill: ipt.skill_name[i],
      dependent: ipt.skill_dependent[i]
    }
    res.push(thisPerson)
  }
  return res
}

const parse_Battle_Log = (ipt) => {
  const res: BattleLog[] = []
  for (const x of ipt) {
    const thisTurn = new BattleLog(
      x.attacker_type,
      x.defender_type,
      x.attacker_index,
      x.defender_index,
      x.damage
    )
    res.push(thisTurn)
  }
  return res
}

const bound_skill = (npc: CharacterData) => {
  const tgt_skill = npc.CombatAttribute
  for (const skill of npc.skills) {
    if (tgt_skill == null) {
      skill.isBound = false
    } else {
      if (
        skill.name == tgt_skill.name &&
        skill.description == tgt_skill.description &&
        skill.dependent == tgt_skill.dependent
      ) {
        skill.isBound = !skill.isBound
      } else {
        skill.isBound = false
      }
    }
  }
}

const set_NPC_cards = (jsonData) => {
  const Server_teammates: CharacterData[] = []
  const Server_NPCs: CharacterData[] = []
  const Server_enemys: CharacterData[] = []
  //先把自己给加进去
  Server_teammates.push({
    profile: jsonData.player.profile,
    hp: jsonData.player.hp,
    name: jsonData.player.name,
    type: 'player',
    level: jsonData.player.level,
    exp: jsonData.player.exp,
    expToNextLevel: 1000,
    CON: jsonData.player.CON,
    DEX: jsonData.player.DEX,
    INT: jsonData.player.INT,
    skills: jsonData.player._skills,
    CombatAttribute: jsonData.player.CombatAttribute,
    can_be_recruited: false,
    disabled: false
  })
  for (const npc of jsonData.player._partys) {
    Server_teammates.push({
      profile: npc.profile,
      hp: npc.hp,
      name: npc.name,
      type: 'party',
      level: npc.level,
      exp: npc.exp,
      expToNextLevel: 1000,
      CON: npc.CON,
      DEX: npc.DEX,
      INT: npc.INT,
      skills: npc._skills,
      CombatAttribute: npc.CombatAttribute,
      can_be_recruited: false,
      disabled: false
    })
  }

  // 还需要筛选"当前世界"的NPC
  const world_npc_map = new Map<string, string>(jsonData.world_npc_map)
  for (const npc of jsonData.NPCs) {
    if ((world_npc_map.get(npc.name) ?? '') == jsonData.current_world) {
      Server_NPCs.push({
        profile: npc.profile,
        hp: npc.hp,
        name: npc.name,
        type: 'npc',
        level: npc.level,
        exp: npc.exp,
        expToNextLevel: 1000,
        CON: npc.CON,
        DEX: npc.DEX,
        INT: npc.INT,
        skills: npc._skills,
        CombatAttribute: npc.CombatAttribute,
        can_be_recruited: npc.potential_member,
        disabled: false
      })
    }
  }

  for (const enemy of jsonData.enemy) {
    Server_enemys.push({
      profile: enemy.profile,
      hp: enemy.hp,
      name: enemy.name,
      type: 'enemy',
      level: 0,
      exp: 0,
      expToNextLevel: 0,
      CON: 0,
      DEX: 0,
      INT: 0,
      skills: [],
      CombatAttribute: null,
      can_be_recruited: false,
      disabled: false
    })
  }

  for (const npc of Server_teammates) {
    bound_skill(npc)
  }
  teammates.value = Server_teammates
  npcs.value = Server_NPCs
  enemys.value = Server_enemys
  EGO.value = jsonData.player.EGO
  quests.value = jsonData.quests
}

const update_NPC_cards = async () => {
  const store = useSharedStore()
  await fetch(`http://localhost:${Client_Port}/setProfiles`)
  const response = await fetch(`http://${store.MCP_Server}:${store.port}/game/status`)
  const jsonData = await response.json()
  set_NPC_cards(jsonData)
}

const recruit = async (name: string) => {
  const store = useSharedStore()
  await fetch(`http://${store.MCP_Server}:${store.port}/add_to_party`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: name })
  })
  const response = await fetch(`http://${store.MCP_Server}:${store.port}/game/status`)
  const jsonData = await response.json()
  set_NPC_cards(jsonData)
}

const clientstore = ClientPortStore()
const Client_Port = clientstore.port //调试时写为3001
console.log(Client_Port)

const Client_update_Settings = async () => {
  //将pinia的settings传给服务端
  const store = useSharedStore()
  const response = await fetch(`http://localhost:${Client_Port}/update_settings`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ json_string: store.toJson() })
  })
  return response.ok
}

const game_init = async () => {
  const store = useSharedStore()
  await fetch(`http://${store.MCP_Server}:${store.port}/initialize`)
}

const update_historys = (jsonData: any) => {
  //将服务端的history拿过来
  const store = useSharedStore()
  for (const res of jsonData.display_history) {
    add_new_history(res)
    store.display_history.push(res)
  }
  store.history.length = 0
  for (const his of jsonData.history) {
    store.history.push({
      role: his.role,
      content: his.content
    })
  }
}

const Command_dialogue = async (ipt: string, initialize_Prompt: boolean = false) => {
  const connect = await Client_update_Settings()
  if (!connect) {
    console.log('连接LLM出错', '无法将历史记录传给LLM,请检查MCP_Client是否开启')
    return
  }
  const response = await fetch(`http://localhost:${Client_Port}/Command_dialogue`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ ipt: ipt, initialize_Prompt: initialize_Prompt })
  })
  const jsonData = await response.json()
  update_historys(jsonData)
  await update_NPC_cards()
}

const public_talk = async (ipt: string) => {
  const connect = await Client_update_Settings()
  if (!connect) {
    console.log('连接LLM出错', '无法将历史记录传给LLM,请检查MCP_Client是否开启')
    return
  }
  const response = await fetch(`http://localhost:${Client_Port}/public_talk`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ ipt: ipt })
  })
  const jsonData = await response.json()
  update_historys(jsonData)
  await update_NPC_cards()
}

const private_talk = async (npc: string, ipt: string) => {
  const connect = await Client_update_Settings()
  if (!connect) {
    console.log('连接LLM出错', '无法将历史记录传给LLM,请检查MCP_Client是否开启')
    return
  }
  const response = await fetch(`http://localhost:${Client_Port}/private_talk`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ npc: npc, ipt: ipt })
  })
  const jsonData = await response.json()
  return jsonData.npc_result
}

const refresh_enemy = async () => {
  const connect = await Client_update_Settings()
  if (!connect) {
    console.log('连接LLM出错', '无法将历史记录传给LLM,请检查MCP_Client是否开启')
    return
  }
  await fetch(`http://localhost:${Client_Port}/refresh_enemy`)
  await update_NPC_cards()
}

const set_player_profile = async (ipt: string) => {
  await fetch(`http://localhost:${Client_Port}/set_player_profile`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ ipt: ipt })
  })
}

const get_current_page = () => {
  return current_page.value
}

const loadingInfo = ref<string[]>([])
const add_loadingInfo = (s: string) => {
  loadingInfo.value.push(s)
}

const clean_loadingInfo = () => {
  loadingInfo.value = []
}

const new_history = ref<string[]>([])

// 修改 displayedOldHistory 的类型
const displayedOldHistory = ref<(string | ContentSegment[])[]>([])

const add_new_history = (ipt: string) => {
  new_history.value.push(ipt)
}

const clear_new_history = () => {
  new_history.value.length = 0
}

// 判断是否为图片(base64格式)
const isImage = (text: string): boolean => {
  return text.startsWith('data:image/') && text.includes('base64')
}

const npcRespondPattern = /<NPC_respond>(.*?)<\/NPC_respond>/s

// 解析文本中的 NPC_respond 标签
const parseNPCTags = (text: string): ContentSegment[] => {
  if (isImage(text)) {
    return [{ type: 'image', content: text }]
  }

  if (npcRespondPattern.test(text)) {
    const match = npcRespondPattern.exec(text)
    if (match === null) return []
    return [
      {
        type: 'npc_respond',
        content: match[1] // 提取标签内的内容
      }
    ]
  }
  return [
    {
      type: 'text',
      content: text
    }
  ]
}

const characterData = ref<CharacterData>({
  name: '玩家',
  profile: '',
  hp: 30,
  level: 1,
  exp: 0,
  expToNextLevel: 1000,
  CON: 10,
  DEX: 10,
  INT: 10,
  skills: [],
  CombatAttribute: null,
  type: 'player',
  can_be_recruited: false,
  disabled: false
})

const scrollToBottom = () => {
  nextTick(() => {
    const historyContainer = document.querySelector('.history-container') as HTMLElement
    if (historyContainer) {
      // 直接设置滚动容器的滚动位置
      historyContainer.scrollTop = historyContainer.scrollHeight
    }
  })
}

const current_background_img = ref(`url(${main_world})`)
const currentBackground = async () => {
  const background_dict = new Map([
    ['奇点侦测站', main_world],
    ['齿轮', world1],
    ['源法', world2],
    ['混元', world3],
    ['黯蚀', world4],
    ['终焉', final_world]
  ])
  const store = useSharedStore()
  const response = await fetch(`http://${store.MCP_Server}:${store.port}/game/status`)
  const jsonData = await response.json()
  const current_world = jsonData.current_world
  const res = background_dict.get(current_world) ?? main_world
  current_background_img.value = `url(${res})`
}

当前面临的挑战

1、NPC的对话与主对话无法严丝合缝地契合。我想要在游戏中添加"自我值"的设定,让主对话变为玩家角色的感知滤镜,所以不能简单地将主对话的内容给到NPC

2、AI总喜欢"哄"玩家(比如玩家想要观察周围是否有伤员,那么AI就一定会给出一个"伤员")

3、AI对剧情长度不可控,且有时会生成一些较为"无趣"的片段

4、每次给到AI的信息量过多(世界观信息以及工具函数信息)且权重相等,导致某些信息被选择性忽视

这有可能是因为我对于提示词工程能力的缺陷。

我期望有更多的感兴趣的人能够玩到游戏,有技术的人能够给出宝贵建议。

相关推荐
得贤招聘官2 小时前
智能招聘革新:破解校招低效困局的核心方案
人工智能
青云交2 小时前
Java 大视界 -- Java 大数据机器学习模型在电商用户流失预测与留存策略制定中的应用
随机森林·机器学习·特征工程·java 大数据·spark mllib·电商用户流失·留存策略
大模型教程2 小时前
传统RAG的局限被打破!三个轻量级智能体分工协作,如何让问答系统更精准?
程序员·llm·agent
AI大模型2 小时前
大模型入门第一课:彻底搞懂Token!
程序员·llm·agent
乌恩大侠2 小时前
【Spark】操作记录
人工智能·spark·usrp
AI大模型2 小时前
大模型入门第二课:初识Embedding——让文字拥有"位置"的魔法
程序员·llm·agent
一水鉴天2 小时前
整体设计 全面梳理复盘 之27 九宫格框文法 Type 0~Ⅲ型文法和 bnf/abnf/ebnf 之1
人工智能·状态模式·公共逻辑
极客BIM工作室2 小时前
GAN vs. VAE:生成对抗网络 vs. 变分自编码机
人工智能·神经网络·生成对抗网络
咋吃都不胖lyh2 小时前
小白零基础教程:安装 Conda + VSCode 配置 Python 开发环境
人工智能·python·conda