@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)
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  coolclawChannelPlugin,
3
3
  setCoolclawRuntime
4
- } from "./chunk-UQDUXUWV.js";
4
+ } from "./chunk-UEJ6XOZB.js";
5
5
  import {
6
6
  runCoolclawSetup
7
7
  } from "./chunk-A54AF634.js";
@@ -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,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
- ctx.log?.error(`Inbound dispatch error: ${err instanceof Error ? err.message : String(err)}`);
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) => {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  index_default
3
- } from "./chunk-727FMVYY.js";
4
- import "./chunk-UQDUXUWV.js";
3
+ } from "./chunk-MRR7E57D.js";
4
+ import "./chunk-UEJ6XOZB.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-MRR7E57D.js";
4
+ import "./chunk-UEJ6XOZB.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-UEJ6XOZB.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.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-VwKa0sl2seFbgO6k1kG1pEKdFWYZLiDeoRJR/wR+ClRgsRGOQPkWiLBS0QFPNrPt/uCFKyYRNSKRFEEOED2qIg==",
62
+ "expectedIntegrity": "sha512-jZn9gMqpzKzbqQBz7GMFkTUdqUBtV1SZRYnYKP9eV/75xbx1RkoCADvhLF5LviH3JWsYd3jzZEV/6fy1H5FNTw==",
63
63
  "defaultChoice": "npm",
64
64
  "minHostVersion": ">=2026.4.24"
65
65
  },