chatccc 0.2.0 → 0.2.2
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 +48 -10
- package/package.json +4 -3
- package/src/feishu-api.ts +128 -0
- package/src/index.ts +33 -11
package/README.md
CHANGED
|
@@ -67,21 +67,29 @@ npm run dev
|
|
|
67
67
|
|
|
68
68
|
**添加应用能力**:开启「机器人」功能。
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
**权限配置(重要)**(在「权限管理」中按前缀搜索,**将以下三类前缀开头的权限全部开通**):
|
|
71
71
|
|
|
72
|
+
| 前缀 | 用途(简要) |
|
|
73
|
+
| ---- | ------------ |
|
|
74
|
+
| `im:message` | 收发与读取消息、以机器人身份发言、与会话相关的消息能力等 |
|
|
75
|
+
| `im:chat` | 创建与管理群聊、与会话绑定、群成员与聊天场景相关能力等 |
|
|
76
|
+
| `cardkit:` | 群卡片展示、流式更新、卡片按钮与交互回调相关能力等 |
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
| ------------------------------- | --------- |
|
|
75
|
-
| `im:chat` | 创建和管理群聊 |
|
|
76
|
-
| `im:message` | 收发消息 |
|
|
77
|
-
| `im:message:send_as_bot` | 以机器人身份发消息 |
|
|
78
|
-
| `im:message.p2p_msg:readonly` | 读取私聊消息 |
|
|
79
|
-
| `im:message.group_msg:readonly` | 读取群聊消息 |
|
|
78
|
+
控制台中同一前缀下往往有多条子权限,请逐项勾选开通,避免遗漏导致收不到消息或卡片异常。
|
|
80
79
|
|
|
80
|
+
<p align="center">
|
|
81
|
+
<img src="images/img_readme_permission.png" alt="飞书应用权限配置" width="280" />
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
**事件订阅(重要)**(在「事件与回调」中):订阅 `im.message.receive_v1` 和 `card.action.trigger` 事件。
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
<p align="center">
|
|
87
|
+
<img src="images/img_readme_event.png" alt="飞书事件订阅" width="280" />
|
|
88
|
+
 
