@nexusgame/dev 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -17
- package/dist/console/assets/room-CiSjgtT0.js +4 -0
- package/dist/console/assets/room-CxWGYLZL.css +1 -0
- package/dist/console/assets/styles-BDGeSlj5.js +40 -0
- package/dist/console/assets/styles-DhMceq41.css +1 -0
- package/dist/console/assets/workspace-B2jb_b0k.css +1 -0
- package/dist/console/assets/workspace-DS4AI-v8.js +1 -0
- package/dist/console/index.html +15 -0
- package/dist/console/workspace.html +15 -0
- package/dist/engine-worker.js +801 -781
- package/dist/index.js +3040 -206
- package/package.json +7 -3
package/dist/engine-worker.js
CHANGED
|
@@ -2670,6 +2670,15 @@ var sign2 = Jwt.sign;
|
|
|
2670
2670
|
|
|
2671
2671
|
// src/runtime/game-worker-client.ts
|
|
2672
2672
|
function resolveGameWorkerClient(gameId, baseUrl, env) {
|
|
2673
|
+
const normalizedBaseUrl = (baseUrl || "").replace(/\/$/, "");
|
|
2674
|
+
if (normalizedBaseUrl) {
|
|
2675
|
+
return {
|
|
2676
|
+
async fetch(path, init) {
|
|
2677
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
2678
|
+
return fetch(`${normalizedBaseUrl}${cleanPath}`, init);
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2673
2682
|
const bindingName = `GAME_${gameId.toUpperCase()}`;
|
|
2674
2683
|
const binding = env[bindingName];
|
|
2675
2684
|
if (binding && typeof binding.fetch === "function") {
|
|
@@ -2681,14 +2690,7 @@ function resolveGameWorkerClient(gameId, baseUrl, env) {
|
|
|
2681
2690
|
}
|
|
2682
2691
|
};
|
|
2683
2692
|
}
|
|
2684
|
-
|
|
2685
|
-
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
2686
|
-
return {
|
|
2687
|
-
async fetch(path, init) {
|
|
2688
|
-
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
2689
|
-
return fetch(`${normalizedBaseUrl}${cleanPath}`, init);
|
|
2690
|
-
}
|
|
2691
|
-
};
|
|
2693
|
+
throw new Error(`No game worker URL or service binding for game ${gameId}`);
|
|
2692
2694
|
}
|
|
2693
2695
|
|
|
2694
2696
|
// src/managers/base.ts
|
|
@@ -2698,6 +2700,18 @@ var BaseManager = class {
|
|
|
2698
2700
|
}
|
|
2699
2701
|
};
|
|
2700
2702
|
|
|
2703
|
+
// src/identity.ts
|
|
2704
|
+
function generateLlmUserId(modelName) {
|
|
2705
|
+
const uuid = crypto.randomUUID().slice(0, 8);
|
|
2706
|
+
return `llm:${modelName}:${uuid}`;
|
|
2707
|
+
}
|
|
2708
|
+
function getRoleForUser(roleMapping, userId) {
|
|
2709
|
+
for (const [roleId, uid] of Object.entries(roleMapping)) {
|
|
2710
|
+
if (uid === userId) return roleId;
|
|
2711
|
+
}
|
|
2712
|
+
return null;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2701
2715
|
// src/managers/presence.ts
|
|
2702
2716
|
var PresenceManager = class extends BaseManager {
|
|
2703
2717
|
async handleSession(ws, userId, displayName) {
|
|
@@ -2729,7 +2743,10 @@ var PresenceManager = class extends BaseManager {
|
|
|
2729
2743
|
this.room.connections.set(ws, userId);
|
|
2730
2744
|
this.room.sessions.set(userId, ws);
|
|
2731
2745
|
await this.room.persist("players");
|
|
2732
|
-
this.sendSyncState(ws, userId);
|
|
2746
|
+
await this.sendSyncState(ws, userId);
|
|
2747
|
+
if (isOwner) {
|
|
2748
|
+
this.room.sendPendingJoinRequests(ws);
|
|
2749
|
+
}
|
|
2733
2750
|
if (isAlreadyMember || isOwner) {
|
|
2734
2751
|
this.broadcastSyncStateExcept(userId);
|
|
2735
2752
|
}
|
|
@@ -2814,7 +2831,7 @@ var PresenceManager = class extends BaseManager {
|
|
|
2814
2831
|
ownerDisplayName: isAuthorized ? this.room.ownerDisplayName : "Room Owner",
|
|
2815
2832
|
name: isAuthorized ? this.room.name : "",
|
|
2816
2833
|
isPublic: isAuthorized ? this.room.isPublic : true,
|
|
2817
|
-
phase: isAuthorized ? this.room.phase :
|
|
2834
|
+
phase: isAuthorized ? this.room.phase : null,
|
|
2818
2835
|
players: clientPlayers,
|
|
2819
2836
|
gameConfig: isAuthorized && this.room.gameConfig ? {
|
|
2820
2837
|
gameId: this.room.gameConfig.gameId,
|
|
@@ -2856,8 +2873,6 @@ var PresenceManager = class extends BaseManager {
|
|
|
2856
2873
|
};
|
|
2857
2874
|
|
|
2858
2875
|
// src/managers/room.ts
|
|
2859
|
-
var WORKER_VERIFY_PATH = "/__nexus_worker_verify";
|
|
2860
|
-
var WORKER_VERIFY_SIGNATURE = "NEXUS_GAME_WORKER_VERIFIED_V1";
|
|
2861
2876
|
var RoomManager = class extends BaseManager {
|
|
2862
2877
|
async handleSelectRole(userId, roleId) {
|
|
2863
2878
|
if (this.room.phase !== "lobby") {
|
|
@@ -2887,6 +2902,7 @@ var RoomManager = class extends BaseManager {
|
|
|
2887
2902
|
}
|
|
2888
2903
|
}
|
|
2889
2904
|
delete this.room.players[userId];
|
|
2905
|
+
await this.room.clearJoinRequest(userId);
|
|
2890
2906
|
const ws = this.room.sessions.get(userId);
|
|
2891
2907
|
if (ws) {
|
|
2892
2908
|
try {
|
|
@@ -2899,116 +2915,20 @@ var RoomManager = class extends BaseManager {
|
|
|
2899
2915
|
await this.room.persist("players", "roleMapping");
|
|
2900
2916
|
this.room.broadcastSyncState();
|
|
2901
2917
|
}
|
|
2902
|
-
async
|
|
2903
|
-
if (!this.room.requireOwner(userId)) return;
|
|
2904
|
-
if (this.room.phase !== "lobby") {
|
|
2905
|
-
return this.room.sendErrorToUser(userId, "Can only set game in lobby phase");
|
|
2906
|
-
}
|
|
2907
|
-
try {
|
|
2908
|
-
const baseUrl = payload.gameWorkerUrl.replace(/\/$/, "");
|
|
2909
|
-
const client = resolveGameWorkerClient(payload.gameId, baseUrl, this.room.bindings);
|
|
2910
|
-
const verifyRes = await client.fetch(WORKER_VERIFY_PATH);
|
|
2911
|
-
if (!verifyRes.ok) {
|
|
2912
|
-
return this.room.sendErrorToUser(userId, "Game worker verification failed");
|
|
2913
|
-
}
|
|
2914
|
-
const signature = (await verifyRes.text()).trim();
|
|
2915
|
-
if (signature !== WORKER_VERIFY_SIGNATURE) {
|
|
2916
|
-
return this.room.sendErrorToUser(userId, "Game worker verification signature mismatch");
|
|
2917
|
-
}
|
|
2918
|
-
const metaRes = await client.fetch("/metadata");
|
|
2919
|
-
if (!metaRes.ok) {
|
|
2920
|
-
return this.room.sendErrorToUser(userId, "Failed to fetch game metadata");
|
|
2921
|
-
}
|
|
2922
|
-
const metadata = await metaRes.json();
|
|
2923
|
-
let roleIds;
|
|
2924
|
-
let resolvedPlayerCount;
|
|
2925
|
-
if (Array.isArray(metadata.roleIds)) {
|
|
2926
|
-
roleIds = metadata.roleIds;
|
|
2927
|
-
resolvedPlayerCount = roleIds.length;
|
|
2928
|
-
} else {
|
|
2929
|
-
const counts = Object.keys(metadata.roleIds).map(Number).sort((a, b) => a - b);
|
|
2930
|
-
const selectedCount = payload.selectedPlayerCount;
|
|
2931
|
-
const effectiveCount = selectedCount && metadata.roleIds[selectedCount] ? selectedCount : counts[0];
|
|
2932
|
-
roleIds = metadata.roleIds[effectiveCount] || [];
|
|
2933
|
-
resolvedPlayerCount = effectiveCount;
|
|
2934
|
-
}
|
|
2935
|
-
this.room.gameConfig = {
|
|
2936
|
-
gameWorkerUrl: baseUrl,
|
|
2937
|
-
gameId: payload.gameId || metadata.id,
|
|
2938
|
-
maxPlayers: roleIds.length,
|
|
2939
|
-
roleIds,
|
|
2940
|
-
selectedPlayerCount: resolvedPlayerCount,
|
|
2941
|
-
enable_llm_memory: metadata.enable_llm_memory,
|
|
2942
|
-
auto_save_mode: metadata.auto_save_mode
|
|
2943
|
-
};
|
|
2944
|
-
this.room.roleMapping = {};
|
|
2945
|
-
await this.room.persist("gameConfig", "roleMapping");
|
|
2946
|
-
this.room.broadcastSyncState();
|
|
2947
|
-
this.room.waitUntil(this.room.syncRoomMeta());
|
|
2948
|
-
} catch (e) {
|
|
2949
|
-
console.error("[RoomManager] Failed to set game:", e);
|
|
2950
|
-
this.room.sendErrorToUser(userId, "Failed to set game");
|
|
2951
|
-
}
|
|
2952
|
-
}
|
|
2953
|
-
async handleAdminRemovePlayer(userId, targetUserId) {
|
|
2954
|
-
if (!this.room.requireOwner(userId)) return;
|
|
2955
|
-
if (targetUserId === this.room.ownerId) {
|
|
2956
|
-
return this.room.sendErrorToUser(userId, "Cannot remove the room owner");
|
|
2957
|
-
}
|
|
2958
|
-
if (!this.room.players[targetUserId]) {
|
|
2959
|
-
return this.room.sendErrorToUser(userId, "Player not found");
|
|
2960
|
-
}
|
|
2961
|
-
for (const [rId, uId] of Object.entries(this.room.roleMapping)) {
|
|
2962
|
-
if (uId === targetUserId) {
|
|
2963
|
-
delete this.room.roleMapping[rId];
|
|
2964
|
-
}
|
|
2965
|
-
}
|
|
2966
|
-
delete this.room.players[targetUserId];
|
|
2967
|
-
const targetWs = this.room.sessions.get(targetUserId);
|
|
2968
|
-
if (targetWs) {
|
|
2969
|
-
this.room.sendMessage(targetWs, { type: "KICKED", payload: "Removed by room owner" });
|
|
2970
|
-
try {
|
|
2971
|
-
targetWs.close(1e3, "Kicked");
|
|
2972
|
-
} catch (_) {
|
|
2973
|
-
}
|
|
2974
|
-
this.room.connections.delete(targetWs);
|
|
2975
|
-
this.room.sessions.delete(targetUserId);
|
|
2976
|
-
}
|
|
2977
|
-
await this.room.persist("players", "roleMapping");
|
|
2978
|
-
this.room.broadcastSyncState();
|
|
2979
|
-
}
|
|
2980
|
-
async handleAdminAssignRole(userId, payload) {
|
|
2981
|
-
if (!this.room.requireOwner(userId)) return;
|
|
2982
|
-
if (this.room.phase !== "lobby") {
|
|
2983
|
-
return this.room.sendErrorToUser(userId, "Can only assign roles in lobby phase");
|
|
2984
|
-
}
|
|
2985
|
-
if (!this.room.players[payload.userId]) {
|
|
2986
|
-
return this.room.sendErrorToUser(userId, `Player ${payload.userId} not found`);
|
|
2987
|
-
}
|
|
2988
|
-
if (this.room.gameConfig && !this.room.gameConfig.roleIds.includes(payload.roleId)) {
|
|
2989
|
-
return this.room.sendErrorToUser(userId, `Invalid role: ${payload.roleId}`);
|
|
2990
|
-
}
|
|
2991
|
-
for (const [rId, uId] of Object.entries(this.room.roleMapping)) {
|
|
2992
|
-
if (uId === payload.userId) {
|
|
2993
|
-
delete this.room.roleMapping[rId];
|
|
2994
|
-
}
|
|
2995
|
-
}
|
|
2996
|
-
delete this.room.roleMapping[payload.roleId];
|
|
2997
|
-
this.room.roleMapping[payload.roleId] = payload.userId;
|
|
2998
|
-
await this.room.persist("roleMapping");
|
|
2999
|
-
this.room.broadcastSyncState();
|
|
3000
|
-
}
|
|
3001
|
-
async handleAdminApproveJoin(userId, payload) {
|
|
3002
|
-
if (!this.room.requireOwner(userId)) return;
|
|
2918
|
+
async handleAdminApproveJoin(_userId, payload) {
|
|
3003
2919
|
this.room.players[payload.userId] = {
|
|
3004
2920
|
displayName: payload.displayName,
|
|
3005
2921
|
connected: true,
|
|
3006
2922
|
isOwner: false,
|
|
3007
2923
|
type: "human"
|
|
3008
2924
|
};
|
|
2925
|
+
await this.room.clearJoinRequest(payload.userId);
|
|
3009
2926
|
await this.room.persist("players");
|
|
3010
2927
|
this.room.broadcastSyncState();
|
|
3011
2928
|
}
|
|
2929
|
+
async handleAdminIgnoreJoin(_userId, payload) {
|
|
2930
|
+
await this.room.clearJoinRequest(payload.userId);
|
|
2931
|
+
}
|
|
3012
2932
|
};
|
|
3013
2933
|
|
|
3014
2934
|
// src/monitor-store.ts
|
|
@@ -3206,6 +3126,78 @@ async function insertMonitorLog(db, input) {
|
|
|
3206
3126
|
return getMonitorLogById(db, input.interactionId);
|
|
3207
3127
|
}
|
|
3208
3128
|
|
|
3129
|
+
// src/managers/task-prompts.ts
|
|
3130
|
+
var TASK_PROMPT_NO_MEMORY = `
|
|
3131
|
+
# \u4EFB\u52A1\u8981\u6C42
|
|
3132
|
+
\u8BF7\u5206\u6790\u5F53\u524D\u5C40\u52BF\uFF0C\u9009\u62E9\u4E00\u4E2A\u6700\u4F18\u884C\u52A8\u3002\u4F60\u5FC5\u987B\u4E25\u683C\u6309\u7167\u4EE5\u4E0BJSON\u683C\u5F0F\u8FD4\u56DE\uFF1A
|
|
3133
|
+
|
|
3134
|
+
\`\`\`json
|
|
3135
|
+
{
|
|
3136
|
+
"reasoning": "\u4F60\u7684\u51B3\u7B56\u601D\u8DEF",
|
|
3137
|
+
"action_id": "\u884C\u52A8ID\u5B57\u7B26\u4E32\uFF08\u4ECE\u4E0A\u8FF0\u53EF\u7528\u884C\u52A8\u5217\u8868\u4E2D\u9009\u62E9\uFF0C\u4F8B\u5982 'place'\uFF09",
|
|
3138
|
+
"params": {
|
|
3139
|
+
"\u53C2\u6570\u540D": "\u53C2\u6570\u503C"
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
\`\`\`
|
|
3143
|
+
|
|
3144
|
+
**\u91CD\u8981\u63D0\u793A**\uFF1A
|
|
3145
|
+
1. action_id \u5FC5\u987B\u662F\u5B8C\u5168\u5339\u914D\u4E0A\u8FF0\u53EF\u7528\u884C\u52A8\u5217\u8868\u4E2D\u7684\u67D0\u4E2A action_id \u7684\u7EAF\u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u5305\u542B\u53C2\u6570\uFF08\u9519\u8BEF\u793A\u4F8B\uFF1A\u274C "place(row: 7, col: 6)"\uFF09
|
|
3146
|
+
2. \u5982\u679C\u884C\u52A8\u9700\u8981\u53C2\u6570\uFF0Cparams \u5B57\u6BB5\u5FC5\u987B\u5305\u542B\u6240\u6709\u5FC5\u9700\u7684\u53C2\u6570
|
|
3147
|
+
3. \u53C2\u6570\u7684\u7C7B\u578B\u548C\u503C\u5FC5\u987B\u7B26\u5408\u884C\u52A8\u5B9A\u4E49\u7684\u7EA6\u675F
|
|
3148
|
+
4. \u53EA\u8FD4\u56DEJSON\u5BF9\u8C61\uFF0C\u4E0D\u8981\u5305\u542B\u5176\u4ED6\u6587\u5B57\u8BF4\u660E`;
|
|
3149
|
+
var TASK_PROMPT_WITH_MEMORY = `
|
|
3150
|
+
# \u4EFB\u52A1\u8981\u6C42
|
|
3151
|
+
\u8BF7\u5206\u6790\u5F53\u524D\u5C40\u52BF\uFF0C\u901A\u8FC7 reasoning \u601D\u8003\u5E76\u9009\u62E9\u4E00\u4E2A\u6700\u4F18\u884C\u52A8\u3002\u5E76\u53EF\u9009\u66F4\u65B0\u8BB0\u5FC6\u3002\u4F60\u5FC5\u987B\u4E25\u683C\u6309\u7167\u4EE5\u4E0BJSON\u683C\u5F0F\u8FD4\u56DE\uFF1A
|
|
3152
|
+
|
|
3153
|
+
\`\`\`json
|
|
3154
|
+
{
|
|
3155
|
+
"reasoning": "\u4F60\u7684\u51B3\u7B56\u601D\u8DEF",
|
|
3156
|
+
"action_id": "\u884C\u52A8ID\u5B57\u7B26\u4E32\uFF08\u4ECE\u4E0A\u8FF0\u53EF\u7528\u884C\u52A8\u5217\u8868\u4E2D\u9009\u62E9\uFF0C\u4F8B\u5982 'place'\uFF09",
|
|
3157
|
+
"params": {
|
|
3158
|
+
"\u53C2\u6570\u540D": "\u53C2\u6570\u503C"
|
|
3159
|
+
},
|
|
3160
|
+
"memory_update": {
|
|
3161
|
+
"mode": "append/replace",
|
|
3162
|
+
"content": "\u672C\u56DE\u5408\u7684\u65B0\u89C2\u5BDF\u548C\u63A8\u7406\uFF08\u53EF\u9009\uFF09"
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
\`\`\`
|
|
3166
|
+
|
|
3167
|
+
|
|
3168
|
+
**\u8BB0\u5FC6\u66F4\u65B0\u8BF4\u660E**\uFF1A
|
|
3169
|
+
- \u4F60\u53EF\u4EE5\u5728 \`memory_update\` \u5B57\u6BB5\u4E2D\u66F4\u65B0\u4F60\u7684\u8BB0\u5FC6
|
|
3170
|
+
- \`mode: "append"\` - \u8FFD\u52A0\u5230\u73B0\u6709\u8BB0\u5FC6
|
|
3171
|
+
- \`mode: "replace"\` - \u66FF\u6362\u8BB0\u5FC6\uFF0C\u5982\u91CD\u65B0\u603B\u7ED3\u5F62\u6210\u65B0\u7684\u8BB0\u5FC6
|
|
3172
|
+
- \u5982\u679C\u672C\u56DE\u5408\u65E0\u9700\u66F4\u65B0\u8BB0\u5FC6\uFF0C\u53EF\u7701\u7565 \`memory_update\` \u5B57\u6BB5
|
|
3173
|
+
|
|
3174
|
+
|
|
3175
|
+
**\u91CD\u8981\u63D0\u793A**\uFF1A
|
|
3176
|
+
1. action_id \u5FC5\u987B\u662F\u5B8C\u5168\u5339\u914D\u4E0A\u8FF0\u53EF\u7528\u884C\u52A8\u5217\u8868\u4E2D\u7684\u67D0\u4E2A action_id \u7684\u7EAF\u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u5305\u542B\u53C2\u6570\uFF08\u9519\u8BEF\u793A\u4F8B\uFF1A\u274C "place(row: 7, col: 6)"\uFF09
|
|
3177
|
+
2. \u5982\u679C\u884C\u52A8\u9700\u8981\u53C2\u6570\uFF0Cparams \u5B57\u6BB5\u5FC5\u987B\u5305\u542B\u6240\u6709\u5FC5\u9700\u7684\u53C2\u6570
|
|
3178
|
+
3. \u53C2\u6570\u7684\u7C7B\u578B\u548C\u503C\u5FC5\u987B\u7B26\u5408\u884C\u52A8\u5B9A\u4E49\u7684\u7EA6\u675F
|
|
3179
|
+
4. \u53EA\u8FD4\u56DEJSON\u5BF9\u8C61\uFF0C\u4E0D\u8981\u5305\u542B\u5176\u4ED6\u6587\u5B57\u8BF4\u660E`;
|
|
3180
|
+
var NO_TASK_PROMPT_WITH_MEMORY = `
|
|
3181
|
+
# \u4EFB\u52A1\u8981\u6C42
|
|
3182
|
+
\u5F53\u524D\u65E0\u9700\u884C\u52A8\uFF0C\u4F46\u4F60\u53EF\u4EE5\u6839\u636E\u6700\u65B0\u4FE1\u606F\u66F4\u65B0\u4F60\u7684\u8BB0\u5FC6\u3002\u4F60\u5FC5\u987B\u4E25\u683C\u6309\u7167\u4EE5\u4E0BJSON\u683C\u5F0F\u8FD4\u56DE\uFF0C\u4E0D\u8981\u5305\u542B\u5176\u4ED6\u6587\u5B57\u8BF4\u660E\uFF1A
|
|
3183
|
+
|
|
3184
|
+
\`\`\`json
|
|
3185
|
+
{
|
|
3186
|
+
"reasoning": "\u4F60\u7684\u89C2\u5BDF\u548C\u601D\u8003\uFF08\u53EF\u9009\uFF09",
|
|
3187
|
+
"memory_update": {
|
|
3188
|
+
"mode": "append",
|
|
3189
|
+
"content": "\u672C\u56DE\u5408\u7684\u65B0\u89C2\u5BDF\u3001\u63A8\u7406\u548C\u91CD\u8981\u4FE1\u606F"
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
\`\`\`
|
|
3193
|
+
|
|
3194
|
+
|
|
3195
|
+
**\u8BB0\u5FC6\u66F4\u65B0\u8BF4\u660E**\uFF1A
|
|
3196
|
+
- \`mode: "append"\` - \u8FFD\u52A0\u5230\u73B0\u6709\u8BB0\u5FC6\uFF08\u63A8\u8350\uFF09
|
|
3197
|
+
- \`mode: "replace"\` - \u5B8C\u5168\u66FF\u6362\u8BB0\u5FC6\uFF0C\u9002\u5408\u91CD\u65B0\u603B\u7ED3\u65F6\u4F7F\u7528
|
|
3198
|
+
- \u5EFA\u8BAE\u8BB0\u5F55\uFF1A\u5176\u4ED6\u73A9\u5BB6\u7684\u884C\u52A8\u3001\u53D1\u8A00\u3001\u6295\u7968\u3001\u6B7B\u4EA1\u4FE1\u606F\u3001\u8EAB\u4EFD\u7EBF\u7D22\u7B49\u5173\u952E\u4FE1\u606F
|
|
3199
|
+
`;
|
|
3200
|
+
|
|
3209
3201
|
// src/managers/action-processor.ts
|
|
3210
3202
|
async function applySuccessfulAction(opts) {
|
|
3211
3203
|
const {
|
|
@@ -3273,357 +3265,74 @@ async function applySuccessfulAction(opts) {
|
|
|
3273
3265
|
})());
|
|
3274
3266
|
}
|
|
3275
3267
|
|
|
3276
|
-
// src/managers/
|
|
3277
|
-
var
|
|
3278
|
-
async
|
|
3279
|
-
if (!this.room.
|
|
3280
|
-
|
|
3281
|
-
return
|
|
3282
|
-
}
|
|
3283
|
-
if (!this.room.gameConfig) {
|
|
3284
|
-
return this.room.sendErrorToUser(userId, "No game selected");
|
|
3268
|
+
// src/managers/llm.ts
|
|
3269
|
+
var LlmManager = class extends BaseManager {
|
|
3270
|
+
async callLlmWebhook(roleId, retryCount = 0) {
|
|
3271
|
+
if (!this.room.llmWebhookUrl) {
|
|
3272
|
+
console.warn(`[LlmManager] Skipping LLM call: LLM_WEBHOOK_URL is not configured (roomId: ${this.room.roomId})`);
|
|
3273
|
+
return;
|
|
3285
3274
|
}
|
|
3286
|
-
const
|
|
3287
|
-
|
|
3288
|
-
)
|
|
3289
|
-
|
|
3290
|
-
return
|
|
3291
|
-
userId,
|
|
3292
|
-
`Missing role assignments: ${missingRoles.join(", ")}`
|
|
3293
|
-
);
|
|
3275
|
+
const userId = this.room.roleMapping[roleId];
|
|
3276
|
+
const player = this.room.players[userId];
|
|
3277
|
+
if (!player || player.type !== "llm" || !player.llmConfig) {
|
|
3278
|
+
console.warn(`[LlmManager] Skipping LLM call: role ${roleId} is not mapped to an LLM player`);
|
|
3279
|
+
return;
|
|
3294
3280
|
}
|
|
3281
|
+
console.log(`[LlmManager] Calling LLM webhook for role ${roleId} (attempt: ${retryCount + 1})`);
|
|
3282
|
+
const interactionGroupId = crypto.randomUUID();
|
|
3283
|
+
const interactionId = crypto.randomUUID();
|
|
3284
|
+
const startTs = Date.now();
|
|
3295
3285
|
try {
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3286
|
+
const perspective = await this.room.fetchPerspective(roleId);
|
|
3287
|
+
if (!perspective) {
|
|
3288
|
+
throw new Error("Failed to fetch perspective from game worker");
|
|
3289
|
+
}
|
|
3290
|
+
const statePrompt = await this.room.fetchStatePrompt(perspective);
|
|
3291
|
+
const hasActions = perspective.action_space_definition?.actions?.length > 0;
|
|
3292
|
+
const memoryEnabled = !!player.llmConfig.memory || player.llmConfig.memory === "";
|
|
3293
|
+
if (!hasActions) {
|
|
3294
|
+
if (!memoryEnabled) {
|
|
3295
|
+
console.log(`[LlmManager] Skipping turn for ${roleId}: No actions and memory disabled`);
|
|
3296
|
+
return;
|
|
3299
3297
|
}
|
|
3298
|
+
console.log(`[LlmManager] No actions available, but memory enabled. Calling LLM for memory-only update.`);
|
|
3300
3299
|
}
|
|
3301
|
-
const
|
|
3302
|
-
|
|
3303
|
-
this.room.
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3300
|
+
const userPrompt = this.buildUserPrompt(roleId, perspective, player.llmConfig, statePrompt || void 0);
|
|
3301
|
+
const reqBody = {
|
|
3302
|
+
roomId: this.room.roomId,
|
|
3303
|
+
roleId,
|
|
3304
|
+
gameId: this.room.gameConfig?.gameId || "",
|
|
3305
|
+
perspective,
|
|
3306
|
+
statePrompt: userPrompt,
|
|
3307
|
+
llmConfig: player.llmConfig,
|
|
3308
|
+
attempt: retryCount + 1,
|
|
3309
|
+
maxAttempts: 3
|
|
3310
|
+
};
|
|
3311
|
+
const response = await fetch(this.room.llmWebhookUrl, {
|
|
3307
3312
|
method: "POST",
|
|
3308
|
-
headers: {
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
}
|
|
3313
|
+
headers: {
|
|
3314
|
+
"Content-Type": "application/json",
|
|
3315
|
+
"x-engine-secret": this.room.bindings.LLM_WEBHOOK_SECRET || ""
|
|
3316
|
+
},
|
|
3317
|
+
body: JSON.stringify(reqBody)
|
|
3312
3318
|
});
|
|
3313
|
-
if (!
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3319
|
+
if (!response.ok) {
|
|
3320
|
+
const errorText = await response.text();
|
|
3321
|
+
await this.logLlmInteraction(
|
|
3322
|
+
interactionId,
|
|
3323
|
+
interactionGroupId,
|
|
3324
|
+
roleId,
|
|
3316
3325
|
userId,
|
|
3317
|
-
|
|
3326
|
+
player,
|
|
3327
|
+
userPrompt,
|
|
3328
|
+
null,
|
|
3329
|
+
null,
|
|
3330
|
+
"failed",
|
|
3331
|
+
errorText,
|
|
3332
|
+
Date.now() - startTs,
|
|
3333
|
+
retryCount + 1
|
|
3318
3334
|
);
|
|
3319
|
-
|
|
3320
|
-
this.room.gameState = await initRes.json();
|
|
3321
|
-
this.room.history = [];
|
|
3322
|
-
this.room.runtimeId = crypto.randomUUID();
|
|
3323
|
-
this.room.stateIndex = 0;
|
|
3324
|
-
this.room.stateHistory = [{
|
|
3325
|
-
index: 0,
|
|
3326
|
-
name: "Initial State",
|
|
3327
|
-
state: this.room.gameState,
|
|
3328
|
-
timestamp: Date.now()
|
|
3329
|
-
}];
|
|
3330
|
-
this.room.phase = "playing";
|
|
3331
|
-
await this.room.persistAll();
|
|
3332
|
-
this.room.broadcastSyncState();
|
|
3333
|
-
await this.room.checkAndTriggerNextTurn();
|
|
3334
|
-
} catch (e) {
|
|
3335
|
-
console.error("[GameExecutor] Failed to start game:", e);
|
|
3336
|
-
this.room.sendErrorToUser(userId, "Failed to start game");
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
async handleAdminStopGame(userId) {
|
|
3340
|
-
if (!this.room.requireOwner(userId)) return;
|
|
3341
|
-
if (this.room.phase === "lobby") {
|
|
3342
|
-
return this.room.sendErrorToUser(userId, "Game has not started");
|
|
3343
|
-
}
|
|
3344
|
-
this.room.phase = "lobby";
|
|
3345
|
-
this.room.gameState = null;
|
|
3346
|
-
this.room.history = [];
|
|
3347
|
-
this.room.stateHistory = [];
|
|
3348
|
-
await this.room.persist("phase", "gameState", "history", "stateHistory");
|
|
3349
|
-
this.room.broadcastSyncState();
|
|
3350
|
-
}
|
|
3351
|
-
async handleAdminRestartGame(userId) {
|
|
3352
|
-
if (!this.room.requireOwner(userId)) return;
|
|
3353
|
-
if (this.room.phase !== "playing" && this.room.phase !== "paused" && this.room.phase !== "finished") {
|
|
3354
|
-
return this.room.sendErrorToUser(userId, "Nothing to restart");
|
|
3355
|
-
}
|
|
3356
|
-
if (!this.room.gameConfig) {
|
|
3357
|
-
return this.room.sendErrorToUser(userId, "No game selected");
|
|
3358
|
-
}
|
|
3359
|
-
try {
|
|
3360
|
-
for (const player of Object.values(this.room.players)) {
|
|
3361
|
-
if (player.type === "llm" && player.llmConfig) {
|
|
3362
|
-
player.llmConfig.memory = "";
|
|
3363
|
-
}
|
|
3364
|
-
}
|
|
3365
|
-
const client = resolveGameWorkerClient(
|
|
3366
|
-
this.room.gameConfig.gameId,
|
|
3367
|
-
this.room.gameConfig.gameWorkerUrl,
|
|
3368
|
-
this.room.bindings
|
|
3369
|
-
);
|
|
3370
|
-
const initRes = await client.fetch("/init", {
|
|
3371
|
-
method: "POST",
|
|
3372
|
-
headers: { "Content-Type": "application/json" },
|
|
3373
|
-
body: JSON.stringify({
|
|
3374
|
-
players: Object.keys(this.room.roleMapping)
|
|
3375
|
-
})
|
|
3376
|
-
});
|
|
3377
|
-
if (!initRes.ok) {
|
|
3378
|
-
return this.room.sendErrorToUser(userId, "Game Worker init failed on restart");
|
|
3379
|
-
}
|
|
3380
|
-
this.room.gameState = await initRes.json();
|
|
3381
|
-
this.room.history = [];
|
|
3382
|
-
this.room.runtimeId = crypto.randomUUID();
|
|
3383
|
-
this.room.stateIndex = 0;
|
|
3384
|
-
this.room.stateHistory = [{
|
|
3385
|
-
index: 0,
|
|
3386
|
-
name: "Initial State",
|
|
3387
|
-
state: this.room.gameState,
|
|
3388
|
-
timestamp: Date.now()
|
|
3389
|
-
}];
|
|
3390
|
-
this.room.phase = "playing";
|
|
3391
|
-
await this.room.persistAll();
|
|
3392
|
-
this.room.broadcastSyncState();
|
|
3393
|
-
await this.room.checkAndTriggerNextTurn();
|
|
3394
|
-
} catch (e) {
|
|
3395
|
-
console.error("[GameExecutor] Failed to restart game:", e);
|
|
3396
|
-
this.room.sendErrorToUser(userId, "Failed to restart game");
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
async handleAction(userId, payload) {
|
|
3400
|
-
const startedAt = Date.now();
|
|
3401
|
-
if (this.room.phase !== "playing") {
|
|
3402
|
-
return this.room.sendErrorToUser(userId, "Game is not in playing phase");
|
|
3403
|
-
}
|
|
3404
|
-
if (!this.room.gameConfig || !this.room.gameState) {
|
|
3405
|
-
return this.room.sendErrorToUser(userId, "Game not initialized");
|
|
3406
|
-
}
|
|
3407
|
-
const roleId = getRoleForUser(this.room.roleMapping, userId);
|
|
3408
|
-
if (!roleId) {
|
|
3409
|
-
return this.room.sendErrorToUser(userId, "You don't have a role assigned");
|
|
3410
|
-
}
|
|
3411
|
-
const currentRoleId = await this.room.getCurrentRole();
|
|
3412
|
-
if (currentRoleId !== roleId) {
|
|
3413
|
-
return this.room.sendErrorToUser(userId, "Not your turn");
|
|
3414
|
-
}
|
|
3415
|
-
const result = await this.room.submitActionToGameWorker(roleId, {
|
|
3416
|
-
action_id: payload.action_id,
|
|
3417
|
-
params: payload.params || {}
|
|
3418
|
-
});
|
|
3419
|
-
const interactionId = crypto.randomUUID();
|
|
3420
|
-
const baseHumanLog = {
|
|
3421
|
-
interactionId,
|
|
3422
|
-
interactionGroupId: interactionId,
|
|
3423
|
-
roomId: this.room.roomId,
|
|
3424
|
-
gameId: this.room.gameConfig.gameId || null,
|
|
3425
|
-
gameName: null,
|
|
3426
|
-
roleId,
|
|
3427
|
-
userId,
|
|
3428
|
-
playerType: "human",
|
|
3429
|
-
modelName: null,
|
|
3430
|
-
systemPrompt: null,
|
|
3431
|
-
userPrompt: null,
|
|
3432
|
-
response: JSON.stringify({ action_id: payload.action_id, params: payload.params || {} }),
|
|
3433
|
-
action_id: payload.action_id,
|
|
3434
|
-
actionParams: payload.params || {},
|
|
3435
|
-
attempt: 1,
|
|
3436
|
-
outerAttempt: 1,
|
|
3437
|
-
maxAttempts: 1,
|
|
3438
|
-
previousError: null,
|
|
3439
|
-
responseTimeMs: Date.now() - startedAt,
|
|
3440
|
-
eventTs: Date.now()
|
|
3441
|
-
};
|
|
3442
|
-
if (!result.success) {
|
|
3443
|
-
this.room.waitUntil(insertMonitorLog(this.room.bindings.DB, {
|
|
3444
|
-
...baseHumanLog,
|
|
3445
|
-
status: "rejected",
|
|
3446
|
-
errorMessage: result.error || "Action rejected"
|
|
3447
|
-
}));
|
|
3448
|
-
return this.room.sendErrorToUser(userId, result.error || "Action rejected");
|
|
3449
|
-
}
|
|
3450
|
-
this.room.waitUntil(insertMonitorLog(this.room.bindings.DB, {
|
|
3451
|
-
...baseHumanLog,
|
|
3452
|
-
status: "success"
|
|
3453
|
-
}));
|
|
3454
|
-
await applySuccessfulAction({
|
|
3455
|
-
room: this.room,
|
|
3456
|
-
roleId,
|
|
3457
|
-
action: { action_id: payload.action_id, params: payload.params || {} },
|
|
3458
|
-
nextState: result.nextState,
|
|
3459
|
-
commands: result.commands,
|
|
3460
|
-
triggerMode: "await"
|
|
3461
|
-
});
|
|
3462
|
-
console.log(`[GameExecutor] Action processed for ${userId} (${roleId}), checking next turn...`);
|
|
3463
|
-
}
|
|
3464
|
-
};
|
|
3465
|
-
|
|
3466
|
-
// src/managers/task-prompts.ts
|
|
3467
|
-
var TASK_PROMPT_NO_MEMORY = `
|
|
3468
|
-
# \u4EFB\u52A1\u8981\u6C42
|
|
3469
|
-
\u8BF7\u5206\u6790\u5F53\u524D\u5C40\u52BF\uFF0C\u9009\u62E9\u4E00\u4E2A\u6700\u4F18\u884C\u52A8\u3002\u4F60\u5FC5\u987B\u4E25\u683C\u6309\u7167\u4EE5\u4E0BJSON\u683C\u5F0F\u8FD4\u56DE\uFF1A
|
|
3470
|
-
|
|
3471
|
-
\`\`\`json
|
|
3472
|
-
{
|
|
3473
|
-
"reasoning": "\u4F60\u7684\u51B3\u7B56\u601D\u8DEF",
|
|
3474
|
-
"action_id": "\u884C\u52A8ID\u5B57\u7B26\u4E32\uFF08\u4ECE\u4E0A\u8FF0\u53EF\u7528\u884C\u52A8\u5217\u8868\u4E2D\u9009\u62E9\uFF0C\u4F8B\u5982 'place'\uFF09",
|
|
3475
|
-
"params": {
|
|
3476
|
-
"\u53C2\u6570\u540D": "\u53C2\u6570\u503C"
|
|
3477
|
-
}
|
|
3478
|
-
}
|
|
3479
|
-
\`\`\`
|
|
3480
|
-
|
|
3481
|
-
**\u91CD\u8981\u63D0\u793A**\uFF1A
|
|
3482
|
-
1. action_id \u5FC5\u987B\u662F\u5B8C\u5168\u5339\u914D\u4E0A\u8FF0\u53EF\u7528\u884C\u52A8\u5217\u8868\u4E2D\u7684\u67D0\u4E2A action_id \u7684\u7EAF\u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u5305\u542B\u53C2\u6570\uFF08\u9519\u8BEF\u793A\u4F8B\uFF1A\u274C "place(row: 7, col: 6)"\uFF09
|
|
3483
|
-
2. \u5982\u679C\u884C\u52A8\u9700\u8981\u53C2\u6570\uFF0Cparams \u5B57\u6BB5\u5FC5\u987B\u5305\u542B\u6240\u6709\u5FC5\u9700\u7684\u53C2\u6570
|
|
3484
|
-
3. \u53C2\u6570\u7684\u7C7B\u578B\u548C\u503C\u5FC5\u987B\u7B26\u5408\u884C\u52A8\u5B9A\u4E49\u7684\u7EA6\u675F
|
|
3485
|
-
4. \u53EA\u8FD4\u56DEJSON\u5BF9\u8C61\uFF0C\u4E0D\u8981\u5305\u542B\u5176\u4ED6\u6587\u5B57\u8BF4\u660E`;
|
|
3486
|
-
var TASK_PROMPT_WITH_MEMORY = `
|
|
3487
|
-
# \u4EFB\u52A1\u8981\u6C42
|
|
3488
|
-
\u8BF7\u5206\u6790\u5F53\u524D\u5C40\u52BF\uFF0C\u901A\u8FC7 reasoning \u601D\u8003\u5E76\u9009\u62E9\u4E00\u4E2A\u6700\u4F18\u884C\u52A8\u3002\u5E76\u53EF\u9009\u66F4\u65B0\u8BB0\u5FC6\u3002\u4F60\u5FC5\u987B\u4E25\u683C\u6309\u7167\u4EE5\u4E0BJSON\u683C\u5F0F\u8FD4\u56DE\uFF1A
|
|
3489
|
-
|
|
3490
|
-
\`\`\`json
|
|
3491
|
-
{
|
|
3492
|
-
"reasoning": "\u4F60\u7684\u51B3\u7B56\u601D\u8DEF",
|
|
3493
|
-
"action_id": "\u884C\u52A8ID\u5B57\u7B26\u4E32\uFF08\u4ECE\u4E0A\u8FF0\u53EF\u7528\u884C\u52A8\u5217\u8868\u4E2D\u9009\u62E9\uFF0C\u4F8B\u5982 'place'\uFF09",
|
|
3494
|
-
"params": {
|
|
3495
|
-
"\u53C2\u6570\u540D": "\u53C2\u6570\u503C"
|
|
3496
|
-
},
|
|
3497
|
-
"memory_update": {
|
|
3498
|
-
"mode": "append/replace",
|
|
3499
|
-
"content": "\u672C\u56DE\u5408\u7684\u65B0\u89C2\u5BDF\u548C\u63A8\u7406\uFF08\u53EF\u9009\uFF09"
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
\`\`\`
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
**\u8BB0\u5FC6\u66F4\u65B0\u8BF4\u660E**\uFF1A
|
|
3506
|
-
- \u4F60\u53EF\u4EE5\u5728 \`memory_update\` \u5B57\u6BB5\u4E2D\u66F4\u65B0\u4F60\u7684\u8BB0\u5FC6
|
|
3507
|
-
- \`mode: "append"\` - \u8FFD\u52A0\u5230\u73B0\u6709\u8BB0\u5FC6
|
|
3508
|
-
- \`mode: "replace"\` - \u66FF\u6362\u8BB0\u5FC6\uFF0C\u5982\u91CD\u65B0\u603B\u7ED3\u5F62\u6210\u65B0\u7684\u8BB0\u5FC6
|
|
3509
|
-
- \u5982\u679C\u672C\u56DE\u5408\u65E0\u9700\u66F4\u65B0\u8BB0\u5FC6\uFF0C\u53EF\u7701\u7565 \`memory_update\` \u5B57\u6BB5
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
**\u91CD\u8981\u63D0\u793A**\uFF1A
|
|
3513
|
-
1. action_id \u5FC5\u987B\u662F\u5B8C\u5168\u5339\u914D\u4E0A\u8FF0\u53EF\u7528\u884C\u52A8\u5217\u8868\u4E2D\u7684\u67D0\u4E2A action_id \u7684\u7EAF\u5B57\u7B26\u4E32\uFF0C\u4E0D\u8981\u5305\u542B\u53C2\u6570\uFF08\u9519\u8BEF\u793A\u4F8B\uFF1A\u274C "place(row: 7, col: 6)"\uFF09
|
|
3514
|
-
2. \u5982\u679C\u884C\u52A8\u9700\u8981\u53C2\u6570\uFF0Cparams \u5B57\u6BB5\u5FC5\u987B\u5305\u542B\u6240\u6709\u5FC5\u9700\u7684\u53C2\u6570
|
|
3515
|
-
3. \u53C2\u6570\u7684\u7C7B\u578B\u548C\u503C\u5FC5\u987B\u7B26\u5408\u884C\u52A8\u5B9A\u4E49\u7684\u7EA6\u675F
|
|
3516
|
-
4. \u53EA\u8FD4\u56DEJSON\u5BF9\u8C61\uFF0C\u4E0D\u8981\u5305\u542B\u5176\u4ED6\u6587\u5B57\u8BF4\u660E`;
|
|
3517
|
-
var NO_TASK_PROMPT_WITH_MEMORY = `
|
|
3518
|
-
# \u4EFB\u52A1\u8981\u6C42
|
|
3519
|
-
\u5F53\u524D\u65E0\u9700\u884C\u52A8\uFF0C\u4F46\u4F60\u53EF\u4EE5\u6839\u636E\u6700\u65B0\u4FE1\u606F\u66F4\u65B0\u4F60\u7684\u8BB0\u5FC6\u3002\u4F60\u5FC5\u987B\u4E25\u683C\u6309\u7167\u4EE5\u4E0BJSON\u683C\u5F0F\u8FD4\u56DE\uFF0C\u4E0D\u8981\u5305\u542B\u5176\u4ED6\u6587\u5B57\u8BF4\u660E\uFF1A
|
|
3520
|
-
|
|
3521
|
-
\`\`\`json
|
|
3522
|
-
{
|
|
3523
|
-
"reasoning": "\u4F60\u7684\u89C2\u5BDF\u548C\u601D\u8003\uFF08\u53EF\u9009\uFF09",
|
|
3524
|
-
"memory_update": {
|
|
3525
|
-
"mode": "append",
|
|
3526
|
-
"content": "\u672C\u56DE\u5408\u7684\u65B0\u89C2\u5BDF\u3001\u63A8\u7406\u548C\u91CD\u8981\u4FE1\u606F"
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3529
|
-
\`\`\`
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
**\u8BB0\u5FC6\u66F4\u65B0\u8BF4\u660E**\uFF1A
|
|
3533
|
-
- \`mode: "append"\` - \u8FFD\u52A0\u5230\u73B0\u6709\u8BB0\u5FC6\uFF08\u63A8\u8350\uFF09
|
|
3534
|
-
- \`mode: "replace"\` - \u5B8C\u5168\u66FF\u6362\u8BB0\u5FC6\uFF0C\u9002\u5408\u91CD\u65B0\u603B\u7ED3\u65F6\u4F7F\u7528
|
|
3535
|
-
- \u5EFA\u8BAE\u8BB0\u5F55\uFF1A\u5176\u4ED6\u73A9\u5BB6\u7684\u884C\u52A8\u3001\u53D1\u8A00\u3001\u6295\u7968\u3001\u6B7B\u4EA1\u4FE1\u606F\u3001\u8EAB\u4EFD\u7EBF\u7D22\u7B49\u5173\u952E\u4FE1\u606F
|
|
3536
|
-
`;
|
|
3537
|
-
|
|
3538
|
-
// src/managers/llm.ts
|
|
3539
|
-
var LlmManager = class extends BaseManager {
|
|
3540
|
-
async handleAdminAddBot(userId, payload) {
|
|
3541
|
-
if (!this.room.requireOwner(userId)) return;
|
|
3542
|
-
if (this.room.phase !== "lobby") {
|
|
3543
|
-
return this.room.sendErrorToUser(userId, "Can only add bots in lobby phase");
|
|
3544
|
-
}
|
|
3545
|
-
const llmUserId = generateLlmUserId(payload.modelName);
|
|
3546
|
-
this.room.players[llmUserId] = {
|
|
3547
|
-
displayName: payload.displayName,
|
|
3548
|
-
connected: false,
|
|
3549
|
-
isOwner: false,
|
|
3550
|
-
type: "llm",
|
|
3551
|
-
llmConfig: {
|
|
3552
|
-
modelName: payload.modelName,
|
|
3553
|
-
systemPrompt: payload.systemPrompt || "\u4F60\u662F\u4E00\u4E2A\u806A\u660E\u7684\u6E38\u620F\u73A9\u5BB6",
|
|
3554
|
-
temperature: payload.temperature ?? 0.7,
|
|
3555
|
-
memory: ""
|
|
3556
|
-
}
|
|
3557
|
-
};
|
|
3558
|
-
await this.room.persist("players");
|
|
3559
|
-
this.room.broadcastSyncState();
|
|
3560
|
-
}
|
|
3561
|
-
async callLlmWebhook(roleId, retryCount = 0) {
|
|
3562
|
-
if (!this.room.llmWebhookUrl) {
|
|
3563
|
-
console.warn(`[LlmManager] Skipping LLM call: LLM_WEBHOOK_URL is not configured (roomId: ${this.room.roomId})`);
|
|
3564
|
-
return;
|
|
3565
|
-
}
|
|
3566
|
-
const userId = this.room.roleMapping[roleId];
|
|
3567
|
-
const player = this.room.players[userId];
|
|
3568
|
-
if (!player || player.type !== "llm" || !player.llmConfig) {
|
|
3569
|
-
console.warn(`[LlmManager] Skipping LLM call: role ${roleId} is not mapped to an LLM player`);
|
|
3570
|
-
return;
|
|
3571
|
-
}
|
|
3572
|
-
console.log(`[LlmManager] Calling LLM webhook for role ${roleId} (attempt: ${retryCount + 1})`);
|
|
3573
|
-
const interactionGroupId = crypto.randomUUID();
|
|
3574
|
-
const interactionId = crypto.randomUUID();
|
|
3575
|
-
const startTs = Date.now();
|
|
3576
|
-
try {
|
|
3577
|
-
const perspective = await this.room.fetchPerspective(roleId);
|
|
3578
|
-
if (!perspective) {
|
|
3579
|
-
throw new Error("Failed to fetch perspective from game worker");
|
|
3580
|
-
}
|
|
3581
|
-
const statePrompt = await this.room.fetchStatePrompt(perspective);
|
|
3582
|
-
const hasActions = perspective.action_space_definition?.actions?.length > 0;
|
|
3583
|
-
const memoryEnabled = !!player.llmConfig.memory || player.llmConfig.memory === "";
|
|
3584
|
-
if (!hasActions) {
|
|
3585
|
-
if (!memoryEnabled) {
|
|
3586
|
-
console.log(`[LlmManager] Skipping turn for ${roleId}: No actions and memory disabled`);
|
|
3587
|
-
return;
|
|
3588
|
-
}
|
|
3589
|
-
console.log(`[LlmManager] No actions available, but memory enabled. Calling LLM for memory-only update.`);
|
|
3590
|
-
}
|
|
3591
|
-
const userPrompt = this.buildUserPrompt(roleId, perspective, player.llmConfig, statePrompt || void 0);
|
|
3592
|
-
const reqBody = {
|
|
3593
|
-
roomId: this.room.roomId,
|
|
3594
|
-
roleId,
|
|
3595
|
-
gameId: this.room.gameConfig?.gameId || "",
|
|
3596
|
-
perspective,
|
|
3597
|
-
statePrompt: userPrompt,
|
|
3598
|
-
llmConfig: player.llmConfig,
|
|
3599
|
-
attempt: retryCount + 1,
|
|
3600
|
-
maxAttempts: 3
|
|
3601
|
-
};
|
|
3602
|
-
const response = await fetch(this.room.llmWebhookUrl, {
|
|
3603
|
-
method: "POST",
|
|
3604
|
-
headers: {
|
|
3605
|
-
"Content-Type": "application/json",
|
|
3606
|
-
"x-engine-secret": this.room.bindings.LLM_WEBHOOK_SECRET || ""
|
|
3607
|
-
},
|
|
3608
|
-
body: JSON.stringify(reqBody)
|
|
3609
|
-
});
|
|
3610
|
-
if (!response.ok) {
|
|
3611
|
-
const errorText = await response.text();
|
|
3612
|
-
await this.logLlmInteraction(
|
|
3613
|
-
interactionId,
|
|
3614
|
-
interactionGroupId,
|
|
3615
|
-
roleId,
|
|
3616
|
-
userId,
|
|
3617
|
-
player,
|
|
3618
|
-
userPrompt,
|
|
3619
|
-
null,
|
|
3620
|
-
null,
|
|
3621
|
-
"failed",
|
|
3622
|
-
errorText,
|
|
3623
|
-
Date.now() - startTs,
|
|
3624
|
-
retryCount + 1
|
|
3625
|
-
);
|
|
3626
|
-
throw new Error(`Webhook failed: ${response.statusText} - ${errorText}`);
|
|
3335
|
+
throw new Error(`Webhook failed: ${response.statusText} - ${errorText}`);
|
|
3627
3336
|
}
|
|
3628
3337
|
const resultJson = await response.json();
|
|
3629
3338
|
const rawContent = resultJson.content;
|
|
@@ -3767,11 +3476,7 @@ var LlmManager = class extends BaseManager {
|
|
|
3767
3476
|
if (workerStatePrompt) {
|
|
3768
3477
|
statePrompt = workerStatePrompt;
|
|
3769
3478
|
} else {
|
|
3770
|
-
statePrompt =
|
|
3771
|
-
${perspective.message}
|
|
3772
|
-
|
|
3773
|
-
` : "";
|
|
3774
|
-
statePrompt += `# \u5F53\u524D\u6E38\u620F\u72B6\u6001
|
|
3479
|
+
statePrompt = `# \u5F53\u524D\u6E38\u620F\u72B6\u6001
|
|
3775
3480
|
${JSON.stringify(perspective.current_state, null, 2)}`;
|
|
3776
3481
|
}
|
|
3777
3482
|
const memoryEnabled = llmConfig.memory !== void 0 && llmConfig.memory !== null && this.room.gameConfig?.enable_llm_memory !== false;
|
|
@@ -3882,66 +3587,452 @@ ${NO_TASK_PROMPT_WITH_MEMORY}`;
|
|
|
3882
3587
|
if (schema.enum && !schema.enum.includes(value)) return { valid: false, error: `\u53C2\u6570 "${key}" \u7684\u503C\u65E0\u6548\u3002\u53EF\u9009\uFF1A${schema.enum.join(", ")}` };
|
|
3883
3588
|
}
|
|
3884
3589
|
}
|
|
3885
|
-
}
|
|
3886
|
-
return { valid: true };
|
|
3590
|
+
}
|
|
3591
|
+
return { valid: true };
|
|
3592
|
+
}
|
|
3593
|
+
extractJsonFromText(raw2) {
|
|
3594
|
+
const trimmed = raw2.trim();
|
|
3595
|
+
const match2 = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
3596
|
+
if (match2?.[1]) {
|
|
3597
|
+
return match2[1].trim();
|
|
3598
|
+
}
|
|
3599
|
+
const firstBrace = trimmed.indexOf("{");
|
|
3600
|
+
const lastBrace = trimmed.lastIndexOf("}");
|
|
3601
|
+
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
3602
|
+
return trimmed.slice(firstBrace, lastBrace + 1);
|
|
3603
|
+
}
|
|
3604
|
+
return trimmed;
|
|
3605
|
+
}
|
|
3606
|
+
isRecord(value) {
|
|
3607
|
+
return typeof value === "object" && value !== null;
|
|
3608
|
+
}
|
|
3609
|
+
async logLlmInteraction(id, groupId, roleId, userId, player, userPrompt, rawResponse, res, status, error, duration, attempt) {
|
|
3610
|
+
try {
|
|
3611
|
+
await insertMonitorLog(this.room.bindings.DB, {
|
|
3612
|
+
interactionId: id,
|
|
3613
|
+
interactionGroupId: groupId,
|
|
3614
|
+
roomId: this.room.roomId,
|
|
3615
|
+
gameId: this.room.gameConfig?.gameId,
|
|
3616
|
+
roleId,
|
|
3617
|
+
userId,
|
|
3618
|
+
playerType: "llm",
|
|
3619
|
+
modelName: player.llmConfig?.modelName,
|
|
3620
|
+
systemPrompt: player.llmConfig?.systemPrompt,
|
|
3621
|
+
userPrompt,
|
|
3622
|
+
response: rawResponse,
|
|
3623
|
+
action_id: res?.action?.action_id,
|
|
3624
|
+
actionParams: res?.action?.params,
|
|
3625
|
+
status,
|
|
3626
|
+
attempt,
|
|
3627
|
+
outerAttempt: 1,
|
|
3628
|
+
maxAttempts: 3,
|
|
3629
|
+
errorMessage: error,
|
|
3630
|
+
responseTimeMs: duration,
|
|
3631
|
+
eventTs: Date.now()
|
|
3632
|
+
});
|
|
3633
|
+
} catch (e) {
|
|
3634
|
+
console.error("[LlmManager] Failed to record monitor log:", e);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
};
|
|
3638
|
+
|
|
3639
|
+
// src/commands/room-commands.ts
|
|
3640
|
+
var WORKER_VERIFY_PATH = "/__nexus_worker_verify";
|
|
3641
|
+
var WORKER_VERIFY_SIGNATURE = "NEXUS_GAME_WORKER_VERIFIED_V1";
|
|
3642
|
+
function fail(error, status = 400) {
|
|
3643
|
+
return { ok: false, error, status };
|
|
3644
|
+
}
|
|
3645
|
+
var RoomCommandService = class {
|
|
3646
|
+
constructor(room) {
|
|
3647
|
+
this.room = room;
|
|
3648
|
+
}
|
|
3649
|
+
// ─── Game configuration ────────────────────────────────
|
|
3650
|
+
async setGame(input) {
|
|
3651
|
+
if (!input.gameWorkerUrl) return fail("gameWorkerUrl is required");
|
|
3652
|
+
if (this.room.phase !== "lobby") {
|
|
3653
|
+
return fail("Can only set game in lobby phase");
|
|
3654
|
+
}
|
|
3655
|
+
try {
|
|
3656
|
+
const baseUrl = input.gameWorkerUrl.replace(/\/$/, "");
|
|
3657
|
+
const client = resolveGameWorkerClient(
|
|
3658
|
+
input.gameId || "",
|
|
3659
|
+
baseUrl,
|
|
3660
|
+
this.room.bindings
|
|
3661
|
+
);
|
|
3662
|
+
const verifyRes = await client.fetch(WORKER_VERIFY_PATH);
|
|
3663
|
+
if (!verifyRes.ok) {
|
|
3664
|
+
return fail("Game worker verification failed", 502);
|
|
3665
|
+
}
|
|
3666
|
+
const signature = (await verifyRes.text()).trim();
|
|
3667
|
+
if (signature !== WORKER_VERIFY_SIGNATURE) {
|
|
3668
|
+
return fail("Game worker verification signature mismatch", 502);
|
|
3669
|
+
}
|
|
3670
|
+
const metaRes = await client.fetch("/metadata");
|
|
3671
|
+
if (!metaRes.ok) {
|
|
3672
|
+
const errText = await metaRes.text();
|
|
3673
|
+
console.error("[RoomCommands] Metadata fetch failed:", metaRes.status, errText);
|
|
3674
|
+
return fail("Failed to fetch game metadata", 502);
|
|
3675
|
+
}
|
|
3676
|
+
const metadata = await metaRes.json();
|
|
3677
|
+
const resolved = resolveRoleIds(metadata, input.selectedPlayerCount);
|
|
3678
|
+
if (!resolved) {
|
|
3679
|
+
return fail("Game metadata has no roleIds", 502);
|
|
3680
|
+
}
|
|
3681
|
+
this.room.gameConfig = {
|
|
3682
|
+
gameWorkerUrl: baseUrl,
|
|
3683
|
+
gameId: input.gameId || metadata.id,
|
|
3684
|
+
maxPlayers: resolved.roleIds.length,
|
|
3685
|
+
roleIds: resolved.roleIds,
|
|
3686
|
+
selectedPlayerCount: resolved.selectedPlayerCount,
|
|
3687
|
+
enable_llm_memory: metadata.enable_llm_memory,
|
|
3688
|
+
auto_save_mode: metadata.auto_save_mode
|
|
3689
|
+
};
|
|
3690
|
+
this.room.roleMapping = {};
|
|
3691
|
+
await this.room.persist("gameConfig", "roleMapping");
|
|
3692
|
+
this.room.broadcastSyncState();
|
|
3693
|
+
this.room.waitUntil(this.room.syncRoomMeta());
|
|
3694
|
+
return {
|
|
3695
|
+
ok: true,
|
|
3696
|
+
data: { gameId: this.room.gameConfig.gameId, roleIds: resolved.roleIds }
|
|
3697
|
+
};
|
|
3698
|
+
} catch (e) {
|
|
3699
|
+
console.error("[RoomCommands] setGame failed:", e);
|
|
3700
|
+
return fail(e.message || "Failed to set game", 500);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
// ─── Lifecycle ─────────────────────────────────────────
|
|
3704
|
+
async startGame() {
|
|
3705
|
+
if (this.room.phase !== "lobby" && this.room.phase !== "finished") {
|
|
3706
|
+
return fail("Game cannot be started in current phase");
|
|
3707
|
+
}
|
|
3708
|
+
const config = this.room.gameConfig;
|
|
3709
|
+
if (!config) return fail("No game selected");
|
|
3710
|
+
const missingRoles = config.roleIds.filter((roleId) => !this.room.roleMapping[roleId]);
|
|
3711
|
+
if (missingRoles.length > 0) {
|
|
3712
|
+
return fail(`Missing role assignments: ${missingRoles.join(", ")}`);
|
|
3713
|
+
}
|
|
3714
|
+
return this.initializeGameState("Game Worker init failed");
|
|
3715
|
+
}
|
|
3716
|
+
async restartGame() {
|
|
3717
|
+
if (this.room.phase !== "playing" && this.room.phase !== "paused" && this.room.phase !== "finished") {
|
|
3718
|
+
return fail("Nothing to restart");
|
|
3719
|
+
}
|
|
3720
|
+
if (!this.room.gameConfig) return fail("No game selected");
|
|
3721
|
+
return this.initializeGameState("Game Worker init failed on restart");
|
|
3722
|
+
}
|
|
3723
|
+
async stopGame() {
|
|
3724
|
+
if (this.room.phase === "lobby") {
|
|
3725
|
+
return fail("Game has not started");
|
|
3726
|
+
}
|
|
3727
|
+
this.room.phase = "lobby";
|
|
3728
|
+
this.room.gameState = null;
|
|
3729
|
+
this.room.history = [];
|
|
3730
|
+
this.room.stateHistory = [];
|
|
3731
|
+
await this.room.persist("phase", "gameState", "history", "stateHistory");
|
|
3732
|
+
this.room.broadcastSyncState();
|
|
3733
|
+
this.room.waitUntil(this.room.syncRoomMeta());
|
|
3734
|
+
return { ok: true, data: { phase: this.room.phase } };
|
|
3735
|
+
}
|
|
3736
|
+
async pauseGame() {
|
|
3737
|
+
if (this.room.phase !== "playing") {
|
|
3738
|
+
return fail("Can only pause a playing game");
|
|
3739
|
+
}
|
|
3740
|
+
this.room.phase = "paused";
|
|
3741
|
+
await this.room.persist("phase");
|
|
3742
|
+
this.room.broadcastSyncState();
|
|
3743
|
+
this.room.waitUntil(this.room.syncRoomMeta());
|
|
3744
|
+
return { ok: true, data: { phase: this.room.phase } };
|
|
3745
|
+
}
|
|
3746
|
+
async resumeGame() {
|
|
3747
|
+
if (this.room.phase !== "paused") {
|
|
3748
|
+
return fail("Game is not paused");
|
|
3749
|
+
}
|
|
3750
|
+
this.room.phase = "playing";
|
|
3751
|
+
await this.room.persist("phase");
|
|
3752
|
+
this.room.broadcastSyncState();
|
|
3753
|
+
this.room.waitUntil(this.room.syncRoomMeta());
|
|
3754
|
+
this.room.waitUntil(this.room.checkAndTriggerNextTurn());
|
|
3755
|
+
return { ok: true, data: { phase: this.room.phase } };
|
|
3756
|
+
}
|
|
3757
|
+
async backtrack(index) {
|
|
3758
|
+
if (this.room.phase !== "paused") {
|
|
3759
|
+
return fail("Game must be paused to backtrack");
|
|
3760
|
+
}
|
|
3761
|
+
if (!Number.isInteger(index)) {
|
|
3762
|
+
return fail("Invalid state index");
|
|
3763
|
+
}
|
|
3764
|
+
const targetState = this.room.stateHistory.find((entry) => entry.index === index);
|
|
3765
|
+
if (!targetState) {
|
|
3766
|
+
return fail("Invalid state index");
|
|
3767
|
+
}
|
|
3768
|
+
this.room.gameState = targetState.state;
|
|
3769
|
+
this.room.stateIndex = targetState.index;
|
|
3770
|
+
this.room.history = this.room.history.filter((event) => event.stateIndex <= targetState.index);
|
|
3771
|
+
await this.room.persist("gameState", "history", "stateIndex");
|
|
3772
|
+
this.room.broadcastSyncState();
|
|
3773
|
+
return { ok: true };
|
|
3774
|
+
}
|
|
3775
|
+
// ─── Room metadata ─────────────────────────────────────
|
|
3776
|
+
async updateRoomMeta(input) {
|
|
3777
|
+
const name = input.name?.trim();
|
|
3778
|
+
if (!name) return fail("Room name is required");
|
|
3779
|
+
this.room.name = name;
|
|
3780
|
+
this.room.isPublic = input.isPublic;
|
|
3781
|
+
await this.room.persist("name", "isPublic");
|
|
3782
|
+
this.room.broadcastSyncState();
|
|
3783
|
+
this.room.waitUntil(this.room.syncRoomMeta());
|
|
3784
|
+
return { ok: true };
|
|
3785
|
+
}
|
|
3786
|
+
// ─── Players & roles ───────────────────────────────────
|
|
3787
|
+
/**
|
|
3788
|
+
* Create a human player without a WebSocket (used by the local console and
|
|
3789
|
+
* CLI to represent `dev:<roleId>`). Idempotent so repeated setups work.
|
|
3790
|
+
*/
|
|
3791
|
+
async createPlayer(input) {
|
|
3792
|
+
if (this.room.phase !== "lobby") {
|
|
3793
|
+
return fail("Can only manage players in lobby phase");
|
|
3794
|
+
}
|
|
3795
|
+
const userId = input.userId || (input.roleId ? `dev:${input.roleId}` : "");
|
|
3796
|
+
if (!userId) return fail("userId or roleId is required");
|
|
3797
|
+
if (this.room.players[userId]) {
|
|
3798
|
+
return { ok: true, data: { userId, created: false } };
|
|
3799
|
+
}
|
|
3800
|
+
this.room.players[userId] = {
|
|
3801
|
+
displayName: input.displayName || input.roleId || userId,
|
|
3802
|
+
connected: false,
|
|
3803
|
+
isOwner: false,
|
|
3804
|
+
type: "human"
|
|
3805
|
+
};
|
|
3806
|
+
await this.room.persist("players");
|
|
3807
|
+
this.room.broadcastSyncState();
|
|
3808
|
+
return { ok: true, data: { userId, created: true } };
|
|
3809
|
+
}
|
|
3810
|
+
async removePlayer(targetUserId) {
|
|
3811
|
+
if (this.room.phase !== "lobby") {
|
|
3812
|
+
return fail("Can only remove players in lobby phase");
|
|
3813
|
+
}
|
|
3814
|
+
if (targetUserId === this.room.ownerId) {
|
|
3815
|
+
return fail("Cannot remove the room owner");
|
|
3816
|
+
}
|
|
3817
|
+
if (!this.room.players[targetUserId]) {
|
|
3818
|
+
return fail("Player not found");
|
|
3819
|
+
}
|
|
3820
|
+
for (const [roleId, userId] of Object.entries(this.room.roleMapping)) {
|
|
3821
|
+
if (userId === targetUserId) {
|
|
3822
|
+
delete this.room.roleMapping[roleId];
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
delete this.room.players[targetUserId];
|
|
3826
|
+
const targetWs = this.room.sessions.get(targetUserId);
|
|
3827
|
+
if (targetWs) {
|
|
3828
|
+
this.room.sendMessage(targetWs, { type: "KICKED", payload: "Removed by room owner" });
|
|
3829
|
+
try {
|
|
3830
|
+
targetWs.close(1e3, "Kicked");
|
|
3831
|
+
} catch (_) {
|
|
3832
|
+
}
|
|
3833
|
+
this.room.connections.delete(targetWs);
|
|
3834
|
+
this.room.sessions.delete(targetUserId);
|
|
3835
|
+
}
|
|
3836
|
+
await this.room.persist("players", "roleMapping");
|
|
3837
|
+
this.room.broadcastSyncState();
|
|
3838
|
+
return { ok: true };
|
|
3839
|
+
}
|
|
3840
|
+
async assignRole(input) {
|
|
3841
|
+
if (this.room.phase !== "lobby") {
|
|
3842
|
+
return fail("Can only assign roles in lobby phase");
|
|
3843
|
+
}
|
|
3844
|
+
if (!this.room.players[input.userId]) {
|
|
3845
|
+
return fail(`Player ${input.userId} not found`);
|
|
3846
|
+
}
|
|
3847
|
+
if (this.room.gameConfig && !this.room.gameConfig.roleIds.includes(input.roleId)) {
|
|
3848
|
+
return fail(`Invalid role: ${input.roleId}`);
|
|
3849
|
+
}
|
|
3850
|
+
for (const [roleId, userId] of Object.entries(this.room.roleMapping)) {
|
|
3851
|
+
if (userId === input.userId) {
|
|
3852
|
+
delete this.room.roleMapping[roleId];
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
delete this.room.roleMapping[input.roleId];
|
|
3856
|
+
this.room.roleMapping[input.roleId] = input.userId;
|
|
3857
|
+
await this.room.persist("roleMapping");
|
|
3858
|
+
this.room.broadcastSyncState();
|
|
3859
|
+
return { ok: true };
|
|
3860
|
+
}
|
|
3861
|
+
async addBot(input) {
|
|
3862
|
+
if (this.room.phase !== "lobby") {
|
|
3863
|
+
return fail("Can only add bots in lobby phase");
|
|
3864
|
+
}
|
|
3865
|
+
if (!input.displayName || !input.modelName) {
|
|
3866
|
+
return fail("displayName and modelName are required");
|
|
3867
|
+
}
|
|
3868
|
+
const llmUserId = generateLlmUserId(input.modelName);
|
|
3869
|
+
this.room.players[llmUserId] = {
|
|
3870
|
+
displayName: input.displayName,
|
|
3871
|
+
connected: false,
|
|
3872
|
+
isOwner: false,
|
|
3873
|
+
type: "llm",
|
|
3874
|
+
llmConfig: {
|
|
3875
|
+
modelName: input.modelName,
|
|
3876
|
+
systemPrompt: input.systemPrompt || "\u4F60\u662F\u4E00\u4E2A\u806A\u660E\u7684\u6E38\u620F\u73A9\u5BB6",
|
|
3877
|
+
temperature: input.temperature ?? 0.7,
|
|
3878
|
+
memory: ""
|
|
3879
|
+
}
|
|
3880
|
+
};
|
|
3881
|
+
await this.room.persist("players");
|
|
3882
|
+
this.room.broadcastSyncState();
|
|
3883
|
+
return { ok: true, data: { userId: llmUserId } };
|
|
3887
3884
|
}
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
return match2[1].trim();
|
|
3885
|
+
// ─── Actions ───────────────────────────────────────────
|
|
3886
|
+
async submitAction(input) {
|
|
3887
|
+
if (this.room.phase !== "playing") {
|
|
3888
|
+
return fail("Game is not in playing phase");
|
|
3893
3889
|
}
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
3897
|
-
return trimmed.slice(firstBrace, lastBrace + 1);
|
|
3890
|
+
if (!this.room.gameConfig || !this.room.gameState) {
|
|
3891
|
+
return fail("Game not initialized");
|
|
3898
3892
|
}
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3893
|
+
if (!input.action?.action_id) {
|
|
3894
|
+
return fail("action_id is required");
|
|
3895
|
+
}
|
|
3896
|
+
const config = this.room.gameConfig;
|
|
3897
|
+
let roleId;
|
|
3898
|
+
if (input.mode === "operator") {
|
|
3899
|
+
if (!input.roleId) return fail("roleId is required for operator actions");
|
|
3900
|
+
if (!config.roleIds.includes(input.roleId)) {
|
|
3901
|
+
return fail(`Invalid role: ${input.roleId}`);
|
|
3902
|
+
}
|
|
3903
|
+
roleId = input.roleId;
|
|
3904
|
+
} else {
|
|
3905
|
+
const assignedRole = getRoleForUser(this.room.roleMapping, input.userId);
|
|
3906
|
+
if (!assignedRole) {
|
|
3907
|
+
return fail("You don't have a role assigned");
|
|
3908
|
+
}
|
|
3909
|
+
const currentRoleId = await this.room.getCurrentRole();
|
|
3910
|
+
if (currentRoleId !== assignedRole) {
|
|
3911
|
+
return fail("Not your turn");
|
|
3912
|
+
}
|
|
3913
|
+
roleId = assignedRole;
|
|
3914
|
+
}
|
|
3915
|
+
const startedAt = Date.now();
|
|
3916
|
+
const action = {
|
|
3917
|
+
action_id: input.action.action_id,
|
|
3918
|
+
params: input.action.params || {}
|
|
3919
|
+
};
|
|
3920
|
+
const result = await this.room.submitActionToGameWorker(roleId, action);
|
|
3921
|
+
const interactionId = crypto.randomUUID();
|
|
3922
|
+
const baseHumanLog = {
|
|
3923
|
+
interactionId,
|
|
3924
|
+
interactionGroupId: interactionId,
|
|
3925
|
+
roomId: this.room.roomId,
|
|
3926
|
+
gameId: config.gameId || null,
|
|
3927
|
+
gameName: null,
|
|
3928
|
+
roleId,
|
|
3929
|
+
userId: input.mode === "operator" ? "operator" : input.userId,
|
|
3930
|
+
playerType: "human",
|
|
3931
|
+
modelName: null,
|
|
3932
|
+
systemPrompt: null,
|
|
3933
|
+
userPrompt: null,
|
|
3934
|
+
response: JSON.stringify(action),
|
|
3935
|
+
action_id: action.action_id,
|
|
3936
|
+
actionParams: action.params,
|
|
3937
|
+
attempt: 1,
|
|
3938
|
+
outerAttempt: 1,
|
|
3939
|
+
maxAttempts: 1,
|
|
3940
|
+
previousError: null,
|
|
3941
|
+
responseTimeMs: Date.now() - startedAt,
|
|
3942
|
+
eventTs: Date.now()
|
|
3943
|
+
};
|
|
3944
|
+
if (!result.success) {
|
|
3945
|
+
this.room.waitUntil(insertMonitorLog(this.room.bindings.DB, {
|
|
3946
|
+
...baseHumanLog,
|
|
3947
|
+
status: "rejected",
|
|
3948
|
+
errorMessage: result.error || "Action rejected"
|
|
3949
|
+
}));
|
|
3950
|
+
return fail(result.error || "Action rejected");
|
|
3951
|
+
}
|
|
3952
|
+
this.room.waitUntil(insertMonitorLog(this.room.bindings.DB, {
|
|
3953
|
+
...baseHumanLog,
|
|
3954
|
+
status: "success"
|
|
3955
|
+
}));
|
|
3956
|
+
await applySuccessfulAction({
|
|
3957
|
+
room: this.room,
|
|
3958
|
+
roleId,
|
|
3959
|
+
action,
|
|
3960
|
+
nextState: result.nextState,
|
|
3961
|
+
commands: result.commands,
|
|
3962
|
+
triggerMode: "await"
|
|
3963
|
+
});
|
|
3964
|
+
return {
|
|
3965
|
+
ok: true,
|
|
3966
|
+
data: { nextState: result.nextState, commands: result.commands }
|
|
3967
|
+
};
|
|
3903
3968
|
}
|
|
3904
|
-
|
|
3969
|
+
// ─── Internals ─────────────────────────────────────────
|
|
3970
|
+
async initializeGameState(errorPrefix) {
|
|
3971
|
+
const config = this.room.gameConfig;
|
|
3972
|
+
if (!config) return fail("No game selected");
|
|
3905
3973
|
try {
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
status,
|
|
3921
|
-
attempt,
|
|
3922
|
-
outerAttempt: 1,
|
|
3923
|
-
maxAttempts: 3,
|
|
3924
|
-
errorMessage: error,
|
|
3925
|
-
responseTimeMs: duration,
|
|
3926
|
-
eventTs: Date.now()
|
|
3974
|
+
for (const player of Object.values(this.room.players)) {
|
|
3975
|
+
if (player.type === "llm" && player.llmConfig) {
|
|
3976
|
+
player.llmConfig.memory = "";
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
const client = resolveGameWorkerClient(
|
|
3980
|
+
config.gameId,
|
|
3981
|
+
config.gameWorkerUrl,
|
|
3982
|
+
this.room.bindings
|
|
3983
|
+
);
|
|
3984
|
+
const initRes = await client.fetch("/init", {
|
|
3985
|
+
method: "POST",
|
|
3986
|
+
headers: { "Content-Type": "application/json" },
|
|
3987
|
+
body: JSON.stringify({ players: config.roleIds })
|
|
3927
3988
|
});
|
|
3989
|
+
if (!initRes.ok) {
|
|
3990
|
+
const errText = await initRes.text();
|
|
3991
|
+
return fail(`${errorPrefix}: ${errText}`, 502);
|
|
3992
|
+
}
|
|
3993
|
+
this.room.gameState = await initRes.json();
|
|
3994
|
+
this.room.history = [];
|
|
3995
|
+
this.room.runtimeId = crypto.randomUUID();
|
|
3996
|
+
this.room.stateIndex = 0;
|
|
3997
|
+
this.room.stateHistory = [{
|
|
3998
|
+
index: 0,
|
|
3999
|
+
name: "Initial State",
|
|
4000
|
+
state: this.room.gameState,
|
|
4001
|
+
timestamp: Date.now()
|
|
4002
|
+
}];
|
|
4003
|
+
this.room.phase = "playing";
|
|
4004
|
+
await this.room.persistAll();
|
|
4005
|
+
this.room.broadcastSyncState();
|
|
4006
|
+
await this.room.checkAndTriggerNextTurn();
|
|
4007
|
+
this.room.waitUntil(this.room.syncRoomMeta());
|
|
4008
|
+
return { ok: true, data: { phase: this.room.phase } };
|
|
3928
4009
|
} catch (e) {
|
|
3929
|
-
console.error("[
|
|
4010
|
+
console.error("[RoomCommands] Failed to initialize game:", e);
|
|
4011
|
+
return fail(errorPrefix, 500);
|
|
3930
4012
|
}
|
|
3931
4013
|
}
|
|
3932
4014
|
};
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
}
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
4015
|
+
function resolveRoleIds(metadata, selectedPlayerCount) {
|
|
4016
|
+
if (Array.isArray(metadata.roleIds)) {
|
|
4017
|
+
return {
|
|
4018
|
+
roleIds: metadata.roleIds,
|
|
4019
|
+
selectedPlayerCount: metadata.roleIds.length
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
if (metadata.roleIds && typeof metadata.roleIds === "object") {
|
|
4023
|
+
const counts = Object.keys(metadata.roleIds).map(Number).sort((a, b) => a - b);
|
|
4024
|
+
const effectiveCount = selectedPlayerCount && metadata.roleIds[selectedPlayerCount] ? selectedPlayerCount : counts[0];
|
|
4025
|
+
if (effectiveCount === void 0) return { roleIds: [] };
|
|
4026
|
+
return {
|
|
4027
|
+
roleIds: metadata.roleIds[effectiveCount] || [],
|
|
4028
|
+
selectedPlayerCount: effectiveCount
|
|
4029
|
+
};
|
|
3942
4030
|
}
|
|
3943
4031
|
return null;
|
|
3944
4032
|
}
|
|
4033
|
+
|
|
4034
|
+
// src/game-do.ts
|
|
4035
|
+
var JOIN_REQUEST_TTL_MS = 10 * 60 * 1e3;
|
|
3945
4036
|
var GameDO = class extends DurableObject {
|
|
3946
4037
|
// userId → ws
|
|
3947
4038
|
constructor(state, env) {
|
|
@@ -3964,6 +4055,8 @@ var GameDO = class extends DurableObject {
|
|
|
3964
4055
|
this.stateIndex = 0;
|
|
3965
4056
|
this.llmWebhookUrl = null;
|
|
3966
4057
|
this.roomMetaHookUrl = null;
|
|
4058
|
+
this.joinRequests = {};
|
|
4059
|
+
// pending lobby join requests, key = userId
|
|
3967
4060
|
// ─── In-memory connection tracking ──────────────────────
|
|
3968
4061
|
this.connections = /* @__PURE__ */ new Map();
|
|
3969
4062
|
// ws → userId
|
|
@@ -3971,7 +4064,7 @@ var GameDO = class extends DurableObject {
|
|
|
3971
4064
|
this.bindings = env;
|
|
3972
4065
|
this.presence = new PresenceManager(this);
|
|
3973
4066
|
this.roomMgr = new RoomManager(this);
|
|
3974
|
-
this.
|
|
4067
|
+
this.commands = new RoomCommandService(this);
|
|
3975
4068
|
this.llm = new LlmManager(this);
|
|
3976
4069
|
this.ctx.blockConcurrencyWhile(async () => {
|
|
3977
4070
|
await this.loadState();
|
|
@@ -3999,6 +4092,7 @@ var GameDO = class extends DurableObject {
|
|
|
3999
4092
|
this.stateIndex = await s.get("stateIndex") || 0;
|
|
4000
4093
|
this.llmWebhookUrl = this.bindings.LLM_WEBHOOK_URL || await s.get("llmWebhookUrl") || null;
|
|
4001
4094
|
this.roomMetaHookUrl = await s.get("roomMetaHookUrl") || null;
|
|
4095
|
+
this.joinRequests = await s.get("joinRequests") || {};
|
|
4002
4096
|
}
|
|
4003
4097
|
async persistAll() {
|
|
4004
4098
|
await this.ctx.storage.put({
|
|
@@ -4017,7 +4111,8 @@ var GameDO = class extends DurableObject {
|
|
|
4017
4111
|
runtimeId: this.runtimeId,
|
|
4018
4112
|
stateIndex: this.stateIndex,
|
|
4019
4113
|
llmWebhookUrl: this.llmWebhookUrl,
|
|
4020
|
-
roomMetaHookUrl: this.roomMetaHookUrl
|
|
4114
|
+
roomMetaHookUrl: this.roomMetaHookUrl,
|
|
4115
|
+
joinRequests: this.joinRequests
|
|
4021
4116
|
});
|
|
4022
4117
|
}
|
|
4023
4118
|
async persist(...keys) {
|
|
@@ -4060,6 +4155,7 @@ var GameDO = class extends DurableObject {
|
|
|
4060
4155
|
this.llmWebhookUrl = this.bindings.LLM_WEBHOOK_URL || null;
|
|
4061
4156
|
this.gameConfig = null;
|
|
4062
4157
|
this.roomMetaHookUrl = body.roomMetaHookUrl || null;
|
|
4158
|
+
this.joinRequests = {};
|
|
4063
4159
|
await this.persistAll();
|
|
4064
4160
|
this.ctx.waitUntil(this.syncRoomMeta());
|
|
4065
4161
|
return c.json({ success: true });
|
|
@@ -4122,192 +4218,98 @@ var GameDO = class extends DurableObject {
|
|
|
4122
4218
|
});
|
|
4123
4219
|
this.app.post("/role-action", async (c) => {
|
|
4124
4220
|
const body = await c.req.json();
|
|
4125
|
-
if (!body.roleId || !body.action)
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
turn: (this.history[this.history.length - 1]?.turn || 0) + 1,
|
|
4130
|
-
roleId: body.roleId,
|
|
4131
|
-
action: body.action,
|
|
4132
|
-
timestamp: Date.now(),
|
|
4133
|
-
stateIndex: this.stateIndex
|
|
4221
|
+
if (!body.roleId || !body.action) {
|
|
4222
|
+
return this.commandResponse(c, {
|
|
4223
|
+
ok: false,
|
|
4224
|
+
error: "roleId and action required"
|
|
4134
4225
|
});
|
|
4135
|
-
this.gameState = res.nextState;
|
|
4136
|
-
this.stateIndex++;
|
|
4137
|
-
await this.persist("gameState", "history", "stateIndex");
|
|
4138
|
-
this.presence.broadcastSyncState();
|
|
4139
4226
|
}
|
|
4140
|
-
|
|
4227
|
+
const result = await this.commands.submitAction({
|
|
4228
|
+
userId: "operator",
|
|
4229
|
+
roleId: body.roleId,
|
|
4230
|
+
action: body.action,
|
|
4231
|
+
mode: "operator"
|
|
4232
|
+
});
|
|
4233
|
+
return this.commandResponse(c, result);
|
|
4141
4234
|
});
|
|
4142
4235
|
this.app.post("/set-game", async (c) => {
|
|
4143
4236
|
const body = await c.req.json();
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
const client = resolveGameWorkerClient(body.gameId || "", baseUrl, this.bindings);
|
|
4151
|
-
const metaRes = await client.fetch("/metadata");
|
|
4152
|
-
if (!metaRes.ok) {
|
|
4153
|
-
const errText = await metaRes.text();
|
|
4154
|
-
console.error("[GameDO] Metadata fetch failed. Status:", metaRes.status, "Body:", errText);
|
|
4155
|
-
return c.json({ error: "Failed to fetch game metadata" }, 502);
|
|
4156
|
-
}
|
|
4157
|
-
const metadata = await metaRes.json();
|
|
4158
|
-
let roleIds;
|
|
4159
|
-
let resolvedPlayerCount;
|
|
4160
|
-
if (Array.isArray(metadata.roleIds)) {
|
|
4161
|
-
roleIds = metadata.roleIds;
|
|
4162
|
-
resolvedPlayerCount = roleIds.length;
|
|
4163
|
-
} else if (metadata.roleIds) {
|
|
4164
|
-
const counts = Object.keys(metadata.roleIds).map(Number).sort((a, b) => a - b);
|
|
4165
|
-
const selectedCount = body.selectedPlayerCount;
|
|
4166
|
-
const effectiveCount = selectedCount && metadata.roleIds[selectedCount] ? selectedCount : counts[0];
|
|
4167
|
-
roleIds = metadata.roleIds[effectiveCount] || [];
|
|
4168
|
-
resolvedPlayerCount = effectiveCount;
|
|
4169
|
-
} else {
|
|
4170
|
-
roleIds = [];
|
|
4171
|
-
}
|
|
4172
|
-
this.gameConfig = {
|
|
4173
|
-
gameWorkerUrl: baseUrl,
|
|
4174
|
-
gameId: body.gameId || metadata.id,
|
|
4175
|
-
maxPlayers: roleIds.length,
|
|
4176
|
-
roleIds,
|
|
4177
|
-
selectedPlayerCount: resolvedPlayerCount,
|
|
4178
|
-
enable_llm_memory: metadata.enable_llm_memory,
|
|
4179
|
-
auto_save_mode: metadata.auto_save_mode
|
|
4180
|
-
};
|
|
4181
|
-
this.roleMapping = {};
|
|
4182
|
-
await this.persist("gameConfig", "roleMapping");
|
|
4183
|
-
this.presence.broadcastSyncState();
|
|
4184
|
-
this.ctx.waitUntil(this.syncRoomMeta());
|
|
4185
|
-
return c.json({ success: true, gameId: this.gameConfig.gameId, roleIds });
|
|
4186
|
-
} catch (e) {
|
|
4187
|
-
console.error("[GameDO] /set-game failed:", e);
|
|
4188
|
-
return c.json({ error: e.message || "Failed to set game" }, 500);
|
|
4189
|
-
}
|
|
4237
|
+
const result = await this.commands.setGame({
|
|
4238
|
+
gameWorkerUrl: body.gameWorkerUrl || "",
|
|
4239
|
+
gameId: body.gameId,
|
|
4240
|
+
selectedPlayerCount: body.selectedPlayerCount
|
|
4241
|
+
});
|
|
4242
|
+
return this.commandResponse(c, result);
|
|
4190
4243
|
});
|
|
4191
4244
|
this.app.post("/start-game", async (c) => {
|
|
4192
|
-
|
|
4193
|
-
return c.json({ error: "Game cannot be started in current phase" }, 400);
|
|
4194
|
-
}
|
|
4195
|
-
if (!this.gameConfig) {
|
|
4196
|
-
return c.json({ error: "No game selected" }, 400);
|
|
4197
|
-
}
|
|
4198
|
-
for (const roleId of this.gameConfig.roleIds) {
|
|
4199
|
-
if (!this.roleMapping[roleId]) {
|
|
4200
|
-
const devUserId = `dev:${roleId}`;
|
|
4201
|
-
this.players[devUserId] = {
|
|
4202
|
-
displayName: roleId,
|
|
4203
|
-
connected: false,
|
|
4204
|
-
isOwner: false,
|
|
4205
|
-
type: "human"
|
|
4206
|
-
};
|
|
4207
|
-
this.roleMapping[roleId] = devUserId;
|
|
4208
|
-
}
|
|
4209
|
-
}
|
|
4210
|
-
try {
|
|
4211
|
-
for (const player of Object.values(this.players)) {
|
|
4212
|
-
if (player.type === "llm" && player.llmConfig) {
|
|
4213
|
-
player.llmConfig.memory = "";
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
const client = resolveGameWorkerClient(
|
|
4217
|
-
this.gameConfig.gameId,
|
|
4218
|
-
this.gameConfig.gameWorkerUrl,
|
|
4219
|
-
this.bindings
|
|
4220
|
-
);
|
|
4221
|
-
const initRes = await client.fetch("/init", {
|
|
4222
|
-
method: "POST",
|
|
4223
|
-
headers: { "Content-Type": "application/json" },
|
|
4224
|
-
body: JSON.stringify({ players: this.gameConfig.roleIds })
|
|
4225
|
-
});
|
|
4226
|
-
if (!initRes.ok) {
|
|
4227
|
-
const errText = await initRes.text();
|
|
4228
|
-
return c.json({ error: `Game Worker init failed: ${errText}` }, 502);
|
|
4229
|
-
}
|
|
4230
|
-
this.gameState = await initRes.json();
|
|
4231
|
-
this.history = [];
|
|
4232
|
-
this.runtimeId = crypto.randomUUID();
|
|
4233
|
-
this.stateIndex = 0;
|
|
4234
|
-
this.stateHistory = [{
|
|
4235
|
-
index: 0,
|
|
4236
|
-
name: "Initial State",
|
|
4237
|
-
state: this.gameState,
|
|
4238
|
-
timestamp: Date.now()
|
|
4239
|
-
}];
|
|
4240
|
-
this.phase = "playing";
|
|
4241
|
-
await this.persistAll();
|
|
4242
|
-
this.presence.broadcastSyncState();
|
|
4243
|
-
await this.checkAndTriggerNextTurn();
|
|
4244
|
-
this.ctx.waitUntil(this.syncRoomMeta());
|
|
4245
|
-
return c.json({ success: true, phase: this.phase });
|
|
4246
|
-
} catch (e) {
|
|
4247
|
-
console.error("[GameDO] /start-game failed:", e);
|
|
4248
|
-
return c.json({ error: e.message || "Failed to start game" }, 500);
|
|
4249
|
-
}
|
|
4245
|
+
return this.commandResponse(c, await this.commands.startGame());
|
|
4250
4246
|
});
|
|
4251
4247
|
this.app.post("/stop-game", async (c) => {
|
|
4252
|
-
|
|
4253
|
-
return c.json({ error: "Game has not started" }, 400);
|
|
4254
|
-
}
|
|
4255
|
-
this.phase = "lobby";
|
|
4256
|
-
this.gameState = null;
|
|
4257
|
-
this.history = [];
|
|
4258
|
-
this.stateHistory = [];
|
|
4259
|
-
await this.persist("phase", "gameState", "history", "stateHistory");
|
|
4260
|
-
this.presence.broadcastSyncState();
|
|
4261
|
-
this.ctx.waitUntil(this.syncRoomMeta());
|
|
4262
|
-
return c.json({ success: true, phase: this.phase });
|
|
4248
|
+
return this.commandResponse(c, await this.commands.stopGame());
|
|
4263
4249
|
});
|
|
4264
4250
|
this.app.post("/restart-game", async (c) => {
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4251
|
+
return this.commandResponse(c, await this.commands.restartGame());
|
|
4252
|
+
});
|
|
4253
|
+
this.app.post("/pause-game", async (c) => {
|
|
4254
|
+
return this.commandResponse(c, await this.commands.pauseGame());
|
|
4255
|
+
});
|
|
4256
|
+
this.app.post("/resume-game", async (c) => {
|
|
4257
|
+
return this.commandResponse(c, await this.commands.resumeGame());
|
|
4258
|
+
});
|
|
4259
|
+
this.app.post("/backtrack", async (c) => {
|
|
4260
|
+
const body = await c.req.json();
|
|
4261
|
+
return this.commandResponse(
|
|
4262
|
+
c,
|
|
4263
|
+
await this.commands.backtrack(body.index)
|
|
4264
|
+
);
|
|
4265
|
+
});
|
|
4266
|
+
this.app.post("/players", async (c) => {
|
|
4267
|
+
const body = await c.req.json();
|
|
4268
|
+
return this.commandResponse(c, await this.commands.createPlayer(body));
|
|
4269
|
+
});
|
|
4270
|
+
this.app.delete("/players/:userId", async (c) => {
|
|
4271
|
+
return this.commandResponse(
|
|
4272
|
+
c,
|
|
4273
|
+
await this.commands.removePlayer(c.req.param("userId"))
|
|
4274
|
+
);
|
|
4275
|
+
});
|
|
4276
|
+
this.app.post("/assign-role", async (c) => {
|
|
4277
|
+
const body = await c.req.json();
|
|
4278
|
+
if (!body.roleId || !body.userId) {
|
|
4279
|
+
return this.commandResponse(c, {
|
|
4280
|
+
ok: false,
|
|
4281
|
+
error: "roleId and userId are required"
|
|
4286
4282
|
});
|
|
4287
|
-
if (!initRes.ok) {
|
|
4288
|
-
return c.json({ error: "Game Worker init failed on restart" }, 502);
|
|
4289
|
-
}
|
|
4290
|
-
this.gameState = await initRes.json();
|
|
4291
|
-
this.history = [];
|
|
4292
|
-
this.runtimeId = crypto.randomUUID();
|
|
4293
|
-
this.stateIndex = 0;
|
|
4294
|
-
this.stateHistory = [{
|
|
4295
|
-
index: 0,
|
|
4296
|
-
name: "Initial State",
|
|
4297
|
-
state: this.gameState,
|
|
4298
|
-
timestamp: Date.now()
|
|
4299
|
-
}];
|
|
4300
|
-
this.phase = "playing";
|
|
4301
|
-
await this.persistAll();
|
|
4302
|
-
this.presence.broadcastSyncState();
|
|
4303
|
-
await this.checkAndTriggerNextTurn();
|
|
4304
|
-
this.ctx.waitUntil(this.syncRoomMeta());
|
|
4305
|
-
return c.json({ success: true, phase: this.phase });
|
|
4306
|
-
} catch (e) {
|
|
4307
|
-
console.error("[GameDO] /restart-game failed:", e);
|
|
4308
|
-
return c.json({ error: e.message || "Failed to restart game" }, 500);
|
|
4309
4283
|
}
|
|
4284
|
+
return this.commandResponse(c, await this.commands.assignRole({
|
|
4285
|
+
roleId: body.roleId,
|
|
4286
|
+
userId: body.userId
|
|
4287
|
+
}));
|
|
4310
4288
|
});
|
|
4289
|
+
this.app.post("/add-bot", async (c) => {
|
|
4290
|
+
const body = await c.req.json();
|
|
4291
|
+
return this.commandResponse(c, await this.commands.addBot({
|
|
4292
|
+
displayName: body.displayName || "",
|
|
4293
|
+
modelName: body.modelName || "",
|
|
4294
|
+
systemPrompt: body.systemPrompt,
|
|
4295
|
+
temperature: body.temperature
|
|
4296
|
+
}));
|
|
4297
|
+
});
|
|
4298
|
+
}
|
|
4299
|
+
/**
|
|
4300
|
+
* Turn a command result into an HTTP response. The same result object is
|
|
4301
|
+
* also what the WebSocket transport inspects (only errors are sent).
|
|
4302
|
+
*/
|
|
4303
|
+
commandResponse(c, result) {
|
|
4304
|
+
if (result.ok) {
|
|
4305
|
+
return c.json(result.data === void 0 ? { ok: true } : { ok: true, data: result.data });
|
|
4306
|
+
}
|
|
4307
|
+
return c.json({ ok: false, error: result.error }, result.status ?? 400);
|
|
4308
|
+
}
|
|
4309
|
+
reportCommandFailure(userId, result) {
|
|
4310
|
+
if (!result.ok) {
|
|
4311
|
+
this.sendErrorToUser(userId, result.error);
|
|
4312
|
+
}
|
|
4311
4313
|
}
|
|
4312
4314
|
async fetch(request) {
|
|
4313
4315
|
return this.app.fetch(request);
|
|
@@ -4325,79 +4327,71 @@ var GameDO = class extends DurableObject {
|
|
|
4325
4327
|
await this.roomMgr.handleLeave(userId);
|
|
4326
4328
|
break;
|
|
4327
4329
|
case "LOBBY_JOIN_REQUEST":
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
}
|
|
4330
|
+
this.joinRequests[userId] = {
|
|
4331
|
+
displayName: msg.payload.displayName,
|
|
4332
|
+
requestedAt: Date.now()
|
|
4333
|
+
};
|
|
4334
|
+
await this.persist("joinRequests");
|
|
4335
|
+
this.notifyOwnerOfJoinRequest(userId);
|
|
4335
4336
|
break;
|
|
4336
4337
|
case "ADMIN_SET_GAME":
|
|
4337
|
-
|
|
4338
|
+
if (!this.requireOwner(userId)) return;
|
|
4339
|
+
this.reportCommandFailure(userId, await this.commands.setGame(msg.payload));
|
|
4338
4340
|
break;
|
|
4339
4341
|
case "ADMIN_ADD_BOT":
|
|
4340
|
-
|
|
4342
|
+
if (!this.requireOwner(userId)) return;
|
|
4343
|
+
this.reportCommandFailure(userId, await this.commands.addBot(msg.payload));
|
|
4341
4344
|
break;
|
|
4342
4345
|
case "ADMIN_REMOVE_PLAYER":
|
|
4343
|
-
|
|
4346
|
+
if (!this.requireOwner(userId)) return;
|
|
4347
|
+
this.reportCommandFailure(userId, await this.commands.removePlayer(msg.payload.userId));
|
|
4344
4348
|
break;
|
|
4345
4349
|
case "ADMIN_APPROVE_JOIN":
|
|
4350
|
+
if (!this.requireOwner(userId)) return;
|
|
4346
4351
|
await this.roomMgr.handleAdminApproveJoin(userId, msg.payload);
|
|
4347
4352
|
break;
|
|
4353
|
+
case "ADMIN_IGNORE_JOIN":
|
|
4354
|
+
if (!this.requireOwner(userId)) return;
|
|
4355
|
+
await this.roomMgr.handleAdminIgnoreJoin(userId, msg.payload);
|
|
4356
|
+
break;
|
|
4348
4357
|
case "ADMIN_ASSIGN_ROLE":
|
|
4349
|
-
|
|
4358
|
+
if (!this.requireOwner(userId)) return;
|
|
4359
|
+
this.reportCommandFailure(userId, await this.commands.assignRole(msg.payload));
|
|
4350
4360
|
break;
|
|
4351
4361
|
case "ADMIN_START_GAME":
|
|
4352
|
-
|
|
4362
|
+
if (!this.requireOwner(userId)) return;
|
|
4363
|
+
this.reportCommandFailure(userId, await this.commands.startGame());
|
|
4353
4364
|
break;
|
|
4354
4365
|
case "ADMIN_STOP_GAME":
|
|
4355
|
-
|
|
4366
|
+
if (!this.requireOwner(userId)) return;
|
|
4367
|
+
this.reportCommandFailure(userId, await this.commands.stopGame());
|
|
4356
4368
|
break;
|
|
4357
4369
|
case "ADMIN_RESTART_GAME":
|
|
4358
|
-
|
|
4370
|
+
if (!this.requireOwner(userId)) return;
|
|
4371
|
+
this.reportCommandFailure(userId, await this.commands.restartGame());
|
|
4359
4372
|
break;
|
|
4360
4373
|
case "ADMIN_PAUSE_GAME":
|
|
4361
4374
|
if (!this.requireOwner(userId)) return;
|
|
4362
|
-
this.
|
|
4363
|
-
await this.persist("phase");
|
|
4364
|
-
this.presence.broadcastSyncState();
|
|
4365
|
-
this.ctx.waitUntil(this.syncRoomMeta());
|
|
4375
|
+
this.reportCommandFailure(userId, await this.commands.pauseGame());
|
|
4366
4376
|
break;
|
|
4367
4377
|
case "ADMIN_RESUME_GAME":
|
|
4368
4378
|
if (!this.requireOwner(userId)) return;
|
|
4369
|
-
this.
|
|
4370
|
-
await this.persist("phase");
|
|
4371
|
-
this.presence.broadcastSyncState();
|
|
4372
|
-
this.ctx.waitUntil(this.syncRoomMeta());
|
|
4373
|
-
this.ctx.waitUntil(this.checkAndTriggerNextTurn());
|
|
4379
|
+
this.reportCommandFailure(userId, await this.commands.resumeGame());
|
|
4374
4380
|
break;
|
|
4375
4381
|
case "ADMIN_BACKTRACK_STATE":
|
|
4376
4382
|
if (!this.requireOwner(userId)) return;
|
|
4377
|
-
|
|
4378
|
-
return this.sendErrorToUser(userId, "Game must be paused to backtrack");
|
|
4379
|
-
}
|
|
4380
|
-
const historyIdx = msg.payload.index;
|
|
4381
|
-
const targetState = this.stateHistory.find((h) => h.index === historyIdx);
|
|
4382
|
-
if (!targetState) {
|
|
4383
|
-
return this.sendErrorToUser(userId, "Invalid state index");
|
|
4384
|
-
}
|
|
4385
|
-
this.gameState = targetState.state;
|
|
4386
|
-
this.stateIndex = targetState.index;
|
|
4387
|
-
this.history = this.history.filter((h) => h.stateIndex <= targetState.index);
|
|
4388
|
-
await this.persist("gameState", "history", "stateIndex");
|
|
4389
|
-
this.presence.broadcastSyncState();
|
|
4383
|
+
this.reportCommandFailure(userId, await this.commands.backtrack(msg.payload.index));
|
|
4390
4384
|
break;
|
|
4391
4385
|
case "ADMIN_UPDATE_ROOM_META":
|
|
4392
4386
|
if (!this.requireOwner(userId)) return;
|
|
4393
|
-
this.
|
|
4394
|
-
this.isPublic = msg.payload.isPublic;
|
|
4395
|
-
await this.persist("name", "isPublic");
|
|
4396
|
-
this.presence.broadcastSyncState();
|
|
4397
|
-
this.ctx.waitUntil(this.syncRoomMeta());
|
|
4387
|
+
this.reportCommandFailure(userId, await this.commands.updateRoomMeta(msg.payload));
|
|
4398
4388
|
break;
|
|
4399
4389
|
case "ACT":
|
|
4400
|
-
|
|
4390
|
+
this.reportCommandFailure(userId, await this.commands.submitAction({
|
|
4391
|
+
userId,
|
|
4392
|
+
action: msg.payload,
|
|
4393
|
+
mode: "player"
|
|
4394
|
+
}));
|
|
4401
4395
|
break;
|
|
4402
4396
|
default:
|
|
4403
4397
|
this.presence.sendErrorToUser(userId, `Unknown message type: ${msg.type}`);
|
|
@@ -4455,6 +4449,43 @@ var GameDO = class extends DurableObject {
|
|
|
4455
4449
|
sendErrorToUser(userId, msg) {
|
|
4456
4450
|
this.presence.sendErrorToUser(userId, msg);
|
|
4457
4451
|
}
|
|
4452
|
+
// ─── Lobby join requests ────────────────────────────────
|
|
4453
|
+
/** Push one pending join request to the owner's live session, if any. */
|
|
4454
|
+
notifyOwnerOfJoinRequest(userId) {
|
|
4455
|
+
const request = this.joinRequests[userId];
|
|
4456
|
+
const ownerWs = this.sessions.get(this.ownerId);
|
|
4457
|
+
if (!request || !ownerWs) return;
|
|
4458
|
+
this.sendMessage(ownerWs, {
|
|
4459
|
+
type: "JOIN_REQUEST_INTERNAL",
|
|
4460
|
+
payload: { userId, displayName: request.displayName }
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
/**
|
|
4464
|
+
* Replay every unexpired pending join request to a newly connected owner.
|
|
4465
|
+
* Called when the owner (re)connects so requests sent during a disconnect
|
|
4466
|
+
* are not lost.
|
|
4467
|
+
*/
|
|
4468
|
+
sendPendingJoinRequests(ws) {
|
|
4469
|
+
const cutoff = Date.now() - JOIN_REQUEST_TTL_MS;
|
|
4470
|
+
let changed = false;
|
|
4471
|
+
for (const [userId, request] of Object.entries(this.joinRequests)) {
|
|
4472
|
+
if (request.requestedAt < cutoff || this.players[userId]) {
|
|
4473
|
+
delete this.joinRequests[userId];
|
|
4474
|
+
changed = true;
|
|
4475
|
+
continue;
|
|
4476
|
+
}
|
|
4477
|
+
this.sendMessage(ws, {
|
|
4478
|
+
type: "JOIN_REQUEST_INTERNAL",
|
|
4479
|
+
payload: { userId, displayName: request.displayName }
|
|
4480
|
+
});
|
|
4481
|
+
}
|
|
4482
|
+
if (changed) this.ctx.waitUntil(this.persist("joinRequests"));
|
|
4483
|
+
}
|
|
4484
|
+
async clearJoinRequest(userId) {
|
|
4485
|
+
if (!this.joinRequests[userId]) return;
|
|
4486
|
+
delete this.joinRequests[userId];
|
|
4487
|
+
await this.persist("joinRequests");
|
|
4488
|
+
}
|
|
4458
4489
|
requireOwner(userId) {
|
|
4459
4490
|
if (userId !== this.ownerId) {
|
|
4460
4491
|
this.sendErrorToUser(userId, "Only the room owner can perform this action");
|
|
@@ -4577,7 +4608,8 @@ var GameDO = class extends DurableObject {
|
|
|
4577
4608
|
return {
|
|
4578
4609
|
success: data.success,
|
|
4579
4610
|
nextState: data.nextState,
|
|
4580
|
-
error: data.error
|
|
4611
|
+
error: data.error,
|
|
4612
|
+
commands: data.commands
|
|
4581
4613
|
};
|
|
4582
4614
|
} catch (e) {
|
|
4583
4615
|
console.error("[GameDO] Worker action failed:", e);
|
|
@@ -4639,6 +4671,8 @@ var MonitorDO = class extends DurableObject2 {
|
|
|
4639
4671
|
this.subscribers = /* @__PURE__ */ new Map();
|
|
4640
4672
|
this.lastProcessedTs = Date.now();
|
|
4641
4673
|
this.isPolling = false;
|
|
4674
|
+
// Workers' setInterval returns a number; keep the type independent of the
|
|
4675
|
+
// ambient lib (vitest pulls in Node's `Timeout`, which the test program sees).
|
|
4642
4676
|
this.heartbeatTimer = null;
|
|
4643
4677
|
this.setupRoutes();
|
|
4644
4678
|
this.startHeartbeat();
|
|
@@ -4903,6 +4937,19 @@ function verifyAdmin(c) {
|
|
|
4903
4937
|
const authHeader = c.req.header("Authorization");
|
|
4904
4938
|
return authHeader === `Bearer ${c.env.ADMIN_SECRET}`;
|
|
4905
4939
|
}
|
|
4940
|
+
function getRoomStub(c, roomId) {
|
|
4941
|
+
const id = roomId.length === 64 ? c.env.GAME_DO.idFromString(roomId) : c.env.GAME_DO.idFromName(roomId);
|
|
4942
|
+
return c.env.GAME_DO.get(id);
|
|
4943
|
+
}
|
|
4944
|
+
async function proxyRoom(c, roomId, doPath, init) {
|
|
4945
|
+
let stub;
|
|
4946
|
+
try {
|
|
4947
|
+
stub = getRoomStub(c, roomId);
|
|
4948
|
+
} catch {
|
|
4949
|
+
return c.json({ ok: false, error: "Invalid ID format" }, 400);
|
|
4950
|
+
}
|
|
4951
|
+
return stub.fetch(`http://do${doPath}`, init);
|
|
4952
|
+
}
|
|
4906
4953
|
app.get("/api/monitor/logs", async (c) => {
|
|
4907
4954
|
if (!verifyAdmin(c)) return c.json({ error: "Unauthorized" }, 401);
|
|
4908
4955
|
if (!c.env.DB) return c.json({ error: "Monitor logging is disabled" }, 404);
|
|
@@ -4950,75 +4997,48 @@ app.get("/api/monitor/logs/:interactionId", async (c) => {
|
|
|
4950
4997
|
if (!data) return c.json({ error: "Not found" }, 404);
|
|
4951
4998
|
return c.json({ data });
|
|
4952
4999
|
});
|
|
4953
|
-
app.get("/api/
|
|
5000
|
+
app.get("/api/engine/rooms/:id", async (c) => {
|
|
4954
5001
|
if (!verifyAdmin(c)) return c.json({ error: "Unauthorized" }, 401);
|
|
4955
|
-
|
|
4956
|
-
let id;
|
|
4957
|
-
try {
|
|
4958
|
-
if (idParam.length === 64) {
|
|
4959
|
-
id = c.env.GAME_DO.idFromString(idParam);
|
|
4960
|
-
} else {
|
|
4961
|
-
id = c.env.GAME_DO.idFromName(idParam);
|
|
4962
|
-
}
|
|
4963
|
-
} catch (e) {
|
|
4964
|
-
return c.json({ error: "Invalid ID format" }, 400);
|
|
4965
|
-
}
|
|
4966
|
-
const stub = c.env.GAME_DO.get(id);
|
|
4967
|
-
const res = await stub.fetch("http://do/state");
|
|
4968
|
-
if (!res.ok) {
|
|
4969
|
-
return c.json({ error: "Failed to fetch room state" }, res.status);
|
|
4970
|
-
}
|
|
4971
|
-
const state = await res.json();
|
|
4972
|
-
return c.json({ data: state });
|
|
5002
|
+
return proxyRoom(c, c.req.param("id"), "/state");
|
|
4973
5003
|
});
|
|
4974
|
-
app.get("/api/
|
|
5004
|
+
app.get("/api/engine/rooms/:id/debug", async (c) => {
|
|
4975
5005
|
if (!verifyAdmin(c)) return c.json({ error: "Unauthorized" }, 401);
|
|
4976
|
-
|
|
4977
|
-
const roleId = c.req.query("roleId");
|
|
4978
|
-
if (!roleId) return c.json({ error: "roleId is required" }, 400);
|
|
4979
|
-
const id = c.env.GAME_DO.idFromName(roomId);
|
|
4980
|
-
const stub = c.env.GAME_DO.get(id);
|
|
4981
|
-
const res = await stub.fetch(`http://do/perspective?roleId=${encodeURIComponent(roleId)}`);
|
|
4982
|
-
if (!res.ok) {
|
|
4983
|
-
return c.json({ error: "Failed to fetch perspective" }, res.status);
|
|
4984
|
-
}
|
|
4985
|
-
return c.json(await res.json());
|
|
5006
|
+
return proxyRoom(c, c.req.param("id"), "/debug");
|
|
4986
5007
|
});
|
|
4987
|
-
app.
|
|
5008
|
+
app.get("/api/engine/rooms/:id/perspective", async (c) => {
|
|
4988
5009
|
if (!verifyAdmin(c)) return c.json({ error: "Unauthorized" }, 401);
|
|
4989
|
-
const
|
|
4990
|
-
|
|
4991
|
-
const id = c.env.GAME_DO.idFromName(roomId);
|
|
4992
|
-
const stub = c.env.GAME_DO.get(id);
|
|
4993
|
-
const res = await stub.fetch("http://do/role-action", {
|
|
4994
|
-
method: "POST",
|
|
4995
|
-
headers: { "Content-Type": "application/json" },
|
|
4996
|
-
body: JSON.stringify(body)
|
|
4997
|
-
});
|
|
4998
|
-
if (!res.ok) {
|
|
4999
|
-
return c.json({ error: "Failed to submit action" }, res.status);
|
|
5000
|
-
}
|
|
5001
|
-
return c.json(await res.json());
|
|
5010
|
+
const query = new URL(c.req.url).search;
|
|
5011
|
+
return proxyRoom(c, c.req.param("id"), `/perspective${query}`);
|
|
5002
5012
|
});
|
|
5003
|
-
var
|
|
5004
|
-
|
|
5005
|
-
|
|
5013
|
+
var roomPostEndpoints = [
|
|
5014
|
+
"role-action",
|
|
5015
|
+
"set-game",
|
|
5016
|
+
"start-game",
|
|
5017
|
+
"stop-game",
|
|
5018
|
+
"restart-game",
|
|
5019
|
+
"pause-game",
|
|
5020
|
+
"resume-game",
|
|
5021
|
+
"backtrack",
|
|
5022
|
+
"players",
|
|
5023
|
+
"assign-role",
|
|
5024
|
+
"add-bot"
|
|
5025
|
+
];
|
|
5026
|
+
for (const endpoint of roomPostEndpoints) {
|
|
5027
|
+
app.post(`/api/engine/rooms/:id/${endpoint}`, async (c) => {
|
|
5006
5028
|
if (!verifyAdmin(c)) return c.json({ error: "Unauthorized" }, 401);
|
|
5007
|
-
const
|
|
5008
|
-
|
|
5009
|
-
const stub = c.env.GAME_DO.get(id);
|
|
5010
|
-
const body = endpoint === "set-game" ? await c.req.json() : {};
|
|
5011
|
-
const res = await stub.fetch(`http://do/${endpoint}`, {
|
|
5029
|
+
const body = await c.req.text();
|
|
5030
|
+
return proxyRoom(c, c.req.param("id"), `/${endpoint}`, {
|
|
5012
5031
|
method: "POST",
|
|
5013
5032
|
headers: { "Content-Type": "application/json" },
|
|
5014
|
-
body:
|
|
5033
|
+
body: body || "{}"
|
|
5015
5034
|
});
|
|
5016
|
-
if (!res.ok) {
|
|
5017
|
-
return c.json(await res.json(), res.status);
|
|
5018
|
-
}
|
|
5019
|
-
return c.json(await res.json());
|
|
5020
5035
|
});
|
|
5021
5036
|
}
|
|
5037
|
+
app.delete("/api/engine/rooms/:id/players/:userId", async (c) => {
|
|
5038
|
+
if (!verifyAdmin(c)) return c.json({ error: "Unauthorized" }, 401);
|
|
5039
|
+
const userId = encodeURIComponent(c.req.param("userId"));
|
|
5040
|
+
return proxyRoom(c, c.req.param("id"), `/players/${userId}`, { method: "DELETE" });
|
|
5041
|
+
});
|
|
5022
5042
|
app.get("/debug/:roomId", async (c) => {
|
|
5023
5043
|
const roomId = c.req.param("roomId");
|
|
5024
5044
|
const id = c.env.GAME_DO.idFromName(roomId);
|