@teamclaw/feishu-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -0
- package/package.json +49 -0
- package/src/cli/commands/auth.ts +309 -0
- package/src/cli/commands/calendar.ts +305 -0
- package/src/cli/commands/config.ts +48 -0
- package/src/cli/commands/contact.ts +90 -0
- package/src/cli/commands/init.ts +128 -0
- package/src/cli/commands/setup.ts +327 -0
- package/src/cli/commands/todo.ts +114 -0
- package/src/cli/commands/whoami.ts +63 -0
- package/src/cli/index.ts +68 -0
- package/src/core/auth-manager.ts +214 -0
- package/src/core/calendar.ts +276 -0
- package/src/core/client.ts +100 -0
- package/src/core/config.ts +174 -0
- package/src/core/contact.ts +83 -0
- package/src/core/introspection.ts +103 -0
- package/src/core/todo.ts +109 -0
- package/src/index.ts +76 -0
- package/src/types/index.ts +199 -0
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Feishu Agent
|
|
2
|
+
|
|
3
|
+
Feishu Agent is a TypeScript/Node.js middleware layer for Feishu (Lark) API integration, designed for AI agents via MCP protocol.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📅 Calendar management (list calendars, events, create/delete events)
|
|
8
|
+
- ✅ Todo management via Bitable
|
|
9
|
+
- 👥 Contact management (list users, search by name/email)
|
|
10
|
+
- 🔐 OAuth 2.0 authentication support
|
|
11
|
+
- 🚀 CLI interface with commander
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Global Install (Recommended)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add -g @teamclaw/feishu-agent
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
After installation, you can use the `feishu_agent` command directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
feishu_agent calendar list
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Run with bunx (No Install)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bunx @teamclaw/feishu-agent calendar list
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Local Development
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bun install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Setup
|
|
42
|
+
|
|
43
|
+
Run the setup command to configure your Feishu app credentials:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
feishu_agent setup
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or export environment variables:
|
|
50
|
+
```bash
|
|
51
|
+
export FEISHU_APP_ID=cli_xxx
|
|
52
|
+
export FEISHU_APP_SECRET=xxx
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Authenticate
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
feishu_agent auth
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This will open a browser window for OAuth 2.0 authorization.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Calendar
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# List calendars
|
|
69
|
+
feishu_agent calendar list
|
|
70
|
+
|
|
71
|
+
# List events
|
|
72
|
+
feishu_agent calendar events
|
|
73
|
+
|
|
74
|
+
# Create event
|
|
75
|
+
feishu_agent calendar create --summary "Meeting" --start "2026-03-01 14:00" --end "2026-03-01 15:00"
|
|
76
|
+
|
|
77
|
+
# Create event with attendees
|
|
78
|
+
feishu_agent calendar create --summary "Meeting" --start "2026-03-01 14:00" --end "2026-03-01 15:00" --attendee-name "张三"
|
|
79
|
+
|
|
80
|
+
# Delete event
|
|
81
|
+
feishu_agent calendar delete --event-id "xxx"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Todo
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# List todos (requires FEISHU_BASE_TOKEN env)
|
|
88
|
+
feishu_agent todo list
|
|
89
|
+
|
|
90
|
+
# Create todo
|
|
91
|
+
feishu_agent todo create --title "Task" --priority "High"
|
|
92
|
+
|
|
93
|
+
# Mark todo as done
|
|
94
|
+
feishu_agent todo done --record-id "xxx"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Contact
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# List users
|
|
101
|
+
feishu_agent contact list --dept "0"
|
|
102
|
+
|
|
103
|
+
# Search users
|
|
104
|
+
feishu_agent contact search "张三"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Configuration
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Set config
|
|
111
|
+
feishu_agent config set appId cli_xxx
|
|
112
|
+
|
|
113
|
+
# Get config
|
|
114
|
+
feishu_agent config get appId
|
|
115
|
+
|
|
116
|
+
# List all config
|
|
117
|
+
feishu_agent config list
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Commands
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
feishu_agent <command> [options]
|
|
124
|
+
|
|
125
|
+
Commands:
|
|
126
|
+
setup Initialize configuration
|
|
127
|
+
auth Authenticate with Feishu OAuth
|
|
128
|
+
whoami Show current user info
|
|
129
|
+
config Manage configuration
|
|
130
|
+
calendar Manage calendar events
|
|
131
|
+
todo Manage todos
|
|
132
|
+
contact Manage contacts
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Build
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Build binary
|
|
139
|
+
bun run build
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Test
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
bun test
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Architecture
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
src/
|
|
152
|
+
├── core/ # Business logic - Feishu API wrappers
|
|
153
|
+
│ ├── client.ts # HTTP client with auth
|
|
154
|
+
│ ├── auth-manager.ts # Token lifecycle management
|
|
155
|
+
│ ├── config.ts # Global config management
|
|
156
|
+
│ ├── calendar.ts # Calendar API
|
|
157
|
+
│ ├── contact.ts # Contact API
|
|
158
|
+
│ └── todo.ts # Bitable Todo API
|
|
159
|
+
├── index.ts # Main entry point with CLI router
|
|
160
|
+
└── types/ # TypeScript interfaces
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Configuration
|
|
164
|
+
|
|
165
|
+
Global config is stored at `~/.feishu-agent/config.json`:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"appId": "cli_xxx",
|
|
170
|
+
"appSecret": "xxx",
|
|
171
|
+
"userAccessToken": "xxx",
|
|
172
|
+
"refreshToken": "xxx"
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@teamclaw/feishu-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Feishu Agent CLI for AI assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"feishu_agent": "src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "bun test",
|
|
12
|
+
"start:mcp": "bun run src/mcp/server.ts",
|
|
13
|
+
"start": "bun run src/index.ts",
|
|
14
|
+
"build": "bun build ./src/index.ts --compile --outfile=feishu_agent"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/bun": "latest"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"typescript": "^5"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
24
|
+
"commander": "^14.0.3",
|
|
25
|
+
"zod": "^4.3.6"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"bun": ">=1.0.0"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"feishu",
|
|
36
|
+
"lark",
|
|
37
|
+
"mcp",
|
|
38
|
+
"ai",
|
|
39
|
+
"assistant",
|
|
40
|
+
"calendar",
|
|
41
|
+
"todo",
|
|
42
|
+
"cli"
|
|
43
|
+
],
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/teamclaw/feishu-agent.git"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT"
|
|
49
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
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
|
+
}
|