@teamclaw/feishu-agent 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.
- package/dist/cli.js +0 -0
- package/dist/index.js +0 -0
- package/package.json +6 -9
- package/src/cli/commands/auth.ts +0 -309
- package/src/cli/commands/calendar.ts +0 -305
- package/src/cli/commands/config.ts +0 -48
- package/src/cli/commands/contact.ts +0 -90
- package/src/cli/commands/init.ts +0 -128
- package/src/cli/commands/setup.ts +0 -327
- package/src/cli/commands/todo.ts +0 -114
- package/src/cli/commands/whoami.ts +0 -63
- package/src/cli/index.ts +0 -68
- package/src/core/auth-manager.ts +0 -214
- package/src/core/calendar.ts +0 -276
- package/src/core/client.ts +0 -100
- package/src/core/config.ts +0 -174
- package/src/core/contact.ts +0 -83
- package/src/core/introspection.ts +0 -103
- package/src/core/todo.ts +0 -109
- package/src/index.ts +0 -76
- package/src/types/index.ts +0 -199
package/dist/cli.js
ADDED
|
Binary file
|
package/dist/index.js
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamclaw/feishu-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Feishu Agent CLI for AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": {
|
|
8
|
-
"
|
|
8
|
+
"feishu-agent": "./dist/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "bun test",
|
|
12
|
-
"start
|
|
13
|
-
"
|
|
14
|
-
"
|
|
12
|
+
"start": "bun run src/cli/index.ts",
|
|
13
|
+
"build": "bun build ./src/cli/index.ts --target=bun --compile --outfile=dist/cli.js",
|
|
14
|
+
"prepublishOnly": "bun run build"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@types/bun": "latest"
|
|
18
18
|
},
|
|
19
|
-
"peerDependencies": {
|
|
20
|
-
"typescript": "^5"
|
|
21
|
-
},
|
|
22
19
|
"dependencies": {
|
|
23
20
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
24
21
|
"commander": "^14.0.3",
|
|
@@ -28,7 +25,7 @@
|
|
|
28
25
|
"bun": ">=1.0.0"
|
|
29
26
|
},
|
|
30
27
|
"files": [
|
|
31
|
-
"
|
|
28
|
+
"dist",
|
|
32
29
|
"README.md"
|
|
33
30
|
],
|
|
34
31
|
"keywords": [
|
package/src/cli/commands/auth.ts
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { parseArgs } from "node:util";
|
|
3
|
-
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
4
|
-
import { loadConfig, saveGlobalConfig } from "../../core/config";
|
|
5
|
-
|
|
6
|
-
interface UserAccessTokenResponse {
|
|
7
|
-
code: number;
|
|
8
|
-
msg: string;
|
|
9
|
-
data?: {
|
|
10
|
-
access_token: string;
|
|
11
|
-
refresh_token: string;
|
|
12
|
-
token_type: string;
|
|
13
|
-
expires_in: number;
|
|
14
|
-
name: string;
|
|
15
|
-
en_name: string;
|
|
16
|
-
avatar: string;
|
|
17
|
-
user_id: string;
|
|
18
|
-
union_id: string;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function authCommand(args: string[]) {
|
|
23
|
-
const { values } = parseArgs({
|
|
24
|
-
args,
|
|
25
|
-
strict: false,
|
|
26
|
-
options: {
|
|
27
|
-
port: { type: "string", default: "3000" },
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Load global config
|
|
32
|
-
const config = await loadConfig();
|
|
33
|
-
const appId = config.appId;
|
|
34
|
-
const appSecret = config.appSecret;
|
|
35
|
-
const port = parseInt(values["port"] as string || "3000", 10);
|
|
36
|
-
|
|
37
|
-
if (!appId || !appSecret) {
|
|
38
|
-
console.error("Error: FEISHU_APP_ID and FEISHU_APP_SECRET must be set.");
|
|
39
|
-
console.error("Run 'feishu-agent setup' or export environment variables.");
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
console.log("\n=== Feishu OAuth 2.0 Authorization ===\n");
|
|
44
|
-
console.log("Step 1: Generating authorization URL...\n");
|
|
45
|
-
|
|
46
|
-
// Generate a random state for security
|
|
47
|
-
const state = generateRandomString(32);
|
|
48
|
-
const redirectUri = `http://localhost:${port}/callback`;
|
|
49
|
-
|
|
50
|
-
// URL encode the redirect_uri
|
|
51
|
-
const encodedRedirectUri = encodeURIComponent(redirectUri);
|
|
52
|
-
|
|
53
|
-
// Construct the authorization URL
|
|
54
|
-
// Note: Scopes must be configured in Feishu Developer Console, not in the URL
|
|
55
|
-
const authUrl = `https://open.feishu.cn/open-apis/authen/v1/index?app_id=${appId}&redirect_uri=${encodedRedirectUri}&state=${state}`;
|
|
56
|
-
|
|
57
|
-
console.log("Redirect URI (configure this in Feishu Developer Console):");
|
|
58
|
-
console.log(` ${redirectUri}\n`);
|
|
59
|
-
console.log("Authorization URL:");
|
|
60
|
-
console.log(` ${authUrl}\n`);
|
|
61
|
-
console.log("Required permissions (configure in Feishu Developer Console):");
|
|
62
|
-
console.log(" - calendar:calendar (查看和管理用户的日历)");
|
|
63
|
-
console.log(" - calendar:event (管理用户日历下的日程)");
|
|
64
|
-
console.log("\nInstructions:");
|
|
65
|
-
console.log(" 1. Make sure the redirect URI is configured in your Feishu app settings (安全设置)");
|
|
66
|
-
console.log(" 2. Make sure the required permissions are enabled in your app (权限管理)");
|
|
67
|
-
console.log(" 3. Click the URL above or paste it in your browser");
|
|
68
|
-
console.log(" 4. Authorize the application");
|
|
69
|
-
console.log(" 5. You will be redirected to localhost and the token will be captured\n");
|
|
70
|
-
|
|
71
|
-
// Store auth info for verification
|
|
72
|
-
let authCode: string | null = null;
|
|
73
|
-
let receivedState: string | null = null;
|
|
74
|
-
let authResult: { success: boolean; message: string } | null = null;
|
|
75
|
-
|
|
76
|
-
// Create local server to receive callback
|
|
77
|
-
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
78
|
-
if (req.url?.startsWith("/callback")) {
|
|
79
|
-
const url = new URL(req.url, `http://localhost:${port}`);
|
|
80
|
-
const code = url.searchParams.get("code");
|
|
81
|
-
receivedState = url.searchParams.get("state");
|
|
82
|
-
const error = url.searchParams.get("error");
|
|
83
|
-
|
|
84
|
-
if (error) {
|
|
85
|
-
authResult = { success: false, message: `Authorization error: ${error}` };
|
|
86
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
87
|
-
res.end(`
|
|
88
|
-
<html>
|
|
89
|
-
<head><title>Authorization Failed</title></head>
|
|
90
|
-
<body>
|
|
91
|
-
<h1>Authorization Failed</h1>
|
|
92
|
-
<p>Error: ${error}</p>
|
|
93
|
-
<p>You can close this window.</p>
|
|
94
|
-
</body>
|
|
95
|
-
</html>
|
|
96
|
-
`);
|
|
97
|
-
server.close();
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!code) {
|
|
102
|
-
authResult = { success: false, message: "No authorization code received" };
|
|
103
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
104
|
-
res.end(`
|
|
105
|
-
<html>
|
|
106
|
-
<head><title>Authorization Failed</title></head>
|
|
107
|
-
<body>
|
|
108
|
-
<h1>Authorization Failed</h1>
|
|
109
|
-
<p>No authorization code received.</p>
|
|
110
|
-
<p>You can close this window.</p>
|
|
111
|
-
</body>
|
|
112
|
-
</html>
|
|
113
|
-
`);
|
|
114
|
-
server.close();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
authCode = code;
|
|
119
|
-
|
|
120
|
-
// Verify state
|
|
121
|
-
if (receivedState !== state) {
|
|
122
|
-
authResult = { success: false, message: "State mismatch - possible CSRF attack" };
|
|
123
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
124
|
-
res.end(`
|
|
125
|
-
<html>
|
|
126
|
-
<head><title>Authorization Failed</title></head>
|
|
127
|
-
<body>
|
|
128
|
-
<h1>Authorization Failed</h1>
|
|
129
|
-
<p>State verification failed. Please try again.</p>
|
|
130
|
-
<p>You can close this window.</p>
|
|
131
|
-
</body>
|
|
132
|
-
</html>
|
|
133
|
-
`);
|
|
134
|
-
server.close();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Show success page
|
|
139
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
140
|
-
res.end(`
|
|
141
|
-
<html>
|
|
142
|
-
<head><title>Authorization Successful</title></head>
|
|
143
|
-
<body>
|
|
144
|
-
<h1>Authorization Successful!</h1>
|
|
145
|
-
<p>You can close this window and return to the terminal.</p>
|
|
146
|
-
<script>setTimeout(() => window.close(), 5000);</script>
|
|
147
|
-
</body>
|
|
148
|
-
</html>
|
|
149
|
-
`);
|
|
150
|
-
} else {
|
|
151
|
-
res.writeHead(404);
|
|
152
|
-
res.end("Not found");
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
server.listen(port, () => {
|
|
157
|
-
console.log(`Step 2: Local server started on http://localhost:${port}`);
|
|
158
|
-
console.log(`Step 3: Opening authorization URL in your browser...\n`);
|
|
159
|
-
|
|
160
|
-
// Try to open the browser
|
|
161
|
-
openBrowser(authUrl);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// Wait for callback with timeout
|
|
165
|
-
const timeout = 5 * 60 * 1000; // 5 minutes
|
|
166
|
-
const startTime = Date.now();
|
|
167
|
-
|
|
168
|
-
const checkInterval = setInterval(async () => {
|
|
169
|
-
if (authCode) {
|
|
170
|
-
clearInterval(checkInterval);
|
|
171
|
-
clearTimeout(timeoutId);
|
|
172
|
-
|
|
173
|
-
console.log("\nStep 4: Authorization code received, exchanging for access token...\n");
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
const tokenData = await exchangeCodeForToken(appId, appSecret, authCode, redirectUri);
|
|
177
|
-
|
|
178
|
-
if (tokenData.code === 0 && tokenData.data) {
|
|
179
|
-
console.log("========================================");
|
|
180
|
-
console.log(" SUCCESS! User Access Token Received");
|
|
181
|
-
console.log("========================================\n");
|
|
182
|
-
console.log(` User ID: ${tokenData.data.user_id}`);
|
|
183
|
-
console.log(` Name: ${tokenData.data.name}`);
|
|
184
|
-
console.log(` Union ID: ${tokenData.data.union_id}`);
|
|
185
|
-
console.log(` Access Token: ${tokenData.data.access_token}`);
|
|
186
|
-
console.log(` Expires In: ${tokenData.data.expires_in} seconds`);
|
|
187
|
-
console.log(` Refresh Token: ${tokenData.data.refresh_token}`);
|
|
188
|
-
console.log("\n========================================\n");
|
|
189
|
-
|
|
190
|
-
// Save to global config
|
|
191
|
-
try {
|
|
192
|
-
await saveGlobalConfig({
|
|
193
|
-
userAccessToken: tokenData.data.access_token,
|
|
194
|
-
refreshToken: tokenData.data.refresh_token,
|
|
195
|
-
});
|
|
196
|
-
console.log("Tokens saved to ~/.feishu-agent/config.json\n");
|
|
197
|
-
console.log("You can now use calendar and other commands that require user authorization.");
|
|
198
|
-
} catch (saveError) {
|
|
199
|
-
console.error("Warning: Failed to save tokens to config file:");
|
|
200
|
-
console.error(saveError);
|
|
201
|
-
console.log("\nPlease save the tokens manually:");
|
|
202
|
-
console.log(` export FEISHU_USER_ACCESS_TOKEN="${tokenData.data.access_token}"`);
|
|
203
|
-
console.log(` export FEISHU_REFRESH_TOKEN="${tokenData.data.refresh_token}"`);
|
|
204
|
-
}
|
|
205
|
-
console.log("\nNote: Token will expire in", Math.floor(tokenData.data.expires_in / 60), "minutes.");
|
|
206
|
-
console.log("Token refresh is automatic. Just run 'feishu-agent auth' again when it expires.\n");
|
|
207
|
-
} else {
|
|
208
|
-
console.error("Failed to exchange code for token:");
|
|
209
|
-
console.error(tokenData.msg || "Unknown error");
|
|
210
|
-
}
|
|
211
|
-
} catch (error) {
|
|
212
|
-
console.error("Error exchanging token:", error);
|
|
213
|
-
} finally {
|
|
214
|
-
server.close();
|
|
215
|
-
process.exit(authResult?.success !== false ? 0 : 1);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Check timeout
|
|
220
|
-
if (Date.now() - startTime > timeout) {
|
|
221
|
-
clearInterval(checkInterval);
|
|
222
|
-
console.error("\nAuthorization timed out after 5 minutes.");
|
|
223
|
-
console.error("Please run the command again and complete authorization.");
|
|
224
|
-
server.close();
|
|
225
|
-
process.exit(1);
|
|
226
|
-
}
|
|
227
|
-
}, 500);
|
|
228
|
-
|
|
229
|
-
const timeoutId = setTimeout(() => {
|
|
230
|
-
clearInterval(checkInterval);
|
|
231
|
-
console.error("\nAuthorization timed out.");
|
|
232
|
-
server.close();
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}, timeout);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function generateRandomString(length: number): string {
|
|
238
|
-
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
|
|
239
|
-
let result = "";
|
|
240
|
-
for (let i = 0; i < length; i++) {
|
|
241
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
242
|
-
}
|
|
243
|
-
return result;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function openBrowser(url: string): void {
|
|
247
|
-
const platform = process.platform;
|
|
248
|
-
let cmd: string;
|
|
249
|
-
|
|
250
|
-
switch (platform) {
|
|
251
|
-
case "win32":
|
|
252
|
-
cmd = `start ${url}`;
|
|
253
|
-
break;
|
|
254
|
-
case "darwin":
|
|
255
|
-
cmd = `open "${url}"`;
|
|
256
|
-
break;
|
|
257
|
-
default:
|
|
258
|
-
cmd = `xdg-open "${url}"`;
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
import("node:child_process").then(({ exec }) => {
|
|
263
|
-
exec(cmd, (error) => {
|
|
264
|
-
if (error) {
|
|
265
|
-
console.log("Could not auto-open browser. Please manually visit the URL:");
|
|
266
|
-
console.log(url);
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async function exchangeCodeForToken(
|
|
273
|
-
appId: string,
|
|
274
|
-
appSecret: string,
|
|
275
|
-
code: string,
|
|
276
|
-
redirectUri: string
|
|
277
|
-
): Promise<UserAccessTokenResponse> {
|
|
278
|
-
// 使用飞书身份认证 v1 API
|
|
279
|
-
// https://open.feishu.cn/document/ukTMukTMukTM/ukDMz4SMxQjL0MjN
|
|
280
|
-
const response = await fetch("https://open.feishu.cn/open-apis/authen/v1/access_token", {
|
|
281
|
-
method: "POST",
|
|
282
|
-
headers: { "Content-Type": "application/json" },
|
|
283
|
-
body: JSON.stringify({
|
|
284
|
-
grant_type: "authorization_code",
|
|
285
|
-
code: code,
|
|
286
|
-
app_id: appId,
|
|
287
|
-
app_secret: appSecret,
|
|
288
|
-
}),
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
const text = await response.text();
|
|
292
|
-
console.log("Token exchange response status:", response.status);
|
|
293
|
-
|
|
294
|
-
// 处理空响应
|
|
295
|
-
if (!text || text.trim() === "") {
|
|
296
|
-
throw new Error("Empty response from Feishu API");
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
const data = JSON.parse(text);
|
|
301
|
-
if (data.code !== 0) {
|
|
302
|
-
throw new Error(`Feishu API error: ${data.msg || data.message || JSON.stringify(data)}`);
|
|
303
|
-
}
|
|
304
|
-
return data;
|
|
305
|
-
} catch (parseError) {
|
|
306
|
-
console.log("Raw response:", text.substring(0, 500));
|
|
307
|
-
throw new Error(`Failed to parse JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import { FeishuClient } from "../../core/client";
|
|
3
|
-
import { CalendarManager } from "../../core/calendar";
|
|
4
|
-
import { ContactManager } from "../../core/contact";
|
|
5
|
-
import { loadContactCache } from "../../core/config";
|
|
6
|
-
import { FeishuConfig } from "../../types";
|
|
7
|
-
|
|
8
|
-
interface CalendarOptions {
|
|
9
|
-
calendarId?: string;
|
|
10
|
-
summary?: string;
|
|
11
|
-
start?: string;
|
|
12
|
-
end?: string;
|
|
13
|
-
attendee?: string[];
|
|
14
|
-
attendeeName?: string[];
|
|
15
|
-
eventId?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function createCalendarCommands(program: Command, config: FeishuConfig) {
|
|
19
|
-
program
|
|
20
|
-
.command("list")
|
|
21
|
-
.description("List all calendars")
|
|
22
|
-
.action(async () => {
|
|
23
|
-
await handleListCalendars(config);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
program
|
|
27
|
-
.command("events")
|
|
28
|
-
.description("List events in a calendar")
|
|
29
|
-
.option("--calendar-id <string>", "Specify calendar ID")
|
|
30
|
-
.action(async (options: CalendarOptions) => {
|
|
31
|
-
await handleListEvents(config, options.calendarId);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
program
|
|
35
|
-
.command("create")
|
|
36
|
-
.description("Create a new event")
|
|
37
|
-
.requiredOption("--summary <string>", "Event title")
|
|
38
|
-
.requiredOption("--start <string>", "Event start time")
|
|
39
|
-
.requiredOption("--end <string>", "Event end time")
|
|
40
|
-
.option("--attendee <ids...>", "User IDs (union_id) to invite")
|
|
41
|
-
.option("--attendee-name <names...>", "Contact names to invite")
|
|
42
|
-
.option("--calendar-id <string>", "Specify calendar ID")
|
|
43
|
-
.action(async (options: CalendarOptions) => {
|
|
44
|
-
await handleCreateEvent(config, options);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
program
|
|
48
|
-
.command("delete")
|
|
49
|
-
.description("Delete an event")
|
|
50
|
-
.requiredOption("--event-id <string>", "Event ID")
|
|
51
|
-
.option("--calendar-id <string>", "Specify calendar ID")
|
|
52
|
-
.action(async (options: CalendarOptions) => {
|
|
53
|
-
await handleDeleteEvent(config, options);
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function handleListCalendars(config: FeishuConfig) {
|
|
58
|
-
if (!config.appId || !config.appSecret) {
|
|
59
|
-
console.error("Error: FEISHU_APP_ID and FEISHU_APP_SECRET must be set.");
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
if (!config.userAccessToken) {
|
|
63
|
-
console.error("Error: User authorization required.");
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const client = new FeishuClient(config);
|
|
68
|
-
const calendarManager = new CalendarManager(client);
|
|
69
|
-
|
|
70
|
-
console.log("\n📅 Your Calendars\n");
|
|
71
|
-
console.log("=".repeat(60));
|
|
72
|
-
|
|
73
|
-
const calendars = await calendarManager.listCalendars();
|
|
74
|
-
|
|
75
|
-
if (!calendars.calendar_list || calendars.calendar_list.length === 0) {
|
|
76
|
-
console.log("No calendars found.");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const primary = calendars.calendar_list.filter(c => c.type === "primary");
|
|
81
|
-
const subscribed = calendars.calendar_list.filter(c => c.type === "shared" || c.type === "exchange");
|
|
82
|
-
const other = calendars.calendar_list.filter(c => c.type !== "primary" && c.type !== "shared" && c.type !== "exchange");
|
|
83
|
-
|
|
84
|
-
if (primary.length > 0) {
|
|
85
|
-
console.log("\n【Primary】");
|
|
86
|
-
primary.forEach(c => {
|
|
87
|
-
console.log(` • ${c.summary}`);
|
|
88
|
-
console.log(` ID: ${c.calendar_id}`);
|
|
89
|
-
console.log(` Role: ${c.role}`);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (subscribed.length > 0) {
|
|
94
|
-
console.log("\n【Subscribed】");
|
|
95
|
-
subscribed.forEach(c => {
|
|
96
|
-
console.log(` • ${c.summary}`);
|
|
97
|
-
console.log(` ID: ${c.calendar_id}`);
|
|
98
|
-
console.log(` Role: ${c.role}`);
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (other.length > 0) {
|
|
103
|
-
console.log("\n【Other】");
|
|
104
|
-
other.forEach(c => {
|
|
105
|
-
console.log(` • ${c.summary} (${c.type})`);
|
|
106
|
-
console.log(` ID: ${c.calendar_id}`);
|
|
107
|
-
console.log(` Role: ${c.role}`);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
console.log("\n" + "=".repeat(60));
|
|
112
|
-
console.log(`Total: ${calendars.calendar_list.length} calendar(s)\n`);
|
|
113
|
-
|
|
114
|
-
if (primary.length > 0) {
|
|
115
|
-
console.log(`Tip: Use --calendar-id "${primary[0].calendar_id}" to list events.`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function handleListEvents(config: FeishuConfig, calendarId?: string) {
|
|
120
|
-
if (!config.appId || !config.appSecret || !config.userAccessToken) {
|
|
121
|
-
console.error("Error: Authorization required. Run 'feishu-agent auth'.");
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const client = new FeishuClient(config);
|
|
126
|
-
const calendarManager = new CalendarManager(client);
|
|
127
|
-
|
|
128
|
-
if (!calendarId) {
|
|
129
|
-
const calendars = await calendarManager.listCalendars();
|
|
130
|
-
const primary = calendars.calendar_list?.find(c => c.type === "primary");
|
|
131
|
-
if (primary) {
|
|
132
|
-
calendarId = primary.calendar_id;
|
|
133
|
-
console.log(`Using primary calendar: ${primary.summary}\n`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (!calendarId) {
|
|
138
|
-
console.error("Error: No calendar available.");
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
console.log(`\n📅 Events\n`);
|
|
143
|
-
console.log("=".repeat(60));
|
|
144
|
-
|
|
145
|
-
const events = await calendarManager.listEvents(calendarId);
|
|
146
|
-
if (!events.items || events.items.length === 0) {
|
|
147
|
-
console.log("No events found.");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const activeEvents = events.items.filter(e => e.status !== "cancelled");
|
|
152
|
-
if (activeEvents.length === 0) {
|
|
153
|
-
console.log("No active events found.");
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const contactCache = await loadContactCache();
|
|
158
|
-
|
|
159
|
-
for (const e of activeEvents) {
|
|
160
|
-
const start = e.start_time.timestamp
|
|
161
|
-
? new Date(parseInt(e.start_time.timestamp) * 1000).toLocaleString()
|
|
162
|
-
: e.start_time.date;
|
|
163
|
-
const end = e.end_time.timestamp
|
|
164
|
-
? new Date(parseInt(e.end_time.timestamp) * 1000).toLocaleString()
|
|
165
|
-
: e.end_time.date;
|
|
166
|
-
|
|
167
|
-
console.log(`\n📅 ${e.summary || "(No title)"}`);
|
|
168
|
-
console.log(` 🕐 ${start} - ${end}`);
|
|
169
|
-
console.log(` ID: ${e.event_id}`);
|
|
170
|
-
if (e.status && e.status !== "confirmed") {
|
|
171
|
-
console.log(` Status: ${e.status}`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
const attendees = await calendarManager.getEventAttendees(calendarId, e.event_id);
|
|
176
|
-
if (attendees && attendees.length > 0) {
|
|
177
|
-
const attendeeDisplay = attendees.map(a => {
|
|
178
|
-
if (a.type === "user" && a.user_id) {
|
|
179
|
-
// Lookup attendee name from cache by open_id or union_id
|
|
180
|
-
for (const [unionId, entry] of Object.entries(contactCache)) {
|
|
181
|
-
if (entry.open_id === a.user_id || unionId === a.user_id) {
|
|
182
|
-
return `${entry.name}${a.is_optional ? " (optional)" : ""}`;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return `${a.user_id}${a.is_optional ? " (optional)" : ""}`;
|
|
186
|
-
}
|
|
187
|
-
if (a.type === "chat") return `Chat: ${a.chat_id}`;
|
|
188
|
-
if (a.type === "third_party" && a.third_party_email) {
|
|
189
|
-
return `${a.third_party_email} (external)`;
|
|
190
|
-
}
|
|
191
|
-
return a.type;
|
|
192
|
-
});
|
|
193
|
-
console.log(` 👥 Attendees: ${attendeeDisplay.join(", ")}`);
|
|
194
|
-
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
// Ignore errors
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
console.log("\n" + "=".repeat(60));
|
|
201
|
-
console.log(`Total: ${activeEvents.length} event(s)\n`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function handleCreateEvent(config: FeishuConfig, options: CalendarOptions) {
|
|
205
|
-
if (!config.appId || !config.appSecret || !config.userAccessToken) {
|
|
206
|
-
console.error("Error: Authorization required.");
|
|
207
|
-
process.exit(1);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const client = new FeishuClient(config);
|
|
211
|
-
const calendarManager = new CalendarManager(client);
|
|
212
|
-
const contactManager = new ContactManager(client);
|
|
213
|
-
|
|
214
|
-
const { summary, start, end, attendee, attendeeName, calendarId } = options;
|
|
215
|
-
|
|
216
|
-
if (!summary || !start || !end) {
|
|
217
|
-
console.error("Error: --summary, --start, and --end are required.");
|
|
218
|
-
process.exit(1);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Resolve attendee names to union_ids
|
|
222
|
-
let attendeeUserIds: string[] = attendee || [];
|
|
223
|
-
if (attendeeName && attendeeName.length > 0) {
|
|
224
|
-
console.log("\n🔍 Resolving attendee names...");
|
|
225
|
-
for (const name of attendeeName) {
|
|
226
|
-
const results = await contactManager.searchUser(name);
|
|
227
|
-
if (results.length === 0) {
|
|
228
|
-
console.error(`Error: No contact found for "${name}".`);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
if (results.length > 1) {
|
|
232
|
-
console.log(` Multiple matches for "${name}":`);
|
|
233
|
-
results.forEach((r, i) => {
|
|
234
|
-
console.log(` ${i + 1}. ${r.name} (${r.email || r.union_id})`);
|
|
235
|
-
});
|
|
236
|
-
console.log(" Using the first match.");
|
|
237
|
-
}
|
|
238
|
-
attendeeUserIds.push(results[0].union_id);
|
|
239
|
-
console.log(` ✓ "${name}" -> ${results[0].name} (${results[0].union_id})`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Get calendar
|
|
244
|
-
let targetCalendarId = calendarId;
|
|
245
|
-
if (!targetCalendarId) {
|
|
246
|
-
const calendars = await calendarManager.listCalendars();
|
|
247
|
-
const primary = calendars.calendar_list?.find(c => c.type === "primary");
|
|
248
|
-
if (primary) targetCalendarId = primary.calendar_id;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (!targetCalendarId) {
|
|
252
|
-
console.error("Error: No calendar available.");
|
|
253
|
-
process.exit(1);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const startTimestamp = Math.floor(new Date(start).getTime() / 1000).toString();
|
|
257
|
-
const endTimestamp = Math.floor(new Date(end).getTime() / 1000).toString();
|
|
258
|
-
|
|
259
|
-
const event = await calendarManager.createEvent(targetCalendarId, {
|
|
260
|
-
summary,
|
|
261
|
-
startTime: { timestamp: startTimestamp },
|
|
262
|
-
endTime: { timestamp: endTimestamp },
|
|
263
|
-
attendeeUserIds: attendeeUserIds.length > 0 ? attendeeUserIds : undefined,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
console.log("\n✅ Event created!");
|
|
267
|
-
console.log(` Title: ${summary}`);
|
|
268
|
-
console.log(` Time: ${new Date(parseInt(startTimestamp) * 1000).toLocaleString()} - ${new Date(parseInt(endTimestamp) * 1000).toLocaleString()}`);
|
|
269
|
-
console.log(` Calendar ID: ${targetCalendarId}`);
|
|
270
|
-
if (attendeeUserIds.length > 0) {
|
|
271
|
-
console.log(` Attendees: ${attendeeUserIds.join(", ")}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async function handleDeleteEvent(config: FeishuConfig, options: CalendarOptions) {
|
|
276
|
-
if (!config.appId || !config.appSecret || !config.userAccessToken) {
|
|
277
|
-
console.error("Error: Authorization required.");
|
|
278
|
-
process.exit(1);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const client = new FeishuClient(config);
|
|
282
|
-
const calendarManager = new CalendarManager(client);
|
|
283
|
-
|
|
284
|
-
const { eventId, calendarId } = options;
|
|
285
|
-
let targetCalendarId = calendarId;
|
|
286
|
-
|
|
287
|
-
if (!targetCalendarId) {
|
|
288
|
-
const calendars = await calendarManager.listCalendars();
|
|
289
|
-
const primary = calendars.calendar_list?.find(c => c.type === "primary");
|
|
290
|
-
if (primary) targetCalendarId = primary.calendar_id;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (!targetCalendarId) {
|
|
294
|
-
console.error("Error: No calendar available.");
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!eventId) {
|
|
299
|
-
console.error("Error: --event-id is required.");
|
|
300
|
-
process.exit(1);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
await calendarManager.deleteEvent(targetCalendarId, eventId);
|
|
304
|
-
console.log(`\n✅ Event deleted: ${eventId}\n`);
|
|
305
|
-
}
|