项目概述
我开发了一个基于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的信息量过多(世界观信息以及工具函数信息)且权重相等,导致某些信息被选择性忽视
这有可能是因为我对于提示词工程能力的缺陷。
我期望有更多的感兴趣的人能够玩到游戏,有技术的人能够给出宝贵建议。