@coolclaw/coolclaw 0.3.3 → 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 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 null;
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,12 +718,199 @@ 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 {
443
910
  constructor(options) {
444
911
  this.options = options;
445
912
  }
913
+ options;
446
914
  socket;
447
915
  heartbeatTimer;
448
916
  reconnectTimer;
@@ -713,6 +1181,32 @@ function logAckFailure(params) {
713
1181
  const target = params.target ? ` target=${params.target}` : "";
714
1182
  params.log(`${params.channel} ack cleanup failed${target}: ${String(params.error)}`);
715
1183
  }
1184
+ async function submitGameActionWithLog(action, meta, account, token, log, source) {
1185
+ if (!account.gatewayUrl || !account.agentId) {
1186
+ log?.error?.(`[GAME-ACTION] submit skipped: missing gatewayUrl/agentId eventId=${meta.eventId}`);
1187
+ return;
1188
+ }
1189
+ log?.info?.(
1190
+ `[GAME-ACTION] submit start source=${source} eventType=${meta.eventType} actionType=${action.actionType} gameId=${meta.gameId} turnSeq=${meta.turnSeq} eventId=${meta.eventId}`
1191
+ );
1192
+ const result = await submitGameAction({
1193
+ gatewayUrl: account.gatewayUrl,
1194
+ token,
1195
+ agentId: String(account.agentId),
1196
+ gameId: meta.gameId,
1197
+ actionType: action.actionType,
1198
+ actionData: action.actionData,
1199
+ turnSeq: meta.turnSeq,
1200
+ eventId: meta.eventId,
1201
+ traceId: meta.traceId,
1202
+ log
1203
+ });
1204
+ if (!result.success) {
1205
+ log?.error?.(
1206
+ `[GAME-ACTION] submit failed source=${source} eventId=${meta.eventId} attempts=${result.attempts} status=${result.status ?? "n/a"} err=${result.error ?? "unknown"}`
1207
+ );
1208
+ }
1209
+ }
716
1210
  var runtimeClients = /* @__PURE__ */ new Map();
717
1211
  function setRuntimeClient(accountKey, client) {
718
1212
  runtimeClients.set(accountKey, client);
@@ -866,6 +1360,10 @@ var coolclawChannelPlugin = createChatChannelPlugin({
866
1360
  }), channel: "coolclaw", reason: "runtime not available; skipping dispatch" });
867
1361
  return;
868
1362
  }
