chatccc 0.1.1 → 0.1.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 +142 -105
- package/bin/chatccc.mjs +10 -7
- package/package.json +40 -40
- package/src/cards.ts +3 -3
- package/src/config.ts +18 -7
- package/src/index.ts +12 -12
- package/src/session.ts +22 -41
package/README.md
CHANGED
|
@@ -1,106 +1,143 @@
|
|
|
1
|
-
# ChatCCC
|
|
2
|
-
|
|
3
|
-
**飞书聊天控制 Claude Code,未来支持 Cursor、Codex**
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 解决的核心痛点
|
|
8
|
-
|
|
9
|
-
传统的 Claude Code(尤其是使用第三方 API 的,如 DeepSeek),需要坐在电脑桌前才能用。**离开电脑就没法用了。**
|
|
10
|
-
|
|
11
|
-
ChatCCC 把 Claude Code 接入了飞书群聊:
|
|
12
|
-
|
|
13
|
-
- **手机上也能用 Claude Code** —— 在飞书群里发消息就等于在终端输入指令,Claude 的思考和回复会流式展示在群卡片里
|
|
14
|
-
- **多会话并行** —— 一个群就是一个 Claude 会话,完全隔离、互不干扰,并行工作效率更高
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
一句话:**在任何设备上打开飞书,就能让 Claude 帮你写代码、排查问题、分析项目。**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
# ChatCCC
|
|
2
|
+
|
|
3
|
+
**飞书聊天控制 Claude Code,未来支持 Cursor、Codex**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 解决的核心痛点
|
|
8
|
+
|
|
9
|
+
传统的 Claude Code(尤其是使用第三方 API 的,如 DeepSeek),需要坐在电脑桌前才能用。**离开电脑就没法用了。**
|
|
10
|
+
|
|
11
|
+
ChatCCC 把 Claude Code 接入了飞书群聊:
|
|
12
|
+
|
|
13
|
+
- **手机上也能用 Claude Code** —— 在飞书群里发消息就等于在终端输入指令,Claude 的思考和回复会流式展示在群卡片里
|
|
14
|
+
- **多会话并行** —— 一个群就是一个 Claude 会话,完全隔离、互不干扰,并行工作效率更高
|
|
15
|
+
- **体验丝滑** —— 思考过程完整,响应迅速
|
|
16
|
+
|
|
17
|
+
一句话:**在任何设备上打开飞书,就能让 Claude 帮你写代码、排查问题、分析项目。**
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="images/img_readme_0.jpg" alt="飞书群聊中使用 ChatCCC" width="280" />
|
|
21
|
+
 
