chatccc 0.2.51 → 0.2.53

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.
Files changed (48) hide show
  1. package/README.md +103 -217
  2. package/bin/chatccc.mjs +23 -23
  3. package/config.sample.json +34 -30
  4. package/demo/ilink_echo_probe.ts +222 -222
  5. package/im-skills/feishu-skill/download-video.mjs +162 -162
  6. package/im-skills/feishu-skill/receive-send-file.md +66 -66
  7. package/im-skills/feishu-skill/receive-send-image.md +27 -27
  8. package/im-skills/feishu-skill/send-file.mjs +50 -50
  9. package/im-skills/feishu-skill/send-image.mjs +50 -50
  10. package/im-skills/feishu-skill/skill.md +10 -10
  11. package/package.json +59 -59
  12. package/src/__tests__/agent-image-rpc.test.ts +33 -33
  13. package/src/__tests__/agent-rpc-body.test.ts +41 -41
  14. package/src/__tests__/card-plain-text.test.ts +42 -0
  15. package/src/__tests__/claude-adapter.test.ts +54 -1
  16. package/src/__tests__/codex-adapter.test.ts +304 -304
  17. package/src/__tests__/config-reload.test.ts +58 -41
  18. package/src/__tests__/config-sample.test.ts +19 -0
  19. package/src/__tests__/crash-logging.test.ts +62 -62
  20. package/src/__tests__/fixtures/codex_simple_text.jsonl +4 -4
  21. package/src/__tests__/fixtures/codex_with_tool.jsonl +6 -6
  22. package/src/__tests__/im-skills.test.ts +46 -46
  23. package/src/__tests__/session.test.ts +95 -2
  24. package/src/__tests__/sim-agent.test.ts +173 -173
  25. package/src/__tests__/sim-platform.test.ts +76 -76
  26. package/src/__tests__/sim-store.test.ts +213 -213
  27. package/src/__tests__/stream-state.test.ts +134 -134
  28. package/src/__tests__/wechat-platform.test.ts +57 -0
  29. package/src/adapters/claude-adapter.ts +23 -2
  30. package/src/adapters/codex-adapter.ts +294 -294
  31. package/src/adapters/codex-session-meta-store.ts +130 -130
  32. package/src/agent-file-rpc.ts +156 -156
  33. package/src/agent-image-rpc.ts +152 -152
  34. package/src/agent-rpc-body.ts +48 -48
  35. package/src/card-plain-text.ts +108 -0
  36. package/src/cards.ts +4 -4
  37. package/src/config.ts +775 -742
  38. package/src/feishu-api.ts +6 -0
  39. package/src/im-skills.ts +109 -109
  40. package/src/index.ts +155 -788
  41. package/src/orchestrator.ts +1032 -0
  42. package/src/platform-adapter.ts +61 -0
  43. package/src/session-chat-binding.ts +7 -0
  44. package/src/session.ts +278 -113
  45. package/src/sim-agent.ts +167 -156
  46. package/src/trace.ts +50 -50
  47. package/src/web-ui.ts +1891 -1718
  48. package/src/wechat-platform.ts +517 -0
