@mumulinya167/cc-web 1.0.0 → 1.0.1

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.
@@ -1,23 +0,0 @@
1
- {
2
- "name": "mcp-feishu",
3
- "version": "1.0.0",
4
- "description": "飞书消息访问工具 - MCP Server (Claude Code) + CLI (所有 agent)",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "bin": {
8
- "feishu-cli": "dist/cli.js"
9
- },
10
- "scripts": {
11
- "build": "tsc",
12
- "start": "node dist/index.js",
13
- "cli": "node dist/cli.js"
14
- },
15
- "dependencies": {
16
- "@modelcontextprotocol/sdk": "^1.12.0",
17
- "zod": "^3.23.0"
18
- },
19
- "devDependencies": {
20
- "typescript": "^5.5.0",
21
- "@types/node": "^22.0.0"
22
- }
23
- }
@@ -1,239 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFileSync } from "node:fs";
3
- import { resolve, dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { FeishuClient, parseMessageContent, formatTimestamp } from "./feishu-client.js";
6
-
7
- // 从 .env 文件加载环境变量(如果环境变量未设置)
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const envPath = resolve(__dirname, "..", ".env");
10
- if (!process.env.FEISHU_APP_ID || !process.env.FEISHU_APP_SECRET) {
11
- try {
12
- const envContent = readFileSync(envPath, "utf-8");
13
- for (const line of envContent.split("\n")) {
14
- const trimmed = line.trim();
15
- if (!trimmed || trimmed.startsWith("#")) continue;
16
- const eqIdx = trimmed.indexOf("=");
17
- if (eqIdx === -1) continue;
18
- const key = trimmed.slice(0, eqIdx).trim();
19
- const val = trimmed.slice(eqIdx + 1).trim();
20
- if (!process.env[key]) process.env[key] = val;
21
- }
22
- } catch {
23
- // .env 文件不存在,忽略
24
- }
25
- }
26
-
27
- const appId = process.env.FEISHU_APP_ID;
28
- const appSecret = process.env.FEISHU_APP_SECRET;
29
-
30
- if (!appId || !appSecret) {
31
- console.error("错误: 请设置环境变量 FEISHU_APP_ID 和 FEISHU_APP_SECRET");
32
- console.error(` 方式1: 创建 ${envPath} 文件,写入 FEISHU_APP_ID=xxx 和 FEISHU_APP_SECRET=xxx`);
33
- console.error(" 方式2: 设置系统环境变量");
34
- process.exit(1);
35
- }
36
-
37
- const client = new FeishuClient(appId, appSecret);
38
-
39
- // 简易参数解析
40
- const args = process.argv.slice(2);
41
- const command = args[0];
42
-
43
- function getArg(name: string): string | undefined {
44
- const idx = args.indexOf(`--${name}`);
45
- if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
46
- return undefined;
47
- }
48
-
49
- function getArgNum(name: string, def: number): number {
50
- const val = getArg(name);
51
- return val ? Number(val) : def;
52
- }
53
-
54
- async function listChats() {
55
- const pageSize = getArgNum("page-size", 20);
56
- const pageToken = getArg("page-token");
57
- const data = await client.listChats(pageSize, pageToken);
58
- const items = data.items || [];
59
-
60
- console.log(`找到 ${items.length} 个群聊:\n`);
61
- for (let i = 0; i < items.length; i++) {
62
- const chat = items[i];
63
- console.log(`${i + 1}. ${chat.name || "未命名群聊"}`);
64
- console.log(` chat_id: ${chat.chat_id}`);
65
- console.log(` 成员数: ${chat.user_count || "?"}`);
66
- console.log(` 描述: ${chat.description || "无"}`);
67
- console.log();
68
- }
69
-
70
- if (data.page_token) {
71
- console.log(`下一页标记: ${data.page_token}`);
72
- }
73
- }
74
-
75
- async function getHistory() {
76
- const chatId = getArg("chat-id");
77
- if (!chatId) {
78
- console.error("错误: 请提供 --chat-id 参数(通过 list-chats 获取)");
79
- process.exit(1);
80
- }
81
-
82
- const startTime = getArg("start-time");
83
- const endTime = getArg("end-time");
84
- const pageSize = getArgNum("page-size", 20);
85
- const pageToken = getArg("page-token");
86
-
87
- const [history, chatInfo] = await Promise.all([
88
- client.getChatHistory(chatId, startTime || undefined, endTime || undefined, pageSize, pageToken || undefined),
89
- client.getChatInfo(chatId).catch(() => null),
90
- ]);
91
-
92
- const chatName = chatInfo?.name || chatId;
93
- const items = history.items || [];
94
-
95
- console.log(`群聊「${chatName}」的消息记录(共 ${items.length} 条):\n`);
96
-
97
- for (const msg of items) {
98
- const sender = msg.sender?.id || "未知";
99
- const time = formatTimestamp(msg.create_time);
100
- const content = parseMessageContent(msg.msg_type, msg.body?.content || "{}");
101
- console.log(`[${time}] ${sender}: ${content}`);
102
- }
103
-
104
- if (history.page_token) {
105
- console.log(`\n下一页标记: ${history.page_token}`);
106
- }
107
- }
108
-
109
- async function search() {
110
- const query = getArg("query");
111
- if (!query) {
112
- console.error("错误: 请提供 --query 参数");
113
- process.exit(1);
114
- }
115
-
116
- const chatId = getArg("chat-id");
117
- const startTime = getArg("start-time");
118
- const endTime = getArg("end-time");
119
-
120
- const chatIds: string[] = [];
121
- if (chatId) {
122
- chatIds.push(chatId);
123
- } else {
124
- console.error("正在获取所有群聊...");
125
- let pageToken: string | undefined;
126
- do {
127
- const chats = await client.listChats(100, pageToken);
128
- for (const chat of chats.items || []) {
129
- chatIds.push(chat.chat_id);
130
- }
131
- pageToken = chats.page_token;
132
- } while (pageToken);
133
- }
134
-
135
- console.error(`正在搜索 ${chatIds.length} 个群聊中的「${query}」...`);
136
- const matches: string[] = [];
137
- const queryLower = query!.toLowerCase();
138
-
139
- for (const id of chatIds) {
140
- try {
141
- const history = await client.getChatHistory(id, startTime || undefined, endTime || undefined, 50);
142
- const chatInfo = await client.getChatInfo(id).catch(() => null);
143
- const chatName = chatInfo?.name || id;
144
-
145
- for (const msg of history.items || []) {
146
- const content = parseMessageContent(msg.msg_type, msg.body?.content || "{}");
147
- if (content.toLowerCase().includes(queryLower)) {
148
- const time = formatTimestamp(msg.create_time);
149
- const sender = msg.sender?.id || "未知";
150
- matches.push(`[群聊: ${chatName}] [${time}] ${sender}: ${content}`);
151
- }
152
- }
153
- } catch {
154
- // 跳过无权限的群聊
155
- }
156
- }
157
-
158
- if (matches.length === 0) {
159
- console.log(`搜索「${query}」未找到匹配消息。`);
160
- return;
161
- }
162
-
163
- console.log(`\n搜索「${query}」找到 ${matches.length} 条匹配消息:\n`);
164
- for (const m of matches) {
165
- console.log(m);
166
- }
167
- }
168
-
169
- async function getDetail() {
170
- const messageId = getArg("message-id");
171
- if (!messageId) {
172
- console.error("错误: 请提供 --message-id 参数");
173
- process.exit(1);
174
- }
175
-
176
- const data = await client.getMessageDetail(messageId!);
177
- const msg = data.items?.[0] || data;
178
- const content = parseMessageContent(msg.msg_type, msg.body?.content || "{}");
179
- const time = formatTimestamp(msg.create_time);
180
-
181
- console.log(`消息详情:`);
182
- console.log(` ID: ${msg.message_id}`);
183
- console.log(` 类型: ${msg.msg_type}`);
184
- console.log(` 发送者: ${msg.sender?.id || "未知"}`);
185
- console.log(` 时间: ${time}`);
186
- console.log(` 内容:`);
187
- console.log(content);
188
- }
189
-
190
- function printUsage() {
191
- console.log(`飞书消息 CLI 工具
192
-
193
- 用法:
194
- feishu-cli <command> [options]
195
-
196
- 命令:
197
- list-chats 列出机器人所在的群聊
198
- get-history 获取群聊历史消息
199
- search 搜索包含关键词的消息
200
- get-detail 获取消息详情
201
-
202
- 选项:
203
- --chat-id <id> 群聊ID(get-history/search 必填或可选)
204
- --query <keyword> 搜索关键词(search 必填)
205
- --message-id <id> 消息ID(get-detail 忣填)
206
- --start-time <ts> 起始时间,Unix时间戳(秒)
207
- --end-time <ts> 结束时间,Unix时间戳(秒)
208
- --page-size <n> 每页数量(默认20)
209
- --page-token <token> 分页标记
210
-
211
- 示例:
212
- feishu-cli list-chats
213
- feishu-cli get-history --chat-id oc_xxxxx
214
- feishu-cli search --query "需求" --chat-id oc_xxxxx
215
- feishu-cli get-detail --message-id om_xxxxx`);
216
- }
217
-
218
- const commands: Record<string, () => Promise<void>> = {
219
- "list-chats": listChats,
220
- "get-history": getHistory,
221
- search: search,
222
- "get-detail": getDetail,
223
- };
224
-
225
- if (!command || command === "--help" || command === "-h") {
226
- printUsage();
227
- process.exit(0);
228
- }
229
-
230
- if (!commands[command]) {
231
- console.error(`未知命令: ${command}\n`);
232
- printUsage();
233
- process.exit(1);
234
- }
235
-
236
- commands[command]().catch((err) => {
237
- console.error("执行失败:", err.message);
238
- process.exit(1);
239
- });
@@ -1,209 +0,0 @@
1
- interface TokenCache {
2
- token: string;
3
- expiry: number;
4
- }
5
-
6
- interface FeishuResponse {
7
- code: number;
8
- msg: string;
9
- data: any;
10
- }
11
-
12
- export class FeishuClient {
13
- private appId: string;
14
- private appSecret: string;
15
- private tokenCache: TokenCache | null = null;
16
- private baseUrl = "https://open.feishu.cn/open-apis";
17
-
18
- constructor(appId: string, appSecret: string) {
19
- this.appId = appId;
20
- this.appSecret = appSecret;
21
- }
22
-
23
- async getAccessToken(): Promise<string> {
24
- if (this.tokenCache && Date.now() < this.tokenCache.expiry) {
25
- return this.tokenCache.token;
26
- }
27
-
28
- const res = await fetch(
29
- `${this.baseUrl}/auth/v3/tenant_access_token/internal`,
30
- {
31
- method: "POST",
32
- headers: { "Content-Type": "application/json" },
33
- body: JSON.stringify({
34
- app_id: this.appId,
35
- app_secret: this.appSecret,
36
- }),
37
- }
38
- );
39
-
40
- const data = (await res.json()) as {
41
- code: number;
42
- msg: string;
43
- tenant_access_token: string;
44
- expire: number;
45
- };
46
-
47
- if (data.code !== 0) {
48
- throw new Error(`获取 tenant_access_token 失败: ${data.msg}`);
49
- }
50
-
51
- this.tokenCache = {
52
- token: data.tenant_access_token,
53
- // 提前 5 分钟刷新
54
- expiry: Date.now() + (data.expire - 300) * 1000,
55
- };
56
-
57
- return this.tokenCache.token;
58
- }
59
-
60
- private async request(
61
- method: string,
62
- path: string,
63
- params?: Record<string, string>,
64
- body?: any
65
- ): Promise<any> {
66
- const token = await this.getAccessToken();
67
- const url = new URL(`${this.baseUrl}${path}`);
68
- if (params) {
69
- for (const [k, v] of Object.entries(params)) {
70
- if (v !== undefined && v !== "") url.searchParams.set(k, v);
71
- }
72
- }
73
-
74
- const options: RequestInit = {
75
- method,
76
- headers: {
77
- Authorization: `Bearer ${token}`,
78
- "Content-Type": "application/json",
79
- },
80
- };
81
- if (body) options.body = JSON.stringify(body);
82
-
83
- const res = await fetch(url.toString(), options);
84
- const data = (await res.json()) as FeishuResponse;
85
-
86
- if (data.code !== 0) {
87
- throw new Error(`飞书 API 错误 (${path}): [${data.code}] ${data.msg}`);
88
- }
89
-
90
- return data.data;
91
- }
92
-
93
- async listChats(pageSize = 20, pageToken?: string) {
94
- return this.request("GET", "/im/v1/chats", {
95
- page_size: String(pageSize),
96
- page_token: pageToken || "",
97
- });
98
- }
99
-
100
- async getChatHistory(
101
- chatId: string,
102
- startTime?: string,
103
- endTime?: string,
104
- pageSize = 20,
105
- pageToken?: string
106
- ) {
107
- return this.request("GET", "/im/v1/messages", {
108
- container_id_type: "chat",
109
- container_id: chatId,
110
- start_time: startTime || "",
111
- end_time: endTime || "",
112
- sort_type: "ByCreateTimeDesc",
113
- page_size: String(Math.min(pageSize, 50)),
114
- page_token: pageToken || "",
115
- });
116
- }
117
-
118
- async getMessageDetail(messageId: string) {
119
- return this.request("GET", `/im/v1/messages/${messageId}`);
120
- }
121
-
122
- async getChatInfo(chatId: string) {
123
- return this.request("GET", `/im/v1/chats/${chatId}`);
124
- }
125
-
126
- async getChatMembers(chatId: string) {
127
- return this.request("GET", `/im/v1/chats/${chatId}/members`);
128
- }
129
- }
130
-
131
- export function parseMessageContent(msgType: string, content: string): string {
132
- try {
133
- const parsed = JSON.parse(content);
134
- switch (msgType) {
135
- case "text":
136
- return parsed.text || "";
137
-
138
- case "post": {
139
- // 富文本消息,递归提取文本
140
- const lines: string[] = [];
141
- const title = parsed.title || "";
142
- if (title) lines.push(title);
143
-
144
- const content = parsed.content || parsed.zh_cn?.content || parsed.en_us?.content || [];
145
- for (const line of content) {
146
- if (Array.isArray(line)) {
147
- const texts = line
148
- .map((node: any) => {
149
- if (node.tag === "text") return node.text || "";
150
- if (node.tag === "a") return `${node.text || ""}(${node.href || ""})`;
151
- if (node.tag === "at") return `@${node.user_name || node.user_id || ""}`;
152
- if (node.tag === "img") return "[图片]";
153
- if (node.tag === "media") return "[媒体]";
154
- if (node.tag === "emotion") return "[表情]";
155
- return "";
156
- })
157
- .filter(Boolean)
158
- .join("");
159
- if (texts) lines.push(texts);
160
- }
161
- }
162
- return lines.join("\n") || "[富文本消息]";
163
- }
164
-
165
- case "interactive": {
166
- // 卡片消息
167
- const elements = parsed.elements || parsed.i18n_elements?.zh_cn || [];
168
- const texts: string[] = [];
169
- if (parsed.header?.title?.content) {
170
- texts.push(parsed.header.title.content);
171
- }
172
- for (const el of elements) {
173
- if (el.tag === "div" && el.text?.content) texts.push(el.text.content);
174
- if (el.tag === "markdown" && el.content) texts.push(el.content);
175
- if (el.tag === "plain_text" && el.content) texts.push(el.content);
176
- }
177
- return texts.join("\n") || "[卡片消息]";
178
- }
179
-
180
- case "image":
181
- return "[图片消息]";
182
- case "file":
183
- return `[文件消息: ${parsed.file_name || "未知文件"}]`;
184
- case "audio":
185
- return "[语音消息]";
186
- case "media":
187
- return "[视频消息]";
188
- case "sticker":
189
- return "[表情包]";
190
- case "share_chat":
191
- return `[分享群聊: ${parsed.chat_name || ""}]`;
192
- case "share_user":
193
- return `[分享名片]`;
194
- case "system":
195
- return "[系统消息]";
196
- default:
197
- return `[未支持的消息类型: ${msgType}]`;
198
- }
199
- } catch {
200
- return `[消息解析失败: ${msgType}]`;
201
- }
202
- }
203
-
204
- export function formatTimestamp(ts: string): string {
205
- // 飞书时间戳是毫秒级字符串
206
- const date = new Date(Number(ts));
207
- const pad = (n: number) => String(n).padStart(2, "0");
208
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
209
- }
@@ -1,55 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { resolve, dirname } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { FeishuClient } from "./feishu-client.js";
7
- import { registerTools } from "./tools.js";
8
-
9
- // 从 .env 文件加载环境变量(如果环境变量未设置)
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const envPath = resolve(__dirname, "..", ".env");
12
- if (!process.env.FEISHU_APP_ID || !process.env.FEISHU_APP_SECRET) {
13
- try {
14
- const envContent = readFileSync(envPath, "utf-8");
15
- for (const line of envContent.split("\n")) {
16
- const trimmed = line.trim();
17
- if (!trimmed || trimmed.startsWith("#")) continue;
18
- const eqIdx = trimmed.indexOf("=");
19
- if (eqIdx === -1) continue;
20
- const key = trimmed.slice(0, eqIdx).trim();
21
- const val = trimmed.slice(eqIdx + 1).trim();
22
- if (!process.env[key]) process.env[key] = val;
23
- }
24
- } catch {
25
- // .env 文件不存在,忽略
26
- }
27
- }
28
-
29
- const appId = process.env.FEISHU_APP_ID;
30
- const appSecret = process.env.FEISHU_APP_SECRET;
31
-
32
- if (!appId || !appSecret) {
33
- console.error(`错误: 请在 ${envPath} 中设置 FEISHU_APP_ID 和 FEISHU_APP_SECRET`);
34
- process.exit(1);
35
- }
36
-
37
- const feishuClient = new FeishuClient(appId, appSecret);
38
-
39
- const server = new McpServer({
40
- name: "mcp-feishu",
41
- version: "1.0.0",
42
- });
43
-
44
- registerTools(server, feishuClient);
45
-
46
- async function main() {
47
- const transport = new StdioServerTransport();
48
- await server.connect(transport);
49
- console.error("mcp-feishu server 已启动 (stdio)");
50
- }
51
-
52
- main().catch((err) => {
53
- console.error("启动失败:", err);
54
- process.exit(1);
55
- });