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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
558
|
+
chatType,
|
|
554
559
|
undefined,
|
|
555
560
|
action.commandId,
|
|
556
561
|
).catch((err) =>
|
package/src/orchestrator.ts
CHANGED
|
@@ -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
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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") {
|