|
|
89
|
+
<img src="images/img_readme_callback.png" alt="飞书请求网址与回调" width="280" />
|
|
90
|
+
</p>
|
|
83
91
|
|
|
84
|
-
|
|
92
|
+
**发布版本(不要忘了)**:配置完成后创建应用版本并发布(仅企业内部可用即可)。
|
|
85
93
|
|
|
86
94
|
### 3. 获取凭证
|
|
87
95
|
|
|
@@ -112,6 +120,36 @@ CHATCCC_ANTHROPIC_MODEL=default
|
|
|
112
120
|
CHATCCC_ANTHROPIC_EFFORT=default
|
|
113
121
|
```
|
|
114
122
|
|
|
123
|
+
#### 注意!第三方 API(如 DeepSeek)与鉴权 403
|
|
124
|
+
|
|
125
|
+
若你通过 **Anthropic 兼容网关**(例如 DeepSeek 的 `/anthropic` 端点)调用模型,除了官方 Claude 以外,需要配置 **`ANTHROPIC_API_KEY`**(API 密钥)与 **`ANTHROPIC_BASE_URL`**(写入系统环境变量,见下表)。
|
|
126
|
+
|
|
127
|
+
不少用户会把这些写在 **`~/.claude/settings.json`**(Windows 一般为 `C:\Users\<用户名>\.claude\settings.json`)的 `env` 字段里,供本机 **交互式 Claude Code** 使用。但 **Claude Agent SDK 在启动时拉起的是子进程**,其行为与直接打开终端跑 Claude Code 并不完全一致,**子进程未必会按你预期读取 `.claude` 目录下 `settings.json` 里的 `ANTHROPIC_API_KEY`、`ANTHROPIC_BASE_URL` 等**,从而出现类似:
|
|
128
|
+
|
|
129
|
+
`Failed to authenticate. API Error: 403 Request not allowed`
|
|
130
|
+
|
|
131
|
+
**建议(最稳妥)**:把 **`ANTHROPIC_BASE_URL`** 与 **`ANTHROPIC_API_KEY`**(API 密钥)写进 **操作系统环境变量**(当前用户的「用户变量」即可,一般无需管理员),例如:
|
|
132
|
+
|
|
133
|
+
| 变量名 | 说明 |
|
|
134
|
+
| ------ | ---- |
|
|
135
|
+
| `ANTHROPIC_API_KEY` | 在服务商控制台获取的 **API 密钥**,填入本环境变量。Anthropic 兼容网关普遍使用该名称;若服务商文档要求使用其它环境变量名,以文档为准。 |
|
|
136
|
+
| `ANTHROPIC_BASE_URL` | 兼容 Anthropic API 的 base URL,例如 DeepSeek:`https://api.deepseek.com/anthropic` |
|
|
137
|
+
|
|
138
|
+
若你在 `settings.json` 里还为 Sonnet/Haiku/Opus 配置了默认模型名,可一并同步到用户环境变量(如 `ANTHROPIC_MODEL`、`ANTHROPIC_DEFAULT_SONNET_MODEL` 等),与文档及网关要求保持一致。
|
|
139
|
+
|
|
140
|
+
修改环境变量后,请 **完全退出并重新打开** 终端、IDE(如 Cursor)以及 ChatCCC 进程,再试飞书对话或本地 `npm run demo:claude-hi`,确保子进程继承到新变量。
|
|
141
|
+
|
|
142
|
+
在 **Windows** 上可用 PowerShell 写入当前用户的持久变量(示例,请把第二行里的字符串换成你在服务商控制台复制的 **API 密钥**,即 **`ANTHROPIC_API_KEY`** 的值):
|
|
143
|
+
|
|
144
|
+
```powershell
|
|
145
|
+
[Environment]::SetEnvironmentVariable("ANTHROPIC_BASE_URL", "https://api.deepseek.com/anthropic", "User")
|
|
146
|
+
[Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", "你的_API_密钥", "User")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
也可在 **系统属性 → 环境变量** 中手动添加。Linux / macOS 可将 `export` 写入 `~/.bashrc`、`~/.zshrc` 等,或使用 systemd、`launchctl` 等按你的部署方式注入。
|
|
150
|
+
|
|
151
|
+
> **说明**:项目根目录的 `.env` 若通过 `tsx --env-file=.env` 加载,其中的变量会进入 Node 进程环境,**多数情况下**会随进程继承给 SDK 子进程;但各家网关与 Claude Code 版本组合较多,**仍推荐**将 **`ANTHROPIC_API_KEY`** 与 **`ANTHROPIC_BASE_URL`** 放在系统/用户环境变量中,与 `settings.json` 形成双保险,避免仅依赖 `.claude/settings.json` 时在 SDK 场景下踩坑。
|
|
152
|
+
|
|
115
153
|
`**CHATCCC_PORT`(可选)**:默认端口为 `18080`。若在一台机器上同时运行多个 ChatCCC 实例,可在各自的 `.env` 中设置不同端口:
|
|
116
154
|
|
|
117
155
|
```env
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Feishu bot bridge for Claude Code",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
"demo:bot-test:local": "tsx --env-file=.env demo/bot_test.ts --local",
|
|
23
23
|
"demo:create-group": "tsx --env-file=.env src/index.ts",
|
|
24
24
|
"demo:create-group:local": "tsx --env-file=.env src/index.ts --local",
|
|
25
|
-
"demo:permission-check": "tsx --env-file=.env demo/permission_check.ts"
|
|
25
|
+
"demo:permission-check": "tsx --env-file=.env demo/permission_check.ts",
|
|
26
|
+
"demo:claude-hi": "tsx --env-file=.env demo/claude_say_hi.ts"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@anthropic-ai/claude-agent-sdk": "
|
|
29
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
29
30
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
30
31
|
"tsx": "^4.0.0",
|
|
31
32
|
"ws": "^8.18.0"
|
package/src/feishu-api.ts
CHANGED
|
@@ -32,6 +32,134 @@ export async function getTenantAccessToken(): Promise<string> {
|
|
|
32
32
|
return data.tenant_access_token;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Permission verification (startup check)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
interface PermissionDef {
|
|
40
|
+
scope: string;
|
|
41
|
+
description: string;
|
|
42
|
+
method: "GET" | "POST" | "PATCH" | "DELETE";
|
|
43
|
+
path: string;
|
|
44
|
+
body?: string; // JSON string, supports __TOKEN__ placeholder
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 项目所需的所有飞书权限及对应的非破坏性测试请求 */
|
|
48
|
+
const REQUIRED_PERMISSIONS: PermissionDef[] = [
|
|
49
|
+
{
|
|
50
|
+
scope: "im:chat",
|
|
51
|
+
description: "读取/创建/更新群聊(创建会话群、改名、读群信息)",
|
|
52
|
+
method: "GET",
|
|
53
|
+
path: "/im/v1/chats?page_size=1",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
scope: "im:message:send_as_bot",
|
|
57
|
+
description: "以机器人身份发送消息(文本/卡片回复)",
|
|
58
|
+
method: "POST",
|
|
59
|
+
path: "/im/v1/messages?receive_id_type=chat_id",
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
receive_id: "oc_000000000000000000000000",
|
|
62
|
+
msg_type: "text",
|
|
63
|
+
content: JSON.stringify({ text: "permcheck" }),
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
scope: "im:message:reaction",
|
|
68
|
+
description: "添加消息表情回应(Give a like)",
|
|
69
|
+
method: "POST",
|
|
70
|
+
path: "/im/v1/messages/om_000000000000000000000000/reactions",
|
|
71
|
+
body: JSON.stringify({ reaction_type: { emoji_type: "Get" } }),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
scope: "im:message",
|
|
75
|
+
description: "撤回/更新消息(关闭卡片、撤回卡片消息)",
|
|
76
|
+
method: "PATCH",
|
|
77
|
+
path: "/im/v1/messages/om_000000000000000000000000",
|
|
78
|
+
body: JSON.stringify({ content: JSON.stringify({ elements: [{ tag: "markdown", content: " " }] }) }),
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
interface PermissionResult {
|
|
83
|
+
scope: string;
|
|
84
|
+
description: string;
|
|
85
|
+
ok: boolean;
|
|
86
|
+
detail: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 对单个权限做非破坏性探针请求。返回 false 表示权限缺失。 */
|
|
90
|
+
async function probePermission(token: string, def: PermissionDef): Promise<PermissionResult> {
|
|
91
|
+
const url = `${BASE_URL}${def.path}`;
|
|
92
|
+
try {
|
|
93
|
+
const resp = await fetch(url, {
|
|
94
|
+
method: def.method,
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `Bearer ${token}`,
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
},
|
|
99
|
+
body: def.body ?? undefined,
|
|
100
|
+
});
|
|
101
|
+
const data = (await resp.json()) as { code: number; msg?: string };
|
|
102
|
+
if (data.code === 0) {
|
|
103
|
+
return { scope: def.scope, description: def.description, ok: true, detail: "通过" };
|
|
104
|
+
}
|
|
105
|
+
const msg = (data.msg ?? "").toLowerCase();
|
|
106
|
+
// 飞书权限缺失时 msg 通常含 "permission" / "scope" / "denied" / "unauthorized"
|
|
107
|
+
// 权限 OK 但资源不存在时 msg 含 "not found" / "chat" / "message" / "不存在"
|
|
108
|
+
const deniedKeywords = ["permission", "scope", "denied", "unauthorized", "无权限", "权限", "access denied"];
|
|
109
|
+
const isDenied = deniedKeywords.some((kw) => msg.includes(kw));
|
|
110
|
+
if (isDenied) {
|
|
111
|
+
return { scope: def.scope, description: def.description, ok: false, detail: `权限缺失 → code=${data.code} msg=${data.msg}` };
|
|
112
|
+
}
|
|
113
|
+
// 资源不存在 = 通过了权限检查,只是目标资源不存在(预期内)
|
|
114
|
+
return { scope: def.scope, description: def.description, ok: true, detail: `通过(资源不存在: code=${data.code} msg=${data.msg})` };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { scope: def.scope, description: def.description, ok: false, detail: `网络异常: ${(err as Error).message}` };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 验证所有必需权限。返回失败的权限列表。
|
|
122
|
+
* 若全部通过返回空数组。
|
|
123
|
+
*/
|
|
124
|
+
export async function verifyAllPermissions(token: string): Promise<PermissionResult[]> {
|
|
125
|
+
const results: PermissionResult[] = [];
|
|
126
|
+
for (const def of REQUIRED_PERMISSIONS) {
|
|
127
|
+
const r = await probePermission(token, def);
|
|
128
|
+
results.push(r);
|
|
129
|
+
}
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 输出权限验证结果到控制台和文件日志,并返回是否有失败 */
|
|
134
|
+
export function reportPermissionResults(
|
|
135
|
+
results: PermissionResult[],
|
|
136
|
+
log: (msg: string) => void
|
|
137
|
+
): boolean {
|
|
138
|
+
const failed = results.filter((r) => !r.ok);
|
|
139
|
+
const passed = results.filter((r) => r.ok);
|
|
140
|
+
|
|
141
|
+
log(`\n${"-".repeat(60)}`);
|
|
142
|
+
log(`[权限验证] 飞书应用权限检查完成: ${passed.length}/${results.length} 通过`);
|
|
143
|
+
for (const r of passed) {
|
|
144
|
+
log(` [通过] ${r.scope} — ${r.description}`);
|
|
145
|
+
}
|
|
146
|
+
for (const r of failed) {
|
|
147
|
+
log(` [失败] ${r.scope} — ${r.description}`);
|
|
148
|
+
log(` 原因: ${r.detail}`);
|
|
149
|
+
}
|
|
150
|
+
if (failed.length > 0) {
|
|
151
|
+
log(`\n[权限验证] 以下权限未在飞书开放平台中配置:`);
|
|
152
|
+
for (const r of failed) {
|
|
153
|
+
log(` - ${r.scope}: ${r.description}`);
|
|
154
|
+
}
|
|
155
|
+
log(`\n请到飞书开放平台 → 应用详情 → 权限管理 中开通上述权限后重试。`);
|
|
156
|
+
log(`${"-".repeat(60)}\n`);
|
|
157
|
+
} else {
|
|
158
|
+
log(`${"-".repeat(60)}\n`);
|
|
159
|
+
}
|
|
160
|
+
return failed.length > 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
35
163
|
// ---------------------------------------------------------------------------
|
|
36
164
|
// Group chat CRUD
|
|
37
165
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,8 @@ import {
|
|
|
63
63
|
updateCardMessage,
|
|
64
64
|
updateChatInfo,
|
|
65
65
|
sendRestartCard,
|
|
66
|
+
verifyAllPermissions,
|
|
67
|
+
reportPermissionResults,
|
|
66
68
|
} from "./feishu-api.ts";
|
|
67
69
|
import { buildHelpCard, buildStatusCard, buildThinkingCardV2, buildCdContent, buildSessionsCard } from "./cards.ts";
|
|
68
70
|
import { updateCardKitCard } from "./cardkit.ts";
|
|
@@ -281,9 +283,10 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
|
|
|
281
283
|
return;
|
|
282
284
|
}
|
|
283
285
|
|
|
286
|
+
const cwd = await getDefaultCwd();
|
|
284
287
|
await sendCardReply(
|
|
285
288
|
freshToken, newChatId, "Claude Session Ready",
|
|
286
|
-
`群聊已创建,这是你的 Claude 会话群。\n\n**Session ID:** ${sessionId}\n\n直接在这里发消息即可与 Claude
|
|
289
|
+
`群聊已创建,这是你的 Claude 会话群。\n\n**Session ID:** ${sessionId}\n**工作目录:** \`${cwd}\`\n\n直接在这里发消息即可与 Claude 对话。\n\n发送 **/sessions** 查看所有会话状态。`,
|
|
287
290
|
"green"
|
|
288
291
|
);
|
|
289
292
|
|
|
@@ -464,14 +467,14 @@ async function main(): Promise<void> {
|
|
|
464
467
|
process.exit(1);
|
|
465
468
|
}
|
|
466
469
|
|
|
467
|
-
console.log(`\n[启动 1/
|
|
470
|
+
console.log(`\n[启动 1/7] 单实例:按 PID 文件清理旧 ChatCCC 进程`);
|
|
468
471
|
console.log(` PID 文件: ${PID_FILE}`);
|
|
469
472
|
appendStartupTrace("main: before ensureSingleInstance", { PID_FILE, CHATCCC_PORT });
|
|
470
473
|
ensureSingleInstance(PID_FILE);
|
|
471
474
|
appendStartupTrace("main: after ensureSingleInstance");
|
|
472
475
|
console.log(" 完成。\n");
|
|
473
476
|
|
|
474
|
-
console.log(`[启动 2/
|
|
477
|
+
console.log(`[启动 2/7] 环境与凭证检查`);
|
|
475
478
|
reportEnvironmentVariableReadout();
|
|
476
479
|
console.log(` 工作目录: ${process.cwd()}`);
|
|
477
480
|
console.log(` 包根目录: ${PROJECT_ROOT}`);
|
|
@@ -485,7 +488,7 @@ async function main(): Promise<void> {
|
|
|
485
488
|
console.log(` 必填项校验通过(App ID 摘要: ${maskAppId(APP_ID)})。\n`);
|
|
486
489
|
appendStartupTrace("main: feishu credentials ok", { appIdMask: maskAppId(APP_ID) });
|
|
487
490
|
|
|
488
|
-
console.log(`[启动 3/
|
|
491
|
+
console.log(`[启动 3/7] 启动本地 WebSocket 中继(ws://127.0.0.1:${CHATCCC_PORT})…`);
|
|
489
492
|
appendStartupTrace("main: before freeRelayListenPort", { CHATCCC_PORT });
|
|
490
493
|
freeRelayListenPort(CHATCCC_PORT);
|
|
491
494
|
appendStartupTrace("main: after freeRelayListenPort", { CHATCCC_PORT });
|
|
@@ -501,7 +504,7 @@ async function main(): Promise<void> {
|
|
|
501
504
|
console.log(` In a Claude session group, send any message to resume & prompt.`);
|
|
502
505
|
console.log(`${"=".repeat(60)}`);
|
|
503
506
|
|
|
504
|
-
console.log(`\n[启动 4/
|
|
507
|
+
console.log(`\n[启动 4/7] 向飞书开放平台申请 tenant_access_token …`);
|
|
505
508
|
let token: string;
|
|
506
509
|
try {
|
|
507
510
|
token = await getTenantAccessToken();
|
|
@@ -510,15 +513,34 @@ async function main(): Promise<void> {
|
|
|
510
513
|
console.error(" 失败:无法获取 tenant_access_token。");
|
|
511
514
|
console.error(` 接口: POST ${BASE_URL}/auth/v3/tenant_access_token/internal`);
|
|
512
515
|
console.error(" 常见原因:");
|
|
516
|
+
console.error(
|
|
517
|
+
" - 本机网络无法访问 open.feishu.cn(可尝试:关闭系统/终端代理、检查防火墙;Windows 可管理员运行 netsh winsock reset 后重启)",
|
|
518
|
+
);
|
|
513
519
|
console.error(" - App ID / App Secret 与开放平台「凭证与基础信息」不一致");
|
|
514
520
|
console.error(" - 自建应用尚未创建/发布可用版本");
|
|
515
|
-
console.error(" - 本机网络无法访问 open.feishu.cn");
|
|
516
521
|
console.error(` 详情: ${msg}`);
|
|
517
522
|
printServiceDidNotStart("无法从飞书开放平台获取 tenant_access_token(凭证或网络问题)");
|
|
518
523
|
process.exit(1);
|
|
519
524
|
}
|
|
520
525
|
console.log(` 完成。当前 App ID 摘要: ${maskAppId(APP_ID)}\n`);
|
|
521
526
|
|
|
527
|
+
console.log(`\n[启动 5/7] 验证飞书应用权限(非破坏性探针)…`);
|
|
528
|
+
const permResults = await verifyAllPermissions(token);
|
|
529
|
+
const hasFailed = reportPermissionResults(permResults, (msg) => {
|
|
530
|
+
console.log(msg);
|
|
531
|
+
});
|
|
532
|
+
if (hasFailed) {
|
|
533
|
+
appendStartupTrace("main: permissions check failed", {
|
|
534
|
+
failed: permResults.filter((r) => !r.ok).map((r) => r.scope).join(", "),
|
|
535
|
+
});
|
|
536
|
+
printServiceDidNotStart(
|
|
537
|
+
`飞书权限不足: ${permResults.filter((r) => !r.ok).map((r) => r.scope).join(", ")}`
|
|
538
|
+
);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
appendStartupTrace("main: permissions check ok");
|
|
542
|
+
console.log(` 完成。所有必需权限已验证通过。\n`);
|
|
543
|
+
|
|
522
544
|
console.log(`[${ts()}] [AUTH] Token obtained`);
|
|
523
545
|
|
|
524
546
|
const eventDispatcher = new EventDispatcher({});
|
|
@@ -613,14 +635,14 @@ async function main(): Promise<void> {
|
|
|
613
635
|
});
|
|
614
636
|
|
|
615
637
|
if (USE_LOCAL) {
|
|
616
|
-
console.log(`\n[启动
|
|
638
|
+
console.log(`\n[启动 6/7] 本地区 relay 模式:正在连接 ${LOCAL_RELAY_URL} …`);
|
|
617
639
|
console.log(" 若失败:请先在 SDK 模式下启动主进程,或确认本机中继已在该地址监听。");
|
|
618
640
|
let localRelayOpened = false;
|
|
619
641
|
const ws = new WebSocket(LOCAL_RELAY_URL);
|
|
620
642
|
ws.on("open", () => {
|
|
621
643
|
localRelayOpened = true;
|
|
622
644
|
console.log("[WS] Connected to local relay");
|
|
623
|
-
console.log("[启动
|
|
645
|
+
console.log("[启动 7/7] 已连接本地中继,可接收转发事件。\n");
|
|
624
646
|
printServiceRunningHint("local");
|
|
625
647
|
});
|
|
626
648
|
ws.on("message", (raw: Buffer) => {
|
|
@@ -648,7 +670,7 @@ async function main(): Promise<void> {
|
|
|
648
670
|
ws.on("close", () => { console.log("[WS] Local relay disconnected"); process.exit(0); });
|
|
649
671
|
ws.on("error", (err: Error) => {
|
|
650
672
|
if (!localRelayOpened) {
|
|
651
|
-
console.error(`[启动
|
|
673
|
+
console.error(`[启动 6/7] 失败:无法连接本地中继。`);
|
|
652
674
|
console.error(` ${err.message}`);
|
|
653
675
|
console.error(` 目标: ${LOCAL_RELAY_URL}`);
|
|
654
676
|
printServiceDidNotStart(`无法连接本地中继 ${LOCAL_RELAY_URL}`);
|
|
@@ -666,7 +688,7 @@ async function main(): Promise<void> {
|
|
|
666
688
|
onReconnected: () => resetState(),
|
|
667
689
|
});
|
|
668
690
|
|
|
669
|
-
console.log(`\n[启动
|
|
691
|
+
console.log(`\n[启动 6/7] 飞书长连接:正在通过 SDK 建立 WebSocket …`);
|
|
670
692
|
try {
|
|
671
693
|
await wsClient.start({ eventDispatcher });
|
|
672
694
|
} catch (err) {
|
|
@@ -678,7 +700,7 @@ async function main(): Promise<void> {
|
|
|
678
700
|
process.exit(1);
|
|
679
701
|
}
|
|
680
702
|
console.log("[WS] Feishu WebSocket connected (SDK)");
|
|
681
|
-
console.log("[启动
|
|
703
|
+
console.log("[启动 7/7] 服务已就绪,等待飞书消息(群聊 / 卡片回调)。\n");
|
|
682
704
|
printServiceRunningHint("sdk");
|
|
683
705
|
|
|
684
706
|
sendRestartCard(token).catch((err) =>
|