1363
+ const isGameEvent = envelope.metadata?.gameEvent === true;
1364
+ const gameMeta = isGameEvent ? envelope.metadata : null;
1365
+ let gameSubmitted = false;
1366
+ let gameFallbackReason = null;
869
1367
  try {
870
1368
  const isGroup = envelope.conversationId.startsWith("group:");
871
1369
  const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
@@ -889,17 +1387,34 @@ var coolclawChannelPlugin = createChatChannelPlugin({
889
1387
  ctx.cfg.session?.store,
890
1388
  { agentId: route.agentId }
891
1389
  );
892
- const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : "unknown";
1390
+ const senderLabel = envelope.sender ? `${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}` : isGameEvent ? `game:${gameMeta.gameId}` : "unknown";
893
1391
  let deliveryTarget;
894
1392
  if (envelope.group) {
895
1393
  deliveryTarget = `coolclaw:group:${envelope.group.groupId}`;
896
1394
  } else if (envelope.sender) {
897
1395
  deliveryTarget = `coolclaw:${envelope.sender.userType.toLowerCase()}:${envelope.sender.userId}`;
1396
+ } else if (isGameEvent) {
1397
+ deliveryTarget = `coolclaw:agent:${account.agentId}`;
898
1398
  } else {
899
1399
  deliveryTarget = normalizeCoolclawTarget(envelope.conversationId);
900
1400
  }
901
1401
  const agentHint = envelope.metadata?.agentHint;
902
1402
  const bodyForAgent = agentHint ? envelope.text + agentHint : envelope.text;
1403
+ if (typeof runtime.channel.reply?.finalizeInboundContext !== "function") {
1404
+ throw new Error(
1405
+ "CoolClaw requires runtime.channel.reply.finalizeInboundContext. Please upgrade OpenClaw to >=2026.3.22."
1406
+ );
1407
+ }
1408
+ if (typeof runtime.channel.reply?.dispatchReplyWithBufferedBlockDispatcher !== "function") {
1409
+ throw new Error(
1410
+ "CoolClaw requires runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher. Please upgrade OpenClaw to >=2026.3.22."
1411
+ );
1412
+ }
1413
+ if (typeof runtime.channel.session?.recordInboundSession !== "function") {
1414
+ throw new Error(
1415
+ "CoolClaw requires runtime.channel.session.recordInboundSession. Please upgrade OpenClaw to >=2026.3.22."
1416
+ );
1417
+ }
903
1418
  const ctxPayload = runtime.channel.reply.finalizeInboundContext({
904
1419
  Body: envelope.text,
905
1420
  BodyForAgent: bodyForAgent,
@@ -928,12 +1443,30 @@ var coolclawChannelPlugin = createChatChannelPlugin({
928
1443
  ctx.log?.warn(`recordInboundSession failed: ${err instanceof Error ? err.message : String(err)}`);
929
1444
  }
930
1445
  });
1446
+ const gameBuffer = [];
931
1447
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
932
1448
  ctx: ctxPayload,
933
1449
  cfg: ctx.cfg,
934
1450
  dispatcherOptions: {
935
1451
  deliver: async (payload) => {
936
1452
  if (!payload.text) return;
1453
+ if (isGameEvent && gameMeta) {
1454
+ if (gameSubmitted) return;
1455
+ gameBuffer.push(String(payload.text));
1456
+ const full = gameBuffer.join("");
1457
+ const parsed = parseAgentAction(full);
1458
+ if ("error" in parsed) return;
1459
+ gameSubmitted = true;
1460
+ await submitGameActionWithLog(
1461
+ parsed,
1462
+ gameMeta,
1463
+ account,
1464
+ token,
1465
+ ctx.log,
1466
+ "llm"
1467
+ );
1468
+ return;
1469
+ }
937
1470
  try {
938
1471
  let replyTarget;
939
1472
  if (envelope.group) {
@@ -953,8 +1486,36 @@ var coolclawChannelPlugin = createChatChannelPlugin({
953
1486
  }
954
1487
  }
955
1488
  });
1489
+ if (isGameEvent && gameMeta && !gameSubmitted) {
1490
+ gameFallbackReason = "no_valid_action_in_llm_output";
1491
+ }
956
1492
  } catch (err) {
957
- ctx.log?.error(`Inbound dispatch error: ${err instanceof Error ? err.message : String(err)}`);
1493
+ const errMsg = err instanceof Error ? err.message : String(err);
1494
+ ctx.log?.error(`Inbound dispatch error: ${errMsg}`);
1495
+ if (isGameEvent && gameMeta && !gameSubmitted) {
1496
+ gameFallbackReason = `dispatch_error: ${errMsg}`;
1497
+ }
1498
+ } finally {
1499
+ if (isGameEvent && gameMeta && !gameSubmitted) {
1500
+ try {
1501
+ const fb = fallbackActionFor(gameMeta.eventType, gameMeta.eventData);
1502
+ ctx.log?.warn?.(
1503
+ `[GAME-ACTION] parse fallback eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} reason=${gameFallbackReason ?? "unknown"}`
1504
+ );
1505
+ await submitGameActionWithLog(
1506
+ fb,
1507
+ gameMeta,
1508
+ account,
1509
+ token,
1510
+ ctx.log,
1511
+ "fallback"
1512
+ );
1513
+ } catch (fbErr) {
1514
+ ctx.log?.error?.(
1515
+ `[GAME-ACTION] fallback submit threw eventId=${gameMeta.eventId} err=${fbErr instanceof Error ? fbErr.message : String(fbErr)}`
1516
+ );
1517
+ }
1518
+ }
958
1519
  }
959
1520
  },
960
1521
  sendAck: async (ackFrame) => {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  coolclawChannelPlugin,
3
3
  setCoolclawRuntime
4
- } from "./chunk-UQDUXUWV.js";
4
+ } from "./chunk-IPHJPPD4.js";
5
5
  import {
6
6
  runCoolclawSetup
7
7
  } from "./chunk-A54AF634.js";
@@ -62,29 +62,41 @@ function splitCsv(value) {
62
62
  }
63
63
 
64
64
  // src/compat.ts
65
- var SUPPORTED_HOST_MIN = "2026.4.24";
65
+ var SUPPORTED_HOST_MIN = "2026.3.22";
66
+ var MAX_TESTED_VERSION = "2026.5.7";
67
+ function parseVersion(v) {
68
+ const clean = v.split("-")[0].replace(/\s*\(.*\)/, "").trim();
69
+ const [y, m, d] = clean.split(".").map(Number);
70
+ return [y || 0, m || 0, d || 0];
71
+ }
72
+ function cmp(a, b) {
73
+ const [ay, am, ad] = parseVersion(a);
74
+ const [by, bm, bd] = parseVersion(b);
75
+ if (ay !== by) return ay > by ? 1 : -1;
76
+ if (am !== bm) return am > bm ? 1 : -1;
77
+ if (ad !== bd) return ad > bd ? 1 : -1;
78
+ return 0;
79
+ }
66
80
  function assertHostCompatibility(hostVersion) {
67
81
  if (!hostVersion || hostVersion === "unknown") return;
68
- if (isHostVersionSupported(hostVersion)) return;
82
+ if (cmp(hostVersion, SUPPORTED_HOST_MIN) >= 0) return;
69
83
  throw new Error(
70
84
  `This version of @coolclaw/coolclaw requires OpenClaw >=${SUPPORTED_HOST_MIN}, but found ${hostVersion}. Please upgrade OpenClaw:
71
85
  npm install -g openclaw@latest
72
86
  Then reinstall the plugin:
73
87
  openclaw plugins install @coolclaw/coolclaw
74
88
 
75
- Or use the one-command installer (requires @coolclaw/coolclaw-cli published to npm):
89
+ Or use the one-command installer:
76
90
  npx @coolclaw/coolclaw-cli install`
77
91
  );
78
92
  }
79
- function isHostVersionSupported(hostVersion) {
80
- const clean = hostVersion.split("-")[0];
81
- const host = clean.split(".").map(Number);
82
- const min = SUPPORTED_HOST_MIN.split(".").map(Number);
83
- for (let i = 0; i < 3; i++) {
84
- if (host[i] > min[i]) return true;
85
- if (host[i] < min[i]) return false;
93
+ function advisoryHostCompatibility(hostVersion) {
94
+ if (!hostVersion || hostVersion === "unknown") return;
95
+ if (cmp(hostVersion, MAX_TESTED_VERSION) > 0) {
96
+ console.warn(
97
+ `[coolclaw] Host version ${hostVersion} is newer than the latest tested version (${MAX_TESTED_VERSION}). Proceeding without version gate; please report incompatibilities.`
98
+ );
86
99
  }
87
- return true;
88
100
  }
89
101
 
90
102
  // index.ts
@@ -112,6 +124,7 @@ var entry = defineChannelPluginEntry({
112
124
  },
113
125
  registerFull(api) {
114
126
  assertHostCompatibility(api.runtime?.version);
127
+ advisoryHostCompatibility(api.runtime?.version);
115
128
  setCoolclawRuntime(api.runtime);
116
129
  }
117
130
  });
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  index_default
3
- } from "./chunk-727FMVYY.js";
4
- import "./chunk-UQDUXUWV.js";
3
+ } from "./chunk-NY6UBBGB.js";
4
+ import "./chunk-IPHJPPD4.js";
5
5
  import "./chunk-A54AF634.js";
6
6
  import "./chunk-Q3NF4NWE.js";
7
7
 
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  index_default
3
- } from "./chunk-727FMVYY.js";
4
- import "./chunk-UQDUXUWV.js";
3
+ } from "./chunk-NY6UBBGB.js";
4
+ import "./chunk-IPHJPPD4.js";
5
5
  import "./chunk-A54AF634.js";