|
|
22
|
+
<img src="images/img_readme_1.jpg" alt="思考过程和工具调用" width="280" />
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 怎么部署
|
|
28
|
+
|
|
29
|
+
### 1. 安装
|
|
30
|
+
|
|
31
|
+
#### npm 全局安装(推荐)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g chatccc
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
要求 Node.js >= 20。安装完成后在项目目录创建 `.env`文件(参照.env.example文件),然后输入启动指令就启动了:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
chatccc
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### 从源码安装
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/wzj998/ChatCCC.git
|
|
47
|
+
cd ChatCCC
|
|
48
|
+
npm install
|
|
49
|
+
npm run dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
启动后机器人通过 WebSocket 连接飞书服务器,日志会写入 `logs/` 目录。
|
|
53
|
+
|
|
54
|
+
### 2. 创建飞书应用
|
|
55
|
+
|
|
56
|
+
打开 [飞书开放平台](https://open.feishu.cn),创建一个**企业自建应用**。
|
|
57
|
+
|
|
58
|
+
**添加应用能力**:开启「机器人」功能。
|
|
59
|
+
|
|
60
|
+
**权限配置**(在「权限管理」中搜索并开通以下权限):
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
| 权限 | 用途 |
|
|
64
|
+
| ------------------------------- | --------- |
|
|
65
|
+
| `im:chat` | 创建和管理群聊 |
|
|
66
|
+
| `im:message` | 收发消息 |
|
|
67
|
+
| `im:message:send_as_bot` | 以机器人身份发消息 |
|
|
68
|
+
| `im:message.p2p_msg:readonly` | 读取私聊消息 |
|
|
69
|
+
| `im:message.group_msg:readonly` | 读取群聊消息 |
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
**事件订阅**(在「事件与回调」中):订阅 `im.message.receive_v1` 和 `card.action.trigger` 事件。
|
|
73
|
+
|
|
74
|
+
**发布版本**:配置完成后创建应用版本并发布(仅企业内部可用即可)。
|
|
75
|
+
|
|
76
|
+
### 3. 获取凭证
|
|
77
|
+
|
|
78
|
+
在飞书应用详情页的「凭证与基础信息」中,复制 **App ID** 和 **App Secret**。
|
|
79
|
+
|
|
80
|
+
### 4. 配置环境变量
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cp .env.example .env
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
编辑 `.env`,填入上一步拿到的凭证:
|
|
87
|
+
|
|
88
|
+
```env
|
|
89
|
+
FEISHU_CLAUDER_APP_ID=cli_xxxxxxxxxxxx
|
|
90
|
+
FEISHU_CLAUDER_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
可选的高级配置:
|
|
94
|
+
|
|
95
|
+
```env
|
|
96
|
+
# Claude 模型(见下方优先级说明)
|
|
97
|
+
CHATCCC_ANTHROPIC_MODEL=dashscope/deepseek-v4-pro-anthropic
|
|
98
|
+
|
|
99
|
+
# 思考深度,可选值: low | medium | high | max
|
|
100
|
+
CHATCCC_ANTHROPIC_EFFORT=max
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`**CHATCCC_ANTHROPIC_MODEL` 优先级**:环境变量设置的值 > 代码内默认值 `dashscope/deepseek-v4-pro-anthropic`。不设或设为空白时自动使用默认模型。
|
|
104
|
+
|
|
105
|
+
`**CHATCCC_ANTHROPIC_EFFORT` 优先级**:环境变量设置的值 > 代码内默认值 `max`。
|
|
106
|
+
|
|
107
|
+
`**CHATCCC_PORT` 端口配置**:默认端口为 `18080`。如果你需要在一台机器上同时运行多个 ChatCCC 实例(例如用不同飞书应用分别接入不同项目),可以在各自的 `.env` 中设置不同的端口:
|
|
108
|
+
|
|
109
|
+
```env
|
|
110
|
+
# 实例 A(项目1)的 .env
|
|
111
|
+
CHATCCC_PORT=18080
|
|
112
|
+
|
|
113
|
+
# 实例 B(项目2)的 .env
|
|
114
|
+
CHATCCC_PORT=18081
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
不同端口的实例互不冲突,启动时会自动清理各自端口上的旧进程。
|
|
118
|
+
|
|
119
|
+
> **权限说明**:目前 ChatCCC 以 `bypassPermissions` 模式运行,即跳过所有权限确认、允许所有操作。后续会考虑引入细粒度权限控制,让你可以按需放行特定操作。
|
|
120
|
+
|
|
121
|
+
> **Linux 用户注意**:不能将项目装在 `/root` 目录下运行。
|
|
122
|
+
|
|
123
|
+
### 5. 开始使用
|
|
124
|
+
|
|
125
|
+
在飞书中找到你的机器人,发送 `/new`,机器人会自动创建一个群聊并把你的 Claude 会话绑定到该群。之后直接在群里发消息就能和 Claude 对话。
|
|
126
|
+
|
|
127
|
+
### 可用指令
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
| 指令 | 作用 |
|
|
131
|
+
| ---------- | ---------------------------- |
|
|
132
|
+
| `/new` | 创建新的 Claude 会话(绑定一个新群聊) |
|
|
133
|
+
| `/stop` | 停止当前正在生成的回复 |
|
|
134
|
+
| `/status` | 查看当前会话的状态(轮数、模型、上下文 token 等) |
|
|
135
|
+
| `/cd` | 查看/切换工作目录 |
|
|
136
|
+
| `/restart` | 重启机器人进程 |
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 技术栈
|
|
142
|
+
|
|
106
143
|
TypeScript / Node.js >= 20 / tsx / Anthropic Claude Agent SDK / 飞书 WebSocket API / CardKit
|
package/bin/chatccc.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { register } from "node:module";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire, register } from "node:module";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const pkgRoot = new URL("../", import.meta.url);
|
|
6
|
+
// 勿用 pathToFileURL("./"):会相对于进程 cwd。用 createRequire 从本脚本所在包解析 tsx
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
register(pathToFileURL(require.resolve("tsx/esm")), pkgRoot);
|
|
9
|
+
|
|
10
|
+
await import(new URL("../src/index.ts", import.meta.url));
|
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "chatccc",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./src/index.ts",
|
|
7
|
-
"bin": {
|
|
8
|
-
"chatccc": "
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"src/",
|
|
12
|
-
"bin/",
|
|
13
|
-
"package.json",
|
|
14
|
-
"README.md"
|
|
15
|
-
],
|
|
16
|
-
"scripts": {
|
|
17
|
-
"dev": "tsx --env-file=.env src/index.ts",
|
|
18
|
-
"chatccc": "tsx --env-file=.env src/index.ts",
|
|
19
|
-
"start": "tsx --env-file=.env src/index.ts",
|
|
20
|
-
"demo:bot-test": "tsx --env-file=.env demo/bot_test.ts",
|
|
21
|
-
"demo:bot-test:local": "tsx --env-file=.env demo/bot_test.ts --local",
|
|
22
|
-
"demo:create-group": "tsx --env-file=.env src/index.ts",
|
|
23
|
-
"demo:create-group:local": "tsx --env-file=.env src/index.ts --local",
|
|
24
|
-
"demo:permission-check": "tsx --env-file=.env demo/permission_check.ts"
|
|
25
|
-
},
|
|
26
|
-
"dependencies": {
|
|
27
|
-
"@anthropic-ai/claude-agent-sdk": "^0.2.133",
|
|
28
|
-
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
29
|
-
"tsx": "^4.0.0",
|
|
30
|
-
"ws": "^8.18.0"
|
|
31
|
-
},
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"@types/node": "^20.0.0",
|
|
34
|
-
"@types/ws": "^8.18.1",
|
|
35
|
-
"typescript": "^5.0.0"
|
|
36
|
-
},
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=20"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "chatccc",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"chatccc": "bin/chatccc.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"bin/",
|
|
13
|
+
"package.json",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "tsx --env-file=.env src/index.ts",
|
|
18
|
+
"chatccc": "tsx --env-file=.env src/index.ts",
|
|
19
|
+
"start": "tsx --env-file=.env src/index.ts",
|
|
20
|
+
"demo:bot-test": "tsx --env-file=.env demo/bot_test.ts",
|
|
21
|
+
"demo:bot-test:local": "tsx --env-file=.env demo/bot_test.ts --local",
|
|
22
|
+
"demo:create-group": "tsx --env-file=.env src/index.ts",
|
|
23
|
+
"demo:create-group:local": "tsx --env-file=.env src/index.ts --local",
|
|
24
|
+
"demo:permission-check": "tsx --env-file=.env demo/permission_check.ts"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.133",
|
|
28
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
29
|
+
"tsx": "^4.0.0",
|
|
30
|
+
"ws": "^8.18.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"@types/ws": "^8.18.1",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/cards.ts
CHANGED
|
@@ -93,7 +93,7 @@ export function buildThinkingCardV2(
|
|
|
93
93
|
schema: "2.0",
|
|
94
94
|
config: {
|
|
95
95
|
update_multi: true,
|
|
96
|
-
streaming_mode:
|
|
96
|
+
streaming_mode: false,
|
|
97
97
|
},
|
|
98
98
|
header: {
|
|
99
99
|
template: headerTemplate,
|
|
@@ -133,8 +133,8 @@ export function buildCdContent(
|
|
|
133
133
|
const listing = display.map(e => e.isDir ? `📁 ${e.name}/` : `📄 ${e.name}`).join("\n");
|
|
134
134
|
|
|
135
135
|
const statusLine = isUpdate
|
|
136
|
-
?
|
|
137
|
-
:
|
|
136
|
+
? `**新会话工作路径已切换至:** \`${dirPath}\``
|
|
137
|
+
: `**新会话默认工作路径:** \`${dirPath}\``;
|
|
138
138
|
|
|
139
139
|
return [
|
|
140
140
|
statusLine,
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
|
|
5
5
|
import { setupFileLogging } from "./shared.ts";
|
|
6
6
|
|
|
@@ -38,25 +38,36 @@ export const APP_SECRET: string = process.env.FEISHU_CLAUDER_APP_SECRET ?? "";
|
|
|
38
38
|
export const BASE_URL = "https://open.feishu.cn/open-apis";
|
|
39
39
|
export const LOCAL_RELAY_URL = "ws://127.0.0.1:18080";
|
|
40
40
|
|
|
41
|
+
export const CHATCCC_PORT = parseInt(process.env.CHATCCC_PORT?.trim() ?? "18080", 10);
|
|
42
|
+
|
|
41
43
|
export const CLAUDE_MODEL =
|
|
42
44
|
process.env.CHATCCC_ANTHROPIC_MODEL?.trim() || "dashscope/deepseek-v4-pro-anthropic";
|
|
43
45
|
|
|
44
46
|
export const CLAUDE_EFFORT =
|
|
45
47
|
process.env.CHATCCC_ANTHROPIC_EFFORT?.trim() || "max";
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
// 新建会话的默认工作路径(/cd 命令设置,持久化到本地文件)
|
|
50
|
+
// 该路径仅影响通过 /new 新建的 Claude 会话,不影响已有会话的 resume。
|
|
51
|
+
export const DEFAULT_CWD_FILE = join(PROJECT_ROOT, ".claude", "working_dir.txt");
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
/** 读取 /cd 设置的默认工作路径。若文件不存在或路径已失效,回退到 PROJECT_ROOT。 */
|
|
54
|
+
export async function getDefaultCwd(): Promise<string> {
|
|
50
55
|
try {
|
|
51
|
-
const content = await readFile(
|
|
56
|
+
const content = await readFile(DEFAULT_CWD_FILE, "utf-8");
|
|
52
57
|
const dir = content.trim();
|
|
53
|
-
if (dir)
|
|
58
|
+
if (dir) {
|
|
59
|
+
try {
|
|
60
|
+
const s = await stat(dir);
|
|
61
|
+
if (s.isDirectory()) return dir;
|
|
62
|
+
} catch { /* path gone, fall through */ }
|
|
63
|
+
}
|
|
54
64
|
} catch { /* file doesn't exist yet */ }
|
|
55
65
|
return PROJECT_ROOT;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
/** 设置新建会话的默认工作路径(由 /cd 命令调用) */
|
|
69
|
+
export async function setDefaultCwd(dir: string): Promise<void> {
|
|
70
|
+
await writeFile(DEFAULT_CWD_FILE, dir, "utf-8");
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import WebSocket from "ws";
|
|
|
30
30
|
|
|
31
31
|
import { ensureSingleInstance, createRelayServer } from "./shared.ts";
|
|
32
32
|
import {
|
|
33
|
+
CHATCCC_PORT,
|
|
33
34
|
APP_ID,
|
|
34
35
|
APP_SECRET,
|
|
35
36
|
BASE_URL,
|
|
@@ -40,8 +41,8 @@ import {
|
|
|
40
41
|
USE_LOCAL,
|
|
41
42
|
appendChatLog,
|
|
42
43
|
fileLog,
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
getDefaultCwd,
|
|
45
|
+
setDefaultCwd,
|
|
45
46
|
ts,
|
|
46
47
|
} from "./config.ts";
|
|
47
48
|
import {
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
sendRestartCard,
|
|
59
60
|
} from "./feishu-api.ts";
|
|
60
61
|
import { buildHelpCard, buildStatusCard, buildThinkingCardV2, buildCdContent } from "./cards.ts";
|
|
61
|
-
import {
|
|
62
|
+
import { updateCardKitCard } from "./cardkit.ts";
|
|
62
63
|
import {
|
|
63
64
|
MAX_PROCESSED,
|
|
64
65
|
chatSessionMap,
|
|
@@ -171,7 +172,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
171
172
|
|
|
172
173
|
if (text === "/cd" || text.startsWith("/cd ")) {
|
|
173
174
|
const cdToken = await getTenantAccessToken();
|
|
174
|
-
const currentDir = await
|
|
175
|
+
const currentDir = await getDefaultCwd();
|
|
175
176
|
const arg = text.slice(3).trim(); // everything after "/cd" (may be empty)
|
|
176
177
|
|
|
177
178
|
// Resolve target directory
|
|
@@ -188,18 +189,18 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
188
189
|
try {
|
|
189
190
|
const s = await stat(targetDir);
|
|
190
191
|
if (!s.isDirectory()) {
|
|
191
|
-
await sendCardReply(cdToken, chatId, "
|
|
192
|
+
await sendCardReply(cdToken, chatId, "新会话工作路径", `路径存在但不是目录:\n\`${targetDir}\``, "red");
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
194
195
|
} catch {
|
|
195
|
-
await sendCardReply(cdToken, chatId, "
|
|
196
|
+
await sendCardReply(cdToken, chatId, "新会话工作路径", `路径不存在:\n\`${targetDir}\``, "red");
|
|
196
197
|
return;
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
// Change working dir if user provided a path
|
|
200
201
|
const isUpdate = !!arg && targetDir !== currentDir;
|
|
201
202
|
if (isUpdate) {
|
|
202
|
-
await
|
|
203
|
+
await setDefaultCwd(targetDir);
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
// Read directory entries
|
|
@@ -207,7 +208,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
207
208
|
try {
|
|
208
209
|
entries = await readdir(targetDir);
|
|
209
210
|
} catch (err) {
|
|
210
|
-
await sendCardReply(cdToken, chatId, "
|
|
211
|
+
await sendCardReply(cdToken, chatId, "新会话工作路径", `无法读取目录:\n\`${targetDir}\`\n\n${(err as Error).message}`, "red");
|
|
211
212
|
return;
|
|
212
213
|
}
|
|
213
214
|
|
|
@@ -225,7 +226,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
225
226
|
});
|
|
226
227
|
|
|
227
228
|
const content = buildCdContent(targetDir, withStats, isUpdate);
|
|
228
|
-
await sendCardReply(cdToken, chatId, "
|
|
229
|
+
await sendCardReply(cdToken, chatId, "新会话工作路径", content, "blue");
|
|
229
230
|
return;
|
|
230
231
|
}
|
|
231
232
|
|
|
@@ -386,7 +387,6 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
386
387
|
{ showStop: false, headerTitle: "已中断", headerTemplate: "yellow" }
|
|
387
388
|
);
|
|
388
389
|
let nextSeq = existing.sequence + 1;
|
|
389
|
-
await setCardKitSettings(freshToken, cardId, { streaming_mode: false }, nextSeq++).catch(() => {});
|
|
390
390
|
await updateCardKitCard(freshToken, cardId, interruptedCard, nextSeq).catch((err) => {
|
|
391
391
|
console.error(`[${ts()}] [INTERRUPT] CardKit update failed: ${(err as Error).message}`);
|
|
392
392
|
});
|
|
@@ -427,14 +427,14 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
427
427
|
// ---------------------------------------------------------------------------
|
|
428
428
|
|
|
429
429
|
async function main(): Promise<void> {
|
|
430
|
-
ensureSingleInstance(PID_FILE,
|
|
430
|
+
ensureSingleInstance(PID_FILE, CHATCCC_PORT);
|
|
431
431
|
|
|
432
432
|
if (!APP_ID || !APP_SECRET) {
|
|
433
433
|
console.log("ERROR: FEISHU_CLAUDER_APP_ID / FEISHU_CLAUDER_APP_SECRET not set");
|
|
434
434
|
process.exit(1);
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
const { server: relayServer, broadcast } = createRelayServer(
|
|
437
|
+
const { server: relayServer, broadcast } = createRelayServer(CHATCCC_PORT);
|
|
438
438
|
broadcastToRelay = broadcast;
|
|
439
439
|
|
|
440
440
|
const modeTag = USE_LOCAL ? " (local relay mode)" : "";
|
package/src/session.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { unstable_v2_createSession, unstable_v2_resumeSession } from "@anthropic-ai/claude-agent-sdk";
|
|
1
|
+
import { getSessionInfo, unstable_v2_createSession, unstable_v2_resumeSession } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
|
|
3
|
-
import { CLAUDE_EFFORT, CLAUDE_MODEL, fileLog, ts } from "./config.ts";
|
|
3
|
+
import { CLAUDE_EFFORT, CLAUDE_MODEL, fileLog, getDefaultCwd, ts } from "./config.ts";
|
|
4
4
|
import { buildThinkingCardV2, getToolEmoji, truncateContent } from "./cards.ts";
|
|
5
5
|
import {
|
|
6
6
|
createCardKitCard,
|
|
7
7
|
sendCardKitMessage,
|
|
8
|
-
setCardKitSettings,
|
|
9
|
-
streamCardKitElement,
|
|
10
8
|
updateCardKitCard,
|
|
11
9
|
} from "./cardkit.ts";
|
|
12
10
|
import { sendTextReply } from "./feishu-api.ts";
|
|
@@ -58,11 +56,13 @@ export function resetState(): void {
|
|
|
58
56
|
// ---------------------------------------------------------------------------
|
|
59
57
|
|
|
60
58
|
export async function initClaudeSession(): Promise<string> {
|
|
61
|
-
|
|
59
|
+
const cwd = await getDefaultCwd();
|
|
60
|
+
console.log(`[${ts()}] [STEP 1/5] Creating Claude session via SDK (model=${CLAUDE_MODEL}, effort=${CLAUDE_EFFORT}, cwd=${cwd})`);
|
|
62
61
|
|
|
63
62
|
const session = unstable_v2_createSession({
|
|
64
63
|
model: CLAUDE_MODEL,
|
|
65
64
|
effort: CLAUDE_EFFORT,
|
|
65
|
+
cwd,
|
|
66
66
|
permissionMode: "bypassPermissions",
|
|
67
67
|
allowDangerouslySkipPermissions: true,
|
|
68
68
|
autoCompactEnabled: true,
|
|
@@ -104,18 +104,19 @@ export async function resumeAndPrompt(
|
|
|
104
104
|
chatId: string,
|
|
105
105
|
msgTimestamp: number
|
|
106
106
|
): Promise<void> {
|
|
107
|
-
|
|
107
|
+
const cwd = (await getSessionInfo(sessionId))?.cwd ?? (await getDefaultCwd());
|
|
108
|
+
console.log(`[${ts()}] Resuming Claude session: ${sessionId} (model=${CLAUDE_MODEL}, effort=${CLAUDE_EFFORT}, cwd=${cwd})`);
|
|
108
109
|
|
|
109
110
|
const session = unstable_v2_resumeSession(sessionId, {
|
|
110
111
|
model: CLAUDE_MODEL,
|
|
111
112
|
effort: CLAUDE_EFFORT,
|
|
113
|
+
cwd,
|
|
112
114
|
permissionMode: "bypassPermissions",
|
|
113
115
|
allowDangerouslySkipPermissions: true,
|
|
114
116
|
autoCompactEnabled: true,
|
|
115
117
|
} as any);
|
|
116
118
|
|
|
117
119
|
let cardId: string | null = null;
|
|
118
|
-
let streamingEnabled = false;
|
|
119
120
|
chatSessionMap.set(chatId, {
|
|
120
121
|
gen: ++sessionGen,
|
|
121
122
|
close: () => session.close(),
|
|
@@ -152,25 +153,16 @@ export async function resumeAndPrompt(
|
|
|
152
153
|
});
|
|
153
154
|
if (cardId) {
|
|
154
155
|
const cEntry = chatSessionMap.get(chatId);
|
|
155
|
-
if (cEntry) { cEntry.cardId = cardId; }
|
|
156
|
-
const
|
|
157
|
-
console.error(`[${ts()}] [CARDIKT] enableStreaming FAIL: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
158
|
-
fileLog.flush();
|
|
159
|
-
return false;
|
|
160
|
-
});
|
|
161
|
-
const settingsSucceeded = settingsOk !== false;
|
|
162
|
-
if (settingsSucceeded && cEntry) { cEntry.sequence = 1; }
|
|
163
|
-
const sendOk = settingsSucceeded ? await sendCardKitMessage(token, chatId, cardId).catch((err) => {
|
|
156
|
+
if (cEntry) { cEntry.cardId = cardId; cEntry.sequence = 1; }
|
|
157
|
+
const sendOk = await sendCardKitMessage(token, chatId, cardId).catch((err) => {
|
|
164
158
|
console.error(`[${ts()}] [CARDIKT] sendMessage FAIL: chatId=${chatId} cardId=${cardId} ${(err as Error).message}`);
|
|
165
159
|
fileLog.flush();
|
|
166
160
|
return false;
|
|
167
|
-
})
|
|
161
|
+
});
|
|
168
162
|
if (!sendOk) {
|
|
169
163
|
sendTextReply(token, chatId, "⚠️ 卡片发送失败,将使用文本回复。").catch(() => {});
|
|
170
164
|
cardId = null;
|
|
171
165
|
if (cEntry) { cEntry.cardId = null; cEntry.sequence = 0; }
|
|
172
|
-
} else {
|
|
173
|
-
streamingEnabled = true;
|
|
174
166
|
}
|
|
175
167
|
}
|
|
176
168
|
|
|
@@ -181,8 +173,6 @@ export async function resumeAndPrompt(
|
|
|
181
173
|
let finalText = "";
|
|
182
174
|
|
|
183
175
|
let cardCreatedAt = Date.now();
|
|
184
|
-
let cardStartThinkingLen = 0;
|
|
185
|
-
let cardStartTextLen = 0;
|
|
186
176
|
const CARD_ROTATE_MS = 9 * 60 * 1000; // 飞书 CardKit 流式超时 10 分钟,提前 1 分钟切换
|
|
187
177
|
|
|
188
178
|
let dotCount = 0;
|
|
@@ -199,26 +189,20 @@ export async function resumeAndPrompt(
|
|
|
199
189
|
cEntry.cardBusy = true;
|
|
200
190
|
const oldCardId = cardId;
|
|
201
191
|
try {
|
|
202
|
-
// 1.
|
|
192
|
+
// 1. 用当前累积内容更新旧卡片为静态版本
|
|
203
193
|
const oldSeqBase = cEntry.sequence;
|
|
204
|
-
await setCardKitSettings(token, oldCardId!, { streaming_mode: false }, oldSeqBase + 1).catch(() => {});
|
|
205
|
-
// 2. 用当前累积内容更新旧卡片为静态版本
|
|
206
194
|
const oldDisplay = truncateContent(accumulatedThinking + finalText) || "处理中...";
|
|
207
195
|
const oldCard = buildThinkingCardV2(oldDisplay, { showStop: false, headerTitle: "生成中...(上轮)" });
|
|
208
|
-
await updateCardKitCard(token, oldCardId!, oldCard, oldSeqBase +
|
|
209
|
-
//
|
|
196
|
+
await updateCardKitCard(token, oldCardId!, oldCard, oldSeqBase + 1).catch(() => {});
|
|
197
|
+
// 2. 创建新卡片并发送
|
|
210
198
|
const newCardId = await createCardKitCard(token, buildThinkingCardV2("", { showStop: true, headerTitle: "生成中..." }));
|
|
211
199
|
if (!newCardId) throw new Error("createCardKitCard returned empty");
|
|
212
|
-
// 4. 开启流式模式并发送
|
|
213
|
-
await setCardKitSettings(token, newCardId, { streaming_mode: true }, 1);
|
|
214
200
|
await sendCardKitMessage(token, chatId, newCardId);
|
|
215
|
-
//
|
|
201
|
+
// 3. 切换到新卡片
|
|
216
202
|
cardId = newCardId;
|
|
217
203
|
cEntry.cardId = newCardId;
|
|
218
204
|
cEntry.sequence = 1;
|
|
219
205
|
cardCreatedAt = Date.now();
|
|
220
|
-
cardStartThinkingLen = accumulatedThinking.length;
|
|
221
|
-
cardStartTextLen = finalText.length;
|
|
222
206
|
lastSentContent = "";
|
|
223
207
|
streamErrorNotified = false;
|
|
224
208
|
console.log(`[${ts()}] [CARDIKT] rotated: old=${oldCardId} new=${newCardId} (9min timeout)`);
|
|
@@ -231,27 +215,27 @@ export async function resumeAndPrompt(
|
|
|
231
215
|
}
|
|
232
216
|
|
|
233
217
|
dotCount = (dotCount % 9) + 1;
|
|
234
|
-
const
|
|
235
|
-
const content = truncateContent(currentContent + "\n" + ".".repeat(dotCount));
|
|
218
|
+
const content = truncateContent(accumulatedThinking + finalText + "\n" + "。".repeat(dotCount));
|
|
236
219
|
if (content === lastSentContent) return;
|
|
237
220
|
|
|
238
221
|
lastSentContent = content;
|
|
239
222
|
cEntry.cardBusy = true;
|
|
240
223
|
const mySeq = cEntry.sequence + 1;
|
|
241
224
|
try {
|
|
242
|
-
|
|
225
|
+
const card = buildThinkingCardV2(content, { showStop: true, headerTitle: "生成中..." });
|
|
226
|
+
await updateCardKitCard(token, cardId!, card, mySeq);
|
|
243
227
|
cEntry.sequence = mySeq;
|
|
244
228
|
cEntry.accumulatedThinking = accumulatedThinking;
|
|
245
229
|
streamErrorNotified = false;
|
|
246
230
|
healthLogTicks++;
|
|
247
231
|
if (healthLogTicks % 10 === 0) {
|
|
248
|
-
console.log(`[${ts()}] [CARDIKT]
|
|
232
|
+
console.log(`[${ts()}] [CARDIKT] update health: seq=${mySeq} thinking=${accumulatedThinking.length}chars text=${finalText.length}chars cardAge=${Math.round((Date.now() - cardCreatedAt) / 1000)}s`);
|
|
249
233
|
}
|
|
250
234
|
} catch (err) {
|
|
251
|
-
console.error(`[${ts()}] CardKit
|
|
235
|
+
console.error(`[${ts()}] CardKit update error: chatId=${chatId} cardId=${cardId} seq=${mySeq} ${(err as Error).message}`);
|
|
252
236
|
if (!streamErrorNotified) {
|
|
253
237
|
streamErrorNotified = true;
|
|
254
|
-
sendTextReply(token, chatId, "⚠️
|
|
238
|
+
sendTextReply(token, chatId, "⚠️ 卡片更新失败,结果将以文本形式发送。").catch(() => {});
|
|
255
239
|
}
|
|
256
240
|
} finally {
|
|
257
241
|
cEntry.cardBusy = false;
|
|
@@ -335,10 +319,7 @@ export async function resumeAndPrompt(
|
|
|
335
319
|
while (cEntry.cardBusy) {
|
|
336
320
|
await new Promise(r => setTimeout(r, 20));
|
|
337
321
|
}
|
|
338
|
-
|
|
339
|
-
if (streamingEnabled) {
|
|
340
|
-
await setCardKitSettings(token, cardId, { streaming_mode: false }, nextSeq++).catch(() => {});
|
|
341
|
-
}
|
|
322
|
+
const nextSeq = cEntry.sequence + 1;
|
|
342
323
|
if (wasStopped) {
|
|
343
324
|
const stopCard = buildThinkingCardV2(accumulatedThinking || "已停止", { showStop: false, headerTitle: "已停止", headerTemplate: "red" });
|
|
344
325
|
await updateCardKitCard(token, cardId, stopCard, nextSeq).catch((err) => {
|