feishu-user-plugin 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/.claude-plugin/plugin.json +9 -0
- package/.env.example +18 -0
- package/.mcp.json.example +13 -0
- package/CHANGELOG.md +62 -0
- package/LICENSE +21 -0
- package/README.md +473 -0
- package/package.json +57 -0
- package/proto/lark.proto +317 -0
- package/skills/feishu-user-plugin/SKILL.md +103 -0
- package/skills/feishu-user-plugin/references/CLAUDE.md +94 -0
- package/skills/feishu-user-plugin/references/digest.md +26 -0
- package/skills/feishu-user-plugin/references/doc.md +27 -0
- package/skills/feishu-user-plugin/references/drive.md +24 -0
- package/skills/feishu-user-plugin/references/reply.md +23 -0
- package/skills/feishu-user-plugin/references/search.md +22 -0
- package/skills/feishu-user-plugin/references/send.md +28 -0
- package/skills/feishu-user-plugin/references/status.md +22 -0
- package/skills/feishu-user-plugin/references/table.md +32 -0
- package/skills/feishu-user-plugin/references/wiki.md +26 -0
- package/src/client.js +364 -0
- package/src/index.js +697 -0
- package/src/oauth-auto.js +196 -0
- package/src/oauth.js +215 -0
- package/src/official.js +365 -0
- package/src/test-all.js +324 -0
- package/src/test-comprehensive.js +301 -0
- package/src/test-send.js +67 -0
- package/src/utils.js +39 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Automated OAuth flow using Playwright — single page, no extra tabs
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const { chromium } = require('playwright');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const dotenv = require('dotenv');
|
|
8
|
+
|
|
9
|
+
dotenv.config({ path: path.join(__dirname, '..', '.env') });
|
|
10
|
+
|
|
11
|
+
const APP_ID = process.env.LARK_APP_ID;
|
|
12
|
+
const APP_SECRET = process.env.LARK_APP_SECRET;
|
|
13
|
+
const COOKIE_STR = process.env.LARK_COOKIE;
|
|
14
|
+
const PORT = 9997;
|
|
15
|
+
const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`;
|
|
16
|
+
const SCOPES = 'im:message im:message:readonly im:chat:readonly contact:user.base:readonly';
|
|
17
|
+
|
|
18
|
+
function parseCookies(cookieStr) {
|
|
19
|
+
return cookieStr.split(';').map(c => {
|
|
20
|
+
const [name, ...rest] = c.trim().split('=');
|
|
21
|
+
return { name: name.trim(), value: rest.join('=').trim(), domain: '.feishu.cn', path: '/' };
|
|
22
|
+
}).filter(c => c.name && c.value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function saveToken(tokenData) {
|
|
26
|
+
const envPath = path.join(__dirname, '..', '.env');
|
|
27
|
+
let envContent = '';
|
|
28
|
+
try { envContent = fs.readFileSync(envPath, 'utf8'); } catch {}
|
|
29
|
+
const updates = {
|
|
30
|
+
LARK_USER_ACCESS_TOKEN: tokenData.access_token,
|
|
31
|
+
LARK_USER_REFRESH_TOKEN: tokenData.refresh_token || '',
|
|
32
|
+
LARK_UAT_SCOPE: tokenData.scope || '',
|
|
33
|
+
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + tokenData.expires_in)),
|
|
34
|
+
};
|
|
35
|
+
for (const [key, val] of Object.entries(updates)) {
|
|
36
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
37
|
+
if (regex.test(envContent)) {
|
|
38
|
+
envContent = envContent.replace(regex, `${key}=${val}`);
|
|
39
|
+
} else {
|
|
40
|
+
envContent += `\n${key}=${val}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
fs.writeFileSync(envPath, envContent.trim() + '\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function exchangeCode(code) {
|
|
47
|
+
console.log('[token] Exchanging code via v2...');
|
|
48
|
+
const res = await fetch('https://open.feishu.cn/open-apis/authen/v2/oauth/token', {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'content-type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({ grant_type: 'authorization_code', client_id: APP_ID, client_secret: APP_SECRET, code, redirect_uri: REDIRECT_URI }),
|
|
52
|
+
});
|
|
53
|
+
const raw = await res.text();
|
|
54
|
+
console.log('[token] Response:', raw.slice(0, 300));
|
|
55
|
+
const data = JSON.parse(raw);
|
|
56
|
+
if (data.access_token) return data;
|
|
57
|
+
if (data.data?.access_token) return data.data;
|
|
58
|
+
throw new Error(`Token exchange failed: ${raw.slice(0, 200)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function run() {
|
|
62
|
+
// Start callback server to capture the code
|
|
63
|
+
let resolveCode;
|
|
64
|
+
const codePromise = new Promise(resolve => { resolveCode = resolve; });
|
|
65
|
+
|
|
66
|
+
const server = http.createServer((req, res) => {
|
|
67
|
+
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
|
68
|
+
if (url.pathname === '/callback') {
|
|
69
|
+
const code = url.searchParams.get('code');
|
|
70
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
71
|
+
res.end(code ? '<h2>OK</h2>' : '<h2>No code</h2>');
|
|
72
|
+
if (code) resolveCode(code);
|
|
73
|
+
} else {
|
|
74
|
+
res.writeHead(404); res.end();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
server.listen(PORT, '127.0.0.1');
|
|
78
|
+
console.log(`[server] Listening on port ${PORT}`);
|
|
79
|
+
|
|
80
|
+
// Launch browser — use the first page directly (no extra about:blank)
|
|
81
|
+
const browser = await chromium.launch({ headless: false });
|
|
82
|
+
const context = await browser.newContext({ viewport: { width: 1200, height: 800 } });
|
|
83
|
+
await context.addCookies(parseCookies(COOKIE_STR));
|
|
84
|
+
|
|
85
|
+
// Listen for any new page (popup) that might open
|
|
86
|
+
context.on('page', async newPage => {
|
|
87
|
+
const url = newPage.url();
|
|
88
|
+
console.log('[context] New page opened:', url.slice(0, 200));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Get the default page instead of creating a new one
|
|
92
|
+
const pages = context.pages();
|
|
93
|
+
const page = pages.length > 0 ? pages[0] : await context.newPage();
|
|
94
|
+
page.setDefaultTimeout(30000);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${APP_ID}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&response_type=code&scope=${encodeURIComponent(SCOPES)}`;
|
|
98
|
+
console.log('\n[auth] Opening authorize URL...');
|
|
99
|
+
|
|
100
|
+
// Use route interception to capture the callback redirect directly
|
|
101
|
+
let codeFromRoute = null;
|
|
102
|
+
await context.route('**/callback**', async route => {
|
|
103
|
+
const url = route.request().url();
|
|
104
|
+
console.log('[route] Intercepted callback:', url.slice(0, 200));
|
|
105
|
+
const parsed = new URL(url);
|
|
106
|
+
const code = parsed.searchParams.get('code');
|
|
107
|
+
if (code) {
|
|
108
|
+
codeFromRoute = code;
|
|
109
|
+
resolveCode(code);
|
|
110
|
+
}
|
|
111
|
+
await route.continue();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await page.goto(authUrl, { waitUntil: 'load' });
|
|
115
|
+
await page.waitForTimeout(2000);
|
|
116
|
+
|
|
117
|
+
const currentUrl = page.url();
|
|
118
|
+
console.log('[auth] Current URL:', currentUrl.slice(0, 200));
|
|
119
|
+
|
|
120
|
+
if (currentUrl.includes('callback?code=')) {
|
|
121
|
+
console.log('[auth] Auto-authorized!');
|
|
122
|
+
} else {
|
|
123
|
+
// Find and click authorize button
|
|
124
|
+
const authorizeBtn = page.locator('button:has-text("授权")').first();
|
|
125
|
+
if (await authorizeBtn.isVisible().catch(() => false)) {
|
|
126
|
+
console.log('[auth] Found 授权 button, clicking...');
|
|
127
|
+
await authorizeBtn.click();
|
|
128
|
+
console.log('[auth] Clicked, waiting for redirect...');
|
|
129
|
+
await page.waitForTimeout(5000);
|
|
130
|
+
console.log('[auth] After click URL:', page.url().slice(0, 200));
|
|
131
|
+
|
|
132
|
+
// Check all pages in context
|
|
133
|
+
const allPages = context.pages();
|
|
134
|
+
console.log(`[auth] Total pages: ${allPages.length}`);
|
|
135
|
+
for (let i = 0; i < allPages.length; i++) {
|
|
136
|
+
const pUrl = allPages[i].url();
|
|
137
|
+
console.log(` [${i}] ${pUrl.slice(0, 200)}`);
|
|
138
|
+
if (pUrl.includes('callback?code=')) {
|
|
139
|
+
const parsed = new URL(pUrl);
|
|
140
|
+
resolveCode(parsed.searchParams.get('code'));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
console.log('[auth] No authorize button found!');
|
|
145
|
+
await page.screenshot({ path: '/tmp/feishu-oauth-nobutton.png' });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Wait for the code
|
|
150
|
+
console.log('\n[token] Waiting for code...');
|
|
151
|
+
const code = await Promise.race([
|
|
152
|
+
codePromise,
|
|
153
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('Timeout (30s)')), 30000)),
|
|
154
|
+
]);
|
|
155
|
+
console.log('[token] Got code:', code.slice(0, 20) + '...');
|
|
156
|
+
|
|
157
|
+
const tokenData = await exchangeCode(code);
|
|
158
|
+
saveToken(tokenData);
|
|
159
|
+
console.log('\n=== SUCCESS ===');
|
|
160
|
+
console.log('access_token:', tokenData.access_token?.slice(0, 30) + '...');
|
|
161
|
+
console.log('scope:', tokenData.scope);
|
|
162
|
+
console.log('expires_in:', tokenData.expires_in, 's');
|
|
163
|
+
|
|
164
|
+
// Test P2P message reading
|
|
165
|
+
console.log('\n[test] Testing P2P message reading...');
|
|
166
|
+
const testRes = await fetch('https://open.feishu.cn/open-apis/im/v1/messages?container_id_type=chat&container_id=oc_97a52756ee2c4351a2a86e6aa33e8ca4&page_size=2&sort_type=ByCreateTimeDesc', {
|
|
167
|
+
headers: { 'Authorization': `Bearer ${tokenData.access_token}` },
|
|
168
|
+
});
|
|
169
|
+
const testData = await testRes.json();
|
|
170
|
+
if (testData.code === 0) {
|
|
171
|
+
console.log('[test] P2P: SUCCESS!', testData.data?.items?.length, 'messages');
|
|
172
|
+
} else {
|
|
173
|
+
console.log('[test] P2P: Error', testData.code, testData.msg);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Test group messages
|
|
177
|
+
const grpRes = await fetch('https://open.feishu.cn/open-apis/im/v1/messages?container_id_type=chat&container_id=oc_6ae081b457d07e9651d615493b7f1096&page_size=2&sort_type=ByCreateTimeDesc', {
|
|
178
|
+
headers: { 'Authorization': `Bearer ${tokenData.access_token}` },
|
|
179
|
+
});
|
|
180
|
+
const grpData = await grpRes.json();
|
|
181
|
+
if (grpData.code === 0) {
|
|
182
|
+
console.log('[test] Group: SUCCESS!', grpData.data?.items?.length, 'messages');
|
|
183
|
+
} else {
|
|
184
|
+
console.log('[test] Group: Error', grpData.code, grpData.msg);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.error('\nError:', e.message);
|
|
189
|
+
await page.screenshot({ path: '/tmp/feishu-oauth-error.png' }).catch(() => {});
|
|
190
|
+
} finally {
|
|
191
|
+
await browser.close();
|
|
192
|
+
server.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
run();
|
package/src/oauth.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OAuth 授权脚本 — 获取带 IM 权限的 user_access_token
|
|
4
|
+
*
|
|
5
|
+
* 用法: node src/oauth.js
|
|
6
|
+
*
|
|
7
|
+
* 流程 (新版 End User Consent):
|
|
8
|
+
* 1. 查询应用信息,提示用户选择正确的飞书账号
|
|
9
|
+
* 2. 启动本地 HTTP 服务器 (端口 9997)
|
|
10
|
+
* 3. 打开 accounts.feishu.cn 授权页面 (新版 OAuth 2.0)
|
|
11
|
+
* 4. 用户点击"授权"后,用 /authen/v2/oauth/token 交换 token
|
|
12
|
+
* 5. 保存 access_token + refresh_token 到 .env
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const http = require('http');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const dotenv = require('dotenv');
|
|
20
|
+
|
|
21
|
+
dotenv.config({ path: path.join(__dirname, '..', '.env') });
|
|
22
|
+
|
|
23
|
+
const APP_ID = process.env.LARK_APP_ID;
|
|
24
|
+
const APP_SECRET = process.env.LARK_APP_SECRET;
|
|
25
|
+
const PORT = 9997;
|
|
26
|
+
const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`;
|
|
27
|
+
// offline_access is required to get refresh_token for auto-renewal
|
|
28
|
+
const SCOPES = 'offline_access im:message im:message:readonly im:chat:readonly contact:user.base:readonly';
|
|
29
|
+
|
|
30
|
+
if (!APP_ID || !APP_SECRET) {
|
|
31
|
+
console.error('Missing LARK_APP_ID or LARK_APP_SECRET in .env');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Fetch app info to help user pick the right account ---
|
|
36
|
+
|
|
37
|
+
async function getAppInfo() {
|
|
38
|
+
try {
|
|
39
|
+
// Get app_access_token to query app details
|
|
40
|
+
const tokenRes = await fetch('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'content-type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET }),
|
|
44
|
+
});
|
|
45
|
+
const tokenData = await tokenRes.json();
|
|
46
|
+
if (!tokenData.app_access_token) return null;
|
|
47
|
+
|
|
48
|
+
// Get app info — try the direct app query first, fall back to underauditlist
|
|
49
|
+
let appName = null;
|
|
50
|
+
const directRes = await fetch(`https://open.feishu.cn/open-apis/application/v6/applications/${APP_ID}?lang=zh_cn`, {
|
|
51
|
+
headers: { 'Authorization': `Bearer ${tokenData.app_access_token}` },
|
|
52
|
+
});
|
|
53
|
+
const directData = await directRes.json();
|
|
54
|
+
appName = directData?.data?.app?.app_name;
|
|
55
|
+
|
|
56
|
+
if (!appName) {
|
|
57
|
+
const listRes = await fetch('https://open.feishu.cn/open-apis/application/v6/applications/underauditlist?lang=zh_cn&page_size=1', {
|
|
58
|
+
headers: { 'Authorization': `Bearer ${tokenData.app_access_token}` },
|
|
59
|
+
});
|
|
60
|
+
const listData = await listRes.json();
|
|
61
|
+
appName = listData?.data?.items?.[0]?.app_name;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { appName, tenantKey: tokenData.tenant_key };
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function exchangeCode(code) {
|
|
71
|
+
const body = {
|
|
72
|
+
grant_type: 'authorization_code',
|
|
73
|
+
client_id: APP_ID,
|
|
74
|
+
client_secret: APP_SECRET,
|
|
75
|
+
code,
|
|
76
|
+
redirect_uri: REDIRECT_URI,
|
|
77
|
+
};
|
|
78
|
+
console.log('Token exchange request:', JSON.stringify({ ...body, client_secret: '***' }));
|
|
79
|
+
const tokenRes = await fetch('https://open.feishu.cn/open-apis/authen/v2/oauth/token', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'content-type': 'application/json' },
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
});
|
|
84
|
+
const raw = await tokenRes.text();
|
|
85
|
+
console.log('Token exchange raw response:', raw.slice(0, 500));
|
|
86
|
+
let tokenData;
|
|
87
|
+
try { tokenData = JSON.parse(raw); } catch (e) {
|
|
88
|
+
throw new Error(`Response not JSON: ${raw.slice(0, 200)}`);
|
|
89
|
+
}
|
|
90
|
+
if (tokenData.error) {
|
|
91
|
+
throw new Error(`${tokenData.error}: ${tokenData.error_description}`);
|
|
92
|
+
}
|
|
93
|
+
if (tokenData.code && tokenData.code !== 0) {
|
|
94
|
+
throw new Error(`Error ${tokenData.code}: ${tokenData.msg || JSON.stringify(tokenData)}`);
|
|
95
|
+
}
|
|
96
|
+
// v2 success: access_token at top level
|
|
97
|
+
if (tokenData.access_token) return tokenData;
|
|
98
|
+
if (tokenData.data?.access_token) return tokenData.data;
|
|
99
|
+
throw new Error(`No access_token in response: ${JSON.stringify(tokenData)}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function saveToken(tokenData) {
|
|
103
|
+
const envPath = path.join(__dirname, '..', '.env');
|
|
104
|
+
let envContent = '';
|
|
105
|
+
try { envContent = fs.readFileSync(envPath, 'utf8'); } catch {}
|
|
106
|
+
|
|
107
|
+
const updates = {
|
|
108
|
+
LARK_USER_ACCESS_TOKEN: tokenData.access_token,
|
|
109
|
+
LARK_USER_REFRESH_TOKEN: tokenData.refresh_token || '',
|
|
110
|
+
LARK_UAT_SCOPE: tokenData.scope || '',
|
|
111
|
+
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + tokenData.expires_in)),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
for (const [key, val] of Object.entries(updates)) {
|
|
115
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
116
|
+
if (regex.test(envContent)) {
|
|
117
|
+
envContent = envContent.replace(regex, `${key}=${val}`);
|
|
118
|
+
} else {
|
|
119
|
+
envContent += `\n${key}=${val}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fs.writeFileSync(envPath, envContent.trim() + '\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const server = http.createServer(async (req, res) => {
|
|
127
|
+
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
|
128
|
+
|
|
129
|
+
if (url.pathname === '/callback') {
|
|
130
|
+
const code = url.searchParams.get('code');
|
|
131
|
+
if (!code) {
|
|
132
|
+
res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' });
|
|
133
|
+
res.end('<h2>授权失败:未收到 code</h2>');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const tokenData = await exchangeCode(code);
|
|
139
|
+
saveToken(tokenData);
|
|
140
|
+
|
|
141
|
+
const hasRefresh = !!tokenData.refresh_token;
|
|
142
|
+
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
|
|
143
|
+
res.end(`<h2>✅ 授权成功!</h2>
|
|
144
|
+
<p>access_token: ${tokenData.access_token.slice(0, 20)}...</p>
|
|
145
|
+
<p>scope: ${tokenData.scope}</p>
|
|
146
|
+
<p>expires_in: ${tokenData.expires_in}s</p>
|
|
147
|
+
<p>refresh_token: ${hasRefresh ? '✅ 已获取(30天有效,支持自动续期)' : '❌ 未返回(token 将在 2 小时后过期,需重新授权)'}</p>
|
|
148
|
+
<p>已保存到 .env,可以关闭此页面。</p>`);
|
|
149
|
+
|
|
150
|
+
console.log('\n=== OAuth 授权成功 ===');
|
|
151
|
+
console.log('scope:', tokenData.scope);
|
|
152
|
+
console.log('expires_in:', tokenData.expires_in, 's');
|
|
153
|
+
console.log('refresh_token:', hasRefresh ? '✅ 已获取' : '❌ 未返回');
|
|
154
|
+
if (!hasRefresh) {
|
|
155
|
+
console.log('\n⚠️ 未获取到 refresh_token。可能原因:');
|
|
156
|
+
console.log(' - 飞书应用未启用 offline_access 权限');
|
|
157
|
+
console.log(' - 授权时 scope 中未包含 offline_access');
|
|
158
|
+
console.log(' Token 将在 2 小时后过期,届时需要重新运行此脚本。');
|
|
159
|
+
}
|
|
160
|
+
console.log('token 已保存到 .env');
|
|
161
|
+
|
|
162
|
+
setTimeout(() => { server.close(); process.exit(0); }, 1000);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
res.writeHead(500, { 'content-type': 'text/html; charset=utf-8' });
|
|
165
|
+
res.end(`<h2>Token 交换失败</h2><p>${e.message}</p>`);
|
|
166
|
+
console.error('Token exchange error:', e.message);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
res.writeHead(404);
|
|
172
|
+
res.end('Not found');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
server.listen(PORT, '127.0.0.1', async () => {
|
|
176
|
+
const authUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${APP_ID}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&response_type=code&scope=${encodeURIComponent(SCOPES)}`;
|
|
177
|
+
|
|
178
|
+
console.log('='.repeat(60));
|
|
179
|
+
console.log(' 飞书 OAuth 授权');
|
|
180
|
+
console.log('='.repeat(60));
|
|
181
|
+
console.log(` 应用 ID: ${APP_ID}`);
|
|
182
|
+
|
|
183
|
+
// Try to get app info for better guidance
|
|
184
|
+
const appInfo = await getAppInfo();
|
|
185
|
+
if (appInfo?.appName) {
|
|
186
|
+
console.log(` 应用名称: ${appInfo.appName}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(' ⚠️ 重要:请在浏览器中选择正确的飞书账号!');
|
|
191
|
+
console.log(' 如果你有多个飞书账号(个人/公司),请确保选择');
|
|
192
|
+
console.log(` 与应用 ${APP_ID} 同一租户的账号。`);
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(' 如果浏览器显示了错误的账号,请:');
|
|
195
|
+
console.log(' 1. 先在 feishu.cn 切换到正确的租户/账号');
|
|
196
|
+
console.log(' 2. 然后重新运行此脚本');
|
|
197
|
+
console.log('='.repeat(60));
|
|
198
|
+
console.log('');
|
|
199
|
+
console.log('OAuth 服务器已启动,端口:', PORT);
|
|
200
|
+
console.log('正在打开浏览器...');
|
|
201
|
+
console.log('授权 URL:', authUrl);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
execSync(`open "${authUrl}"`);
|
|
205
|
+
} catch {
|
|
206
|
+
console.log('\n请手动在浏览器中打开上面的 URL');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log('\n等待授权回调... (120 秒超时)');
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
console.error('\n超时,未收到授权回调。');
|
|
212
|
+
server.close();
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}, 120000);
|
|
215
|
+
});
|