chatccc 0.2.41 → 0.2.42
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/demo/ilink_echo_probe.ts +222 -222
- package/package.json +59 -59
- package/src/__tests__/cards.test.ts +22 -6
- package/src/__tests__/crash-logging.test.ts +62 -62
- package/src/__tests__/session.test.ts +124 -11
- package/src/cards.ts +5 -3
- package/src/index.ts +116 -13
- package/src/session.ts +131 -7
- package/src/shared.ts +63 -63
package/demo/ilink_echo_probe.ts
CHANGED
|
@@ -1,222 +1,222 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenILink WeChat Echo Probe
|
|
3
|
-
* ===========================
|
|
4
|
-
* Minimal scan-login and text echo demo for the WeChat iLink channel.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* npm run demo:ilink-echo
|
|
8
|
-
*
|
|
9
|
-
* Behavior:
|
|
10
|
-
* When a user sends text to the logged-in WeChat account, this demo replies
|
|
11
|
-
* with "收到:" plus the received text.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
-
import { createRequire } from "node:module";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
Client as OpenIlinkWire,
|
|
21
|
-
extractText,
|
|
22
|
-
type GetUpdatesResponse,
|
|
23
|
-
type WeixinMessage,
|
|
24
|
-
} from "@openilink/openilink-sdk-node";
|
|
25
|
-
|
|
26
|
-
import { setupFileLogging } from "../src/shared.ts";
|
|
27
|
-
|
|
28
|
-
interface TerminalQrRenderer {
|
|
29
|
-
generate(
|
|
30
|
-
input: string,
|
|
31
|
-
opts: { small: boolean },
|
|
32
|
-
callback: (qrcode: string) => void,
|
|
33
|
-
): void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface EchoProbeSnapshot {
|
|
37
|
-
token?: string;
|
|
38
|
-
baseUrl?: string;
|
|
39
|
-
pollCursor?: string;
|
|
40
|
-
botId?: string;
|
|
41
|
-
userId?: string;
|
|
42
|
-
lastSeenAt?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
46
|
-
const PROJECT_ROOT = join(__dirname, "..");
|
|
47
|
-
const SNAPSHOT_PATH = join(PROJECT_ROOT, "state", "ilink-echo-probe.json");
|
|
48
|
-
const LOG_DIR = join(__dirname, "logs");
|
|
49
|
-
const requireFromDemo = createRequire(import.meta.url);
|
|
50
|
-
const terminalQr = requireFromDemo("qrcode-terminal") as TerminalQrRenderer;
|
|
51
|
-
|
|
52
|
-
setupFileLogging(LOG_DIR, "ilink-echo-probe");
|
|
53
|
-
|
|
54
|
-
let acceptingEvents = true;
|
|
55
|
-
|
|
56
|
-
function ensureParentDir(filePath: string): void {
|
|
57
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function readSnapshot(): EchoProbeSnapshot {
|
|
61
|
-
if (!existsSync(SNAPSHOT_PATH)) {
|
|
62
|
-
return {};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
return JSON.parse(readFileSync(SNAPSHOT_PATH, "utf8")) as EchoProbeSnapshot;
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.warn(`Cannot read saved iLink probe state: ${(error as Error).message}`);
|
|
69
|
-
return {};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function writeSnapshot(snapshot: EchoProbeSnapshot): void {
|
|
74
|
-
ensureParentDir(SNAPSHOT_PATH);
|
|
75
|
-
writeFileSync(SNAPSHOT_PATH, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function updateSnapshot(patch: Partial<EchoProbeSnapshot>): EchoProbeSnapshot {
|
|
79
|
-
const next = {
|
|
80
|
-
...readSnapshot(),
|
|
81
|
-
...patch,
|
|
82
|
-
lastSeenAt: new Date().toISOString(),
|
|
83
|
-
};
|
|
84
|
-
writeSnapshot(next);
|
|
85
|
-
return next;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function peerKeyOf(message: WeixinMessage): string {
|
|
89
|
-
return String(message.from_user_id ?? "");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function textOf(message: WeixinMessage): string {
|
|
93
|
-
return extractText(message).trim();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function printScanMaterial(content: string): void {
|
|
97
|
-
console.log("\n========== iLink scan content ==========");
|
|
98
|
-
console.log(content);
|
|
99
|
-
console.log("========== iLink terminal QR ==========");
|
|
100
|
-
terminalQr.generate(content, { small: true }, (renderedQr) => {
|
|
101
|
-
console.log(renderedQr);
|
|
102
|
-
});
|
|
103
|
-
console.log("========== end iLink QR ==========\n");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function prepareProbeWire(saved: EchoProbeSnapshot): Promise<OpenIlinkWire> {
|
|
107
|
-
const ilinkWire = new OpenIlinkWire("", {
|
|
108
|
-
base_url: saved.baseUrl,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
console.log("Starting iLink QR login. The QR is shown on every run.");
|
|
112
|
-
const loginResult = await ilinkWire.loginWithQr({
|
|
113
|
-
on_qrcode: (content) => {
|
|
114
|
-
printScanMaterial(content);
|
|
115
|
-
},
|
|
116
|
-
on_scanned: () => {
|
|
117
|
-
console.log("QR scanned. Confirm login in WeChat.");
|
|
118
|
-
},
|
|
119
|
-
on_expired: (attempt, maxAttempts) => {
|
|
120
|
-
console.log(`QR expired, refreshing (${attempt}/${maxAttempts}).`);
|
|
121
|
-
},
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
if (!loginResult.connected) {
|
|
125
|
-
throw new Error(`iLink QR login failed: ${loginResult.message}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
updateSnapshot({
|
|
129
|
-
token: loginResult.bot_token ?? ilinkWire.token,
|
|
130
|
-
baseUrl: loginResult.base_url ?? ilinkWire.baseUrl,
|
|
131
|
-
botId: loginResult.bot_id,
|
|
132
|
-
userId: loginResult.user_id,
|
|
133
|
-
pollCursor: "",
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
console.log(`iLink login ready. BotID=${loginResult.bot_id ?? ""}`);
|
|
137
|
-
return ilinkWire;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function mirrorIncomingLine(
|
|
141
|
-
ilinkWire: OpenIlinkWire,
|
|
142
|
-
message: WeixinMessage,
|
|
143
|
-
): Promise<void> {
|
|
144
|
-
const peerKey = peerKeyOf(message);
|
|
145
|
-
if (!peerKey) {
|
|
146
|
-
console.warn("Skip message without from_user_id.");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const incomingText = textOf(message);
|
|
151
|
-
const outgoingText = incomingText ? `收到:${incomingText}` : "收到:[non-text message]";
|
|
152
|
-
const contextToken = message.context_token;
|
|
153
|
-
|
|
154
|
-
if (contextToken) {
|
|
155
|
-
await ilinkWire.sendText(peerKey, outgoingText, contextToken);
|
|
156
|
-
} else {
|
|
157
|
-
await ilinkWire.push(peerKey, outgoingText);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
console.log(`Echoed to ${peerKey}: ${outgoingText.slice(0, 120)}`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function rememberPollingCursor(response: GetUpdatesResponse): void {
|
|
164
|
-
const pollCursor = response.sync_buf ?? response.get_updates_buf;
|
|
165
|
-
if (!pollCursor) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
updateSnapshot({ pollCursor });
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function installStopHooks(): void {
|
|
172
|
-
const stop = (): void => {
|
|
173
|
-
if (!acceptingEvents) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
acceptingEvents = false;
|
|
177
|
-
console.log("Stopping iLink echo probe...");
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
process.once("SIGINT", stop);
|
|
181
|
-
process.once("SIGTERM", stop);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async function runProbe(): Promise<void> {
|
|
185
|
-
installStopHooks();
|
|
186
|
-
|
|
187
|
-
const saved = readSnapshot();
|
|
188
|
-
const ilinkWire = await prepareProbeWire(saved);
|
|
189
|
-
const latest = readSnapshot();
|
|
190
|
-
|
|
191
|
-
console.log("Listening for WeChat messages. Send text to the small account.");
|
|
192
|
-
console.log(`State file: ${SNAPSHOT_PATH}`);
|
|
193
|
-
console.log(`Log dir: ${LOG_DIR}`);
|
|
194
|
-
|
|
195
|
-
await ilinkWire.monitor(
|
|
196
|
-
async (message) => {
|
|
197
|
-
try {
|
|
198
|
-
await mirrorIncomingLine(ilinkWire, message);
|
|
199
|
-
} catch (error) {
|
|
200
|
-
console.error(`Echo failed: ${(error as Error).stack ?? (error as Error).message}`);
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
initial_buf: latest.pollCursor ?? "",
|
|
205
|
-
on_buf_update: (pollCursor) => updateSnapshot({ pollCursor }),
|
|
206
|
-
on_response: rememberPollingCursor,
|
|
207
|
-
on_error: (error) => {
|
|
208
|
-
console.error(`iLink monitor error: ${error.stack ?? error.message}`);
|
|
209
|
-
},
|
|
210
|
-
on_session_expired: () => {
|
|
211
|
-
console.error("iLink session expired. Restart with --fresh to scan again.");
|
|
212
|
-
acceptingEvents = false;
|
|
213
|
-
},
|
|
214
|
-
should_continue: () => acceptingEvents,
|
|
215
|
-
},
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
runProbe().catch((error: Error) => {
|
|
220
|
-
console.error(`Fatal iLink echo probe error: ${error.stack ?? error.message}`);
|
|
221
|
-
process.exitCode = 1;
|
|
222
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* OpenILink WeChat Echo Probe
|
|
3
|
+
* ===========================
|
|
4
|
+
* Minimal scan-login and text echo demo for the WeChat iLink channel.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npm run demo:ilink-echo
|
|
8
|
+
*
|
|
9
|
+
* Behavior:
|
|
10
|
+
* When a user sends text to the logged-in WeChat account, this demo replies
|
|
11
|
+
* with "收到:" plus the received text.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
Client as OpenIlinkWire,
|
|
21
|
+
extractText,
|
|
22
|
+
type GetUpdatesResponse,
|
|
23
|
+
type WeixinMessage,
|
|
24
|
+
} from "@openilink/openilink-sdk-node";
|
|
25
|
+
|
|
26
|
+
import { setupFileLogging } from "../src/shared.ts";
|
|
27
|
+
|
|
28
|
+
interface TerminalQrRenderer {
|
|
29
|
+
generate(
|
|
30
|
+
input: string,
|
|
31
|
+
opts: { small: boolean },
|
|
32
|
+
callback: (qrcode: string) => void,
|
|
33
|
+
): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface EchoProbeSnapshot {
|
|
37
|
+
token?: string;
|
|
38
|
+
baseUrl?: string;
|
|
39
|
+
pollCursor?: string;
|
|
40
|
+
botId?: string;
|
|
41
|
+
userId?: string;
|
|
42
|
+
lastSeenAt?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
const PROJECT_ROOT = join(__dirname, "..");
|
|
47
|
+
const SNAPSHOT_PATH = join(PROJECT_ROOT, "state", "ilink-echo-probe.json");
|
|
48
|
+
const LOG_DIR = join(__dirname, "logs");
|
|
49
|
+
const requireFromDemo = createRequire(import.meta.url);
|
|
50
|
+
const terminalQr = requireFromDemo("qrcode-terminal") as TerminalQrRenderer;
|
|
51
|
+
|
|
52
|
+
setupFileLogging(LOG_DIR, "ilink-echo-probe");
|
|
53
|
+
|
|
54
|
+
let acceptingEvents = true;
|
|
55
|
+
|
|
56
|
+
function ensureParentDir(filePath: string): void {
|
|
57
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readSnapshot(): EchoProbeSnapshot {
|
|
61
|
+
if (!existsSync(SNAPSHOT_PATH)) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(SNAPSHOT_PATH, "utf8")) as EchoProbeSnapshot;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn(`Cannot read saved iLink probe state: ${(error as Error).message}`);
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeSnapshot(snapshot: EchoProbeSnapshot): void {
|
|
74
|
+
ensureParentDir(SNAPSHOT_PATH);
|
|
75
|
+
writeFileSync(SNAPSHOT_PATH, `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateSnapshot(patch: Partial<EchoProbeSnapshot>): EchoProbeSnapshot {
|
|
79
|
+
const next = {
|
|
80
|
+
...readSnapshot(),
|
|
81
|
+
...patch,
|
|
82
|
+
lastSeenAt: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
writeSnapshot(next);
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function peerKeyOf(message: WeixinMessage): string {
|
|
89
|
+
return String(message.from_user_id ?? "");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function textOf(message: WeixinMessage): string {
|
|
93
|
+
return extractText(message).trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function printScanMaterial(content: string): void {
|
|
97
|
+
console.log("\n========== iLink scan content ==========");
|
|
98
|
+
console.log(content);
|
|
99
|
+
console.log("========== iLink terminal QR ==========");
|
|
100
|
+
terminalQr.generate(content, { small: true }, (renderedQr) => {
|
|
101
|
+
console.log(renderedQr);
|
|
102
|
+
});
|
|
103
|
+
console.log("========== end iLink QR ==========\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function prepareProbeWire(saved: EchoProbeSnapshot): Promise<OpenIlinkWire> {
|
|
107
|
+
const ilinkWire = new OpenIlinkWire("", {
|
|
108
|
+
base_url: saved.baseUrl,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log("Starting iLink QR login. The QR is shown on every run.");
|
|
112
|
+
const loginResult = await ilinkWire.loginWithQr({
|
|
113
|
+
on_qrcode: (content) => {
|
|
114
|
+
printScanMaterial(content);
|
|
115
|
+
},
|
|
116
|
+
on_scanned: () => {
|
|
117
|
+
console.log("QR scanned. Confirm login in WeChat.");
|
|
118
|
+
},
|
|
119
|
+
on_expired: (attempt, maxAttempts) => {
|
|
120
|
+
console.log(`QR expired, refreshing (${attempt}/${maxAttempts}).`);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!loginResult.connected) {
|
|
125
|
+
throw new Error(`iLink QR login failed: ${loginResult.message}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
updateSnapshot({
|
|
129
|
+
token: loginResult.bot_token ?? ilinkWire.token,
|
|
130
|
+
baseUrl: loginResult.base_url ?? ilinkWire.baseUrl,
|
|
131
|
+
botId: loginResult.bot_id,
|
|
132
|
+
userId: loginResult.user_id,
|
|
133
|
+
pollCursor: "",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log(`iLink login ready. BotID=${loginResult.bot_id ?? ""}`);
|
|
137
|
+
return ilinkWire;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function mirrorIncomingLine(
|
|
141
|
+
ilinkWire: OpenIlinkWire,
|
|
142
|
+
message: WeixinMessage,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
const peerKey = peerKeyOf(message);
|
|
145
|
+
if (!peerKey) {
|
|
146
|
+
console.warn("Skip message without from_user_id.");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const incomingText = textOf(message);
|
|
151
|
+
const outgoingText = incomingText ? `收到:${incomingText}` : "收到:[non-text message]";
|
|
152
|
+
const contextToken = message.context_token;
|
|
153
|
+
|
|
154
|
+
if (contextToken) {
|
|
155
|
+
await ilinkWire.sendText(peerKey, outgoingText, contextToken);
|
|
156
|
+
} else {
|
|
157
|
+
await ilinkWire.push(peerKey, outgoingText);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`Echoed to ${peerKey}: ${outgoingText.slice(0, 120)}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function rememberPollingCursor(response: GetUpdatesResponse): void {
|
|
164
|
+
const pollCursor = response.sync_buf ?? response.get_updates_buf;
|
|
165
|
+
if (!pollCursor) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
updateSnapshot({ pollCursor });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function installStopHooks(): void {
|
|
172
|
+
const stop = (): void => {
|
|
173
|
+
if (!acceptingEvents) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
acceptingEvents = false;
|
|
177
|
+
console.log("Stopping iLink echo probe...");
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
process.once("SIGINT", stop);
|
|
181
|
+
process.once("SIGTERM", stop);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runProbe(): Promise<void> {
|
|
185
|
+
installStopHooks();
|
|
186
|
+
|
|
187
|
+
const saved = readSnapshot();
|
|
188
|
+
const ilinkWire = await prepareProbeWire(saved);
|
|
189
|
+
const latest = readSnapshot();
|
|
190
|
+
|
|
191
|
+
console.log("Listening for WeChat messages. Send text to the small account.");
|
|
192
|
+
console.log(`State file: ${SNAPSHOT_PATH}`);
|
|
193
|
+
console.log(`Log dir: ${LOG_DIR}`);
|
|
194
|
+
|
|
195
|
+
await ilinkWire.monitor(
|
|
196
|
+
async (message) => {
|
|
197
|
+
try {
|
|
198
|
+
await mirrorIncomingLine(ilinkWire, message);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(`Echo failed: ${(error as Error).stack ?? (error as Error).message}`);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
initial_buf: latest.pollCursor ?? "",
|
|
205
|
+
on_buf_update: (pollCursor) => updateSnapshot({ pollCursor }),
|
|
206
|
+
on_response: rememberPollingCursor,
|
|
207
|
+
on_error: (error) => {
|
|
208
|
+
console.error(`iLink monitor error: ${error.stack ?? error.message}`);
|
|
209
|
+
},
|
|
210
|
+
on_session_expired: () => {
|
|
211
|
+
console.error("iLink session expired. Restart with --fresh to scan again.");
|
|
212
|
+
acceptingEvents = false;
|
|
213
|
+
},
|
|
214
|
+
should_continue: () => acceptingEvents,
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
runProbe().catch((error: Error) => {
|
|
220
|
+
console.error(`Fatal iLink echo probe error: ${error.stack ?? error.message}`);
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
});
|
package/package.json
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "chatccc",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./src/index.ts",
|
|
8
|
-
"bin": {
|
|
9
|
-
"chatccc": "bin/chatccc.mjs"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"src/",
|
|
13
|
-
"bin/",
|
|
14
|
-
"demo/ilink_echo_probe.ts",
|
|
15
|
-
"im-skills/",
|
|
16
|
-
"images/img_readme_*.jpg",
|
|
17
|
-
"images/img_readme_*.png",
|
|
18
|
-
"images/avatars/status_*.png",
|
|
19
|
-
"images/avatars/badges/",
|
|
20
|
-
"package.json",
|
|
21
|
-
"README.md",
|
|
22
|
-
"config.sample.json"
|
|
23
|
-
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"dev": "tsx src/index.ts",
|
|
26
|
-
"chatccc": "tsx src/index.ts",
|
|
27
|
-
"start": "tsx src/index.ts",
|
|
28
|
-
"demo:bot-test": "tsx demo/bot_test.ts",
|
|
29
|
-
"demo:bot-test:local": "tsx demo/bot_test.ts --local",
|
|
30
|
-
"demo:create-group": "tsx src/index.ts",
|
|
31
|
-
"demo:create-group:local": "tsx src/index.ts --local",
|
|
32
|
-
"demo:permission-check": "tsx demo/permission_check.ts",
|
|
33
|
-
"demo:claude-hi": "tsx demo/claude_say_hi.ts",
|
|
34
|
-
"demo:codex-hi": "tsx demo/codex_say_hi.ts",
|
|
35
|
-
"demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
|
|
36
|
-
"test": "vitest run",
|
|
37
|
-
"test:watch": "vitest"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
41
|
-
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
42
|
-
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
43
|
-
"nodemailer": "^8.0.7",
|
|
44
|
-
"qrcode-terminal": "^0.12.0",
|
|
45
|
-
"sharp": "^0.34.5",
|
|
46
|
-
"tsx": "^4.0.0",
|
|
47
|
-
"ws": "^8.18.0"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@types/node": "^20.0.0",
|
|
51
|
-
"@types/qrcode-terminal": "^0.12.2",
|
|
52
|
-
"@types/ws": "^8.18.1",
|
|
53
|
-
"typescript": "^5.0.0",
|
|
54
|
-
"vitest": "^4.1.5"
|
|
55
|
-
},
|
|
56
|
-
"engines": {
|
|
57
|
-
"node": ">=20"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "chatccc",
|
|
3
|
+
"version": "0.2.42",
|
|
4
|
+
"description": "Feishu bot bridge for Claude Code",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"chatccc": "bin/chatccc.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/",
|
|
13
|
+
"bin/",
|
|
14
|
+
"demo/ilink_echo_probe.ts",
|
|
15
|
+
"im-skills/",
|
|
16
|
+
"images/img_readme_*.jpg",
|
|
17
|
+
"images/img_readme_*.png",
|
|
18
|
+
"images/avatars/status_*.png",
|
|
19
|
+
"images/avatars/badges/",
|
|
20
|
+
"package.json",
|
|
21
|
+
"README.md",
|
|
22
|
+
"config.sample.json"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "tsx src/index.ts",
|
|
26
|
+
"chatccc": "tsx src/index.ts",
|
|
27
|
+
"start": "tsx src/index.ts",
|
|
28
|
+
"demo:bot-test": "tsx demo/bot_test.ts",
|
|
29
|
+
"demo:bot-test:local": "tsx demo/bot_test.ts --local",
|
|
30
|
+
"demo:create-group": "tsx src/index.ts",
|
|
31
|
+
"demo:create-group:local": "tsx src/index.ts --local",
|
|
32
|
+
"demo:permission-check": "tsx demo/permission_check.ts",
|
|
33
|
+
"demo:claude-hi": "tsx demo/claude_say_hi.ts",
|
|
34
|
+
"demo:codex-hi": "tsx demo/codex_say_hi.ts",
|
|
35
|
+
"demo:ilink-echo": "tsx demo/ilink_echo_probe.ts",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.133",
|
|
41
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
42
|
+
"@openilink/openilink-sdk-node": "^0.6.0",
|
|
43
|
+
"nodemailer": "^8.0.7",
|
|
44
|
+
"qrcode-terminal": "^0.12.0",
|
|
45
|
+
"sharp": "^0.34.5",
|
|
46
|
+
"tsx": "^4.0.0",
|
|
47
|
+
"ws": "^8.18.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
52
|
+
"@types/ws": "^8.18.1",
|
|
53
|
+
"typescript": "^5.0.0",
|
|
54
|
+
"vitest": "^4.1.5"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -311,7 +311,7 @@ describe("buildSessionsCard", () => {
|
|
|
311
311
|
|
|
312
312
|
it("returns valid JSON with session listing", () => {
|
|
313
313
|
const card = buildSessionsCard([
|
|
314
|
-
{ sessionId: "abc123", active: true, turnCount: 5, elapsedSeconds: 120, model: "Claude Opus 4.7", tool: "claude" },
|
|
314
|
+
{ sessionId: "abc123", chatName: "test-group", active: true, turnCount: 5, elapsedSeconds: 120, model: "Claude Opus 4.7", tool: "claude" },
|
|
315
315
|
]);
|
|
316
316
|
const parsed = JSON.parse(card);
|
|
317
317
|
expect(parsed.elements[0].text.content).toContain("共 **1** 个会话");
|
|
@@ -322,7 +322,7 @@ describe("buildSessionsCard", () => {
|
|
|
322
322
|
|
|
323
323
|
it("shows idle status for inactive sessions", () => {
|
|
324
324
|
const card = buildSessionsCard([
|
|
325
|
-
{ sessionId: "xyz", active: false, turnCount: 0, elapsedSeconds: null, model: "Claude Sonnet 4.6", tool: "claude" },
|
|
325
|
+
{ sessionId: "xyz", chatName: "", active: false, turnCount: 0, elapsedSeconds: null, model: "Claude Sonnet 4.6", tool: "claude" },
|
|
326
326
|
]);
|
|
327
327
|
const parsed = JSON.parse(card);
|
|
328
328
|
expect(parsed.elements[0].text.content).toContain("⚪ 空闲");
|
|
@@ -330,7 +330,7 @@ describe("buildSessionsCard", () => {
|
|
|
330
330
|
|
|
331
331
|
it("shows elapsed time for active sessions", () => {
|
|
332
332
|
const card = buildSessionsCard([
|
|
333
|
-
{ sessionId: "active123", active: true, turnCount: 3, elapsedSeconds: 95, model: "Claude Opus 4.7", tool: "claude" },
|
|
333
|
+
{ sessionId: "active123", chatName: "", active: true, turnCount: 3, elapsedSeconds: 95, model: "Claude Opus 4.7", tool: "claude" },
|
|
334
334
|
]);
|
|
335
335
|
const parsed = JSON.parse(card);
|
|
336
336
|
expect(parsed.elements[0].text.content).toContain("1分35秒");
|
|
@@ -338,8 +338,8 @@ describe("buildSessionsCard", () => {
|
|
|
338
338
|
|
|
339
339
|
it("separates Claude Code and Cursor sessions", () => {
|
|
340
340
|
const card = buildSessionsCard([
|
|
341
|
-
{ sessionId: "c1", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
|
|
342
|
-
{ sessionId: "c2", active: false, turnCount: 2, elapsedSeconds: null, model: "claude-opus-4-7-max", tool: "cursor" },
|
|
341
|
+
{ sessionId: "c1", chatName: "", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
|
|
342
|
+
{ sessionId: "c2", chatName: "", active: false, turnCount: 2, elapsedSeconds: null, model: "claude-opus-4-7-max", tool: "cursor" },
|
|
343
343
|
]);
|
|
344
344
|
const parsed = JSON.parse(card);
|
|
345
345
|
const content: string = parsed.elements[0].text.content;
|
|
@@ -349,7 +349,7 @@ describe("buildSessionsCard", () => {
|
|
|
349
349
|
|
|
350
350
|
it("omits Cursor section when no Cursor sessions", () => {
|
|
351
351
|
const card = buildSessionsCard([
|
|
352
|
-
{ sessionId: "c1", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
|
|
352
|
+
{ sessionId: "c1", chatName: "", active: false, turnCount: 1, elapsedSeconds: null, model: "(留空)", tool: "claude" },
|
|
353
353
|
]);
|
|
354
354
|
const parsed = JSON.parse(card);
|
|
355
355
|
const content: string = parsed.elements[0].text.content;
|
|
@@ -357,6 +357,22 @@ describe("buildSessionsCard", () => {
|
|
|
357
357
|
expect(content).not.toContain("Cursor 会话");
|
|
358
358
|
});
|
|
359
359
|
|
|
360
|
+
it("displays chatName when provided", () => {
|
|
361
|
+
const card = buildSessionsCard([
|
|
362
|
+
{ sessionId: "abc123", chatName: "帮我写代码-src", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
|
|
363
|
+
]);
|
|
364
|
+
const parsed = JSON.parse(card);
|
|
365
|
+
expect(parsed.elements[0].text.content).toContain("帮我写代码-src");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it("includes /session help text in non-empty card", () => {
|
|
369
|
+
const card = buildSessionsCard([
|
|
370
|
+
{ sessionId: "abc123", chatName: "", active: false, turnCount: 2, elapsedSeconds: null, model: "Claude Opus 4.7", tool: "claude" },
|
|
371
|
+
]);
|
|
372
|
+
const parsed = JSON.parse(card);
|
|
373
|
+
expect(parsed.elements[2].text.content).toContain("/session 数字");
|
|
374
|
+
});
|
|
375
|
+
|
|
360
376
|
it("includes close button", () => {
|
|
361
377
|
const card = buildSessionsCard([]);
|
|
362
378
|
const parsed = JSON.parse(card);
|