chatccc 0.2.140 → 0.2.142

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.
@@ -1,43 +1,29 @@
1
- ## Repeated Successful Command Guard
1
+ ## 重复成功命令保护
2
2
 
3
- When working in this project through Claude Agent SDK, repeated successful shell commands are a completion signal, not a reason to keep using tools.
3
+ 通过 Claude Agent SDK 在本项目中工作时,重复成功的 shell 命令是完成信号,不应继续使用工具。
4
4
 
5
- A shell command is considered the same command when all of these are true:
5
+ 以下条件**全部**满足时,视为相同命令:
6
+ - 命令文本基本相同
7
+ - 工作目录相同
8
+ - 任务目标未变
9
+ - 没有新的用户输入改变任务
10
+ - 没有新的错误输出需要重新处理
6
11
 
7
- - The command text is effectively the same.
8
- - The working directory is the same.
9
- - The current task goal has not changed.
10
- - There is no new user input that changes the task.
11
- - There is no new error output that explains why the command must be run again.
12
+ 同一命令**连续成功两次**后,停止调用工具,直接给出最终回复。不要"再验证一次"。
12
13
 
13
- If the same shell command succeeds more than once consecutively, do not call it again.
14
+ 此规则适用于所有 shell 命令,包括测试、构建、git、安装、格式化、项目脚本、状态检查等。
14
15
 
15
- After the second consecutive successful execution of the same command, the next assistant action must be a final user-facing response. Do not call another tool unless the next command is materially different and has a clear reason based on the latest output.
16
+ ## 硬停止规则
16
17
 
17
- This guard applies to all shell commands, including but not limited to:
18
+ 同一命令在本项目中**不得成功执行三次**。若连续两次成功,第三次必须是最终回复。
18
19
 
19
- - test commands
20
- - build commands
21
- - git commands
22
- - install commands
23
- - formatting commands
24
- - project scripts
25
- - status or inspection commands
20
+ ## 卡住自检
26
21
 
27
- Do not "verify one more time" by repeating the same successful command. If verification is needed, use a different command that checks a different fact, or explain the current result to the user.
22
+ 如果你感觉任务已经完成,但发现自己陷入了循环(反复调用相同工具、输出 "stuck in a loop" 等),**立即**调用以下接口:
28
23
 
29
- If you notice that you are about to repeat a successful command with the same arguments, stop using tools and respond to the user with the current result.
24
+ POST {{stop_stuck_url}}
25
+ Content-Type: application/json
30
26
 
31
- If you produce or observe text like "I'm stuck in a loop", do not call any more tools. Immediately send the final user-facing response explaining:
27
+ {"session_id": "{{session_id}}"}
32
28
 
33
- - what has completed,
34
- - what is still uncertain, if anything,
35
- - and what the user can do next, if action is needed.
36
-
37
- ## Hard Stop Rule
38
-
39
- Never execute the same successful shell command three times for the same task in this project.
40
-
41
- If the same command has already succeeded twice in a row, the next assistant action must be a final response to the user, not another tool call.
42
-
43
- Successful repeated command execution is a terminal condition. Prefer a concise final response over further tool calls.
29
+ 调用后停止所有工具调用,直接输出最终回复即可。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.140",
3
+ "version": "0.2.142",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -463,6 +463,30 @@ describe("buildClaudePromptText", () => {
463
463
  expect(buildClaudePromptText("hello", " ")).toBe("hello");
464
464
  expect(buildClaudePromptText("hello", null)).toBe("hello");
465
465
  });
466
+
467
+ it("replaces {{stop_stuck_url}} and {{session_id}} placeholders when sessionId is provided", () => {
468
+ const result = buildClaudePromptText(
469
+ "[User message]\nhello\n[/User message]",
470
+ "Call POST {{stop_stuck_url}} with {\"session_id\": \"{{session_id}}\"}",
471
+ "test-sid-123",
472
+ );
473
+
474
+ expect(result).toContain("http://127.0.0.1:");
475
+ expect(result).toContain("/api/agent/stop-stuck-loop");
476
+ expect(result).toContain("\"session_id\": \"test-sid-123\"");
477
+ expect(result).not.toContain("{{stop_stuck_url}}");
478
+ expect(result).not.toContain("{{session_id}}");
479
+ });
480
+
481
+ it("does not replace placeholders when sessionId is not provided", () => {
482
+ const result = buildClaudePromptText(
483
+ "hello",
484
+ "Call POST {{stop_stuck_url}} with {\"session_id\": \"{{session_id}}\"}",
485
+ );
486
+
487
+ expect(result).toContain("{{stop_stuck_url}}");
488
+ expect(result).toContain("{{session_id}}");
489
+ });
466
490
  });