6
6
  import "./chunk-Q3NF4NWE.js";
7
7
  export {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  coolclawChannelPlugin
3
- } from "./chunk-UQDUXUWV.js";
3
+ } from "./chunk-IPHJPPD4.js";
4
4
  import "./chunk-Q3NF4NWE.js";
5
5
 
6
6
  // setup-entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coolclaw/coolclaw",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "OpenClaw native channel plugin for Riddle/CoolClaw chat.",
5
5
  "type": "module",
6
6
  "files": [
@@ -41,7 +41,7 @@
41
41
  "vitest": "latest"
42
42
  },
43
43
  "peerDependencies": {
44
- "openclaw": ">=2026.4.24"
44
+ "openclaw": ">=2026.3.22 <2027"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "openclaw": {
@@ -59,9 +59,9 @@
59
59
  "runtimeSetupEntry": "./dist/setup-entry.js",
60
60
  "install": {
61
61
  "npmSpec": "@coolclaw/coolclaw",
62
- "expectedIntegrity": "sha512-VwKa0sl2seFbgO6k1kG1pEKdFWYZLiDeoRJR/wR+ClRgsRGOQPkWiLBS0QFPNrPt/uCFKyYRNSKRFEEOED2qIg==",
62
+ "expectedIntegrity": "sha512-jZn9gMqpzKzbqQBz7GMFkTUdqUBtV1SZRYnYKP9eV/75xbx1RkoCADvhLF5LviH3JWsYd3jzZEV/6fy1H5FNTw==",
63
63
  "defaultChoice": "npm",
64
- "minHostVersion": ">=2026.4.24"
64
+ "minHostVersion": ">=2026.3.22"
65
65
  },
66
66
  "channel": {
67
67
  "id": "coolclaw",
@@ -76,13 +76,15 @@
76
76
  }
77
77
  },
78
78
  "compat": {
79
- "pluginApi": ">=2026.4.24",
80
- "minGatewayVersion": "2026.4.24",
81
- "sdkImports": ["openclaw/plugin-sdk/core"]
79
+ "pluginApi": ">=2026.3.22",
80
+ "minGatewayVersion": "2026.3.22",
81
+ "sdkImports": [
82
+ "openclaw/plugin-sdk/core"
83
+ ]
82
84
  },
83
85
  "build": {
84
- "openclawVersion": "2026.5.6",
86
+ "openclawVersion": "2026.5.7",
85
87
  "pluginSdkVersion": "2026.4.29"
86
88
  }
87
89
  }
88
- }
90
+ }