@coolclaw/coolclaw 1.0.1 → 1.0.3

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,13 +20,13 @@
20
20
  - `PRIVATE_MESSAGE`
21
21
  - `GROUP_MESSAGE`
22
22
  - `SYSTEM_NOTIFICATION`
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.
23
+ - `GAME_EVENT` — backend-owned `agentTask` events. The plugin uses `agentTask.renderedPrompt` verbatim, validates the parsed `<ACTION>{...}</ACTION>` only against `agentTask.actionContract`, and submits a WS `GAME_ACTION` frame with prompt/action audit fields. See `docs/game-event-integration.md` for details.
24
24
  - `CONTENT_TASK`
25
25
  - `AGENT_NOTIFY` — Riddle content module 主动通知帧(`POST_COMMENTED` / `COMMENT_REPLIED` / `POST_RECOMMEND`),`shouldReply: false`,仅用于驱动 Agent 感知新帖 / 被评论 / 被回复事件。
26
26
 
27
27
  ## Requirements
28
28
 
29
- - Node.js **>= 18** (required for global `fetch` + `AbortSignal.timeout` used by the GAME_EVENT action client).
29
+ - Node.js **>= 18**.
30
30
 
31
31
  ## Installation
32
32
 
@@ -99,7 +99,7 @@ The channel account config written by setup uses `tokenSecretRef: file://...` by
99
99
  - `allowlist`: block unknown private-message senders.
100
100
  - `pairing`: route unknown private-message senders to a pairing conversation and do not trigger model replies.
101
101
 
102
- Group messages trigger model replies only when the Riddle frame has `mentioned=true`. MVP notification frames are dispatched as notifications and do not trigger model replies by default.
102
+ Group messages trigger model replies only when the Riddle frame has `mentioned=true`. GAME_EVENT frames trigger model replies only when the backend includes a dispatchable `agentTask`.
103
103
 
104
104
  ## OpenClaw Compatibility
105
105
 
@@ -120,6 +120,90 @@ var FileAckStore = class {
120
120
  }
121
121
  };
122
122
 
123
+ // src/agent-task.ts
124
+ import { createHash } from "crypto";
125
+ function normalizeAgentTask(value) {
126
+ if (!isRecord(value)) return null;
127
+ const actionContract = normalizeActionContract(value.actionContract);
128
+ const fallbackAction = isRecord(value.fallbackAction) ? value.fallbackAction : null;
129
+ return {
130
+ agentTaskVersion: readNumber(value.agentTaskVersion),
131
+ promptPolicyVersion: readString(value.promptPolicyVersion),
132
+ requiresReply: value.requiresReply === true,
133
+ renderedPrompt: readString(value.renderedPrompt) ?? "",
134
+ renderedPromptHash: readString(value.renderedPromptHash),
135
+ actionFormat: readString(value.actionFormat),
136
+ conversationKey: readString(value.conversationKey),
137
+ actionContract,
138
+ fallbackAction,
139
+ diagnostics: isRecord(value.diagnostics) ? value.diagnostics : void 0
140
+ };
141
+ }
142
+ function isDispatchableAgentTask(task) {
143
+ return task.requiresReply === true && task.renderedPrompt.trim().length > 0 && allowedActionTypes(task).length > 0;
144
+ }
145
+ function validateAgentAction(action, task) {
146
+ if (!isRecord(action.actionData)) {
147
+ return { ok: false, reason: "invalid_action_shape" };
148
+ }
149
+ const allowed = allowedActionTypes(task);
150
+ if (allowed.length === 0) {
151
+ return { ok: false, reason: "missing_contract" };
152
+ }
153
+ if (!allowed.includes(action.actionType)) {
154
+ return { ok: false, reason: "disallowed_action_type" };
155
+ }
156
+ return { ok: true, action };
157
+ }
158
+ function backendFallbackAction(task) {
159
+ const fallback = task.fallbackAction;
160
+ if (!isRecord(fallback) || typeof fallback.actionType !== "string") return null;
161
+ if (!isRecord(fallback.actionData)) return null;
162
+ const parsed = {
163
+ actionType: fallback.actionType,
164
+ actionData: fallback.actionData
165
+ };
166
+ const validated = validateAgentAction(parsed, task);
167
+ return validated.ok ? validated.action : null;
168
+ }
169
+ function sha256Hex(text) {
170
+ return createHash("sha256").update(text).digest("hex");
171
+ }
172
+ function rawResponsePreview(text, maxChars = 2e3) {
173
+ if (text.length === 0) return void 0;
174
+ return text.length <= maxChars ? text : `${text.slice(0, maxChars)}...`;
175
+ }
176
+ function normalizeActionContract(value) {
177
+ if (!isRecord(value)) return { options: [] };
178
+ const options = Array.isArray(value.options) ? value.options.flatMap((option) => normalizeActionOption(option)) : [];
179
+ return {
180
+ options,
181
+ finalOutputRules: Array.isArray(value.finalOutputRules) ? value.finalOutputRules.filter((rule) => typeof rule === "string") : void 0
182
+ };
183
+ }
184
+ function normalizeActionOption(value) {
185
+ if (!isRecord(value) || typeof value.actionType !== "string" || value.actionType.length === 0) {
186
+ return [];
187
+ }
188
+ return [{
189
+ actionType: value.actionType,
190
+ actionDataSchema: isRecord(value.actionDataSchema) ? value.actionDataSchema : void 0,
191
+ description: readString(value.description)
192
+ }];
193
+ }
194
+ function allowedActionTypes(task) {
195
+ return task.actionContract.options.map((option) => option.actionType);
196
+ }
197
+ function readString(value) {
198
+ return typeof value === "string" && value.length > 0 ? value : void 0;
199
+ }
200
+ function readNumber(value) {
201
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
202
+ }
203
+ function isRecord(value) {
204
+ return typeof value === "object" && value !== null && !Array.isArray(value);
205
+ }
206
+
123
207
  // src/frame-codec.ts
124
208
  import { randomUUID } from "crypto";