467
491
 
468
492
  describe("buildSdkEnv", () => {
@@ -21,6 +21,7 @@ import type {
21
21
  UnifiedBlock,
22
22
  UnifiedStreamMessage,
23
23
  } from "./adapter-interface.ts";
24
+ import { CHATCCC_PORT } from "../config.ts";
24
25
  import {
25
26
  defaultClaudeSessionMetaStore,
26
27
  type ClaudeSessionMetaStore,
@@ -260,10 +261,18 @@ export function readClaudeSpecificInjectionPrompt(): string | null {
260
261
  export function buildClaudePromptText(
261
262
  userText: string,
262
263
  injectionPrompt: string | null = readClaudeSpecificInjectionPrompt(),
264
+ sessionId?: string,
263
265
  ): string {
264
- const prompt = injectionPrompt?.trim();
266
+ let prompt = injectionPrompt?.trim();
265
267
  if (!prompt) return userText;
266
268
 
269
+ // 动态替换注入提示词中的占位符(端口、session_id 等)
270
+ if (sessionId) {
271
+ prompt = prompt
272
+ .replace(/\{\{stop_stuck_url\}\}/g, `http://127.0.0.1:${CHATCCC_PORT}/api/agent/stop-stuck-loop`)
273
+ .replace(/\{\{session_id\}\}/g, sessionId);
274
+ }
275
+
267
276
  return [
268
277
  "[ChatCCC Claude-specific injection prompt]",
269
278
  prompt,
@@ -461,7 +470,7 @@ class ClaudeAdapter implements ToolAdapter {
461
470
  );
462
471
 
463
472
  try {
464
- await session.send(buildClaudePromptText(userText));
473
+ await session.send(buildClaudePromptText(userText, undefined, sessionId));
465
474
  for await (const raw of session.stream()) {
466
475
  if (abortController.signal.aborted) {
467
476
  aborted = true;
@@ -0,0 +1,89 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ import { readUtf8JsonBody } from "./agent-rpc-body.ts";
4
+ import { activePrompts, getChatsForSession, cancelQueuedMessage } from "./session-chat-binding.ts";
5
+ import { getPlatformForChat } from "./session.ts";
6
+ import { readStreamState, writeStreamState } from "./stream-state.ts";
7
+ import { ts } from "./config.ts";
8
+
9
+ export const AGENT_STOP_STUCK_PATH = "/api/agent/stop-stuck-loop";
10
+
11
+ const MAX_REQUEST_BYTES = 8 * 1024;
12
+
13
+ function jsonReply(res: ServerResponse, status: number, data: unknown): void {
14
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
15
+ res.end(JSON.stringify(data));
16
+ }
17
+
18
+ export async function handleAgentStopStuckRequest(
19
+ req: IncomingMessage,
20
+ res: ServerResponse,
21
+ ): Promise<boolean> {
22
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
23
+ if (url.pathname !== AGENT_STOP_STUCK_PATH) return false;
24
+
25
+ if (req.method !== "POST") {
26
+ jsonReply(res, 405, { error: "Method not allowed, use POST" });
27
+ return true;
28
+ }
29
+
30
+ let body: { session_id?: string };
31
+ try {
32
+ body = await readUtf8JsonBody<{ session_id?: string }>(req, MAX_REQUEST_BYTES);
33
+ } catch (err) {
34
+ jsonReply(res, 400, { error: `Invalid request body: ${(err as Error).message}` });
35
+ return true;
36
+ }
37
+
38
+ const sessionId = typeof body?.session_id === "string" ? body.session_id.trim() : "";
39
+ if (!sessionId) {
40
+ jsonReply(res, 400, { error: "session_id is required" });
41
+ return true;
42
+ }
43
+
44
+ const prompt = activePrompts.get(sessionId);
45
+ if (!prompt) {
46
+ jsonReply(res, 404, { error: "Session not found or not running" });
47
+ return true;
48
+ }
49
+
50
+ // 先发"卡住"提示消息给所有绑定的群聊
51
+ const chats = getChatsForSession(sessionId);
52
+ for (const chatId of chats) {
53
+ const platform = getPlatformForChat(chatId);
54
+ if (platform) {
55
+ await platform.sendText(chatId, "⚠️ Agent 检测到自己陷入了循环,正在结束生成…").catch(() => {});
56
+ }
57
+ }
58
+
59
+ // 丢弃缓存队列中的消息
60
+ cancelQueuedMessage(sessionId);
61
+
62
+ // fire-and-forget:立即把 stream-state 标为 done(而非 stopped),
63
+ // 让 display loop 以"正常完成"而非"已停止"来渲染卡片和最终回复。
64
+ // 不设 prompt.stopped = true,这样 runAgentSession 的 finally 也会写 "done"。
65
+ void (async () => {
66
+ try {
67
+ const current = await readStreamState(sessionId);
68
+ if (!current) return;
69
+ if (current.status !== "running") return;
70
+ await writeStreamState({
71
+ ...current,
72
+ status: "done",
73
+ updatedAt: Date.now(),
74
+ });
75
+ } catch (err) {
76
+ console.warn(
77
+ `[${ts()}] [STUCK-LOOP] writeStreamState(done) failed for ${sessionId}: ${(err as Error).message}`,
78
+ );
79
+ }
80
+ })();
81
+
82
+ // 不设 stopped 标记 → finally block 写 "done" → 卡片正常结束
83
+ prompt.controller.abort();
84
+
85
+ console.log(`[${ts()}] [STUCK-LOOP] Session ${sessionId} aborted as done (agent detected stuck loop)`);
86
+
87
+ jsonReply(res, 200, { ok: true });
88
+ return true;
89
+ }
package/src/index.ts CHANGED
@@ -78,6 +78,8 @@ import { SimulatedPlatform, SIM_DEFAULT_CHAT_ID } from "./sim-platform.ts";
78
78
  import { setMessageHandler } from "./sim-store.ts";
79
79
  import { handleAgentImageRequest } from "./agent-image-rpc.ts";
80
80
  import { handleAgentFileRequest } from "./agent-file-rpc.ts";
81
+ import { handleAgentStopStuckRequest } from "./agent-stop-stuck.ts";
82
+ import { applyPrivacy } from "./privacy.ts";
81
83
  import {
82
84
  createCardKitCard,
83
85
  sendCardKitMessage,
@@ -144,13 +146,13 @@ function createFeishuAdapter(): PlatformAdapter {
144
146
 
145
147
  // ---- 进度展示(CardKit 委托) ----
146
148
  async cardCreate(cardJson) {
147
- return createCardKitCard(await auth(), cardJson);
149
+ return createCardKitCard(await auth(), applyPrivacy(cardJson));
148
150
  },
149
151
  async cardSend(chatId, cardId) {
150
152
  return sendCardKitMessage(await auth(), chatId, cardId);
151
153
  },
152
154
  async cardUpdate(cardId, cardJson, sequence) {
153
- return updateCardKitCard(await auth(), cardId, cardJson, sequence);
155
+ return updateCardKitCard(await auth(), cardId, applyPrivacy(cardJson), sequence);
154
156
  },
155
157
  };
156
158
  }
@@ -689,7 +691,7 @@ async function main(): Promise<void> {
689
691
  setExtraApiHandler(async (req, res) => {
690
692
  const injected = await handleSimInjectMessage(req, res);
691
693
  if (injected) return true;
692
- return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
694
+ return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res)) || (await handleAgentStopStuckRequest(req, res));
693
695
  });
694
696
 
695
697
  const simServer = createServer(createUiRouter());
@@ -748,7 +750,7 @@ async function main(): Promise<void> {
748
750
  });
749
751
  });
750
752
  setExtraApiHandler(async (req, res) => {
751
- return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res));
753
+ return (await handleAgentImageRequest(req, res)) || (await handleAgentFileRequest(req, res)) || (await handleAgentStopStuckRequest(req, res));
752
754
  });
753
755
 
754
756
  console.log(`[启动 2/7] 环境与凭证检查`);