@@ -1,153 +1,153 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- import { extname, isAbsolute, resolve } from "node:path";
3
- import { stat } from "node:fs/promises";
4
-
5
- import { getTenantAccessToken, sendImageReply, sendTextReply } from "./feishu-platform.ts";
6
- import { ts } from "./config.ts";
7
- import { readUtf8JsonBody } from "./agent-rpc-body.ts";
8
- import { getAdapterForTool } from "./session.ts";
9
- import { getChatsForSession } from "./session-chat-binding.ts";
10
-
11
- export const AGENT_SEND_IMAGE_PATH = "/api/agent/send-image";
12
-
13
- const MAX_REQUEST_BYTES = 64 * 1024;
14
- const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
15
- const ALLOWED_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"]);
16
-
17
- function jsonReply(res: ServerResponse, status: number, data: unknown): void {
18
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
19
- res.end(JSON.stringify(data));
20
- }
21
-
22
- async function resolveAndValidateImagePath(cwd: string, rawPath: unknown): Promise<string> {
23
- if (typeof rawPath !== "string" || rawPath.trim() === "") {
24
- throw new Error("path must be a non-empty string");
25
- }
26
-
27
- const sessionRoot = resolve(cwd);
28
- const imagePath = isAbsolute(rawPath)
29
- ? resolve(rawPath)
30
- : resolve(sessionRoot, rawPath);
31
-
32
- const ext = extname(imagePath).toLowerCase();
33
- if (!ALLOWED_IMAGE_EXTS.has(ext)) {
34
- throw new Error(`unsupported image extension: ${ext || "(none)"}`);
35
- }
36
-
37
- const st = await stat(imagePath);
38
- if (!st.isFile()) throw new Error("image path is not a file");
39
- if (st.size <= 0) throw new Error("image file is empty");
40
- if (st.size > MAX_IMAGE_BYTES) throw new Error("image file is larger than 10MB");
41
- return imagePath;
42
- }
43
-
44
- export async function handleAgentImageRequest(
45
- req: IncomingMessage,
46
- res: ServerResponse,
47
- ): Promise<boolean> {
48
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
49
- if (url.pathname !== AGENT_SEND_IMAGE_PATH) return false;
50
-
51
- if (req.method !== "POST") {
52
- jsonReply(res, 405, { ok: false, error: "Method not allowed" });
53
- return true;
54
- }
55
-
56
- let payload: { session_id?: unknown; path?: unknown; caption?: unknown };
57
- try {
58
- payload = await readUtf8JsonBody(req, MAX_REQUEST_BYTES);
59
- } catch (err) {
60
- jsonReply(res, 400, { ok: false, error: (err as Error).message || "Invalid JSON" });
61
- return true;
62
- }
63
-
64
- const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
65
- if (!sessionId) {
66
- jsonReply(res, 400, { ok: false, error: "Missing session_id" });
67
- return true;
68
- }
69
-
70
- // 获取 cwd 以校验路径
71
- let cwd: string;
72
- try {
73
- const { getSessionTool } = await import("./session.ts");
74
- const tool = await getSessionTool(sessionId);
75
- const adapter = getAdapterForTool(tool ?? "claude");
76
- const info = await adapter.getSessionInfo(sessionId);
77
- if (!info?.cwd) {
78
- jsonReply(res, 400, { ok: false, error: "Cannot determine cwd for session" });
79
- return true;
80
- }
81
- cwd = info.cwd;
82
- } catch (err) {
83
- jsonReply(res, 500, { ok: false, error: `Failed to get session info: ${(err as Error).message}` });
84
- return true;
85
- }
86
-
87
- let imagePath: string;
88
- try {
89
- imagePath = await resolveAndValidateImagePath(cwd, payload.path);
90
- } catch (err) {
91
- jsonReply(res, 400, { ok: false, error: (err as Error).message });
92
- return true;
93
- }
94
-
95
- // 发送到所有绑定该 session 的群
96
- const chatIds = getChatsForSession(sessionId);
97
- if (chatIds.length === 0) {
98
- jsonReply(res, 404, { ok: false, error: "No chats bound to this session" });
99
- return true;
100
- }
101
-
102
- try {
103
- const token = await getTenantAccessToken();
104
- const caption = typeof payload.caption === "string" ? payload.caption.trim() : "";
105
- let sentCount = 0;
106
- for (const cid of chatIds) {
107
- try {
108
- await sendImageReply(token, cid, imagePath);
109
- if (caption) await sendTextReply(token, cid, caption);
110
- sentCount++;
111
- } catch (err) {
112
- console.error(`[${ts()}] [AGENT-IMAGE] send to ${cid} failed: ${(err as Error).message}`);
113
- }
114
- }
115
- console.log(`[${ts()}] [AGENT-IMAGE] sent image to ${sentCount}/${chatIds.length} chats, session=${sessionId} path=${imagePath}`);
116
- jsonReply(res, 200, { ok: true, sentTo: sentCount, total: chatIds.length });
117
- } catch (err) {
118
- console.error(`[${ts()}] [AGENT-IMAGE] send failed: ${(err as Error).message}`);
119
- jsonReply(res, 500, { ok: false, error: (err as Error).message });
120
- }
121
- return true;
122
- }
123
-
124
- // ---------------------------------------------------------------------------
125
- // 兼容旧版 buildAgentImageCapabilityPrompt(供 im-skills 使用)
126
- // ---------------------------------------------------------------------------
127
-
128
- export function buildAgentImageCapabilityPrompt(input: {
129
- url: string;
130
- sessionId?: string;
131
- cwd?: string;
132
- }): string {
133
- const lines = [
134
- "[ChatCCC local capability: send image]",
135
- "You can send an image to all chats bound to this session by calling this local endpoint.",
136
- "",
137
- `POST ${input.url}`,
138
- "Content-Type: application/json; charset=utf-8",
139
- "",
140
- `Body: {"session_id":"${input.sessionId ?? "YOUR_SESSION_ID"}","path":"absolute image file path","caption":"optional caption"}`,
141
- "",
142
- "Rules:",
143
- "- Save or choose a local image file first, then call the endpoint.",
144
- "- Use an absolute local file path. Do not call Feishu Open Platform directly.",
145
- "- Request body must be UTF-8 encoded JSON bytes; caption supports Unicode text, including Chinese.",
146
- "- Only call this endpoint when the user asked for an image or when an image is useful to the answer.",
147
- "[/ChatCCC local capability: send image]",
148
- ];
149
- if (input.cwd) {
150
- lines.splice(2, 0, `Current working directory: ${input.cwd}`);
151
- }
152
- return lines.join("\n");
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { extname, isAbsolute, resolve } from "node:path";
3
+ import { stat } from "node:fs/promises";
4
+
5
+ import { getTenantAccessToken, sendImageReply, sendTextReply } from "./feishu-platform.ts";
6
+ import { ts } from "./config.ts";
7
+ import { readUtf8JsonBody } from "./agent-rpc-body.ts";
8
+ import { getAdapterForTool } from "./session.ts";
9
+ import { getChatsForSession } from "./session-chat-binding.ts";
10
+
11
+ export const AGENT_SEND_IMAGE_PATH = "/api/agent/send-image";
12
+
13
+ const MAX_REQUEST_BYTES = 64 * 1024;
14
+ const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
15
+ const ALLOWED_IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"]);
16
+
17
+ function jsonReply(res: ServerResponse, status: number, data: unknown): void {
18
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
19
+ res.end(JSON.stringify(data));
20
+ }
21
+
22
+ async function resolveAndValidateImagePath(cwd: string, rawPath: unknown): Promise<string> {
23
+ if (typeof rawPath !== "string" || rawPath.trim() === "") {
24
+ throw new Error("path must be a non-empty string");
25
+ }
26
+
27
+ const sessionRoot = resolve(cwd);
28
+ const imagePath = isAbsolute(rawPath)
29
+ ? resolve(rawPath)
30
+ : resolve(sessionRoot, rawPath);
31
+
32
+ const ext = extname(imagePath).toLowerCase();
33
+ if (!ALLOWED_IMAGE_EXTS.has(ext)) {
34
+ throw new Error(`unsupported image extension: ${ext || "(none)"}`);
35
+ }
36
+
37
+ const st = await stat(imagePath);
38
+ if (!st.isFile()) throw new Error("image path is not a file");
39
+ if (st.size <= 0) throw new Error("image file is empty");
40
+ if (st.size > MAX_IMAGE_BYTES) throw new Error("image file is larger than 10MB");
41
+ return imagePath;
42
+ }
43
+
44
+ export async function handleAgentImageRequest(
45
+ req: IncomingMessage,
46
+ res: ServerResponse,
47
+ ): Promise<boolean> {
48
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
49
+ if (url.pathname !== AGENT_SEND_IMAGE_PATH) return false;
50
+
51
+ if (req.method !== "POST") {
52
+ jsonReply(res, 405, { ok: false, error: "Method not allowed" });
53
+ return true;
54
+ }
55
+
56
+ let payload: { session_id?: unknown; path?: unknown; caption?: unknown };
57
+ try {
58
+ payload = await readUtf8JsonBody(req, MAX_REQUEST_BYTES);
59
+ } catch (err) {
60
+ jsonReply(res, 400, { ok: false, error: (err as Error).message || "Invalid JSON" });
61
+ return true;
62
+ }
63
+
64
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
65
+ if (!sessionId) {
66
+ jsonReply(res, 400, { ok: false, error: "Missing session_id" });
67
+ return true;
68
+ }
69
+
70
+ // 获取 cwd 以校验路径
71
+ let cwd: string;
72
+ try {
73
+ const { getSessionTool } = await import("./session.ts");
74
+ const tool = await getSessionTool(sessionId);
75
+ const adapter = getAdapterForTool(tool ?? "claude");
76
+ const info = await adapter.getSessionInfo(sessionId);
77
+ if (!info?.cwd) {
78
+ jsonReply(res, 400, { ok: false, error: "Cannot determine cwd for session" });
79
+ return true;
80
+ }
81
+ cwd = info.cwd;
82
+ } catch (err) {
83
+ jsonReply(res, 500, { ok: false, error: `Failed to get session info: ${(err as Error).message}` });
84
+ return true;
85
+ }
86
+
87
+ let imagePath: string;
88
+ try {
89
+ imagePath = await resolveAndValidateImagePath(cwd, payload.path);
90
+ } catch (err) {
91
+ jsonReply(res, 400, { ok: false, error: (err as Error).message });
92
+ return true;
93
+ }
94
+
95
+ // 发送到所有绑定该 session 的群
96
+ const chatIds = getChatsForSession(sessionId);
97
+ if (chatIds.length === 0) {
98
+ jsonReply(res, 404, { ok: false, error: "No chats bound to this session" });
99
+ return true;
100
+ }
101
+
102
+ try {
103
+ const token = await getTenantAccessToken();
104
+ const caption = typeof payload.caption === "string" ? payload.caption.trim() : "";
105
+ let sentCount = 0;
106
+ for (const cid of chatIds) {
107
+ try {
108
+ await sendImageReply(token, cid, imagePath);
109
+ if (caption) await sendTextReply(token, cid, caption);
110
+ sentCount++;
111
+ } catch (err) {
112
+ console.error(`[${ts()}] [AGENT-IMAGE] send to ${cid} failed: ${(err as Error).message}`);
113
+ }
114
+ }
115
+ console.log(`[${ts()}] [AGENT-IMAGE] sent image to ${sentCount}/${chatIds.length} chats, session=${sessionId} path=${imagePath}`);
116
+ jsonReply(res, 200, { ok: true, sentTo: sentCount, total: chatIds.length });
117
+ } catch (err) {
118
+ console.error(`[${ts()}] [AGENT-IMAGE] send failed: ${(err as Error).message}`);
119
+ jsonReply(res, 500, { ok: false, error: (err as Error).message });
120
+ }
121
+ return true;
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // 兼容旧版 buildAgentImageCapabilityPrompt(供 im-skills 使用)
126
+ // ---------------------------------------------------------------------------
127
+
128
+ export function buildAgentImageCapabilityPrompt(input: {
129
+ url: string;
130
+ sessionId?: string;
131
+ cwd?: string;
132
+ }): string {
133
+ const lines = [
134
+ "[ChatCCC local capability: send image]",
135
+ "You can send an image to all chats bound to this session by calling this local endpoint.",
136
+ "",
137
+ `POST ${input.url}`,
138
+ "Content-Type: application/json; charset=utf-8",
139
+ "",
140
+ `Body: {"session_id":"${input.sessionId ?? "YOUR_SESSION_ID"}","path":"absolute image file path","caption":"optional caption"}`,
141
+ "",
142
+ "Rules:",
143
+ "- Save or choose a local image file first, then call the endpoint.",
144
+ "- Use an absolute local file path. Do not call Feishu Open Platform directly.",
145
+ "- Request body must be UTF-8 encoded JSON bytes; caption supports Unicode text, including Chinese.",
146
+ "- Only call this endpoint when the user asked for an image or when an image is useful to the answer.",
147
+ "[/ChatCCC local capability: send image]",
148
+ ];
149
+ if (input.cwd) {
150
+ lines.splice(2, 0, `Current working directory: ${input.cwd}`);
151
+ }
152
+ return lines.join("\n");
153
153
  }
@@ -1,48 +1,48 @@
1
- import type { IncomingMessage } from "node:http";
2
- import { TextDecoder } from "node:util";
3
-
4
- function normalizeHeaderValue(value: string | string[] | undefined): string {
5
- if (Array.isArray(value)) return value.join("; ");
6
- return value ?? "";
7
- }
8
-
9
- export function getRequestCharset(req: IncomingMessage): string {
10
- const contentType = normalizeHeaderValue(req.headers["content-type"]);
11
- const match = contentType.match(/(?:^|;)\s*charset\s*=\s*("?)([^";\s]+)\1/i);
12
- return (match?.[2] ?? "utf-8").toLowerCase();
13
- }
14
-
15
- async function readLimitedBodyBuffer(req: IncomingMessage, maxBytes: number): Promise<Buffer> {
16
- const chunks: Buffer[] = [];
17
- let size = 0;
18
-
19
- return new Promise((resolve, reject) => {
20
- req.on("data", (chunk: Buffer) => {
21
- size += chunk.length;
22
- if (size > maxBytes) {
23
- reject(new Error("Request body too large"));
24
- req.destroy();
25
- return;
26
- }
27
- chunks.push(chunk);
28
- });
29
- req.on("end", () => resolve(Buffer.concat(chunks, size)));
30
- req.on("error", reject);
31
- });
32
- }
33
-
34
- export async function readUtf8JsonBody<T>(req: IncomingMessage, maxBytes: number): Promise<T> {
35
- const charset = getRequestCharset(req);
36
- if (charset !== "utf-8" && charset !== "utf8") {
37
- throw new Error(`Unsupported charset: ${charset}. Use UTF-8 JSON.`);
38
- }
39
-
40
- const body = await readLimitedBodyBuffer(req, maxBytes);
41
- let text: string;
42
- try {
43
- text = new TextDecoder("utf-8", { fatal: true }).decode(body);
44
- } catch {
45
- throw new Error("Request body must be valid UTF-8 JSON");
46
- }
47
- return JSON.parse(text) as T;
48
- }
1
+ import type { IncomingMessage } from "node:http";
2
+ import { TextDecoder } from "node:util";
3
+
4
+ function normalizeHeaderValue(value: string | string[] | undefined): string {
5
+ if (Array.isArray(value)) return value.join("; ");
6
+ return value ?? "";
7
+ }
8
+
9
+ export function getRequestCharset(req: IncomingMessage): string {
10
+ const contentType = normalizeHeaderValue(req.headers["content-type"]);
11
+ const match = contentType.match(/(?:^|;)\s*charset\s*=\s*("?)([^";\s]+)\1/i);
12
+ return (match?.[2] ?? "utf-8").toLowerCase();
13
+ }
14
+
15
+ async function readLimitedBodyBuffer(req: IncomingMessage, maxBytes: number): Promise<Buffer> {
16
+ const chunks: Buffer[] = [];
17
+ let size = 0;
18
+
19
+ return new Promise((resolve, reject) => {
20
+ req.on("data", (chunk: Buffer) => {
21
+ size += chunk.length;
22
+ if (size > maxBytes) {
23
+ reject(new Error("Request body too large"));
24
+ req.destroy();
25
+ return;
26
+ }
27
+ chunks.push(chunk);
28
+ });
29
+ req.on("end", () => resolve(Buffer.concat(chunks, size)));
30
+ req.on("error", reject);
31
+ });
32
+ }
33
+
34
+ export async function readUtf8JsonBody<T>(req: IncomingMessage, maxBytes: number): Promise<T> {
35
+ const charset = getRequestCharset(req);
36
+ if (charset !== "utf-8" && charset !== "utf8") {
37
+ throw new Error(`Unsupported charset: ${charset}. Use UTF-8 JSON.`);
38
+ }
39
+
40
+ const body = await readLimitedBodyBuffer(req, maxBytes);
41
+ let text: string;
42
+ try {
43
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body);
44
+ } catch {
45
+ throw new Error("Request body must be valid UTF-8 JSON");
46
+ }
47
+ return JSON.parse(text) as T;
48
+ }
@@ -0,0 +1,108 @@
1
+ type JsonObject = Record<string, unknown>;
2
+
3
+ function isObject(value: unknown): value is JsonObject {
4
+ return !!value && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+
7
+ function stringValue(value: unknown): string | null {
8
+ return typeof value === "string" && value.trim() ? value.trim() : null;
9
+ }
10
+
11
+ function textContent(value: unknown): string | null {
12
+ if (typeof value === "string") return stringValue(value);
13
+ if (!isObject(value)) return null;
14
+ return stringValue(value.content);
15
+ }
16
+
17
+ function parseButtonValue(value: unknown): unknown {
18
+ if (typeof value !== "string") return value;
19
+ try {
20
+ return JSON.parse(value);
21
+ } catch {
22
+ return value;
23
+ }
24
+ }
25
+
26
+ function commandFromValue(value: unknown): string | null {
27
+ const parsed = parseButtonValue(value);
28
+ if (typeof parsed === "string") {
29
+ const trimmed = parsed.trim();
30
+ return trimmed.startsWith("/") ? trimmed : null;
31
+ }
32
+ if (!isObject(parsed)) return null;
33
+
34
+ const cmd = stringValue(parsed.cmd);
35
+ if (cmd) return cmd.startsWith("/") ? cmd : `/${cmd}`;
36
+
37
+ const action = stringValue(parsed.action);
38
+ if (!action) return null;
39
+
40
+ if (action === "cd") {
41
+ const path = stringValue(parsed.path);
42
+ return path ? `/cd ${path}` : "/cd";
43
+ }
44
+ return action.startsWith("/") ? action : `/${action}`;
45
+ }
46
+
47
+ function buttonText(element: JsonObject): string | null {
48
+ const label = textContent(element.text);
49
+ const command = commandFromValue(element.value);
50
+ if (label && command) return `${label}: ${command}`;
51
+ return label ?? command;
52
+ }
53
+
54
+ function elementToText(element: unknown): string[] {
55
+ if (!isObject(element)) return [];
56
+ const tag = stringValue(element.tag);
57
+
58
+ if (tag === "div") {
59
+ const content = textContent(element.text);
60
+ return content ? [content] : [];
61
+ }
62
+
63
+ if (tag === "markdown") {
64
+ const content = stringValue(element.content);
65
+ return content ? [content] : [];
66
+ }
67
+
68
+ if (tag === "button") {
69
+ const content = buttonText(element);
70
+ return content ? [content] : [];
71
+ }
72
+
73
+ if (tag === "action" && Array.isArray(element.actions)) {
74
+ return element.actions.flatMap(elementToText);
75
+ }
76
+
77
+ return [];
78
+ }
79
+
80
+ function rootElements(card: JsonObject): unknown[] {
81
+ if (Array.isArray(card.elements)) return card.elements;
82
+ if (isObject(card.body) && Array.isArray(card.body.elements)) {
83
+ return card.body.elements;
84
+ }
85
+ return [];
86
+ }
87
+
88
+ export function cardJsonToPlainText(cardJson: string): string | null {
89
+ let card: unknown;
90
+ try {
91
+ card = JSON.parse(cardJson);
92
+ } catch {
93
+ return null;
94
+ }
95
+ if (!isObject(card)) return null;
96
+
97
+ const title = isObject(card.header) && isObject(card.header.title)
98
+ ? textContent(card.header.title)
99
+ : null;
100
+ const lines = rootElements(card).flatMap(elementToText);
101
+ const parts = title ? [`# ${title}`, ...lines] : lines;
102
+ const text = parts
103
+ .map((part) => part.trim())
104
+ .filter(Boolean)
105
+ .join("\n\n")
106
+ .trim();
107
+ return text || null;
108
+ }
package/src/cards.ts CHANGED
@@ -115,11 +115,11 @@ export function buildHelpCard(
115
115
  const greeting = opts.greeting ?? `你发送了: ${userText}`;
116
116
  const defaultToolLabel = opts.defaultToolLabel ?? "Claude Code";
117
117
  const lines = [
118
- `发送 **/new** 创建新会话(默认 ${defaultToolLabel})`,
119
- "发送 **/new claude** 创建新 Claude 对话",
118
+ `发送 **/new** 创建新会话(使用 /cd 设置的目录,默认 ${defaultToolLabel})`,
119
+ "发送 **/new claude** 创建新 Claude 会话",
120
120
  "发送 **/new cursor** 创建新 Cursor 会话",
121
121
  "发送 **/new codex** 创建新 Codex 会话",
122
- "发送 **/newh** 重置当前会话(保留工作目录,同一群内继续)",
122
+ "发送 **/newh** 重置当前会话(沿用当前工作目录,不切换)",
123
123
  ].join("\n");
124
124
  return JSON.stringify({
125
125
  config: { wide_screen_mode: true },
@@ -212,7 +212,7 @@ export function buildCdCard(
212
212
  elements.push({ tag: "hr" });
213
213
  elements.push({
214
214
  tag: "div",
215
- text: { tag: "lark_md", content: "**最近使用过的路径(点击切换):**" },
215
+ text: { tag: "lark_md", content: "**最近使用过的路径:**" },
216
216
  });
217
217
  elements.push({
218
218
  tag: "action",