chatccc 0.2.175 → 0.2.176

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
@@ -308,9 +308,9 @@ Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理
308
308
  | `/stop` | 停止当前回复 |
309
309
  | `/state` | 查看当前会话状态 |
310
310
  | `/cd` | 查看或设置当前会话工作目录 |
311
- | `/sessions` | 查看所有会话状态 |
312
- | `/usage` | 查看 Codex 5h 用量和周用量 |
313
- | `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
311
+ | `/sessions` | 查看所有会话状态 |
312
+ | `/usage` | 查看 Codex 5h 用量和周用量 |
313
+ | `/git <子命令>` | 在当前会话工作目录执行 `git ...` 并回传输出 |
314
314
  | `/plan <内容>` | 只读计划模式:仅允许读文件和 stop-stuck-loop 请求,不执行任何写操作 |
315
315
  | `/ask <内容>` | 只读问答模式:与 /plan 相同,仅允许读文件和 stop-stuck-loop 请求 |
316
316
  | `/restart` | 重启机器人进程 |
@@ -1,46 +1,46 @@
1
- ## 重复成功命令保护
2
-
3
- 通过 Claude Agent SDK 在本项目中工作时,重复成功的 shell 命令是完成信号,不应继续使用工具。
4
-
5
- 以下条件**全部**满足时,视为相同命令:
6
- - 命令文本基本相同
7
- - 工作目录相同
8
- - 任务目标未变
9
- - 没有新的用户输入改变任务
10
- - 没有新的错误输出需要重新处理
11
-
12
- 同一命令**连续成功两次**后,停止调用工具,直接给出最终回复。不要"再验证一次"。
13
-
14
- 此规则适用于所有工具调用。
15
-
16
- ## 硬停止规则
17
-
18
- 同一命令在本项目中**不得成功执行三次**。若连续两次成功,第三次必须是最终回复。
19
-
20
- ## 卡住自检
21
-
22
- 如果你感觉任务已经完成,但发现自己陷入了循环(反复调用相同工具、输出 "stuck in a loop" 等),**立即**调用以下接口。
23
-
24
- **重要:必须用 heredoc 写临时文件再 curl,禁止在命令行参数中直接写中文 JSON。** Windows 下命令行参数传中文会变成 `?` 乱码。
25
-
26
- ```bash
27
- cat > /tmp/stop_ccc.json << 'STOPJSON'
28
- {"session_id": "{{session_id}}", "final_reply": "<你给用户的最终回复,总结已完成的工作和结果>"}
29
- STOPJSON
30
- curl -s -X POST {{stop_stuck_url}} -H "Content-Type: application/json" -d @/tmp/stop_ccc.json
31
- ```
32
-
33
- final_reply 注意事项:
34
- - 把 `<...>` 占位符替换为你的实际回复,不要包含双引号 `"` 和反斜杠 `\`
35
- - 不要包含换行,用空格代替
36
- - 保持简短(1-2 句话)
37
-
38
- 调用后停止所有工具调用,直接输出最终回复即可。
39
-
40
- ## 禁止 PushNotification
41
-
42
- **不要调用 PushNotification 工具。** 如果你认为当前会话已经完成任务需要结束,直接调用上述 stop-stuck-loop 接口,不要使用 PushNotification。
43
-
44
- ## 计划/问答模式权限规则
45
-
1
+ ## 重复成功命令保护
2
+
3
+ 通过 Claude Agent SDK 在本项目中工作时,重复成功的 shell 命令是完成信号,不应继续使用工具。
4
+
5
+ 以下条件**全部**满足时,视为相同命令:
6
+ - 命令文本基本相同
7
+ - 工作目录相同
8
+ - 任务目标未变
9
+ - 没有新的用户输入改变任务
10
+ - 没有新的错误输出需要重新处理
11
+
12
+ 同一命令**连续成功两次**后,停止调用工具,直接给出最终回复。不要"再验证一次"。
13
+
14
+ 此规则适用于所有工具调用。
15
+
16
+ ## 硬停止规则
17
+
18
+ 同一命令在本项目中**不得成功执行三次**。若连续两次成功,第三次必须是最终回复。
19
+
20
+ ## 卡住自检
21
+
22
+ 如果你感觉任务已经完成,但发现自己陷入了循环(反复调用相同工具、输出 "stuck in a loop" 等),**立即**调用以下接口。
23
+
24
+ **重要:必须用 heredoc 写临时文件再 curl,禁止在命令行参数中直接写中文 JSON。** Windows 下命令行参数传中文会变成 `?` 乱码。
25
+
26
+ ```bash
27
+ cat > /tmp/stop_ccc.json << 'STOPJSON'
28
+ {"session_id": "{{session_id}}", "final_reply": "<你给用户的最终回复,总结已完成的工作和结果>"}
29
+ STOPJSON
30
+ curl -s -X POST {{stop_stuck_url}} -H "Content-Type: application/json" -d @/tmp/stop_ccc.json
31
+ ```
32
+
33
+ final_reply 注意事项:
34
+ - 把 `<...>` 占位符替换为你的实际回复,不要包含双引号 `"` 和反斜杠 `\`
35
+ - 不要包含换行,用空格代替
36
+ - 保持简短(1-2 句话)
37
+
38
+ 调用后停止所有工具调用,直接输出最终回复即可。
39
+
40
+ ## 禁止 PushNotification
41
+
42
+ **不要调用 PushNotification 工具。** 如果你认为当前会话已经完成任务需要结束,直接调用上述 stop-stuck-loop 接口,不要使用 PushNotification。
43
+
44
+ ## 计划/问答模式权限规则
45
+
46
46
  如果用户消息以 `/plan` 或 `/ask` 开头,说明当前处于只读模式。**遇到任何需要用户同意的权限请求时,不要向用户申请权限,立即调用 stop-stuck-loop 接口提前结束本轮会话。**
@@ -1,3 +1,3 @@
1
- ## 计划/问答模式权限规则
2
-
1
+ ## 计划/问答模式权限规则
2
+
3
3
  如果用户消息以 `/plan` 或 `/ask` 开头,说明当前处于只读模式。**遇到任何需要用户同意的权限请求时,不要向用户申请权限,立即调用 stop-stuck-loop 接口提前结束本轮会话。**
@@ -1,3 +1,3 @@
1
- ## 计划/问答模式权限规则
2
-
1
+ ## 计划/问答模式权限规则
2
+
3
3
  如果用户消息以 `/plan` 或 `/ask` 开头,说明当前处于只读模式。**遇到任何需要用户同意的权限请求时,不要向用户申请权限,立即调用 stop-stuck-loop 接口提前结束本轮会话。**
@@ -1,64 +1,64 @@
1
- # Sending & Downloading Files/Videos
2
-
3
- ## Send Files or Videos
4
-
5
- Videos are sent as regular files (not media), which looks cleaner in Feishu.
6
-
7
- **Use the node script below. Do NOT use curl or raw HTTP — the script correctly handles errors and exits non-zero on failure.**
8
-
9
- ### Script
10
-
11
- ```bash
12
- node "{{send_file_script}}" --url "{{send_file_url}}" --session-id "{{session_id}}" --path "<absolute file path>" --caption "<optional caption>"
13
- ```
14
-
15
- ### Rules
16
-
17
- - Use the node script above — never curl or raw HTTP.
18
- - Save or choose a local file first.
19
- - Use an absolute local path.
20
- - Max file size: 30MB.
21
- - Supported formats: .mp4 .mov .avi .mkv .webm .flv .mp3 .wav .ogg .aac .m4a .pdf .doc .docx .xls .xlsx .csv .ppt .pptx .txt .zip .tar .gz.
22
- - Only send a file/video when the user asked for one or when it materially helps the answer.
23
- - **If the script fails (non-zero exit), read stderr for the error. Do NOT retry with the same path. Either fix the problem (wrong extension, missing file, etc.) or tell the user what the error was. Never retry more than once.**
24
-
25
- ### Video Compression (when file > 30MB)
26
-
27
- If the video exceeds 30MB, compress it with ffmpeg before sending.
28
-
29
- **Ensure ffmpeg is available** (install if missing):
30
-
31
- | OS | Install command |
32
- |----|----------------|
33
- | macOS | `brew install ffmpeg` |
34
- | Linux (Debian/Ubuntu) | `sudo apt install ffmpeg` |
35
- | Linux (RHEL/Fedora) | `sudo dnf install ffmpeg` |
36
- | Windows | `winget install Gyan.FFmpeg` |
37
-
38
- **Two-pass compression** (target ~28MB for 30s video, adjust `b:v` for other durations):
39
-
40
- ```bash
41
- ffmpeg -y -i "<input>" -c:v libx264 -b:v <bitrate>k -pass 1 -f mp4 NUL
42
- ffmpeg -y -i "<input>" -c:v libx264 -b:v <bitrate>k -pass 2 -c:a aac -b:a 128k "<output>"
43
- ```
44
-
45
- Bitrate formula: `bitrate = 28 × 8 × 1000 ÷ duration_seconds - 128` (target ~28MB, safe under 30MB).
46
- On Windows replace `NUL` with `NUL` (same); on Linux/macOS use `/dev/null`.
47
-
48
- If the compressed file still exceeds 30MB, explain to the user that automatic compression wasn't enough and suggest they manually trim or re-encode the source.
49
-
50
- ## Download Files or Videos
51
-
52
- When the user sends a file or video to the bot, the message contains `message_id` and `file_key`. Download it with:
53
-
54
- ```bash
55
- node "{{download_video_script}}" --message-id <message_id> --file-key <file_key> --name <file_name>
56
- ```
57
-
58
- If only `chat_id` and `file_key` are available:
59
-
60
- ```bash
61
- node "{{download_video_script}}" --chat-id <chat_id> --file-key <file_key> --name <file_name>
62
- ```
63
-
1
+ # Sending & Downloading Files/Videos
2
+
3
+ ## Send Files or Videos
4
+
5
+ Videos are sent as regular files (not media), which looks cleaner in Feishu.
6
+
7
+ **Use the node script below. Do NOT use curl or raw HTTP — the script correctly handles errors and exits non-zero on failure.**
8
+
9
+ ### Script
10
+
11
+ ```bash
12
+ node "{{send_file_script}}" --url "{{send_file_url}}" --session-id "{{session_id}}" --path "<absolute file path>" --caption "<optional caption>"
13
+ ```
14
+
15
+ ### Rules
16
+
17
+ - Use the node script above — never curl or raw HTTP.
18
+ - Save or choose a local file first.
19
+ - Use an absolute local path.
20
+ - Max file size: 30MB.
21
+ - Supported formats: .mp4 .mov .avi .mkv .webm .flv .mp3 .wav .ogg .aac .m4a .pdf .doc .docx .xls .xlsx .csv .ppt .pptx .txt .zip .tar .gz.
22
+ - Only send a file/video when the user asked for one or when it materially helps the answer.
23
+ - **If the script fails (non-zero exit), read stderr for the error. Do NOT retry with the same path. Either fix the problem (wrong extension, missing file, etc.) or tell the user what the error was. Never retry more than once.**
24
+
25
+ ### Video Compression (when file > 30MB)
26
+
27
+ If the video exceeds 30MB, compress it with ffmpeg before sending.
28
+
29
+ **Ensure ffmpeg is available** (install if missing):
30
+
31
+ | OS | Install command |
32
+ |----|----------------|
33
+ | macOS | `brew install ffmpeg` |
34
+ | Linux (Debian/Ubuntu) | `sudo apt install ffmpeg` |
35
+ | Linux (RHEL/Fedora) | `sudo dnf install ffmpeg` |
36
+ | Windows | `winget install Gyan.FFmpeg` |
37
+
38
+ **Two-pass compression** (target ~28MB for 30s video, adjust `b:v` for other durations):
39
+
40
+ ```bash
41
+ ffmpeg -y -i "<input>" -c:v libx264 -b:v <bitrate>k -pass 1 -f mp4 NUL
42
+ ffmpeg -y -i "<input>" -c:v libx264 -b:v <bitrate>k -pass 2 -c:a aac -b:a 128k "<output>"
43
+ ```
44
+
45
+ Bitrate formula: `bitrate = 28 × 8 × 1000 ÷ duration_seconds - 128` (target ~28MB, safe under 30MB).
46
+ On Windows replace `NUL` with `NUL` (same); on Linux/macOS use `/dev/null`.
47
+
48
+ If the compressed file still exceeds 30MB, explain to the user that automatic compression wasn't enough and suggest they manually trim or re-encode the source.
49
+
50
+ ## Download Files or Videos
51
+
52
+ When the user sends a file or video to the bot, the message contains `message_id` and `file_key`. Download it with:
53
+
54
+ ```bash
55
+ node "{{download_video_script}}" --message-id <message_id> --file-key <file_key> --name <file_name>
56
+ ```
57
+
58
+ If only `chat_id` and `file_key` are available:
59
+
60
+ ```bash
61
+ node "{{download_video_script}}" --chat-id <chat_id> --file-key <file_key> --name <file_name>
62
+ ```
63
+
64
64
  Downloads are saved under `~/.chatccc/videos/downloads/`.
@@ -1,25 +1,25 @@
1
- # Sending & Receiving Images
2
-
3
- ## Send Images
4
-
5
- **Use the node script below. Do NOT use curl or raw HTTP — the script correctly handles errors and exits non-zero on failure.**
6
-
7
- ### Script
8
-
9
- ```bash
10
- node "{{send_image_script}}" --url "{{send_image_url}}" --session-id "{{session_id}}" --path "<absolute image path>" --caption "<optional caption>"
11
- ```
12
-
13
- ### Rules
14
-
15
- - Use the node script above — never curl or raw HTTP.
16
- - Save or choose a local image file first.
17
- - Use an absolute local path.
18
- - Supported formats: .png, .jpg, .jpeg, .webp, .gif, .bmp.
19
- - Max image size: 10MB.
20
- - Only send an image when the user asked for one or when it materially helps the answer.
21
- - **If the script fails (non-zero exit), read stderr for the error. Do NOT retry with the same path. Either fix the problem (wrong extension, missing file, etc.) or tell the user what the error was. Never retry more than once.**
22
-
23
- ## Receive Images
24
-
1
+ # Sending & Receiving Images
2
+
3
+ ## Send Images
4
+
5
+ **Use the node script below. Do NOT use curl or raw HTTP — the script correctly handles errors and exits non-zero on failure.**
6
+
7
+ ### Script
8
+
9
+ ```bash
10
+ node "{{send_image_script}}" --url "{{send_image_url}}" --session-id "{{session_id}}" --path "<absolute image path>" --caption "<optional caption>"
11
+ ```
12
+
13
+ ### Rules
14
+
15
+ - Use the node script above — never curl or raw HTTP.
16
+ - Save or choose a local image file first.
17
+ - Use an absolute local path.
18
+ - Supported formats: .png, .jpg, .jpeg, .webp, .gif, .bmp.
19
+ - Max image size: 10MB.
20
+ - Only send an image when the user asked for one or when it materially helps the answer.
21
+ - **If the script fails (non-zero exit), read stderr for the error. Do NOT retry with the same path. Either fix the problem (wrong extension, missing file, etc.) or tell the user what the error was. Never retry more than once.**
22
+
23
+ ## Receive Images
24
+
25
25
  Images sent to the bot are automatically downloaded to `~/.chatccc/images/downloads/`. The message contains an `image_key` that maps to the cached file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatccc",
3
- "version": "0.2.175",
3
+ "version": "0.2.176",
4
4
  "description": "Feishu bot bridge for Claude Code",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,60 +1,60 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
-
3
- import { updateCardKitCard } from "../cardkit.ts";
4
-
5
- function abortError(): Error {
6
- const err = new Error("aborted");
7
- err.name = "AbortError";
8
- return err;
9
- }
10
-
11
- describe("CardKit request timeout", () => {
12
- afterEach(() => {
13
- vi.useRealTimers();
14
- vi.restoreAllMocks();
15
- vi.unstubAllGlobals();
16
- });
17
-
18
- it("aborts a hung card update so display.cardBusy can be released", async () => {
19
- vi.useFakeTimers();
20
- vi.spyOn(console, "error").mockImplementation(() => {});
21
-
22
- const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
23
- new Promise<Response>((_resolve, reject) => {
24
- init?.signal?.addEventListener("abort", () => reject(abortError()));
25
- })
26
- ));
27
- vi.stubGlobal("fetch", fetchMock);
28
-
29
- const update = expect(updateCardKitCard("token", "card-1", "{}", 7))
30
- .rejects.toThrow("updateCard cardId=card-1 seq=7 timeout after 15000ms");
31
- await vi.advanceTimersByTimeAsync(15_000);
32
-
33
- await update;
34
- expect(fetchMock).toHaveBeenCalledWith(
35
- expect.stringContaining("/cardkit/v1/cards/card-1"),
36
- expect.objectContaining({ signal: expect.any(AbortSignal) }),
37
- );
38
- });
39
-
40
- it("also aborts when response body reading hangs", async () => {
41
- vi.useFakeTimers();
42
- vi.spyOn(console, "error").mockImplementation(() => {});
43
-
44
- const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
45
- Promise.resolve({
46
- status: 200,
47
- text: () => new Promise<string>((_resolve, reject) => {
48
- init?.signal?.addEventListener("abort", () => reject(abortError()));
49
- }),
50
- } as Response)
51
- ));
52
- vi.stubGlobal("fetch", fetchMock);
53
-
54
- const update = expect(updateCardKitCard("token", "card-2", "{}", 8))
55
- .rejects.toThrow("updateCard cardId=card-2 seq=8 timeout after 15000ms");
56
- await vi.advanceTimersByTimeAsync(15_000);
57
-
58
- await update;
59
- });
60
- });
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { updateCardKitCard } from "../cardkit.ts";
4
+
5
+ function abortError(): Error {
6
+ const err = new Error("aborted");
7
+ err.name = "AbortError";
8
+ return err;
9
+ }
10
+
11
+ describe("CardKit request timeout", () => {
12
+ afterEach(() => {
13
+ vi.useRealTimers();
14
+ vi.restoreAllMocks();
15
+ vi.unstubAllGlobals();
16
+ });
17
+
18
+ it("aborts a hung card update so display.cardBusy can be released", async () => {
19
+ vi.useFakeTimers();
20
+ vi.spyOn(console, "error").mockImplementation(() => {});
21
+
22
+ const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
23
+ new Promise<Response>((_resolve, reject) => {
24
+ init?.signal?.addEventListener("abort", () => reject(abortError()));
25
+ })
26
+ ));
27
+ vi.stubGlobal("fetch", fetchMock);
28
+
29
+ const update = expect(updateCardKitCard("token", "card-1", "{}", 7))
30
+ .rejects.toThrow("updateCard cardId=card-1 seq=7 timeout after 15000ms");
31
+ await vi.advanceTimersByTimeAsync(15_000);
32
+
33
+ await update;
34
+ expect(fetchMock).toHaveBeenCalledWith(
35
+ expect.stringContaining("/cardkit/v1/cards/card-1"),
36
+ expect.objectContaining({ signal: expect.any(AbortSignal) }),
37
+ );
38
+ });
39
+
40
+ it("also aborts when response body reading hangs", async () => {
41
+ vi.useFakeTimers();
42
+ vi.spyOn(console, "error").mockImplementation(() => {});
43
+
44
+ const fetchMock = vi.fn((_url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => (
45
+ Promise.resolve({
46
+ status: 200,
47
+ text: () => new Promise<string>((_resolve, reject) => {
48
+ init?.signal?.addEventListener("abort", () => reject(abortError()));
49
+ }),
50
+ } as Response)
51
+ ));
52
+ vi.stubGlobal("fetch", fetchMock);
53
+
54
+ const update = expect(updateCardKitCard("token", "card-2", "{}", 8))
55
+ .rejects.toThrow("updateCard cardId=card-2 seq=8 timeout after 15000ms");
56
+ await vi.advanceTimersByTimeAsync(15_000);
57
+
58
+ await update;
59
+ });
60
+ });