chatccc 0.2.207 → 0.2.208

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.207",
3
+ "version": "0.2.208",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { resolveFeishuCardActionChatType } from "../card-action-routing.ts";
4
+
5
+ describe("resolveFeishuCardActionChatType", () => {
6
+ it("keeps card commands in a persisted private chat on the p2p route", () => {
7
+ expect(resolveFeishuCardActionChatType("private-chat", {
8
+ "private-chat": { chatType: "p2p" },
9
+ })).toBe("p2p");
10
+ });
11
+
12
+ it("defaults unknown and group chats to the group route", () => {
13
+ expect(resolveFeishuCardActionChatType("group-chat", {
14
+ "group-chat": { chatType: "group" },
15
+ })).toBe("group");
16
+ expect(resolveFeishuCardActionChatType("unknown-chat", {})).toBe("group");
17
+ });
18
+ });
@@ -342,6 +342,43 @@ describe("handleCommand WeChat processing ack", () => {
342
342
  expect(registry["feishu-p2p"]?.sessionId).toBe("sid-feishu-private");
343
343
  });
344
344
 
345
+ it("sends the normal session state card in an established Feishu p2p chat", async () => {
346
+ const platform = mockPlatform("feishu");
347
+ _setAdapterForToolForTest("claude", mockAdapter("sid-feishu-state"));
348
+ await recordSessionRegistry({
349
+ chatId: "feishu-p2p-state",
350
+ sessionId: "sid-feishu-state",
351
+ tool: "claude",
352
+ chatType: "p2p",
353
+ chatName: "飞书私聊",
354
+ turnCount: 2,
355
+ running: false,
356
+ });
357
+
358
+ await handleCommand(platform, "/state", "feishu-p2p-state", "ou-user", Date.now(), "p2p");
359
+
360
+ expect(platform.getChatInfo).not.toHaveBeenCalled();
361
+ expect(platform.sendRawCard).toHaveBeenCalledTimes(1);
362
+ const cardText = vi.mocked(platform.sendRawCard).mock.calls[0][1];
363
+ expect(cardText).toContain("sid-feishu-state");
364
+ expect(cardText).toContain("Claude Code");
365
+ expect(cardText).toContain("2");
366
+ });
367
+
368
+ it("shows an explicit state card without creating an Agent when a Feishu p2p chat is not bound yet", async () => {
369
+ const platform = mockPlatform("feishu");
370
+ const adapter = mockAdapter("should-not-be-created");
371
+ _setAdapterForToolForTest("claude", adapter);
372
+
373
+ await handleCommand(platform, "/state", "feishu-p2p-empty", "ou-user", Date.now(), "p2p");
374
+
375
+ expect(adapter.createSession).not.toHaveBeenCalled();
376
+ expect(platform.sendRawCard).toHaveBeenCalledTimes(1);
377
+ const cardText = vi.mocked(platform.sendRawCard).mock.calls[0][1];
378
+ expect(cardText).toContain("未建立会话");
379
+ expect(cardText).toContain("Claude Code");
380
+ });
381
+
345
382
  it("switches an idle Feishu p2p chat to a fresh session when the default Agent changes", async () => {
346
383
  const platform = mockPlatform("feishu");
347
384
  const oldPrompt = vi.fn(async function* () {
@@ -0,0 +1,14 @@
1
+ export type FeishuCommandChatType = "p2p" | "group";
2
+
3
+ type SessionRegistryForRouting = Record<string, { chatType?: string }>;
4
+
5
+ /**
6
+ * Feishu card action callbacks do not include the chat type. Recover it from
7
+ * the persisted binding so buttons clicked in a private chat stay in p2p.
8
+ */
9
+ export function resolveFeishuCardActionChatType(
10
+ chatId: string,
11
+ registry: SessionRegistryForRouting,
12
+ ): FeishuCommandChatType {
13
+ return registry[chatId]?.chatType === "p2p" ? "p2p" : "group";
14
+ }
package/src/index.ts CHANGED
@@ -112,7 +112,8 @@ import {
112
112
  import { fixStaleStreamStates } from "./stream-state.ts";
113
113
  import { handleCommand, type PlatformAdapter } from "./orchestrator.ts";
114
114
  import { createWechatAdapter, startWechatPlatform } from "./wechat-platform.ts";
115
- import { handleCodexResetCardAction } from "./codex-reset-actions.ts";
115
+ import { handleCodexResetCardAction } from "./codex-reset-actions.ts";
116
+ import { resolveFeishuCardActionChatType } from "./card-action-routing.ts";
116
117
  import { reloadRuntimeConfig } from "./runtime-reload.ts";
117
118
 
118
119
  // ---------------------------------------------------------------------------
@@ -507,16 +508,18 @@ async function startBotServiceCore(): Promise<void> {
507
508
  });
508
509
  if (handledCodexReset) return;
509
510
 
510
- const result = parseCardAction(data);
511
- if (!result) return;
512
- console.log(`[BTN] chat=${result.chatId} text="${result.text}"`);
511
+ const result = parseCardAction(data);
512
+ if (!result) return;
513
+ const registry = await loadSessionRegistryForBinding();
514
+ const chatType = resolveFeishuCardActionChatType(result.chatId, registry);
515
+ console.log(`[BTN] chat=${result.chatId} chatType=${chatType} text="${result.text}"`);
513
516
  handleCommand(
514
517
  feishuPlatform,
515
518
  result.text,
516
519
  result.chatId,
517
520
  result.openId,
518
521
  Date.now(),
519
- "group",
522
+ chatType,
520
523
  undefined,
521
524
  result.commandId,
522
525
  ).catch((err) =>
@@ -542,15 +545,17 @@ async function startBotServiceCore(): Promise<void> {
542
545
  ws.on("message", async (raw: Buffer) => {
543
546
  try {
544
547
  const data = JSON.parse(raw.toString()) as Evt;
545
- const action = parseCardAction(data);
546
- if (action) {
548
+ const action = parseCardAction(data);
549
+ if (action) {
550
+ const registry = await loadSessionRegistryForBinding();
551
+ const chatType = resolveFeishuCardActionChatType(action.chatId, registry);
547
552
  handleCommand(
548
553
  feishuPlatform,
549
554
  action.text,
550
555
  action.chatId,
551
556
  action.openId,
552
557
  Date.now(),
553
- "group",
558
+ chatType,
554
559
  undefined,
555
560
  action.commandId,
556
561
  ).catch((err) =>
@@ -416,6 +416,48 @@ function isFeishuP2p(platform: PlatformAdapter, chatType: string): boolean {
416
416
  return chatType === "p2p" && platform.kind === "feishu";
417
417
  }
418
418
 
419
+ async function sendStateCard(
420
+ platform: PlatformAdapter,
421
+ chatId: string,
422
+ sessionId: string | null,
423
+ toolLabel: string,
424
+ traceId: string,
425
+ ): Promise<void> {
426
+ const status = sessionId ? await getSessionStatus(chatId) : null;
427
+ const isActive = sessionId ? isSessionRunning(sessionId) : false;
428
+ const stateLabel = sessionId
429
+ ? (isActive ? "🟢 运行中" : "⚪ 空闲")
430
+ : "⚪ 未建立会话";
431
+ const statusText = [
432
+ `**群名:** ${status?.chatName || "—"}`,
433
+ `**Session ID:** ${sessionId ? `\`${status?.sessionId ?? sessionId}\`` : "—"}`,
434
+ `**工具:** ${toolLabel}`,
435
+ `**状态:** ${stateLabel}`,
436
+ `**已对话轮数:** ${status?.turnCount ?? 0}`,
437
+ `**模型:** ${sessionId ? (status?.model ?? anthropicConfigDisplay(CLAUDE_MODEL)) : "—"}`,
438
+ ];
439
+ if (status?.effort != null) {
440
+ statusText.push(`**Effort:** ${status.effort}`);
441
+ }
442
+ if (isActive && status) {
443
+ const elapsed = Math.floor((Date.now() - status.startTime) / 1000);
444
+ const mins = Math.floor(elapsed / 60);
445
+ const secs = elapsed % 60;
446
+ statusText.push(`**本轮已运行:** ${mins}分${secs}秒`);
447
+ statusText.push(`**已产出总字符:** ${status.accumulatedLength.toLocaleString()}`);
448
+ }
449
+ if (status?.lastContextTokens) {
450
+ statusText.push(`**上下文 Token 数:** ~${status.lastContextTokens.toLocaleString()}`);
451
+ }
452
+ const card = buildStatusCard(statusText.join("\n"), isActive ? "blue" : "green");
453
+ const ok = await platform.sendRawCard(chatId, card);
454
+ console.log(`[${ts()}] [STATUS] card sent, ok=${ok}`);
455
+ logTrace(traceId, "DONE", {
456
+ outcome: sessionId ? "status" : "status_no_session",
457
+ ok,
458
+ });
459
+ }
460
+
419
461
  interface FeishuP2pRegistryRecord {
420
462
  sessionId: string;
421
463
  tool: string;
@@ -1336,44 +1378,11 @@ export async function handleCommand(
1336
1378
  return;
1337
1379
  }
1338
1380
 
1339
- if (isCommandText && textLower === "/state") {
1340
- logTrace(tid, "BRANCH", { cmd: "/state" });
1341
- const status = await getSessionStatus(chatId);
1342
- const isActive = isSessionRunning(sessionId);
1343
- const statusText = [
1344
- `**群名:** ${status?.chatName || "—"}`,
1345
- `**Session ID:** \`${status?.sessionId ?? sessionId}\``,
1346
- `**工具:** ${toolLabel}`,
1347
- `**状态:** ${isActive ? "🟢 运行中" : "⚪ 空闲"}`,
1348
- `**已对话轮数:** ${status?.turnCount ?? 0}`,
1349
- `**模型:** ${status?.model ?? anthropicConfigDisplay(CLAUDE_MODEL)}`,
1350
- ];
1351
- if (status?.effort != null) {
1352
- statusText.push(`**Effort:** ${status.effort}`);
1353
- }
1354
- if (isActive) {
1355
- const elapsed = Math.floor((Date.now() - status!.startTime) / 1000);
1356
- const mins = Math.floor(elapsed / 60);
1357
- const secs = elapsed % 60;
1358
- statusText.push(`**本轮已运行:** ${mins}分${secs}秒`);
1359
- statusText.push(
1360
- `**已产出总字符:** ${status!.accumulatedLength.toLocaleString()}`,
1361
- );
1362
- }
1363
- if (status?.lastContextTokens) {
1364
- statusText.push(
1365
- `**上下文 Token 数:** ~${status.lastContextTokens.toLocaleString()}`,
1366
- );
1367
- }
1368
- const card = buildStatusCard(
1369
- statusText.join("\n"),
1370
- isActive ? "blue" : "green",
1371
- );
1372
- const ok = await platform.sendRawCard(chatId, card);
1373
- console.log(`[${ts()}] [STATUS] card sent, ok=${ok}`);
1374
- logTrace(tid, "DONE", { outcome: "status", ok });
1375
- return;
1376
- }
1381
+ if (isCommandText && textLower === "/state") {
1382
+ logTrace(tid, "BRANCH", { cmd: "/state" });
1383
+ await sendStateCard(platform, chatId, sessionId, toolLabel, tid);
1384
+ return;
1385
+ }
1377
1386
 
1378
1387
  if (isCommandText && textLower === "/sessions") {
1379
1388
  logTrace(tid, "BRANCH", { cmd: "/sessions" });
@@ -2000,9 +2009,23 @@ export async function handleCommand(
2000
2009
  const card = buildModelCard(currentModel, models, defaultTool);
2001
2010
  await platform.sendRawCard(chatId, card);
2002
2011
  }
2003
- logTrace(tid, "DONE", { outcome: "model_query", defaultTool });
2004
- return;
2005
- }
2012
+ logTrace(tid, "DONE", { outcome: "model_query", defaultTool });
2013
+ return;
2014
+ }
2015
+
2016
+ // A private /state query is useful even before the first Agent session exists.
2017
+ // Keep it read-only and render the same status-card shape as established chats.
2018
+ if (isCommandText && textLower === "/state" && isFeishuP2p(platform, chatType)) {
2019
+ logTrace(tid, "BRANCH", { cmd: "/state", scope: "unbound_p2p" });
2020
+ await sendStateCard(
2021
+ platform,
2022
+ chatId,
2023
+ null,
2024
+ toolDisplayName(resolveDefaultAgentTool()),
2025
+ tid,
2026
+ );
2027
+ return;
2028
+ }
2006
2029
 
2007
2030
  // 无会话上下文 → /sessions 仍是有效指令,不触发飞书私聊自动建群。
2008
2031
  if (isCommandText && textLower === "/effort") {