@coolclaw/coolclaw 0.3.3 → 0.3.4
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
CHANGED
|
@@ -20,9 +20,13 @@
|
|
|
20
20
|
- `PRIVATE_MESSAGE`
|
|
21
21
|
- `GROUP_MESSAGE`
|
|
22
22
|
- `SYSTEM_NOTIFICATION`
|
|
23
|
-
- `GAME_EVENT`
|
|
23
|
+
- `GAME_EVENT` — werewolf-game phase events (`WOLF_TURN`/`WITCH_TURN`/`SEER_TURN`/`DAY_SPEAK_TURN`/`LAST_WORD_TURN`/`DAY_VOTE_TURN`/`HUNTER_SKILL_TURN`); LLM reply must contain a `<ACTION>{...}</ACTION>` block which is POSTed to `/riddle/api/chat/agent/action`. See `docs/game-event-integration.md` for details.
|
|
24
24
|
- `CONTENT_TASK`
|
|
25
25
|
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Node.js **>= 18** (required for global `fetch` + `AbortSignal.timeout` used by the GAME_EVENT action client).
|
|
29
|
+
|
|
26
30
|
## Installation
|
|
27
31
|
|
|
28
32
|
### Quick Install (recommended)
|
|
@@ -148,6 +148,239 @@ function isRecord(value) {
|
|
|
148
148
|
return typeof value === "object" && value !== null;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// src/game-event-prompt.ts
|
|
152
|
+
function asRecord(v) {
|
|
153
|
+
return typeof v === "object" && v !== null ? v : {};
|
|
154
|
+
}
|
|
155
|
+
function asString(v, fallback = "") {
|
|
156
|
+
return typeof v === "string" ? v : fallback;
|
|
157
|
+
}
|
|
158
|
+
function asNumberOrNull(v) {
|
|
159
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
160
|
+
}
|
|
161
|
+
function asNumberArray(v) {
|
|
162
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "number") : [];
|
|
163
|
+
}
|
|
164
|
+
function asRecordArray(v) {
|
|
165
|
+
return Array.isArray(v) ? v.map(asRecord) : [];
|
|
166
|
+
}
|
|
167
|
+
function asStringArray(v) {
|
|
168
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
169
|
+
}
|
|
170
|
+
function renderPlayerInfo(list) {
|
|
171
|
+
if (list.length === 0) return "\uFF08\u65E0\u5EA7\u4F4D\u4FE1\u606F\uFF09";
|
|
172
|
+
return list.map((p) => {
|
|
173
|
+
const seat = asNumberOrNull(p.seat);
|
|
174
|
+
const name = asString(p.name, "\u672A\u77E5");
|
|
175
|
+
const voice = asString(p.voiceDesc, "");
|
|
176
|
+
const alive = p.alive === true ? "\u5B58\u6D3B" : "\u5DF2\u6B7B\u4EA1";
|
|
177
|
+
return `\u5EA7\u4F4D${seat ?? "?"} ${name}\uFF08${voice || "\u672A\u6807\u6CE8"}\uFF0C${alive}\uFF09`;
|
|
178
|
+
}).join("\uFF1B");
|
|
179
|
+
}
|
|
180
|
+
function renderHistory(history) {
|
|
181
|
+
if (history.length === 0) return "\uFF08\u6682\u65E0\u5386\u53F2\u8BB0\u5F55\uFF09";
|
|
182
|
+
return history.map((h, i) => `${i + 1}. ${h}`).join("\n");
|
|
183
|
+
}
|
|
184
|
+
function renderAliveSeats(seats) {
|
|
185
|
+
return seats.length === 0 ? "\uFF08\u65E0\uFF09" : `[${seats.join(", ")}]`;
|
|
186
|
+
}
|
|
187
|
+
function renderHeader(eventType, outer, payload) {
|
|
188
|
+
const round = asNumberOrNull(outer.round) ?? 1;
|
|
189
|
+
const selfSeat = asNumberOrNull(payload.selfSeat);
|
|
190
|
+
const selfRole = asString(payload.selfRole, "\u672A\u77E5");
|
|
191
|
+
const selfName = asString(payload.selfAgentName, "");
|
|
192
|
+
const phase = eventType.startsWith("DAY_") || eventType === "LAST_WORD_TURN" || eventType === "HUNTER_SKILL_TURN" ? "\u767D\u5929" : "\u591C\u665A";
|
|
193
|
+
const aliveSeats = asNumberArray(payload.aliveSeats);
|
|
194
|
+
const playerInfo = renderPlayerInfo(asRecordArray(payload.playerInfoList));
|
|
195
|
+
const history = renderHistory(asStringArray(payload.scopedHistory));
|
|
196
|
+
return [
|
|
197
|
+
`[\u6E38\u620F] \u72FC\u4EBA\u6740 \xB7 \u7B2C ${round} \u8F6E \xB7 ${phase} \xB7 ${describeEventType(eventType)}`,
|
|
198
|
+
``,
|
|
199
|
+
`\u4F60\u662F\u5EA7\u4F4D ${selfSeat ?? "?"} \u7684 ${selfRole}${selfName ? `\uFF08${selfName}\uFF09` : ""}\u3002`,
|
|
200
|
+
`\u5F53\u524D\u5B58\u6D3B\u5EA7\u4F4D\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
|
|
201
|
+
`\u5168\u4F53\u73A9\u5BB6\uFF1A${playerInfo}`,
|
|
202
|
+
``,
|
|
203
|
+
`\u3010\u8FD1\u671F\u5386\u53F2\uFF08\u4EC5\u4F60\u53EF\u89C1\uFF09\u3011`,
|
|
204
|
+
history,
|
|
205
|
+
``
|
|
206
|
+
].join("\n");
|
|
207
|
+
}
|
|
208
|
+
function describeEventType(eventType) {
|
|
209
|
+
switch (eventType) {
|
|
210
|
+
case "WOLF_TURN":
|
|
211
|
+
return "\u72FC\u4EBA\u884C\u52A8";
|
|
212
|
+
case "WITCH_TURN":
|
|
213
|
+
return "\u5973\u5DEB\u884C\u52A8";
|
|
214
|
+
case "SEER_TURN":
|
|
215
|
+
return "\u9884\u8A00\u5BB6\u67E5\u9A8C";
|
|
216
|
+
case "DAY_SPEAK_TURN":
|
|
217
|
+
return "\u767D\u5929\u53D1\u8A00";
|
|
218
|
+
case "LAST_WORD_TURN":
|
|
219
|
+
return "\u9057\u8A00";
|
|
220
|
+
case "DAY_VOTE_TURN":
|
|
221
|
+
return "\u767D\u5929\u6295\u7968";
|
|
222
|
+
case "HUNTER_SKILL_TURN":
|
|
223
|
+
return "\u730E\u4EBA\u6280\u80FD";
|
|
224
|
+
default:
|
|
225
|
+
return eventType;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function renderTask(eventType, payload) {
|
|
229
|
+
switch (eventType) {
|
|
230
|
+
case "WOLF_TURN":
|
|
231
|
+
return renderWolfTurn(payload);
|
|
232
|
+
case "WITCH_TURN":
|
|
233
|
+
return renderWitchTurn(payload);
|
|
234
|
+
case "SEER_TURN":
|
|
235
|
+
return renderSeerTurn(payload);
|
|
236
|
+
case "DAY_SPEAK_TURN":
|
|
237
|
+
return renderDaySpeakTurn(payload);
|
|
238
|
+
case "LAST_WORD_TURN":
|
|
239
|
+
return renderLastWordTurn(payload);
|
|
240
|
+
case "DAY_VOTE_TURN":
|
|
241
|
+
return renderDayVoteTurn(payload);
|
|
242
|
+
case "HUNTER_SKILL_TURN":
|
|
243
|
+
return renderHunterTurn(payload);
|
|
244
|
+
default:
|
|
245
|
+
return renderUnknownTurn(eventType);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function renderWolfTurn(payload) {
|
|
249
|
+
const aliveSeats = asNumberArray(payload.aliveSeats);
|
|
250
|
+
const round = asNumberOrNull(payload.wolfAttemptRound) ?? 1;
|
|
251
|
+
const isFirst = payload.isFirstSpeakerInRound === true;
|
|
252
|
+
const teammateSeat = asNumberOrNull(payload.teammateSeat);
|
|
253
|
+
const teammateName = asString(payload.teammateName, "");
|
|
254
|
+
const tp = asRecord(payload.teammateProposal);
|
|
255
|
+
const teammateProposalStr = tp.targetSeat != null ? `\u540C\u4F34\uFF08\u5EA7\u4F4D ${tp.seat}\uFF09\u672C\u8F6E\u5DF2\u63D0\u8BAE\u51FB\u6740\u5EA7\u4F4D ${tp.targetSeat}${tp.reason ? `\uFF0C\u7406\u7531\uFF1A${tp.reason}` : ""}${tp.speech ? `\uFF0C\u53D1\u8A00\uFF1A"${tp.speech}"` : ""}\u3002` : isFirst ? "\u4F60\u662F\u672C\u8F6E\u9996\u4F4D\u53D1\u8A00\u7684\u72FC\u4EBA\u3002" : "\u540C\u4F34\u5C1A\u672A\u53D1\u8A00\u3002";
|
|
256
|
+
const lastRound = asRecordArray(payload.lastRoundChoices);
|
|
257
|
+
const lastRoundStr = lastRound.length > 0 ? `\u4E0A\u4E00\u8F6E\u6295\u7968\u8BB0\u5F55\uFF1A${lastRound.map((c) => `\u5EA7\u4F4D${c.seat}\u2192\u5EA7\u4F4D${c.targetSeat}`).join("\uFF1B")}\u3002` : "";
|
|
258
|
+
return [
|
|
259
|
+
`\u3010\u4EFB\u52A1\u3011\u72FC\u4EBA\u6740\u4EBA\u534F\u5546\uFF08\u7B2C ${round} \u8F6E\uFF09`,
|
|
260
|
+
teammateSeat != null ? `\u4F60\u7684\u72FC\u540C\u4F34\uFF1A\u5EA7\u4F4D ${teammateSeat} ${teammateName}\u3002` : "\u4F60\u662F\u72EC\u72FC\u3002",
|
|
261
|
+
teammateProposalStr,
|
|
262
|
+
lastRoundStr,
|
|
263
|
+
`\u5408\u6CD5\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)} \u4E2D\u7684\u4EFB\u610F\u4E00\u4E2A\u3002`,
|
|
264
|
+
``,
|
|
265
|
+
`\u8BF7\u5148\u7B80\u77ED\u9648\u8FF0\u4F60\u7684\u60F3\u6CD5\uFF0C\u7136\u540E\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
|
|
266
|
+
`<ACTION>`,
|
|
267
|
+
`{"actionType":"WOLF_KILL","actionData":{"targetSeat":<number>,"reason":"<\u7B80\u8FF0>","speech":"<\u53D1\u8A00>"}}`,
|
|
268
|
+
`</ACTION>`
|
|
269
|
+
].join("\n");
|
|
270
|
+
}
|
|
271
|
+
function renderWitchTurn(payload) {
|
|
272
|
+
const aliveSeats = asNumberArray(payload.aliveSeats);
|
|
273
|
+
const wolfTarget = asNumberOrNull(payload.wolfTargetSeat);
|
|
274
|
+
const antidote = payload.antidoteAvailable === true;
|
|
275
|
+
const poison = payload.poisonAvailable === true;
|
|
276
|
+
const canSelfSave = payload.canSelfSave === true;
|
|
277
|
+
const options = [];
|
|
278
|
+
if (antidote && wolfTarget != null) {
|
|
279
|
+
options.push(
|
|
280
|
+
`\u9009\u9879 A\uFF08\u4F7F\u7528\u89E3\u836F\u6551 ${wolfTarget} \u53F7\uFF09\uFF1A`,
|
|
281
|
+
`<ACTION>{"actionType":"WITCH_SAVE","actionData":{"speech":"<\u53EF\u9009\u53D1\u8A00>"}}</ACTION>`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (poison) {
|
|
285
|
+
options.push(
|
|
286
|
+
`\u9009\u9879 B\uFF08\u4F7F\u7528\u6BD2\u836F\u6BD2\u6740\u67D0\u5EA7\u4F4D\uFF09\uFF1A`,
|
|
287
|
+
`<ACTION>{"actionType":"WITCH_POISON","actionData":{"targetSeat":<number>,"speech":"<\u53EF\u9009\u53D1\u8A00>"}}</ACTION>`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
options.push(
|
|
291
|
+
`\u9009\u9879 C\uFF08\u672C\u8F6E\u4E0D\u7528\u836F\uFF09\uFF1A`,
|
|
292
|
+
`<ACTION>{"actionType":"WITCH_PASS","actionData":{}}</ACTION>`
|
|
293
|
+
);
|
|
294
|
+
return [
|
|
295
|
+
`\u3010\u4EFB\u52A1\u3011\u5973\u5DEB\u884C\u52A8`,
|
|
296
|
+
wolfTarget != null ? `\u4ECA\u665A\u72FC\u4EBA\u76EE\u6807\uFF1A\u5EA7\u4F4D ${wolfTarget}\u3002` : `\u4ECA\u665A\u72FC\u4EBA\u672A\u9009\u62E9\u76EE\u6807\uFF08\u6216\u4F60\u770B\u4E0D\u5230\uFF09\u3002`,
|
|
297
|
+
`\u89E3\u836F${antidote ? "\u53EF\u7528" : "\u5DF2\u7528\u8FC7"}\uFF1B\u6BD2\u836F${poison ? "\u53EF\u7528" : "\u5DF2\u7528\u8FC7"}\u3002${antidote && wolfTarget != null ? canSelfSave ? "\uFF08\u672C\u591C\u5141\u8BB8\u81EA\u6551\uFF09" : "\uFF08\u7B2C 2 \u591C\u8D77\u4E0D\u53EF\u81EA\u6551\uFF09" : ""}`,
|
|
298
|
+
`\u5408\u6CD5\u6BD2\u836F\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
|
|
299
|
+
``,
|
|
300
|
+
`\u8BF7\u4E09\u9009\u4E00\uFF0C\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u5BF9\u5E94\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
|
|
301
|
+
...options
|
|
302
|
+
].join("\n");
|
|
303
|
+
}
|
|
304
|
+
function renderSeerTurn(payload) {
|
|
305
|
+
const aliveSeats = asNumberArray(payload.aliveSeats);
|
|
306
|
+
const history = asStringArray(payload.history);
|
|
307
|
+
return [
|
|
308
|
+
`\u3010\u4EFB\u52A1\u3011\u9884\u8A00\u5BB6\u67E5\u9A8C`,
|
|
309
|
+
history.length > 0 ? `\u4F60\u7684\u5386\u53F2\u67E5\u9A8C\u7ED3\u679C\uFF1A${history.join("\uFF1B")}\u3002` : `\u9996\u6B21\u67E5\u9A8C\uFF0C\u5C1A\u65E0\u5386\u53F2\u3002`,
|
|
310
|
+
`\u5408\u6CD5\u67E5\u9A8C\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
|
|
311
|
+
``,
|
|
312
|
+
`\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
|
|
313
|
+
`<ACTION>`,
|
|
314
|
+
`{"actionType":"SEER_CHECK","actionData":{"targetSeat":<number>,"speech":"<\u53EF\u9009\u53D1\u8A00>"}}`,
|
|
315
|
+
`</ACTION>`
|
|
316
|
+
].join("\n");
|
|
317
|
+
}
|
|
318
|
+
function renderDaySpeakTurn(payload) {
|
|
319
|
+
const maxLen = asNumberOrNull(payload.maxLength) ?? 300;
|
|
320
|
+
return [
|
|
321
|
+
`\u3010\u4EFB\u52A1\u3011\u767D\u5929\u8F6E\u5230\u4F60\u53D1\u8A00`,
|
|
322
|
+
`\u4F60\u9700\u8981\u57FA\u4E8E\u5386\u53F2\u7ED9\u51FA\u6709\u903B\u8F91\u7684\u53D1\u8A00\uFF08\u63A8\u7406\u3001\u7AD9\u8FB9\u3001\u8981\u7968\u7B49\uFF09\uFF0C\u957F\u5EA6\u4E0D\u8D85\u8FC7 ${maxLen} \u5B57\u3002`,
|
|
323
|
+
``,
|
|
324
|
+
`\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
|
|
325
|
+
`<ACTION>`,
|
|
326
|
+
`{"actionType":"DAY_SPEAK","actionData":{"content":"<\u4F60\u7684\u5B8C\u6574\u53D1\u8A00\u6587\u672C>"}}`,
|
|
327
|
+
`</ACTION>`
|
|
328
|
+
].join("\n");
|
|
329
|
+
}
|
|
330
|
+
function renderLastWordTurn(_payload) {
|
|
331
|
+
return [
|
|
332
|
+
`\u3010\u4EFB\u52A1\u3011\u9057\u8A00`,
|
|
333
|
+
`\u4F60\u5DF2\u51FA\u5C40\uFF0C\u53EF\u4EE5\u7559\u4E0B\u9057\u8A00\uFF08\u4E5F\u53EF\u4EE5\u9009\u62E9\u4E0D\u8BF4\uFF09\u3002`,
|
|
334
|
+
``,
|
|
335
|
+
`\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
|
|
336
|
+
`<ACTION>`,
|
|
337
|
+
`{"actionType":"LAST_WORD","actionData":{"content":"<\u9057\u8A00\u6587\u672C\uFF0C\u53EF\u4E3A\u7A7A>"}}`,
|
|
338
|
+
`</ACTION>`
|
|
339
|
+
].join("\n");
|
|
340
|
+
}
|
|
341
|
+
function renderDayVoteTurn(payload) {
|
|
342
|
+
const aliveSeats = asNumberArray(payload.aliveSeats);
|
|
343
|
+
return [
|
|
344
|
+
`\u3010\u4EFB\u52A1\u3011\u767D\u5929\u6295\u7968\u653E\u9010`,
|
|
345
|
+
`\u5408\u6CD5\u6295\u7968\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\uFF1B\u53EF\u4EE5\u5F03\u7968\uFF08targetSeat \u586B null\uFF09\u3002`,
|
|
346
|
+
``,
|
|
347
|
+
`\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
|
|
348
|
+
`<ACTION>`,
|
|
349
|
+
`{"actionType":"DAY_VOTE","actionData":{"targetSeat":<number \u6216 null>}}`,
|
|
350
|
+
`</ACTION>`
|
|
351
|
+
].join("\n");
|
|
352
|
+
}
|
|
353
|
+
function renderHunterTurn(payload) {
|
|
354
|
+
const aliveSeats = asNumberArray(payload.aliveSeats);
|
|
355
|
+
return [
|
|
356
|
+
`\u3010\u4EFB\u52A1\u3011\u730E\u4EBA\u6280\u80FD`,
|
|
357
|
+
`\u4F60\u88AB\u730E\u4EBA\u6280\u80FD\u89E6\u53D1\uFF08\u6B7B\u4EA1\u65F6\u53EF\u5E26\u8D70\u4E00\u540D\u5176\u4ED6\u73A9\u5BB6\uFF0C\u4E5F\u53EF\u9009\u62E9\u4E0D\u5F00\u67AA\uFF09\u3002`,
|
|
358
|
+
`\u5408\u6CD5\u5F00\u67AA\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
|
|
359
|
+
``,
|
|
360
|
+
`\u8BF7\u4E8C\u9009\u4E00\uFF1A`,
|
|
361
|
+
`\u9009\u9879 A\uFF08\u5F00\u67AA\u5E26\u8D70\u67D0\u5EA7\u4F4D\uFF09\uFF1A`,
|
|
362
|
+
`<ACTION>{"actionType":"HUNTER_SHOOT","actionData":{"targetSeat":<number>}}</ACTION>`,
|
|
363
|
+
`\u9009\u9879 B\uFF08\u4E0D\u5F00\u67AA\uFF09\uFF1A`,
|
|
364
|
+
`<ACTION>{"actionType":"HUNTER_PASS","actionData":{}}</ACTION>`
|
|
365
|
+
].join("\n");
|
|
366
|
+
}
|
|
367
|
+
function renderUnknownTurn(eventType) {
|
|
368
|
+
return [
|
|
369
|
+
`\u3010\u63D0\u793A\u3011\u672A\u8BC6\u522B\u7684\u6E38\u620F\u4E8B\u4EF6\u7C7B\u578B\uFF1A${eventType}`,
|
|
370
|
+
`\u8BF7\u5FFD\u7565\u672C\u6761\u4E8B\u4EF6\uFF0C\u65E0\u9700\u58F0\u660E\u52A8\u4F5C\u3002`
|
|
371
|
+
].join("\n");
|
|
372
|
+
}
|
|
373
|
+
function buildGameEventPrompt(eventType, eventData) {
|
|
374
|
+
const outer = asRecord(eventData);
|
|
375
|
+
const payload = asRecord(outer.payload);
|
|
376
|
+
const header = renderHeader(eventType, outer, payload);
|
|
377
|
+
const task = renderTask(eventType, payload);
|
|
378
|
+
const footer = `
|
|
379
|
+
|
|
380
|
+
\uFF08ACTION \u5757\u4E4B\u5916\u7684\u6587\u672C\u4F1A\u4F5C\u4E3A\u65E5\u5FD7\u8BB0\u5F55\uFF0C\u4E0D\u4F1A\u51FA\u73B0\u5728\u6E38\u620F\u4E2D\u3002\uFF09`;
|
|
381
|
+
return `${header}${task}${footer}`;
|
|
382
|
+
}
|
|
383
|
+
|
|
151
384
|
// src/inbound.ts
|
|
152
385
|
function mapInboundFrame(frame) {
|
|
153
386
|
if (frame.type === "PRIVATE_MESSAGE") {
|
|
@@ -223,7 +456,7 @@ function mapNotificationFrame(frame) {
|
|
|
223
456
|
};
|
|
224
457
|
}
|
|
225
458
|
if (frame.type === "GAME_EVENT") {
|
|
226
|
-
return
|
|
459
|
+
return mapGameEventFrame(frame, payload);
|
|
227
460
|
}
|
|
228
461
|
if (frame.type === "CONTENT_TASK") {
|
|
229
462
|
const taskType = typeof payload.taskType === "string" ? payload.taskType : "unknown";
|
|
@@ -240,6 +473,54 @@ function mapNotificationFrame(frame) {
|
|
|
240
473
|
}
|
|
241
474
|
return null;
|
|
242
475
|
}
|
|
476
|
+
function mapGameEventFrame(frame, payload) {
|
|
477
|
+
const eventType = typeof payload.eventType === "string" ? payload.eventType : "UNKNOWN";
|
|
478
|
+
if (eventType === "GAME_START") {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
let eventDataObj = {};
|
|
482
|
+
const rawEventData = payload.eventData;
|
|
483
|
+
if (typeof rawEventData === "string" && rawEventData.length > 0) {
|
|
484
|
+
try {
|
|
485
|
+
const parsed = JSON.parse(rawEventData);
|
|
486
|
+
if (isRecord2(parsed)) eventDataObj = parsed;
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
} else if (isRecord2(rawEventData)) {
|
|
490
|
+
eventDataObj = rawEventData;
|
|
491
|
+
}
|
|
492
|
+
const gameId = Number(payload.gameId ?? 0);
|
|
493
|
+
const roomId = Number(payload.roomId ?? 0);
|
|
494
|
+
const eventId = typeof payload.eventId === "string" ? payload.eventId : frame.id;
|
|
495
|
+
const turnSeq = Number(payload.turnSeq ?? 0);
|
|
496
|
+
const traceId = typeof payload.traceId === "string" ? payload.traceId : "";
|
|
497
|
+
const deadlineEpochMs = Number(payload.deadlineEpochMs ?? 0);
|
|
498
|
+
const seq = typeof payload.seq === "number" ? payload.seq : void 0;
|
|
499
|
+
const meta = {
|
|
500
|
+
gameEvent: true,
|
|
501
|
+
gameId,
|
|
502
|
+
roomId,
|
|
503
|
+
turnSeq,
|
|
504
|
+
eventId,
|
|
505
|
+
traceId,
|
|
506
|
+
eventType,
|
|
507
|
+
eventData: eventDataObj,
|
|
508
|
+
deadlineEpochMs,
|
|
509
|
+
sourceFrameId: frame.id
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
id: eventId,
|
|
513
|
+
channel: "coolclaw",
|
|
514
|
+
// 每个 (roomId, gameId, eventType) 组合作为独立会话键,避免和私聊会话混淆;
|
|
515
|
+
// turnSeq 不参与会话 id,以便同一事件类型的多轮对话能沿用同一上下文。
|
|
516
|
+
conversationId: `game:${roomId}:${gameId}:${eventType}`,
|
|
517
|
+
text: buildGameEventPrompt(eventType, eventDataObj),
|
|
518
|
+
messageType: "GAME_EVENT",
|
|
519
|
+
seq,
|
|
520
|
+
shouldReply: true,
|
|
521
|
+
metadata: meta
|
|
522
|
+
};
|
|
523
|
+
}
|
|
243
524
|
async function ackProcessedSeq(input, envelope) {
|
|
244
525
|
if (typeof envelope.seq === "number") {
|
|
245
526
|
const lastAckedSeq = await input.ackStore.record(input.accountKey, envelope.seq);
|
|
@@ -437,6 +718,192 @@ async function sendMedia(input) {
|
|
|
437
718
|
return response.messageId;
|
|
438
719
|
}
|
|
439
720
|
|
|
721
|
+
// src/game-action-parser.ts
|
|
722
|
+
function extractActionBlock(text) {
|
|
723
|
+
const re = /<ACTION>\s*([\s\S]+?)\s*<\/ACTION>/g;
|
|
724
|
+
let match;
|
|
725
|
+
let last = null;
|
|
726
|
+
while ((match = re.exec(text)) !== null) {
|
|
727
|
+
last = match[1];
|
|
728
|
+
}
|
|
729
|
+
return last;
|
|
730
|
+
}
|
|
731
|
+
function parseAgentAction(text) {
|
|
732
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
733
|
+
return { error: "no_action_block" };
|
|
734
|
+
}
|
|
735
|
+
const body = extractActionBlock(text);
|
|
736
|
+
if (body === null) {
|
|
737
|
+
return { error: "no_action_block" };
|
|
738
|
+
}
|
|
739
|
+
let obj;
|
|
740
|
+
try {
|
|
741
|
+
obj = JSON.parse(body);
|
|
742
|
+
} catch (e) {
|
|
743
|
+
return { error: "invalid_json", detail: e instanceof Error ? e.message : String(e) };
|
|
744
|
+
}
|
|
745
|
+
if (typeof obj !== "object" || obj === null) {
|
|
746
|
+
return { error: "invalid_json", detail: "not an object" };
|
|
747
|
+
}
|
|
748
|
+
const rec = obj;
|
|
749
|
+
if (typeof rec.actionType !== "string" || rec.actionType.length === 0) {
|
|
750
|
+
return { error: "missing_action_type" };
|
|
751
|
+
}
|
|
752
|
+
if (typeof rec.actionData !== "object" || rec.actionData === null || Array.isArray(rec.actionData)) {
|
|
753
|
+
return { error: "missing_action_data" };
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
actionType: rec.actionType,
|
|
757
|
+
actionData: rec.actionData
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/game-action-fallback.ts
|
|
762
|
+
function asNumberArray2(v) {
|
|
763
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "number") : [];
|
|
764
|
+
}
|
|
765
|
+
function asRecord2(v) {
|
|
766
|
+
return typeof v === "object" && v !== null ? v : {};
|
|
767
|
+
}
|
|
768
|
+
function pickRandomAlive(aliveSeats, excludeSeat) {
|
|
769
|
+
const candidates = excludeSeat != null ? aliveSeats.filter((s) => s !== excludeSeat) : aliveSeats;
|
|
770
|
+
if (candidates.length === 0) return null;
|
|
771
|
+
return candidates[Math.floor(Math.random() * candidates.length)];
|
|
772
|
+
}
|
|
773
|
+
function fallbackActionFor(eventType, eventData) {
|
|
774
|
+
const payload = asRecord2(asRecord2(eventData).payload);
|
|
775
|
+
const aliveSeats = asNumberArray2(payload.aliveSeats);
|
|
776
|
+
const selfSeat = typeof payload.selfSeat === "number" ? payload.selfSeat : void 0;
|
|
777
|
+
switch (eventType) {
|
|
778
|
+
case "WOLF_TURN": {
|
|
779
|
+
const target = pickRandomAlive(aliveSeats, selfSeat);
|
|
780
|
+
return {
|
|
781
|
+
actionType: "WOLF_KILL",
|
|
782
|
+
actionData: {
|
|
783
|
+
targetSeat: target ?? (aliveSeats[0] ?? 1),
|
|
784
|
+
reason: "\uFF08\u6258\u7BA1\uFF1ALLM \u672A\u7ED9\u51FA\u5408\u6CD5\u52A8\u4F5C\uFF09",
|
|
785
|
+
speech: ""
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
case "WITCH_TURN": {
|
|
790
|
+
return { actionType: "WITCH_PASS", actionData: {} };
|
|
791
|
+
}
|
|
792
|
+
case "SEER_TURN": {
|
|
793
|
+
const target = pickRandomAlive(aliveSeats, selfSeat);
|
|
794
|
+
return {
|
|
795
|
+
actionType: "SEER_CHECK",
|
|
796
|
+
actionData: {
|
|
797
|
+
targetSeat: target ?? (aliveSeats[0] ?? 1),
|
|
798
|
+
speech: ""
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
case "DAY_SPEAK_TURN":
|
|
803
|
+
return {
|
|
804
|
+
actionType: "DAY_SPEAK",
|
|
805
|
+
actionData: { content: "\uFF08\u6258\u7BA1\u53D1\u8A00\uFF1A\u672C\u8F6E\u8DF3\u8FC7\uFF09" }
|
|
806
|
+
};
|
|
807
|
+
case "LAST_WORD_TURN":
|
|
808
|
+
return {
|
|
809
|
+
actionType: "LAST_WORD",
|
|
810
|
+
actionData: { content: "" }
|
|
811
|
+
};
|
|
812
|
+
case "DAY_VOTE_TURN":
|
|
813
|
+
return {
|
|
814
|
+
actionType: "DAY_VOTE",
|
|
815
|
+
actionData: { targetSeat: null }
|
|
816
|
+
};
|
|
817
|
+
case "HUNTER_SKILL_TURN":
|
|
818
|
+
return { actionType: "HUNTER_PASS", actionData: {} };
|
|
819
|
+
default:
|
|
820
|
+
return { actionType: "UNKNOWN", actionData: {} };
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/game-action-client.ts
|
|
825
|
+
function buildUrl(gatewayUrl) {
|
|
826
|
+
const base = gatewayUrl.replace(/\/+$/, "");
|
|
827
|
+
const tail = base.endsWith("/riddle") ? "/api/chat/agent/action" : "/riddle/api/chat/agent/action";
|
|
828
|
+
return `${base}${tail}`;
|
|
829
|
+
}
|
|
830
|
+
function buildBody(input) {
|
|
831
|
+
return JSON.stringify({
|
|
832
|
+
gameId: input.gameId,
|
|
833
|
+
actionType: input.actionType,
|
|
834
|
+
actionData: input.actionData,
|
|
835
|
+
// AgentActionRequest.timestamp 契约为 String
|
|
836
|
+
timestamp: String(Date.now()),
|
|
837
|
+
turnSeq: input.turnSeq,
|
|
838
|
+
eventId: input.eventId,
|
|
839
|
+
traceId: input.traceId
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
function sleep(ms) {
|
|
843
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
844
|
+
}
|
|
845
|
+
async function submitGameAction(input) {
|
|
846
|
+
const url = buildUrl(input.gatewayUrl);
|
|
847
|
+
const body = buildBody(input);
|
|
848
|
+
const timeoutMs = input.timeoutMs ?? 5e3;
|
|
849
|
+
const maxRetries = input.maxRetries ?? 1;
|
|
850
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
851
|
+
const start = Date.now();
|
|
852
|
+
let attempts = 0;
|
|
853
|
+
let lastError;
|
|
854
|
+
let lastStatus;
|
|
855
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
856
|
+
attempts++;
|
|
857
|
+
const ac = new AbortController();
|
|
858
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
859
|
+
try {
|
|
860
|
+
const resp = await fetchImpl(url, {
|
|
861
|
+
method: "POST",
|
|
862
|
+
headers: {
|
|
863
|
+
"Content-Type": "application/json",
|
|
864
|
+
Authorization: `Bearer ${input.token}`,
|
|
865
|
+
"X-User-Id": input.agentId,
|
|
866
|
+
"X-User-Type": "AGENT"
|
|
867
|
+
},
|
|
868
|
+
body,
|
|
869
|
+
signal: ac.signal
|
|
870
|
+
});
|
|
871
|
+
lastStatus = resp.status;
|
|
872
|
+
if (resp.ok) {
|
|
873
|
+
const elapsedMs2 = Date.now() - start;
|
|
874
|
+
input.log?.info?.(
|
|
875
|
+
`[GAME-ACTION] post ok gameId=${input.gameId} eventId=${input.eventId} actionType=${input.actionType} status=${resp.status} elapsedMs=${elapsedMs2} attempts=${attempts}`
|
|
876
|
+
);
|
|
877
|
+
return { success: true, status: resp.status, elapsedMs: elapsedMs2, attempts };
|
|
878
|
+
}
|
|
879
|
+
let text = "";
|
|
880
|
+
try {
|
|
881
|
+
text = (await resp.text()).slice(0, 500);
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
lastError = `http_${resp.status}: ${text}`;
|
|
885
|
+
input.log?.warn?.(
|
|
886
|
+
`[GAME-ACTION] post non-2xx gameId=${input.gameId} eventId=${input.eventId} status=${resp.status} attempt=${attempt} body=${text}`
|
|
887
|
+
);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
890
|
+
input.log?.warn?.(
|
|
891
|
+
`[GAME-ACTION] post network error gameId=${input.gameId} eventId=${input.eventId} attempt=${attempt} err=${lastError}`
|
|
892
|
+
);
|
|
893
|
+
} finally {
|
|
894
|
+
clearTimeout(timer);
|
|
895
|
+
}
|
|
896
|
+
if (attempt < maxRetries) {
|
|
897
|
+
await sleep(500 * Math.pow(2, attempt));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const elapsedMs = Date.now() - start;
|
|
901
|
+
input.log?.error?.(
|
|
902
|
+
`[GAME-ACTION] post failed gameId=${input.gameId} eventId=${input.eventId} actionType=${input.actionType} status=${lastStatus ?? "n/a"} attempts=${attempts} elapsedMs=${elapsedMs} err=${lastError ?? "unknown"}`
|
|
903
|
+
);
|
|
904
|
+
return { success: false, status: lastStatus, error: lastError, elapsedMs, attempts };
|
|
905
|
+
}
|
|
906
|
+
|
|
440
907
|
// src/ws-client.ts
|
|
441
908
|
import WebSocket from "ws";
|
|
442
909
|
var CoolclawWsClient = class {
|
|
@@ -713,6 +1180,32 @@ function logAckFailure(params) {
|
|
|
713
1180
|
const target = params.target ? ` target=${params.target}` : "";
|
|
714
1181
|
params.log(`${params.channel} ack cleanup failed${target}: ${String(params.error)}`);
|
|
715
1182
|
}
|
|
1183
|
+
async function submitGameActionWithLog(action, meta, account, token, log, source) {
|
|
1184
|
+
if (!account.gatewayUrl || !account.agentId) {
|
|
1185
|
+
log?.error?.(`[GAME-ACTION] submit skipped: missing gatewayUrl/agentId eventId=${meta.eventId}`);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
log?.info?.(
|
|
1189
|
+
`[GAME-ACTION] submit start source=${source} eventType=${meta.eventType} actionType=${action.actionType} gameId=${meta.gameId} turnSeq=${meta.turnSeq} eventId=${meta.eventId}`
|
|
1190
|
+
);
|
|
1191
|
+
const result = await submitGameAction({
|
|
1192
|
+
gatewayUrl: account.gatewayUrl,
|
|
1193
|
+
token,
|
|
1194
|
+
agentId: String(account.agentId),
|
|
1195
|
+
gameId: meta.gameId,
|
|
1196
|
+
actionType: action.actionType,
|
|
1197
|
+
actionData: action.actionData,
|
|
1198
|
+
turnSeq: meta.turnSeq,
|
|
1199
|
+
eventId: meta.eventId,
|
|
1200
|
+
traceId: meta.traceId,
|
|
1201
|
+
log
|
|
1202
|
+
});
|
|
1203
|
+
if (!result.success) {
|
|
1204
|
+
log?.error?.(
|
|
1205
|
+
`[GAME-ACTION] submit failed source=${source} eventId=${meta.eventId} attempts=${result.attempts} status=${result.status ?? "n/a"} err=${result.error ?? "unknown"}`
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
716
1209
|
var runtimeClients = /* @__PURE__ */ new Map();
|
|
717
1210
|
function setRuntimeClient(accountKey, client) {
|
|
718
1211
|
runtimeClients.set(accountKey, client);
|
|
@@ -866,6 +1359,10 @@ var coolclawChannelPlugin = createChatChannelPlugin({
|
|
|
866
1359
|
}), channel: "coolclaw", reason: "runtime not available; skipping dispatch" });
|
|
867
1360
|
return;
|
|
868
1361
|
}
|
|
1362
|
+
const isGameEvent = envelope.metadata?.gameEvent === true;
|
|
1363
|
+
const gameMeta = isGameEvent ? envelope.metadata : null;
|
|
1364
|
+
let gameSubmitted = false;
|
|
1365
|
+
let gameFallbackReason = null;
|
|
869
1366
|
try {
|
|
870
1367
|
const isGroup = envelope.conversationId.startsWith("group:");
|
|
871
1368
|
const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
|
|
@@ -889,12 +1386,14 @@ var coolclawChannelPlugin = createChatChannelPlugin({
|
|
|
889
1386
|
ctx.cfg.session?.store,
|
|
890
1387
|
{ agentId: route.agentId }
|
|
891
1388
|
);
|
|
892
|
-
const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : "unknown";
|
|
1389
|
+
const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : isGameEvent ? `game:${gameMeta.gameId}` : "unknown";
|
|
893
1390
|
let deliveryTarget;
|
|
894
1391
|
if (envelope.group) {
|
|
895
1392
|
deliveryTarget = `coolclaw:group:${envelope.group.groupId}`;
|
|
896
1393
|
} else if (envelope.sender) {
|
|
897
1394
|
deliveryTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
|
|
1395
|
+
} else if (isGameEvent) {
|
|
1396
|
+
deliveryTarget = `coolclaw:agent:${account.agentId}`;
|
|
898
1397
|
} else {
|
|
899
1398
|
deliveryTarget = normalizeCoolclawTarget(envelope.conversationId);
|
|
900
1399
|
}
|
|
@@ -928,12 +1427,30 @@ var coolclawChannelPlugin = createChatChannelPlugin({
|
|
|
928
1427
|
ctx.log?.warn(`recordInboundSession failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
929
1428
|
}
|
|
930
1429
|
});
|
|
1430
|
+
const gameBuffer = [];
|
|
931
1431
|
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
932
1432
|
ctx: ctxPayload,
|
|
933
1433
|
cfg: ctx.cfg,
|
|
934
1434
|
dispatcherOptions: {
|
|
935
1435
|
deliver: async (payload) => {
|
|
936
1436
|
if (!payload.text) return;
|
|
1437
|
+
if (isGameEvent && gameMeta) {
|
|
1438
|
+
if (gameSubmitted) return;
|
|
1439
|
+
gameBuffer.push(String(payload.text));
|
|
1440
|
+
const full = gameBuffer.join("");
|
|
1441
|
+
const parsed = parseAgentAction(full);
|
|
1442
|
+
if ("error" in parsed) return;
|
|
1443
|
+
gameSubmitted = true;
|
|
1444
|
+
await submitGameActionWithLog(
|
|
1445
|
+
parsed,
|
|
1446
|
+
gameMeta,
|
|
1447
|
+
account,
|
|
1448
|
+
token,
|
|
1449
|
+
ctx.log,
|
|
1450
|
+
"llm"
|
|
1451
|
+
);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
937
1454
|
try {
|
|
938
1455
|
let replyTarget;
|
|
939
1456
|
if (envelope.group) {
|
|
@@ -953,8 +1470,36 @@ var coolclawChannelPlugin = createChatChannelPlugin({
|
|
|
953
1470
|
}
|
|
954
1471
|
}
|
|
955
1472
|
});
|
|
1473
|
+
if (isGameEvent && gameMeta && !gameSubmitted) {
|
|
1474
|
+
gameFallbackReason = "no_valid_action_in_llm_output";
|
|
1475
|
+
}
|
|
956
1476
|
} catch (err) {
|
|
957
|
-
|
|
1477
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1478
|
+
ctx.log?.error(`Inbound dispatch error: ${errMsg}`);
|
|
1479
|
+
if (isGameEvent && gameMeta && !gameSubmitted) {
|
|
1480
|
+
gameFallbackReason = `dispatch_error: ${errMsg}`;
|
|
1481
|
+
}
|
|
1482
|
+
} finally {
|
|
1483
|
+
if (isGameEvent && gameMeta && !gameSubmitted) {
|
|
1484
|
+
try {
|
|
1485
|
+
const fb = fallbackActionFor(gameMeta.eventType, gameMeta.eventData);
|
|
1486
|
+
ctx.log?.warn?.(
|
|
1487
|
+
`[GAME-ACTION] parse fallback eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} reason=${gameFallbackReason ?? "unknown"}`
|
|
1488
|
+
);
|
|
1489
|
+
await submitGameActionWithLog(
|
|
1490
|
+
fb,
|
|
1491
|
+
gameMeta,
|
|
1492
|
+
account,
|
|
1493
|
+
token,
|
|
1494
|
+
ctx.log,
|
|
1495
|
+
"fallback"
|
|
1496
|
+
);
|
|
1497
|
+
} catch (fbErr) {
|
|
1498
|
+
ctx.log?.error?.(
|
|
1499
|
+
`[GAME-ACTION] fallback submit threw eventId=${gameMeta.eventId} err=${fbErr instanceof Error ? fbErr.message : String(fbErr)}`
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
958
1503
|
}
|
|
959
1504
|
},
|
|
960
1505
|
sendAck: async (ackFrame) => {
|
package/dist/cli-metadata.js
CHANGED
package/dist/index.js
CHANGED
package/dist/setup-entry.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coolclaw/coolclaw",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "OpenClaw native channel plugin for Riddle/CoolClaw chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"runtimeSetupEntry": "./dist/setup-entry.js",
|
|
60
60
|
"install": {
|
|
61
61
|
"npmSpec": "@coolclaw/coolclaw",
|
|
62
|
-
"expectedIntegrity": "sha512-
|
|
62
|
+
"expectedIntegrity": "sha512-jZn9gMqpzKzbqQBz7GMFkTUdqUBtV1SZRYnYKP9eV/75xbx1RkoCADvhLF5LviH3JWsYd3jzZEV/6fy1H5FNTw==",
|
|
63
63
|
"defaultChoice": "npm",
|
|
64
64
|
"minHostVersion": ">=2026.4.24"
|
|
65
65
|
},
|