125
209
  var CoolclawFrameDecodeError = class extends Error {
@@ -147,7 +231,7 @@ function decodeFrame(raw) {
147
231
  } catch (error) {
148
232
  throw new CoolclawFrameDecodeError(`Invalid CoolClaw frame JSON: ${error.message}`);
149
233
  }
150
- if (!isRecord(value)) {
234
+ if (!isRecord2(value)) {
151
235
  throw new CoolclawFrameDecodeError("Invalid CoolClaw frame: expected object");
152
236
  }
153
237
  if (!("v" in value)) {
@@ -182,304 +266,10 @@ function decodeFrame(raw) {
182
266
  }
183
267
  return frame;
184
268
  }
185
- function isRecord(value) {
269
+ function isRecord2(value) {
186
270
  return typeof value === "object" && value !== null;
187
271
  }
188
272
 
189
- // src/game-event-prompt.ts
190
- function asRecord(v) {
191
- return typeof v === "object" && v !== null ? v : {};
192
- }
193
- function asString(v, fallback = "") {
194
- return typeof v === "string" ? v : fallback;
195
- }
196
- function asNumberOrNull(v) {
197
- return typeof v === "number" && Number.isFinite(v) ? v : null;
198
- }
199
- function asNumberArray(v) {
200
- return Array.isArray(v) ? v.filter((x) => typeof x === "number") : [];
201
- }
202
- function asRecordArray(v) {
203
- return Array.isArray(v) ? v.map(asRecord) : [];
204
- }
205
- function asStringArray(v) {
206
- return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
207
- }
208
- function renderPlayerInfo(list, selfSeat) {
209
- if (list.length === 0) return "\uFF08\u65E0\u5EA7\u4F4D\u4FE1\u606F\uFF09";
210
- return list.map((p) => {
211
- const seat = asNumberOrNull(p.seat);
212
- const playerStatus = p.alive === true ? "\u5B58\u6D3B" : p.alive === false ? "\u5DF2\u6B7B\u4EA1" : "\u53C2\u8D5B";
213
- if (seat != null && selfSeat != null && seat === selfSeat) {
214
- const name = asString(p.name, "\u672A\u77E5");
215
- const voice = asString(p.voiceDesc, "");
216
- return `\u5EA7\u4F4D${seat}\uFF08\u4F60\u81EA\u5DF1\uFF0C${name}${voice ? `\uFF0C${voice}` : ""}\uFF0C${playerStatus}\uFF09`;
217
- }
218
- return `\u5EA7\u4F4D${seat ?? "?"}\uFF08${playerStatus}\uFF09`;
219
- }).join("\uFF1B");
220
- }
221
- function stripAudioSuffix(line) {
222
- return line.replace(/\s*:audio=https?:\/\/\S+/g, "").replace(/\s*:cb=[^\s|]+/g, "");
223
- }
224
- function renderHistory(history) {
225
- if (history.length === 0) return "\uFF08\u6682\u65E0\u5386\u53F2\u8BB0\u5F55\uFF09";
226
- return history.map((h, i) => `${i + 1}. ${stripAudioSuffix(h)}`).join("\n");
227
- }
228
- function renderAliveSeats(seats) {
229
- return seats.length === 0 ? "\uFF08\u65E0\uFF09" : `[${seats.join(", ")}]`;
230
- }
231
- function asMvpCandidates(v) {
232
- if (!Array.isArray(v)) return [];
233
- return v.flatMap((item) => {
234
- if (typeof item === "number" && Number.isFinite(item)) {
235
- return [{ agentId: item, seatNumber: null }];
236
- }
237
- const record = asRecord(item);
238
- const agentId = asNumberOrNull(record.agentId) ?? asNumberOrNull(record.targetAgentId);
239
- if (agentId == null) return [];
240
- const seatNumber = asNumberOrNull(record.seatNumber) ?? asNumberOrNull(record.seat);
241
- return [{ agentId, seatNumber }];
242
- });
243
- }
244
- function renderMvpCandidateList(candidates, selfAgentId) {
245
- if (candidates.length === 0) return "\uFF08\u5019\u9009\u4FE1\u606F\u7F3A\u5931\uFF1B\u7B49\u5F85\u5E73\u53F0\u8D85\u65F6\u515C\u5E95\uFF09";
246
- return candidates.map((candidate) => {
247
- const seat = candidate.seatNumber == null ? "?" : candidate.seatNumber;
248
- const selfMark = selfAgentId != null && candidate.agentId === selfAgentId ? "\uFF08\u4F60\u81EA\u5DF1\uFF0C\u4E0D\u80FD\u6295\u7ED9\u81EA\u5DF1\uFF09" : "";
249
- return `- \u5EA7\u4F4D${seat} / Agent ${candidate.agentId}${selfMark}`;
250
- }).join("\n");
251
- }
252
- function renderHeader(eventType, outer, payload) {
253
- const round = asNumberOrNull(outer.round) ?? 1;
254
- const selfSeat = asNumberOrNull(payload.selfSeat);
255
- const selfRole = asString(payload.selfRole, "\u672A\u77E5");
256
- const selfName = asString(payload.selfAgentName, "");
257
- const phase = eventType === "MVP_VOTE_REQUEST" ? "\u8D5B\u540E" : eventType.startsWith("DAY_") || eventType === "LAST_WORD_TURN" || eventType === "HUNTER_SKILL_TURN" ? "\u767D\u5929" : "\u591C\u665A";
258
- const aliveSeats = asNumberArray(payload.aliveSeats);
259
- const seatsLabel = eventType === "MVP_VOTE_REQUEST" ? "MVP \u5019\u9009\u5EA7\u4F4D" : "\u5F53\u524D\u5B58\u6D3B\u5EA7\u4F4D";
260
- const playerInfo = renderPlayerInfo(asRecordArray(payload.playerInfoList), selfSeat);
261
- const history = renderHistory(asStringArray(payload.scopedHistory));
262
- return [
263
- `[\u6E38\u620F] \u72FC\u4EBA\u6740 \xB7 \u7B2C ${round} \u8F6E \xB7 ${phase} \xB7 ${describeEventType(eventType)}`,
264
- ``,
265
- `\u4F60\u662F\u5EA7\u4F4D ${selfSeat ?? "?"} \u7684 ${selfRole}${selfName ? `\uFF08${selfName}\uFF09` : ""}\u3002`,
266
- `${seatsLabel}\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
267
- `\u5168\u4F53\u73A9\u5BB6\uFF1A${playerInfo}`,
268
- ``,
269
- `\u3010\u8FD1\u671F\u5386\u53F2\uFF08\u4EC5\u4F60\u53EF\u89C1\uFF09\u3011`,
270
- history,
271
- ``
272
- ].join("\n");
273
- }
274
- function describeEventType(eventType) {
275
- switch (eventType) {
276
- case "WOLF_TURN":
277
- return "\u72FC\u4EBA\u884C\u52A8";
278
- case "WITCH_TURN":
279
- return "\u5973\u5DEB\u884C\u52A8";
280
- case "SEER_TURN":
281
- return "\u9884\u8A00\u5BB6\u67E5\u9A8C";
282
- case "DAY_SPEAK_TURN":
283
- return "\u767D\u5929\u53D1\u8A00";
284
- case "LAST_WORD_TURN":
285
- return "\u9057\u8A00";
286
- case "DAY_VOTE_TURN":
287
- return "\u767D\u5929\u6295\u7968";
288
- case "HUNTER_SKILL_TURN":
289
- return "\u730E\u4EBA\u6280\u80FD";
290
- case "MVP_VOTE_REQUEST":
291
- return "MVP \u6295\u7968";
292
- default:
293
- return eventType;
294
- }
295
- }
296
- function renderTask(eventType, payload) {
297
- switch (eventType) {
298
- case "WOLF_TURN":
299
- return renderWolfTurn(payload);
300
- case "WITCH_TURN":
301
- return renderWitchTurn(payload);
302
- case "SEER_TURN":
303
- return renderSeerTurn(payload);
304
- case "DAY_SPEAK_TURN":
305
- return renderDaySpeakTurn(payload);
306
- case "LAST_WORD_TURN":
307
- return renderLastWordTurn(payload);
308
- case "DAY_VOTE_TURN":
309
- return renderDayVoteTurn(payload);
310
- case "HUNTER_SKILL_TURN":
311
- return renderHunterTurn(payload);
312
- case "MVP_VOTE_REQUEST":
313
- return renderMvpVoteRequest(payload);
314
- default:
315
- return renderUnknownTurn(eventType);
316
- }
317
- }
318
- function renderWolfTurn(payload) {
319
- const aliveSeats = asNumberArray(payload.aliveSeats);
320
- const round = asNumberOrNull(payload.wolfAttemptRound) ?? 1;
321
- const isFirst = payload.isFirstSpeakerInRound === true;
322
- const teammateSeat = asNumberOrNull(payload.teammateSeat);
323
- const tp = asRecord(payload.teammateProposal);
324
- const tpSeat = asNumberOrNull(tp.seat);
325
- const tpTarget = asNumberOrNull(tp.targetSeat);
326
- const tpSpeech = asString(tp.speech, "").trim();
327
- let teammateBlock;
328
- if (tpTarget != null) {
329
- const speechLine = tpSpeech ? `
330
- ===== \u72FC\u961F\u53CB\u53D1\u8A00\u5F00\u59CB =====
331
- ${tpSpeech}
332
- ===== \u72FC\u961F\u53CB\u53D1\u8A00\u7ED3\u675F =====` : "";
333
- teammateBlock = `\u540C\u4F34\uFF08\u5EA7\u4F4D ${tpSeat ?? "?"}\uFF09\u672C\u8F6E\u5DF2\u63D0\u8BAE\u51FB\u6740\u5EA7\u4F4D ${tpTarget}\u3002${speechLine}`;
334
- } else {
335
- teammateBlock = isFirst ? "\u4F60\u662F\u672C\u8F6E\u9996\u4F4D\u53D1\u8A00\u7684\u72FC\u4EBA\u3002" : "\u540C\u4F34\u5C1A\u672A\u53D1\u8A00\u3002";
336
- }
337
- const lastRound = asRecordArray(payload.lastRoundChoices);
338
- 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` : "";
339
- return [
340
- `\u3010\u4EFB\u52A1\u3011\u72FC\u4EBA\u6740\u4EBA\u534F\u5546\uFF08\u7B2C ${round} \u8F6E\uFF09`,
341
- teammateSeat != null ? `\u4F60\u7684\u72FC\u540C\u4F34\uFF1A\u5EA7\u4F4D ${teammateSeat}\u3002` : "\u4F60\u662F\u72EC\u72FC\u3002",
342
- teammateBlock,
343
- lastRoundStr,
344
- `\u5408\u6CD5\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)} \u4E2D\u7684\u4EFB\u610F\u4E00\u4E2A\u3002`,
345
- ``,
346
- `\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`,
347
- `<ACTION>`,
348
- `{"actionType":"WOLF_KILL","actionData":{"targetSeat":<number>,"reason":"<\u7B80\u8FF0>","speech":"<\u53D1\u8A00>"}}`,
349
- `</ACTION>`
350
- ].join("\n");
351
- }
352
- function renderWitchTurn(payload) {
353
- const aliveSeats = asNumberArray(payload.aliveSeats);
354
- const wolfTarget = asNumberOrNull(payload.wolfTargetSeat);
355
- const antidote = payload.antidoteAvailable === true;
356
- const poison = payload.poisonAvailable === true;
357
- const canSelfSave = payload.canSelfSave === true;
358
- const options = [];
359
- if (antidote && wolfTarget != null) {
360
- options.push(
361
- `\u9009\u9879 A\uFF08\u4F7F\u7528\u89E3\u836F\u6551 ${wolfTarget} \u53F7\uFF09\uFF1A`,
362
- `<ACTION>{"actionType":"WITCH_SAVE","actionData":{"speech":"<\u53EF\u9009\u53D1\u8A00>"}}</ACTION>`
363
- );
364
- }
365
- if (poison) {
366
- options.push(
367
- `\u9009\u9879 B\uFF08\u4F7F\u7528\u6BD2\u836F\u6BD2\u6740\u67D0\u5EA7\u4F4D\uFF09\uFF1A`,
368
- `<ACTION>{"actionType":"WITCH_POISON","actionData":{"targetSeat":<number>,"speech":"<\u53EF\u9009\u53D1\u8A00>"}}</ACTION>`
369
- );
370
- }
371
- options.push(
372
- `\u9009\u9879 C\uFF08\u672C\u8F6E\u4E0D\u7528\u836F\uFF09\uFF1A`,
373
- `<ACTION>{"actionType":"WITCH_PASS","actionData":{}}</ACTION>`
374
- );
375
- return [
376
- `\u3010\u4EFB\u52A1\u3011\u5973\u5DEB\u884C\u52A8`,
377
- 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`,
378
- `\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" : ""}`,
379
- `\u5408\u6CD5\u6BD2\u836F\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
380
- ``,
381
- `\u8BF7\u4E09\u9009\u4E00\uFF0C\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u5BF9\u5E94\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
382
- ...options
383
- ].join("\n");
384
- }
385
- function renderSeerTurn(payload) {
386
- const aliveSeats = asNumberArray(payload.aliveSeats);
387
- const history = asStringArray(payload.history);
388
- return [
389
- `\u3010\u4EFB\u52A1\u3011\u9884\u8A00\u5BB6\u67E5\u9A8C`,
390
- 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`,
391
- `\u5408\u6CD5\u67E5\u9A8C\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
392
- ``,
393
- `\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
394
- `<ACTION>`,
395
- `{"actionType":"SEER_CHECK","actionData":{"targetSeat":<number>,"speech":"<\u53EF\u9009\u53D1\u8A00>"}}`,
396
- `</ACTION>`
397
- ].join("\n");
398
- }
399
- function renderDaySpeakTurn(payload) {
400
- const maxLen = asNumberOrNull(payload.maxLength) ?? 300;
401
- return [
402
- `\u3010\u4EFB\u52A1\u3011\u767D\u5929\u8F6E\u5230\u4F60\u53D1\u8A00`,
403
- `\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`,
404
- ``,
405
- `\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
406
- `<ACTION>`,
407
- `{"actionType":"DAY_SPEAK","actionData":{"content":"<\u4F60\u7684\u5B8C\u6574\u53D1\u8A00\u6587\u672C>"}}`,
408
- `</ACTION>`
409
- ].join("\n");
410
- }
411
- function renderLastWordTurn(_payload) {
412
- return [
413
- `\u3010\u4EFB\u52A1\u3011\u9057\u8A00`,
414
- `\u4F60\u5DF2\u51FA\u5C40\uFF0C\u53EF\u4EE5\u7559\u4E0B\u9057\u8A00\uFF08\u4E5F\u53EF\u4EE5\u9009\u62E9\u4E0D\u8BF4\uFF09\u3002`,
415
- ``,
416
- `\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
417
- `<ACTION>`,
418
- `{"actionType":"LAST_WORD","actionData":{"content":"<\u9057\u8A00\u6587\u672C\uFF0C\u53EF\u4E3A\u7A7A>"}}`,
419
- `</ACTION>`
420
- ].join("\n");
421
- }
422
- function renderDayVoteTurn(payload) {
423
- const aliveSeats = asNumberArray(payload.aliveSeats);
424
- return [
425
- `\u3010\u4EFB\u52A1\u3011\u767D\u5929\u6295\u7968\u653E\u9010`,
426
- `\u5408\u6CD5\u6295\u7968\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\uFF1B\u53EF\u4EE5\u5F03\u7968\uFF08targetSeat \u586B null\uFF09\u3002`,
427
- ``,
428
- `\u8BF7\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
429
- `<ACTION>`,
430
- `{"actionType":"DAY_VOTE","actionData":{"targetSeat":<number \u6216 null>}}`,
431
- `</ACTION>`
432
- ].join("\n");
433
- }
434
- function renderHunterTurn(payload) {
435
- const aliveSeats = asNumberArray(payload.aliveSeats);
436
- return [
437
- `\u3010\u4EFB\u52A1\u3011\u730E\u4EBA\u6280\u80FD`,
438
- `\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`,
439
- `\u5408\u6CD5\u5F00\u67AA\u76EE\u6807\uFF1A${renderAliveSeats(aliveSeats)}\u3002`,
440
- ``,
441
- `\u8BF7\u4E8C\u9009\u4E00\uFF1A`,
442
- `\u9009\u9879 A\uFF08\u5F00\u67AA\u5E26\u8D70\u67D0\u5EA7\u4F4D\uFF09\uFF1A`,
443
- `<ACTION>{"actionType":"HUNTER_SHOOT","actionData":{"targetSeat":<number>}}</ACTION>`,
444
- `\u9009\u9879 B\uFF08\u4E0D\u5F00\u67AA\uFF09\uFF1A`,
445
- `<ACTION>{"actionType":"HUNTER_PASS","actionData":{}}</ACTION>`
446
- ].join("\n");
447
- }
448
- function renderMvpVoteRequest(payload) {
449
- const candidates = asMvpCandidates(payload.candidates);
450
- const selfAgentId = asNumberOrNull(payload.selfAgentId);
451
- const round = asNumberOrNull(payload.round) ?? 1;
452
- const deadlineSeconds = asNumberOrNull(payload.deadlineSeconds);
453
- return [
454
- `\u3010\u4EFB\u52A1\u3011\u8D5B\u540E MVP \u6295\u7968\uFF08\u7B2C ${round} \u8F6E\uFF09`,
455
- deadlineSeconds != null ? `\u8BF7\u5728 ${deadlineSeconds} \u79D2\u5185\u4ECE\u5019\u9009\u5BF9\u8C61\u91CC\u9009\u51FA\u4F60\u8BA4\u4E3A\u672C\u5C40\u8D21\u732E\u6700\u5927\u7684 Agent\u3002` : `\u8BF7\u4ECE\u5019\u9009\u5BF9\u8C61\u91CC\u9009\u51FA\u4F60\u8BA4\u4E3A\u672C\u5C40\u8D21\u732E\u6700\u5927\u7684 Agent\u3002`,
456
- `\u5019\u9009\u5BF9\u8C61\uFF08targetAgentId \u5FC5\u987B\u4ECE\u8FD9\u91CC\u9009\uFF0C\u4E0D\u80FD\u6295\u7ED9\u81EA\u5DF1\uFF09\uFF1A`,
457
- renderMvpCandidateList(candidates, selfAgentId),
458
- ``,
459
- `\u8BF7\u7EFC\u5408\u53D1\u8A00\u3001\u6295\u7968\u3001\u5173\u952E\u884C\u52A8\u548C\u80DC\u8D1F\u8D21\u732E\uFF0C\u7B80\u77ED\u8BF4\u660E\u7406\u7531\uFF0C\u7136\u540E\u5728\u56DE\u590D\u6700\u540E\u4E25\u683C\u6309\u4EE5\u4E0B\u683C\u5F0F\u58F0\u660E\u52A8\u4F5C\uFF1A`,
460
- `<ACTION>`,
461
- `{"actionType":"MVP_VOTE","actionData":{"targetAgentId":<number>,"round":${round},"reason":"<\u7B80\u8FF0>"}}`,
462
- `</ACTION>`
463
- ].join("\n");
464
- }
465
- function renderUnknownTurn(eventType) {
466
- return [
467
- `\u3010\u63D0\u793A\u3011\u672A\u8BC6\u522B\u7684\u6E38\u620F\u4E8B\u4EF6\u7C7B\u578B\uFF1A${eventType}`,
468
- `\u8BF7\u5FFD\u7565\u672C\u6761\u4E8B\u4EF6\uFF0C\u65E0\u9700\u58F0\u660E\u52A8\u4F5C\u3002`
469
- ].join("\n");
470
- }
471
- function buildGameEventPrompt(eventType, eventData) {
472
- const outer = asRecord(eventData);
473
- const nestedPayload = asRecord(outer.payload);
474
- const payload = Object.keys(nestedPayload).length > 0 ? nestedPayload : outer;
475
- const header = renderHeader(eventType, outer, payload);
476
- const task = renderTask(eventType, payload);
477
- const footer = `
478
-
479
- \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`;
480
- return `${header}${task}${footer}`;
481
- }
482
-
483
273
  // src/inbound.ts
484
274
  function mapInboundFrame(frame) {
485
275
  if (frame.type === "PRIVATE_MESSAGE") {
@@ -533,12 +323,15 @@ function mapInboundFrame(frame) {
533
323
  }
534
324
  async function handleInboundFrame(input) {
535
325
  const envelope = mapInboundFrame(input.frame);
536
- if (!envelope) return;
326
+ if (!envelope) {
327
+ await ackFrameSeq(input);
328
+ return;
329
+ }
537
330
  await ackProcessedSeq(input, envelope);
538
331
  await input.dispatch(envelope);
539
332
  }
540
333
  function mapNotificationFrame(frame) {
541
- const payload = isRecord2(frame.payload) ? frame.payload : {};
334
+ const payload = isRecord3(frame.payload) ? frame.payload : {};
542
335
  const seq = typeof payload.seq === "number" ? payload.seq : void 0;
543
336
  if (frame.type === "SYSTEM_NOTIFICATION") {
544
337
  const title = typeof payload.title === "string" ? payload.title : "System";
@@ -588,7 +381,8 @@ function mapNotificationFrame(frame) {
588
381
  }
589
382
  function mapGameEventFrame(frame, payload) {
590
383
  const eventType = typeof payload.eventType === "string" ? payload.eventType : "UNKNOWN";
591
- if (eventType === "GAME_START") {
384
+ const agentTask = normalizeAgentTask(payload.agentTask);
385
+ if (!agentTask || !isDispatchableAgentTask(agentTask)) {
592
386
  return null;
593
387
  }
594
388
  let eventDataObj = {};
@@ -596,10 +390,10 @@ function mapGameEventFrame(frame, payload) {
596
390
  if (typeof rawEventData === "string" && rawEventData.length > 0) {
597
391
  try {
598
392
  const parsed = JSON.parse(rawEventData);
599
- if (isRecord2(parsed)) eventDataObj = parsed;
393
+ if (isRecord3(parsed)) eventDataObj = parsed;
600
394
  } catch {
601
395
  }
602
- } else if (isRecord2(rawEventData)) {
396
+ } else if (isRecord3(rawEventData)) {
603
397
  eventDataObj = rawEventData;
604
398
  }
605
399
  const gameId = Number(payload.gameId ?? 0);
@@ -618,16 +412,17 @@ function mapGameEventFrame(frame, payload) {
618
412
  traceId,
619
413
  eventType,
620
414
  eventData: eventDataObj,
415
+ agentTask,
416
+ promptPolicyVersion: agentTask.promptPolicyVersion,
417
+ renderedPromptHash: agentTask.renderedPromptHash,
621
418
  deadlineEpochMs,
622
419
  sourceFrameId: frame.id
623
420
  };
624
421
  return {
625
422
  id: eventId,
626
423
  channel: "coolclaw",
627
- // 每个 (roomId, gameId, eventType) 组合作为独立会话键,避免和私聊会话混淆;
628
- // turnSeq 不参与会话 id,以便同一事件类型的多轮对话能沿用同一上下文。
629
- conversationId: `game:${roomId}:${gameId}:${eventType}`,
630
- text: buildGameEventPrompt(eventType, eventDataObj),
424
+ conversationId: agentTask.conversationKey ?? `game:${roomId}:${gameId}:task:${eventId}`,
425
+ text: agentTask.renderedPrompt,
631
426
  messageType: "GAME_EVENT",
632
427
  seq,
633
428
  shouldReply: true,
@@ -636,52 +431,62 @@ function mapGameEventFrame(frame, payload) {
636
431
  }
637
432
  async function ackProcessedSeq(input, envelope) {
638
433
  if (typeof envelope.seq === "number") {
639
- const lastAckedSeq = await input.ackStore.record(input.accountKey, envelope.seq);
640
- await input.sendAck(createFrame("ACK", { lastAckedSeq }));
434
+ await ackSeq(input, envelope.seq);
641
435
  }
642
436
  }
437
+ async function ackFrameSeq(input) {
438
+ const payload = isRecord3(input.frame.payload) ? input.frame.payload : {};
439
+ const seq = typeof payload.seq === "number" ? payload.seq : void 0;
440
+ if (typeof seq === "number") {
441
+ await ackSeq(input, seq);
442
+ }
443
+ }
444
+ async function ackSeq(input, seq) {
445
+ const lastAckedSeq = await input.ackStore.record(input.accountKey, seq);
446
+ await input.sendAck(createFrame("ACK", { lastAckedSeq }));
447
+ }
643
448
  function assertPrivatePayload(value) {
644
- if (!isRecord2(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
449
+ if (!isRecord3(value) || !isUserRef(value.sender) || !isUserRef(value.recipient)) {
645
450
  throw new Error("Invalid PRIVATE_MESSAGE payload");
646
451
  }
647
452
  return {
648
- seq: readNumber(value, "seq"),
649
- messageId: readString(value, "messageId"),
650
- conversationId: readString(value, "conversationId"),
453
+ seq: readNumber2(value, "seq"),
454
+ messageId: readString2(value, "messageId"),
455
+ conversationId: readString2(value, "conversationId"),
651
456
  sender: value.sender,
652
457
  recipient: value.recipient,
653
- messageType: readString(value, "messageType"),
654
- content: readString(value, "content"),
458
+ messageType: readString2(value, "messageType"),
459
+ content: readString2(value, "content"),
655
460
  mentioned: readBoolean(value, "mentioned"),
656
- sentAt: readString(value, "sentAt")
461
+ sentAt: readString2(value, "sentAt")
657
462
  };
658
463
  }
659
464
  function assertGroupPayload(value) {
660
- if (!isRecord2(value) || !isUserRef(value.sender)) {
465
+ if (!isRecord3(value) || !isUserRef(value.sender)) {
661
466
  throw new Error("Invalid GROUP_MESSAGE payload");
662
467
  }
663
468
  return {
664
- seq: readNumber(value, "seq"),
665
- messageId: readString(value, "messageId"),
666
- groupId: readString(value, "groupId"),
667
- groupName: readString(value, "groupName"),
668
- conversationId: readString(value, "conversationId"),
469
+ seq: readNumber2(value, "seq"),
470
+ messageId: readString2(value, "messageId"),
471
+ groupId: readString2(value, "groupId"),
472
+ groupName: readString2(value, "groupName"),
473
+ conversationId: readString2(value, "conversationId"),
669
474
  sender: value.sender,
670
- messageType: readString(value, "messageType"),
671
- content: readString(value, "content"),
475
+ messageType: readString2(value, "messageType"),
476
+ content: readString2(value, "content"),
672
477
  mentioned: readBoolean(value, "mentioned"),
673
- sentAt: readString(value, "sentAt"),
478
+ sentAt: readString2(value, "sentAt"),
674
479
  agentHint: readOptionalString(value, "agentHint")
675
480
  };
676
481
  }
677
- function readString(source, key) {
482
+ function readString2(source, key) {
678
483
  const value = source[key];
679
484
  if (typeof value !== "string" || value.length === 0) {
680
485
  throw new Error(`Invalid inbound payload: missing ${key}`);
681
486
  }
682
487
  return value;
683
488
  }
684
- function readNumber(source, key) {
489
+ function readNumber2(source, key) {
685
490
  const value = source[key];
686
491
  if (typeof value !== "number" || !Number.isInteger(value)) {
687
492
  throw new Error(`Invalid inbound payload: missing ${key}`);
@@ -706,9 +511,9 @@ function readOptionalString(source, key) {
706
511
  return value;
707
512
  }
708
513
  function isUserRef(value) {
709
- return isRecord2(value) && typeof value.userId === "string" && (value.userType === "HUMAN" || value.userType === "AGENT") && (value.displayName === void 0 || typeof value.displayName === "string");
514
+ return isRecord3(value) && typeof value.userId === "string" && (value.userType === "HUMAN" || value.userType === "AGENT") && (value.displayName === void 0 || typeof value.displayName === "string");
710
515
  }
711
- function isRecord2(value) {
516
+ function isRecord3(value) {
712
517
  return typeof value === "object" && value !== null;
713
518
  }
714
519
 
@@ -833,13 +638,20 @@ async function sendMedia(input) {
833
638
  async function sendGameAction(input) {
834
639
  const frame = createFrame("GAME_ACTION", {
835
640
  gameId: input.gameId,
641
+ roomId: input.roomId,
642
+ eventType: input.eventType,
836
643
  actionType: input.actionType,
837
644
  actionData: input.actionData,
838
645
  // AgentActionRequest.timestamp 契约为 String
839
646
  timestamp: String(Date.now()),
840
647
  turnSeq: input.turnSeq,
841
648
  eventId: input.eventId,
842
- traceId: input.traceId
649
+ traceId: input.traceId,
650
+ promptPolicyVersion: input.promptPolicyVersion,
651
+ renderedPromptHash: input.renderedPromptHash,
652
+ parseSource: input.parseSource,
653
+ rawResponseHash: input.rawResponseHash,
654
+ rawResponsePreview: input.rawResponsePreview
843
655
  });
844
656
  const response = await input.client.request(frame);
845
657
  if (response.ok === false) {
@@ -887,107 +699,6 @@ function parseAgentAction(text) {
887
699
  };
888
700
  }
889
701
 
890
- // src/game-action-fallback.ts
891
- function asNumberArray2(v) {
892
- return Array.isArray(v) ? v.filter((x) => typeof x === "number") : [];
893
- }
894
- function asNumberOrNull2(v) {
895
- return typeof v === "number" && Number.isFinite(v) ? v : null;
896
- }
897
- function asRecord2(v) {
898
- return typeof v === "object" && v !== null ? v : {};
899
- }
900
- function hasKeys(record) {
901
- return Object.keys(record).length > 0;
902
- }
903
- function candidateAgentIds(v) {
904
- if (!Array.isArray(v)) return [];
905
- return v.flatMap((item) => {
906
- if (typeof item === "number" && Number.isFinite(item)) return [item];
907
- const record = asRecord2(item);
908
- const agentId = asNumberOrNull2(record.agentId) ?? asNumberOrNull2(record.targetAgentId);
909
- return agentId == null ? [] : [agentId];
910
- });
911
- }
912
- function pickRandomAlive(aliveSeats, excludeSeat) {
913
- const candidates = excludeSeat != null ? aliveSeats.filter((s) => s !== excludeSeat) : aliveSeats;
914
- if (candidates.length === 0) return null;
915
- return candidates[Math.floor(Math.random() * candidates.length)];
916
- }
917
- function pickRandomAgent(candidates, excludeAgentId) {
918
- const eligible = excludeAgentId == null ? candidates : candidates.filter((candidate) => candidate !== excludeAgentId);
919
- if (eligible.length === 0) return candidates[0] ?? null;
920
- return eligible[Math.floor(Math.random() * eligible.length)];
921
- }
922
- function fallbackActionFor(eventType, eventData) {
923
- const outer = asRecord2(eventData);
924
- const nestedPayload = asRecord2(outer.payload);
925
- const payload = hasKeys(nestedPayload) ? nestedPayload : outer;
926
- const aliveSeats = asNumberArray2(payload.aliveSeats);
927
- const selfSeat = typeof payload.selfSeat === "number" ? payload.selfSeat : void 0;
928
- switch (eventType) {
929
- case "WOLF_TURN": {
930
- const target = pickRandomAlive(aliveSeats, selfSeat);
931
- return {
932
- actionType: "WOLF_KILL",
933
- actionData: {
934
- targetSeat: target ?? (aliveSeats[0] ?? 1),
935
- reason: "\uFF08\u6258\u7BA1\uFF1ALLM \u672A\u7ED9\u51FA\u5408\u6CD5\u52A8\u4F5C\uFF09",
936
- speech: ""
937
- }
938
- };
939
- }
940
- case "WITCH_TURN": {
941
- return { actionType: "WITCH_PASS", actionData: {} };
942
- }
943
- case "SEER_TURN": {
944
- const target = pickRandomAlive(aliveSeats, selfSeat);
945
- return {
946
- actionType: "SEER_CHECK",
947
- actionData: {
948
- targetSeat: target ?? (aliveSeats[0] ?? 1),
949
- speech: ""
950
- }
951
- };
952
- }
953
- case "DAY_SPEAK_TURN":
954
- return {
955
- actionType: "DAY_SPEAK",
956
- actionData: { content: "\uFF08\u6258\u7BA1\u53D1\u8A00\uFF1A\u672C\u8F6E\u8DF3\u8FC7\uFF09" }
957
- };
958
- case "LAST_WORD_TURN":
959
- return {
960
- actionType: "LAST_WORD",
961
- actionData: { content: "" }
962
- };
963
- case "DAY_VOTE_TURN":
964
- return {
965
- actionType: "DAY_VOTE",
966
- actionData: { targetSeat: null }
967
- };
968
- case "HUNTER_SKILL_TURN":
969
- return { actionType: "HUNTER_PASS", actionData: {} };
970
- case "MVP_VOTE_REQUEST": {
971
- const selfAgentId = asNumberOrNull2(payload.selfAgentId);
972
- const candidates = candidateAgentIds(payload.candidates);
973
- const targetAgentId = pickRandomAgent(candidates, selfAgentId);
974
- if (targetAgentId == null) {
975
- return { actionType: "UNKNOWN", actionData: {} };
976
- }
977
- return {
978
- actionType: "MVP_VOTE",
979
- actionData: {
980
- targetAgentId,
981
- round: asNumberOrNull2(payload.round) ?? asNumberOrNull2(outer.round) ?? 1,
982
- reason: "\uFF08\u6258\u7BA1\uFF1ALLM \u672A\u7ED9\u51FA\u5408\u6CD5 MVP \u6295\u7968\uFF09"
983
- }
984
- };
985
- }
986
- default:
987
- return { actionType: "UNKNOWN", actionData: {} };
988
- }
989
- }
990
-
991
702
  // src/ws-client.ts
992
703
  import WebSocket from "ws";
993
704
  var CoolclawWsClient = class {
@@ -1200,18 +911,18 @@ var CoolclawWsClient = class {
1200
911
  }
1201
912
  };
1202
913
  function readPingInterval(payload) {
1203
- if (!isRecord3(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
914
+ if (!isRecord4(payload) || typeof payload.pingIntervalMs !== "number" || payload.pingIntervalMs <= 0) {
1204
915
  return void 0;
1205
916
  }
1206
917
  return payload.pingIntervalMs;
1207
918
  }
1208
919
  function readErrorMessage(payload) {
1209
- if (isRecord3(payload) && typeof payload.message === "string") {
920
+ if (isRecord4(payload) && typeof payload.message === "string") {
1210
921
  return payload.message;
1211
922
  }
1212
923
  return "CoolClaw request failed";
1213
924
  }
1214
- function isRecord3(value) {
925
+ function isRecord4(value) {
1215
926
  return typeof value === "object" && value !== null;
1216
927
  }
1217
928
 
@@ -1265,27 +976,35 @@ function logAckFailure(params) {
1265
976
  const target = params.target ? ` target=${params.target}` : "";
1266
977
  params.log(`${params.channel} ack cleanup failed${target}: ${String(params.error)}`);
1267
978
  }
1268
- async function submitGameActionWithLog(action, meta, wsClient, log, source) {
979
+ async function submitGameActionWithLog(action, meta, wsClient, log, source, rawResponse) {
1269
980
  if (!wsClient.isConnected()) {
1270
981
  log?.error?.(`[GAME-ACTION] submit skipped: ws not connected eventId=${meta.eventId}`);
1271
982
  return;
1272
983
  }
984
+ const responseHash = rawResponse && rawResponse.length > 0 ? sha256Hex(rawResponse) : void 0;
1273
985
  log?.info?.(
1274
- `[GAME-ACTION] submit start source=${source} eventType=${meta.eventType} actionType=${action.actionType} gameId=${meta.gameId} turnSeq=${meta.turnSeq} eventId=${meta.eventId}`
986
+ `[GAME-ACTION] submit start source=${source} eventType=${meta.eventType} actionType=${action.actionType} gameId=${meta.gameId} roomId=${meta.roomId} turnSeq=${meta.turnSeq} eventId=${meta.eventId} promptPolicyVersion=${meta.promptPolicyVersion ?? ""} renderedPromptHash=${meta.renderedPromptHash ?? ""} rawResponseHash=${responseHash ?? ""}`
1275
987
  );
1276
988
  const start = Date.now();
1277
989
  try {
1278
990
  await sendGameAction({
1279
991
  client: wsClient,
1280
992
  gameId: meta.gameId,
993
+ roomId: meta.roomId,
994
+ eventType: meta.eventType,
1281
995
  actionType: action.actionType,
1282
996
  actionData: action.actionData,
1283
997
  turnSeq: meta.turnSeq,
1284
998
  eventId: meta.eventId,
1285
- traceId: meta.traceId
999
+ traceId: meta.traceId,
1000
+ promptPolicyVersion: meta.promptPolicyVersion,
1001
+ renderedPromptHash: meta.renderedPromptHash,
1002
+ parseSource: source,
1003
+ rawResponseHash: responseHash,
1004
+ rawResponsePreview: rawResponse ? rawResponsePreview(rawResponse) : void 0
1286
1005
  });
1287
1006
  log?.info?.(
1288
- `[GAME-ACTION] submit ok source=${source} gameId=${meta.gameId} eventId=${meta.eventId} elapsedMs=${Date.now() - start}`
1007
+ `[GAME-ACTION] submit ok source=${source} gameId=${meta.gameId} eventId=${meta.eventId} promptPolicyVersion=${meta.promptPolicyVersion ?? ""} renderedPromptHash=${meta.renderedPromptHash ?? ""} rawResponseHash=${responseHash ?? ""} elapsedMs=${Date.now() - start}`
1289
1008
  );
1290
1009
  } catch (err) {
1291
1010
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -1451,6 +1170,12 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1451
1170
  const gameMeta = isGameEvent ? envelope.metadata : null;
1452
1171
  let gameSubmitted = false;
1453
1172
  let gameFallbackReason = null;
1173
+ const gameBuffer = [];
1174
+ if (isGameEvent && gameMeta) {
1175
+ ctx.log?.info?.(
1176
+ `[GAME-TASK] dispatch start gameId=${gameMeta.gameId} roomId=${gameMeta.roomId} eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} conversationId=${envelope.conversationId}`
1177
+ );
1178
+ }
1454
1179
  try {
1455
1180
  const isGroup = envelope.conversationId.startsWith("group:");
1456
1181
  const peer = isGroup ? { kind: "group", id: envelope.group?.groupId ?? envelope.conversationId } : { kind: "direct", id: envelope.conversationId };
@@ -1530,10 +1255,15 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1530
1255
  ctx.log?.warn(`recordInboundSession failed: ${err instanceof Error ? err.message : String(err)}`);
1531
1256
  }
1532
1257
  });
1533
- const gameBuffer = [];
1534
1258
  await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
1535
1259
  ctx: ctxPayload,
1536
1260
  cfg: ctx.cfg,
1261
+ // 群聊强制走 automatic:CoolClaw 群聊业务语义就是 @ 即回,
1262
+ // 而 OpenClaw 默认对 group/channel 走 message_tool_only,
1263
+ // 模型若不主动调 message 工具就会被 runtime 静默吞掉,
1264
+ // 用户感知为"小甲群里不回"。这里按消息粒度覆盖,
1265
+ // 不污染用户全局 openclaw.json,私聊保持 SDK 默认。
1266
+ replyOptions: isGroup ? { sourceReplyDeliveryMode: "automatic" } : void 0,
1537
1267
  dispatcherOptions: {
1538
1268
  deliver: async (payload) => {
1539
1269
  if (!payload.text) return;
@@ -1543,13 +1273,21 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1543
1273
  const full = gameBuffer.join("");
1544
1274
  const parsed = parseAgentAction(full);
1545
1275
  if ("error" in parsed) return;
1276
+ const validation = validateAgentAction(parsed, gameMeta.agentTask);
1277
+ if (!validation.ok) {
1278
+ ctx.log?.warn?.(
1279
+ `[GAME-ACTION] rejected model action reason=${validation.reason} actionType=${parsed.actionType} eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} rawResponseHash=${sha256Hex(full)}`
1280
+ );
1281
+ return;
1282
+ }
1546
1283
  gameSubmitted = true;
1547
1284
  await submitGameActionWithLog(
1548
- parsed,
1285
+ validation.action,
1549
1286
  gameMeta,
1550
1287
  wsClient,
1551
1288
  ctx.log,
1552
- "llm"
1289
+ "llm",
1290
+ full
1553
1291
  );
1554
1292
  return;
1555
1293
  }
@@ -1582,18 +1320,32 @@ var coolclawChannelPlugin = createChatChannelPlugin({
1582
1320
  gameFallbackReason = `dispatch_error: ${errMsg}`;
1583
1321
  }
1584
1322
  } finally {
1323
+ if (isGameEvent && gameMeta) {
1324
+ const rawResponse = gameBuffer.join("");
1325
+ ctx.log?.info?.(
1326
+ `[GAME-TASK] model-output eventId=${gameMeta.eventId} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} rawHash=${rawResponse ? sha256Hex(rawResponse) : ""} rawPreview=${rawResponsePreview(rawResponse) ?? ""}`
1327
+ );
1328
+ }
1585
1329
  if (isGameEvent && gameMeta && !gameSubmitted) {
1586
1330
  try {
1587
- const fb = fallbackActionFor(gameMeta.eventType, gameMeta.eventData);
1331
+ const fb = backendFallbackAction(gameMeta.agentTask);
1332
+ if (!fb) {
1333
+ ctx.log?.warn?.(
1334
+ `[GAME-ACTION] backend fallback unavailable eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} reason=${gameFallbackReason ?? "unknown"} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""}`
1335
+ );
1336
+ return;
1337
+ }
1338
+ const rawResponse = gameBuffer.join("");
1588
1339
  ctx.log?.warn?.(
1589
- `[GAME-ACTION] parse fallback eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} reason=${gameFallbackReason ?? "unknown"}`
1340
+ `[GAME-ACTION] backend fallback eventType=${gameMeta.eventType} eventId=${gameMeta.eventId} reason=${gameFallbackReason ?? "unknown"} promptPolicyVersion=${gameMeta.promptPolicyVersion ?? ""} renderedPromptHash=${gameMeta.renderedPromptHash ?? ""} rawResponseHash=${rawResponse ? sha256Hex(rawResponse) : ""}`
1590
1341
  );
1591
1342
  await submitGameActionWithLog(
1592
1343
  fb,
1593
1344
  gameMeta,
1594
1345
  wsClient,
1595
1346
  ctx.log,
1596
- "fallback"
1347
+ "backend_fallback",
1348
+ rawResponse || void 0
1597
1349
  );
1598
1350
  } catch (fbErr) {
1599
1351
  ctx.log?.error?.(
@@ -3,7 +3,7 @@ import {
3
3
  coolclawChannelPlugin,
4
4
  defaultBindingFile,
5
5
  setCoolclawRuntime
6
- } from "./chunk-D346LLBV.js";
6
+ } from "./chunk-DVHZ22OJ.js";
7
7
 
8
8
  // index.ts
9
9
  import { defineChannelPluginEntry, buildChannelConfigSchema } from "openclaw/plugin-sdk/core";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  index_default
3
- } from "./chunk-TPXBPYZ2.js";
4
- import "./chunk-D346LLBV.js";
3
+ } from "./chunk-GKJ5WKGA.js";
4
+ import "./chunk-DVHZ22OJ.js";
5
5
 
6
6
  // cli-metadata.ts
7
7
  var cli_metadata_default = index_default;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  index_default
3
- } from "./chunk-TPXBPYZ2.js";
4
- import "./chunk-D346LLBV.js";
3
+ } from "./chunk-GKJ5WKGA.js";
4
+ import "./chunk-DVHZ22OJ.js";
5
5
  export {
6
6
  index_default as default
7
7
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  coolclawChannelPlugin
3
- } from "./chunk-D346LLBV.js";
3
+ } from "./chunk-DVHZ22OJ.js";
4
4
 
5
5
  // setup-entry.ts
6
6
  import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coolclaw/coolclaw",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "OpenClaw native channel plugin for Riddle/CoolClaw chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -71,7 +71,7 @@
71
71
  "runtimeSetupEntry": "./dist/setup-entry.js",
72
72
  "install": {
73
73
  "npmSpec": "@coolclaw/coolclaw",
74
- "expectedIntegrity": "sha512-k3P69ef3+p51LIu6gbIVG4Y48e0wev2sJHPf8EqUd7RvQk8S6HNiVKLNnna6Izg767DYT5gN3GaG6fMUfngsWg==",
74
+ "expectedIntegrity": "sha512-6gIxAI4i3xJ4LpzViGi1KCJvc20JA/3Ob4OVdClgHCPydrkDqzOPbhSea4tpSlkUB5mWh/iEEzKhMcKqM24vVg==",
75
75
  "defaultChoice": "npm",
76
76
  "minHostVersion": ">=2026.3.22"
77
77
  },
@@ -99,4 +99,4 @@
99
99
  "pluginSdkVersion": "2026.4.29"
100
100
  }
101
101
  }
102
- }
102
+ }