feishu-user-plugin 1.1.2 → 1.2.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 +1 -1
- package/CHANGELOG.md +9 -0
- package/package.json +1 -1
- package/skills/feishu-user-plugin/SKILL.md +1 -1
- package/src/cli.js +85 -12
- package/src/client.js +6 -2
- package/src/config.js +188 -0
- package/src/index.js +99 -32
- package/src/oauth-auto.js +4 -25
- package/src/oauth.js +28 -51
- package/src/official.js +43 -66
- package/src/setup.js +96 -102
- package/src/test-comprehensive.js +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 33 tools + 9 skills, 3 auth layers.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "EthanQC"
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.1.3] - 2026-03-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Case-insensitive chat name matching**: All name resolution strategies (bot group list, im.chat.search, search_contacts) now use case-insensitive matching. "ai技术解决" now correctly matches "AI技术解决(内部)".
|
|
11
|
+
- **expires_in NaN bug**: UAT token refresh and OAuth now validate `expires_in` field, defaulting to 7200s if missing/invalid, preventing NaN corruption in config.
|
|
12
|
+
- **_populateSenderNames inefficiency**: Fixed redundant condition in cookie-based name fallback.
|
|
13
|
+
- **OAuth silent persistence failure**: Now logs warnings when token persistence to `~/.claude.json` fails, instead of silently swallowing errors.
|
|
14
|
+
- **Null safety**: Added null check in `resolveToOcId` for undefined chat_id.
|
|
15
|
+
|
|
7
16
|
## [1.1.2] - 2026-03-11
|
|
8
17
|
|
|
9
18
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 33 tools + 9 skills, 3 auth layers.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feishu-user-plugin
|
|
3
|
-
version: "1.1.
|
|
3
|
+
version: "1.1.3"
|
|
4
4
|
description: "All-in-one Feishu plugin — send messages as yourself, read group/P2P chats, manage docs/tables/wiki. Replaces and extends the official Feishu MCP."
|
|
5
5
|
allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_as_user, send_sticker_as_user, send_audio_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, read_p2p_messages, list_user_chats, list_chats, read_messages, reply_message, forward_message, search_docs, read_doc, create_doc, list_bitable_tables, list_bitable_fields, search_bitable_records, create_bitable_record, update_bitable_record, list_wiki_spaces, search_wiki, list_wiki_nodes, list_files, create_folder, find_user
|
|
6
6
|
user_invocable: true
|
package/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* npx feishu-user-plugin setup → Interactive setup wizard
|
|
8
8
|
* npx feishu-user-plugin oauth → Run OAuth flow for UAT
|
|
9
9
|
* npx feishu-user-plugin status → Check auth status
|
|
10
|
+
* npx feishu-user-plugin keepalive → Refresh cookie + UAT (for cron)
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
const cmd = process.argv[2];
|
|
@@ -21,6 +22,9 @@ switch (cmd) {
|
|
|
21
22
|
case 'status':
|
|
22
23
|
checkStatus();
|
|
23
24
|
break;
|
|
25
|
+
case 'keepalive':
|
|
26
|
+
keepalive();
|
|
27
|
+
break;
|
|
24
28
|
case 'help':
|
|
25
29
|
case '--help':
|
|
26
30
|
case '-h':
|
|
@@ -41,6 +45,7 @@ Commands:
|
|
|
41
45
|
setup Interactive setup wizard — writes MCP config
|
|
42
46
|
oauth Run OAuth flow to obtain user_access_token
|
|
43
47
|
status Check authentication status
|
|
48
|
+
keepalive Refresh cookie + UAT to prevent expiration (for cron jobs)
|
|
44
49
|
help Show this help
|
|
45
50
|
|
|
46
51
|
Quick Start (team members):
|
|
@@ -53,20 +58,85 @@ Quick Start (external users):
|
|
|
53
58
|
2. npx feishu-user-plugin setup
|
|
54
59
|
3. npx feishu-user-plugin oauth
|
|
55
60
|
4. Restart Claude Code
|
|
61
|
+
|
|
62
|
+
Auto-renewal (optional):
|
|
63
|
+
Add to crontab to keep tokens alive even when Claude Code is closed:
|
|
64
|
+
crontab -e → add: 0 */4 * * * npx feishu-user-plugin keepalive >> /tmp/feishu-keepalive.log 2>&1
|
|
56
65
|
`);
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
async function keepalive() {
|
|
69
|
+
const { LarkUserClient } = require('./client');
|
|
70
|
+
const { LarkOfficialClient } = require('./official');
|
|
71
|
+
const { findMcpConfig, persistToConfig } = require('./config');
|
|
72
|
+
|
|
73
|
+
const found = findMcpConfig();
|
|
74
|
+
if (!found) {
|
|
75
|
+
console.error('[keepalive] No config found. Run: npx feishu-user-plugin setup');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
const creds = found.serverEnv;
|
|
79
|
+
let ok = true;
|
|
80
|
+
|
|
81
|
+
// 1. Refresh Cookie
|
|
82
|
+
const cookie = creds.LARK_COOKIE;
|
|
83
|
+
if (cookie && cookie !== 'SETUP_NEEDED') {
|
|
84
|
+
try {
|
|
85
|
+
const client = new LarkUserClient(cookie);
|
|
86
|
+
await client.init();
|
|
87
|
+
// init() calls _getCsrfToken which refreshes sl_session
|
|
88
|
+
persistToConfig({ LARK_COOKIE: client.cookieStr });
|
|
89
|
+
console.log(`[keepalive] Cookie refreshed (user: ${client.userName})`);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.error(`[keepalive] Cookie refresh FAILED: ${e.message}`);
|
|
92
|
+
ok = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. Refresh UAT
|
|
97
|
+
const appId = creds.LARK_APP_ID;
|
|
98
|
+
const appSecret = creds.LARK_APP_SECRET;
|
|
99
|
+
const uat = creds.LARK_USER_ACCESS_TOKEN;
|
|
100
|
+
const rt = creds.LARK_USER_REFRESH_TOKEN;
|
|
101
|
+
if (appId && appSecret && uat && uat !== 'SETUP_NEEDED' && rt) {
|
|
102
|
+
try {
|
|
103
|
+
const official = new LarkOfficialClient(appId, appSecret);
|
|
104
|
+
official._uat = uat;
|
|
105
|
+
official._uatRefresh = rt;
|
|
106
|
+
official._uatExpires = 0; // force refresh
|
|
107
|
+
await official._refreshUAT(); // refreshes + persists automatically
|
|
108
|
+
console.log('[keepalive] UAT refreshed');
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error(`[keepalive] UAT refresh FAILED: ${e.message}`);
|
|
111
|
+
ok = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (ok) {
|
|
116
|
+
console.log('[keepalive] All tokens refreshed successfully');
|
|
117
|
+
}
|
|
118
|
+
process.exit(ok ? 0 : 1);
|
|
119
|
+
}
|
|
120
|
+
|
|
59
121
|
async function checkStatus() {
|
|
60
122
|
const { LarkUserClient } = require('./client');
|
|
61
123
|
const { LarkOfficialClient } = require('./official');
|
|
62
|
-
const
|
|
63
|
-
|
|
124
|
+
const { findMcpConfig } = require('./config');
|
|
125
|
+
|
|
126
|
+
const found = findMcpConfig();
|
|
127
|
+
const creds = found ? found.serverEnv : {};
|
|
64
128
|
|
|
65
129
|
console.log('=== feishu-user-plugin Auth Status ===\n');
|
|
130
|
+
if (found) {
|
|
131
|
+
console.log(`Config: ${found.configPath}${found.projectPath ? ` (project: ${found.projectPath})` : ''}`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log('Config: NOT FOUND (run: npx feishu-user-plugin setup)');
|
|
134
|
+
}
|
|
135
|
+
console.log('');
|
|
66
136
|
|
|
67
137
|
// Cookie
|
|
68
|
-
const cookie =
|
|
69
|
-
if (cookie) {
|
|
138
|
+
const cookie = creds.LARK_COOKIE;
|
|
139
|
+
if (cookie && cookie !== 'SETUP_NEEDED') {
|
|
70
140
|
try {
|
|
71
141
|
const client = new LarkUserClient(cookie);
|
|
72
142
|
await client.init();
|
|
@@ -79,21 +149,24 @@ async function checkStatus() {
|
|
|
79
149
|
}
|
|
80
150
|
|
|
81
151
|
// App credentials
|
|
82
|
-
const appId =
|
|
83
|
-
const appSecret =
|
|
152
|
+
const appId = creds.LARK_APP_ID;
|
|
153
|
+
const appSecret = creds.LARK_APP_SECRET;
|
|
84
154
|
console.log(`App credentials: ${appId && appSecret ? 'OK' : 'NOT SET'}`);
|
|
85
155
|
|
|
86
156
|
// UAT
|
|
87
|
-
const uat =
|
|
88
|
-
const rt =
|
|
89
|
-
if (uat) {
|
|
157
|
+
const uat = creds.LARK_USER_ACCESS_TOKEN;
|
|
158
|
+
const rt = creds.LARK_USER_REFRESH_TOKEN;
|
|
159
|
+
if (uat && uat !== 'SETUP_NEEDED') {
|
|
90
160
|
console.log(`UAT: SET (refresh_token: ${rt ? 'YES' : 'NO'})`);
|
|
91
161
|
if (appId && appSecret) {
|
|
92
162
|
const official = new LarkOfficialClient(appId, appSecret);
|
|
93
|
-
|
|
163
|
+
// Set UAT fields directly (bypassing loadUAT which reads from process.env)
|
|
164
|
+
official._uat = uat;
|
|
165
|
+
official._uatRefresh = rt || null;
|
|
166
|
+
official._uatExpires = parseInt(creds.LARK_UAT_EXPIRES || '0');
|
|
94
167
|
try {
|
|
95
|
-
|
|
96
|
-
console.log(
|
|
168
|
+
await official.listChatsAsUser({ pageSize: 1 });
|
|
169
|
+
console.log(' UAT test: OK (can list chats)');
|
|
97
170
|
} catch (e) {
|
|
98
171
|
console.log(` UAT test: FAILED — ${e.message}`);
|
|
99
172
|
}
|
package/src/client.js
CHANGED
|
@@ -90,7 +90,10 @@ class LarkUserClient {
|
|
|
90
90
|
this._heartbeatTimer = setInterval(async () => {
|
|
91
91
|
try {
|
|
92
92
|
await this._getCsrfToken();
|
|
93
|
-
|
|
93
|
+
// Lazy require to avoid circular dependency at module load time
|
|
94
|
+
const { persistToConfig } = require('./config');
|
|
95
|
+
persistToConfig({ LARK_COOKIE: this.cookieStr });
|
|
96
|
+
console.error('[feishu-user-plugin] Cookie heartbeat: session refreshed and persisted');
|
|
94
97
|
} catch (e) {
|
|
95
98
|
console.error('[feishu-user-plugin] Cookie heartbeat failed:', e.message);
|
|
96
99
|
}
|
|
@@ -252,7 +255,8 @@ class LarkUserClient {
|
|
|
252
255
|
const propBuf = this._encode('TextProperty', { content: elem.userId });
|
|
253
256
|
dictionary[elemId] = { tag: 5, property: propBuf };
|
|
254
257
|
} else if (elem.tag === 'a') {
|
|
255
|
-
|
|
258
|
+
// Link element: content stores the URL, display text goes through innerText
|
|
259
|
+
const propBuf = this._encode('TextProperty', { content: elem.href || elem.text || '' });
|
|
256
260
|
dictionary[elemId] = { tag: 6, property: propBuf };
|
|
257
261
|
}
|
|
258
262
|
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SERVER_NAMES = ['feishu-user-plugin', 'feishu'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Search an mcpServers object for a feishu-user-plugin entry.
|
|
8
|
+
* Returns { serverName, serverEnv } or null.
|
|
9
|
+
*/
|
|
10
|
+
function _findInServers(servers) {
|
|
11
|
+
if (!servers || typeof servers !== 'object') return null;
|
|
12
|
+
for (const name of SERVER_NAMES) {
|
|
13
|
+
if (servers[name]) {
|
|
14
|
+
if (!servers[name].env) servers[name].env = {};
|
|
15
|
+
return { serverName: name, serverEnv: servers[name].env };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Discover the MCP config file containing feishu-user-plugin server entry.
|
|
23
|
+
*
|
|
24
|
+
* Search order:
|
|
25
|
+
* 1. ~/.claude.json — top-level mcpServers
|
|
26
|
+
* 2. ~/.claude.json — projects[*].mcpServers (Claude Code project-level config)
|
|
27
|
+
* 3. ~/.claude/.claude.json — same two-level search
|
|
28
|
+
* 4. <cwd>/.mcp.json — top-level mcpServers (reliable in CLI mode)
|
|
29
|
+
*
|
|
30
|
+
* Returns { configPath, config, serverName, serverEnv, projectPath? } or null.
|
|
31
|
+
*/
|
|
32
|
+
function findMcpConfig() {
|
|
33
|
+
const home = process.env.HOME;
|
|
34
|
+
const candidates = [
|
|
35
|
+
...(home ? [
|
|
36
|
+
path.join(home, '.claude.json'),
|
|
37
|
+
path.join(home, '.claude', '.claude.json'),
|
|
38
|
+
] : []),
|
|
39
|
+
path.join(process.cwd(), '.mcp.json'),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const configPath of candidates) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
45
|
+
const config = JSON.parse(raw);
|
|
46
|
+
|
|
47
|
+
// Strategy 1: top-level mcpServers
|
|
48
|
+
const topLevel = _findInServers(config.mcpServers);
|
|
49
|
+
if (topLevel) {
|
|
50
|
+
return { configPath, config, ...topLevel, projectPath: null };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Strategy 2: projects[*].mcpServers (Claude Code nests project-level config here)
|
|
54
|
+
if (config.projects) {
|
|
55
|
+
for (const [projPath, projConfig] of Object.entries(config.projects)) {
|
|
56
|
+
const nested = _findInServers(projConfig.mcpServers);
|
|
57
|
+
if (nested) {
|
|
58
|
+
return { configPath, config, ...nested, projectPath: projPath };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Strategy 3: .mcp.json uses top-level keys as server names (no mcpServers wrapper)
|
|
64
|
+
const bare = _findInServers(config);
|
|
65
|
+
if (bare) {
|
|
66
|
+
return { configPath, config, ...bare, projectPath: null };
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Only warn if the file exists but is invalid (not for missing files)
|
|
70
|
+
if (e.code !== 'ENOENT') {
|
|
71
|
+
console.error(`[feishu-user-plugin] Warning: Failed to parse ${configPath}: ${e.message}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read all LARK_* credentials from the discovered MCP config.
|
|
80
|
+
* Returns an object with all env vars, or {} if no config found.
|
|
81
|
+
*/
|
|
82
|
+
function readCredentials() {
|
|
83
|
+
const found = findMcpConfig();
|
|
84
|
+
if (!found) return {};
|
|
85
|
+
return { ...found.serverEnv };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Persist key-value updates into the MCP config's env block.
|
|
90
|
+
* Uses findMcpConfig() to locate the correct entry, then writes back.
|
|
91
|
+
* Returns true if persisted successfully, false otherwise.
|
|
92
|
+
*/
|
|
93
|
+
function persistToConfig(updates) {
|
|
94
|
+
try {
|
|
95
|
+
const found = findMcpConfig();
|
|
96
|
+
if (!found) {
|
|
97
|
+
console.error('[feishu-user-plugin] WARNING: No MCP config found. Update your config manually.');
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { configPath, config, serverName, projectPath } = found;
|
|
102
|
+
|
|
103
|
+
// Navigate to the correct env object
|
|
104
|
+
let env;
|
|
105
|
+
if (projectPath) {
|
|
106
|
+
env = config.projects[projectPath].mcpServers[serverName].env;
|
|
107
|
+
} else if (config.mcpServers?.[serverName]) {
|
|
108
|
+
env = config.mcpServers[serverName].env;
|
|
109
|
+
} else {
|
|
110
|
+
env = config[serverName].env;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Object.assign(env, updates);
|
|
114
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
115
|
+
console.error(`[feishu-user-plugin] Config persisted to ${configPath}${projectPath ? ` (project: ${projectPath})` : ''}`);
|
|
116
|
+
return true;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error(`[feishu-user-plugin] Failed to persist config: ${e.message}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Write a complete feishu-user-plugin MCP server entry to a config file.
|
|
125
|
+
* Used by the setup wizard.
|
|
126
|
+
*
|
|
127
|
+
* If an existing config is found via findMcpConfig(), updates it in-place
|
|
128
|
+
* (preserving its location — top-level or project-level).
|
|
129
|
+
* Otherwise, writes to ~/.claude.json top-level mcpServers.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} env - The env vars to write
|
|
132
|
+
* @param {string} [configPath] - Override the target config file path
|
|
133
|
+
* @param {string} [projectPath] - If writing to a project-level entry
|
|
134
|
+
* @returns {{ configPath: string }} The path that was written
|
|
135
|
+
*/
|
|
136
|
+
function writeNewConfig(env, configPath, projectPath) {
|
|
137
|
+
if (!configPath) {
|
|
138
|
+
configPath = path.join(process.env.HOME || '', '.claude.json');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (projectPath) {
|
|
142
|
+
// Verify the project entry still exists; warn if it was removed between discovery and write
|
|
143
|
+
let existing = {};
|
|
144
|
+
try { existing = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
|
|
145
|
+
if (!existing.projects?.[projectPath]) {
|
|
146
|
+
console.error(`[feishu-user-plugin] Warning: project entry "${projectPath}" not found in ${configPath}, writing to top-level mcpServers`);
|
|
147
|
+
projectPath = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let config = {};
|
|
152
|
+
try {
|
|
153
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
154
|
+
} catch {}
|
|
155
|
+
|
|
156
|
+
const serverEntry = {
|
|
157
|
+
command: 'npx',
|
|
158
|
+
args: ['-y', 'feishu-user-plugin'],
|
|
159
|
+
env,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (projectPath && config.projects?.[projectPath]) {
|
|
163
|
+
// Write into existing project-level config
|
|
164
|
+
if (!config.projects[projectPath].mcpServers) config.projects[projectPath].mcpServers = {};
|
|
165
|
+
config.projects[projectPath].mcpServers['feishu-user-plugin'] = serverEntry;
|
|
166
|
+
if (config.projects[projectPath].mcpServers.feishu) {
|
|
167
|
+
delete config.projects[projectPath].mcpServers.feishu;
|
|
168
|
+
}
|
|
169
|
+
} else if (configPath.endsWith('.mcp.json') && !config.mcpServers) {
|
|
170
|
+
// Bare .mcp.json format: server entries at top level (no mcpServers wrapper)
|
|
171
|
+
config['feishu-user-plugin'] = serverEntry;
|
|
172
|
+
if (config.feishu) {
|
|
173
|
+
delete config.feishu;
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
// Write to top-level mcpServers (default for ~/.claude.json)
|
|
177
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
178
|
+
config.mcpServers['feishu-user-plugin'] = serverEntry;
|
|
179
|
+
if (config.mcpServers.feishu) {
|
|
180
|
+
delete config.mcpServers.feishu;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
185
|
+
return { configPath };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { findMcpConfig, readCredentials, persistToConfig, writeNewConfig, SERVER_NAMES };
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,8 @@ const {
|
|
|
6
6
|
ListToolsRequestSchema,
|
|
7
7
|
} = require('@modelcontextprotocol/sdk/types.js');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
// Local dev fallback: MCP clients inject env vars from config's env block at spawn time.
|
|
10
|
+
// This dotenv line only matters when running locally with a .env file (e.g. during development).
|
|
9
11
|
require('dotenv').config({ path: path.join(__dirname, '..', '.env') });
|
|
10
12
|
const { LarkUserClient } = require('./client');
|
|
11
13
|
const { LarkOfficialClient } = require('./official');
|
|
@@ -33,20 +35,28 @@ class ChatIdMapper {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
// Case-insensitive name matching helper
|
|
39
|
+
static _nameMatch(haystack, needle, exact = false) {
|
|
40
|
+
if (!haystack || !needle) return false;
|
|
41
|
+
const h = haystack.toLowerCase(), n = needle.toLowerCase();
|
|
42
|
+
return exact ? h === n : h.includes(n);
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
async findByName(name, official) {
|
|
37
46
|
await this._refresh(official);
|
|
38
|
-
// Exact match first
|
|
47
|
+
// Exact match first (case-insensitive)
|
|
39
48
|
for (const [ocId, chatName] of this.nameCache) {
|
|
40
|
-
if (chatName
|
|
49
|
+
if (ChatIdMapper._nameMatch(chatName, name, true)) return ocId;
|
|
41
50
|
}
|
|
42
|
-
// Partial match
|
|
51
|
+
// Partial match (case-insensitive)
|
|
43
52
|
for (const [ocId, chatName] of this.nameCache) {
|
|
44
|
-
if (chatName
|
|
53
|
+
if (ChatIdMapper._nameMatch(chatName, name)) return ocId;
|
|
45
54
|
}
|
|
46
55
|
return null;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
async resolveToOcId(chatIdOrName, official) {
|
|
59
|
+
if (!chatIdOrName) return null;
|
|
50
60
|
if (chatIdOrName.startsWith('oc_')) return chatIdOrName;
|
|
51
61
|
// Also accept raw numeric IDs (from search_contacts)
|
|
52
62
|
if (/^\d+$/.test(chatIdOrName)) return chatIdOrName;
|
|
@@ -58,11 +68,11 @@ class ChatIdMapper {
|
|
|
58
68
|
const results = await official.chatSearch(chatIdOrName);
|
|
59
69
|
for (const chat of results) {
|
|
60
70
|
this.nameCache.set(chat.chat_id, chat.name || '');
|
|
61
|
-
if (chat.name
|
|
71
|
+
if (ChatIdMapper._nameMatch(chat.name, chatIdOrName, true)) return chat.chat_id;
|
|
62
72
|
}
|
|
63
|
-
// Partial match on search results
|
|
73
|
+
// Partial match on search results (case-insensitive)
|
|
64
74
|
for (const chat of results) {
|
|
65
|
-
if (chat.name
|
|
75
|
+
if (ChatIdMapper._nameMatch(chat.name, chatIdOrName)) return chat.chat_id;
|
|
66
76
|
}
|
|
67
77
|
} catch (e) {
|
|
68
78
|
console.error('[feishu-user-plugin] chatSearch fallback failed:', e.message);
|
|
@@ -77,13 +87,13 @@ class ChatIdMapper {
|
|
|
77
87
|
try {
|
|
78
88
|
const results = await userClient.search(chatName);
|
|
79
89
|
const groups = results.filter(r => r.type === 'group');
|
|
80
|
-
// Exact match first
|
|
90
|
+
// Exact match first (case-insensitive)
|
|
81
91
|
for (const g of groups) {
|
|
82
|
-
if (g.title
|
|
92
|
+
if (ChatIdMapper._nameMatch(g.title, chatName, true)) return String(g.id);
|
|
83
93
|
}
|
|
84
|
-
// Partial match
|
|
94
|
+
// Partial match (case-insensitive)
|
|
85
95
|
for (const g of groups) {
|
|
86
|
-
if (g.title
|
|
96
|
+
if (ChatIdMapper._nameMatch(g.title, chatName)) return String(g.id);
|
|
87
97
|
}
|
|
88
98
|
} catch (e) {
|
|
89
99
|
console.error('[feishu-user-plugin] search_contacts fallback failed:', e.message);
|
|
@@ -386,6 +396,17 @@ const TOOLS = [
|
|
|
386
396
|
required: ['document_id'],
|
|
387
397
|
},
|
|
388
398
|
},
|
|
399
|
+
{
|
|
400
|
+
name: 'get_doc_blocks',
|
|
401
|
+
description: '[Official API] Get structured block tree of a document. Returns block types, content, and hierarchy for precise document analysis.',
|
|
402
|
+
inputSchema: {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {
|
|
405
|
+
document_id: { type: 'string', description: 'Document ID (from search_docs or create_doc)' },
|
|
406
|
+
},
|
|
407
|
+
required: ['document_id'],
|
|
408
|
+
},
|
|
409
|
+
},
|
|
389
410
|
{
|
|
390
411
|
name: 'create_doc',
|
|
391
412
|
description: '[Official API] Create a new Feishu document.',
|
|
@@ -514,6 +535,33 @@ const TOOLS = [
|
|
|
514
535
|
},
|
|
515
536
|
},
|
|
516
537
|
|
|
538
|
+
// ========== Upload — Official API ==========
|
|
539
|
+
{
|
|
540
|
+
name: 'upload_image',
|
|
541
|
+
description: '[Official API] Upload an image file to Feishu. Returns image_key for use with send_image_as_user.',
|
|
542
|
+
inputSchema: {
|
|
543
|
+
type: 'object',
|
|
544
|
+
properties: {
|
|
545
|
+
image_path: { type: 'string', description: 'Absolute path to the image file on disk' },
|
|
546
|
+
image_type: { type: 'string', enum: ['message', 'avatar'], description: 'Image usage type (default: message)' },
|
|
547
|
+
},
|
|
548
|
+
required: ['image_path'],
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: 'upload_file',
|
|
553
|
+
description: '[Official API] Upload a file to Feishu. Returns file_key for use with send_file_as_user.',
|
|
554
|
+
inputSchema: {
|
|
555
|
+
type: 'object',
|
|
556
|
+
properties: {
|
|
557
|
+
file_path: { type: 'string', description: 'Absolute path to the file on disk' },
|
|
558
|
+
file_type: { type: 'string', enum: ['opus', 'mp4', 'pdf', 'doc', 'xls', 'ppt', 'stream'], description: 'File type (default: stream for generic files)' },
|
|
559
|
+
file_name: { type: 'string', description: 'Display file name (optional, defaults to basename)' },
|
|
560
|
+
},
|
|
561
|
+
required: ['file_path'],
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
|
|
517
565
|
// ========== Contact — Official API ==========
|
|
518
566
|
{
|
|
519
567
|
name: 'find_user',
|
|
@@ -531,7 +579,7 @@ const TOOLS = [
|
|
|
531
579
|
// --- Server ---
|
|
532
580
|
|
|
533
581
|
const server = new Server(
|
|
534
|
-
{ name: 'feishu-user-plugin', version: '
|
|
582
|
+
{ name: 'feishu-user-plugin', version: require('../package.json').version },
|
|
535
583
|
{ capabilities: { tools: {} } }
|
|
536
584
|
);
|
|
537
585
|
|
|
@@ -563,8 +611,13 @@ async function handleTool(name, args) {
|
|
|
563
611
|
case 'send_to_user': {
|
|
564
612
|
const c = await getUserClient();
|
|
565
613
|
const results = await c.search(args.user_name);
|
|
566
|
-
const
|
|
567
|
-
if (
|
|
614
|
+
const users = results.filter(r => r.type === 'user');
|
|
615
|
+
if (users.length === 0) return text(`User "${args.user_name}" not found. Results: ${JSON.stringify(results)}`);
|
|
616
|
+
if (users.length > 1) {
|
|
617
|
+
const candidates = users.slice(0, 5).map(u => ` - ${u.title} (ID: ${u.id})`).join('\n');
|
|
618
|
+
return text(`Multiple users match "${args.user_name}":\n${candidates}\nUse search_contacts to find the exact user, then create_p2p_chat + send_as_user.`);
|
|
619
|
+
}
|
|
620
|
+
const user = users[0];
|
|
568
621
|
const chatId = await c.createChat(user.id);
|
|
569
622
|
if (!chatId) return text(`Failed to create chat with ${user.title}`);
|
|
570
623
|
const r = await c.sendMessage(chatId, args.text);
|
|
@@ -573,8 +626,13 @@ async function handleTool(name, args) {
|
|
|
573
626
|
case 'send_to_group': {
|
|
574
627
|
const c = await getUserClient();
|
|
575
628
|
const results = await c.search(args.group_name);
|
|
576
|
-
const
|
|
577
|
-
if (
|
|
629
|
+
const groups = results.filter(r => r.type === 'group');
|
|
630
|
+
if (groups.length === 0) return text(`Group "${args.group_name}" not found. Results: ${JSON.stringify(results)}`);
|
|
631
|
+
if (groups.length > 1) {
|
|
632
|
+
const candidates = groups.slice(0, 5).map(g => ` - ${g.title} (ID: ${g.id})`).join('\n');
|
|
633
|
+
return text(`Multiple groups match "${args.group_name}":\n${candidates}\nUse search_contacts to find the exact group, then send_as_user with the ID.`);
|
|
634
|
+
}
|
|
635
|
+
const group = groups[0];
|
|
578
636
|
const r = await c.sendMessage(group.id, args.text);
|
|
579
637
|
return sendResult(r, `Text sent to group "${group.title}" (${group.id})`);
|
|
580
638
|
}
|
|
@@ -625,20 +683,16 @@ async function handleTool(name, args) {
|
|
|
625
683
|
}
|
|
626
684
|
case 'get_user_info': {
|
|
627
685
|
let n = null;
|
|
628
|
-
// Strategy 1:
|
|
686
|
+
// Strategy 1: Official API contact lookup (works for same-tenant users by open_id)
|
|
629
687
|
try {
|
|
630
|
-
const
|
|
631
|
-
n = await
|
|
632
|
-
if (!n && args.user_id) {
|
|
633
|
-
await c.search(args.user_id);
|
|
634
|
-
n = await c.getUserName(args.user_id);
|
|
635
|
-
}
|
|
688
|
+
const official = getOfficialClient();
|
|
689
|
+
n = await official.getUserById(args.user_id, 'open_id');
|
|
636
690
|
} catch {}
|
|
637
|
-
// Strategy 2:
|
|
691
|
+
// Strategy 2: User identity client cache (populated by previous search/init calls)
|
|
638
692
|
if (!n) {
|
|
639
693
|
try {
|
|
640
|
-
const
|
|
641
|
-
n = await
|
|
694
|
+
const c = await getUserClient();
|
|
695
|
+
n = await c.getUserName(args.user_id);
|
|
642
696
|
} catch {}
|
|
643
697
|
}
|
|
644
698
|
return text(n ? `User ${args.user_id}: ${n}` : `Could not resolve user ${args.user_id}. This user may be from an external tenant. Try search_contacts with the user's display name instead.`);
|
|
@@ -663,10 +717,11 @@ async function handleTool(name, args) {
|
|
|
663
717
|
case 'read_p2p_messages': {
|
|
664
718
|
const official = getOfficialClient();
|
|
665
719
|
let chatId = args.chat_id;
|
|
720
|
+
let uc = null;
|
|
721
|
+
let ucError = null;
|
|
722
|
+
try { uc = await getUserClient(); } catch (e) { ucError = e; }
|
|
666
723
|
// If chat_id is not numeric or oc_, try to resolve as user name → P2P chat
|
|
667
724
|
if (!/^\d+$/.test(chatId) && !chatId.startsWith('oc_')) {
|
|
668
|
-
let uc = null;
|
|
669
|
-
try { uc = await getUserClient(); } catch (_) {}
|
|
670
725
|
if (uc) {
|
|
671
726
|
const results = await uc.search(chatId);
|
|
672
727
|
const user = results.find(r => r.type === 'user');
|
|
@@ -681,11 +736,10 @@ async function handleTool(name, args) {
|
|
|
681
736
|
else return text(`Cannot resolve "${args.chat_id}" to a chat. Use search_contacts to find the ID first.`);
|
|
682
737
|
}
|
|
683
738
|
} else {
|
|
684
|
-
|
|
739
|
+
const hint = ucError ? `Cookie auth failed: ${ucError.message}. Fix LARK_COOKIE first, or p` : 'P';
|
|
740
|
+
return text(`"${args.chat_id}" is not a valid chat ID. ${hint}rovide a numeric ID or oc_xxx format. Use search_contacts + create_p2p_chat to get the ID.`);
|
|
685
741
|
}
|
|
686
742
|
}
|
|
687
|
-
let uc = null;
|
|
688
|
-
try { uc = await getUserClient(); } catch (_) {}
|
|
689
743
|
return json(await official.readMessagesAsUser(chatId, {
|
|
690
744
|
pageSize: args.page_size, startTime: args.start_time, endTime: args.end_time,
|
|
691
745
|
sortType: args.sort_type,
|
|
@@ -746,6 +800,8 @@ async function handleTool(name, args) {
|
|
|
746
800
|
return json(await getOfficialClient().searchDocs(args.query));
|
|
747
801
|
case 'read_doc':
|
|
748
802
|
return json(await getOfficialClient().readDoc(args.document_id));
|
|
803
|
+
case 'get_doc_blocks':
|
|
804
|
+
return json(await getOfficialClient().getDocBlocks(args.document_id));
|
|
749
805
|
case 'create_doc':
|
|
750
806
|
return text(`Document created: ${(await getOfficialClient().createDoc(args.title, args.folder_id)).documentId}`);
|
|
751
807
|
|
|
@@ -785,6 +841,17 @@ async function handleTool(name, args) {
|
|
|
785
841
|
case 'find_user':
|
|
786
842
|
return json(await getOfficialClient().findUserByIdentity({ emails: args.email, mobiles: args.mobile }));
|
|
787
843
|
|
|
844
|
+
// --- Upload ---
|
|
845
|
+
|
|
846
|
+
case 'upload_image': {
|
|
847
|
+
const r = await getOfficialClient().uploadImage(args.image_path, args.image_type);
|
|
848
|
+
return text(`Image uploaded: ${r.imageKey}\nUse this image_key with send_image_as_user to send it.`);
|
|
849
|
+
}
|
|
850
|
+
case 'upload_file': {
|
|
851
|
+
const r = await getOfficialClient().uploadFile(args.file_path, args.file_type, args.file_name);
|
|
852
|
+
return text(`File uploaded: ${r.fileKey}\nUse this file_key with send_file_as_user to send it.`);
|
|
853
|
+
}
|
|
854
|
+
|
|
788
855
|
default:
|
|
789
856
|
return text(`Unknown tool: ${name}`);
|
|
790
857
|
}
|
|
@@ -798,7 +865,7 @@ async function main() {
|
|
|
798
865
|
const hasCookie = !!process.env.LARK_COOKIE;
|
|
799
866
|
const hasApp = !!(process.env.LARK_APP_ID && process.env.LARK_APP_SECRET);
|
|
800
867
|
const hasUAT = !!process.env.LARK_USER_ACCESS_TOKEN;
|
|
801
|
-
console.error(`[feishu-user-plugin] MCP Server
|
|
868
|
+
console.error(`[feishu-user-plugin] MCP Server v${require('../package.json').version} — ${TOOLS.length} tools`);
|
|
802
869
|
console.error(`[feishu-user-plugin] Auth: Cookie=${hasCookie ? 'YES' : 'NO'} App=${hasApp ? 'YES' : 'NO'} UAT=${hasUAT ? 'YES' : 'NO'}`);
|
|
803
870
|
if (!hasCookie) console.error('[feishu-user-plugin] WARNING: LARK_COOKIE not set — user identity tools (send_to_user, etc.) will fail');
|
|
804
871
|
if (!hasApp) console.error('[feishu-user-plugin] WARNING: LARK_APP_ID/SECRET not set — official API tools (read_messages, docs, etc.) will fail');
|
package/src/oauth-auto.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Automated OAuth
|
|
2
|
+
// DEV ONLY: Automated OAuth using local Playwright (not used in production).
|
|
3
|
+
// Uses .env directly; not migrated to config module.
|
|
4
|
+
// Requires: npm install playwright (not in package.json dependencies)
|
|
3
5
|
const http = require('http');
|
|
4
6
|
const { chromium } = require('playwright');
|
|
5
7
|
const fs = require('fs');
|
|
@@ -30,7 +32,7 @@ function saveToken(tokenData) {
|
|
|
30
32
|
LARK_USER_ACCESS_TOKEN: tokenData.access_token,
|
|
31
33
|
LARK_USER_REFRESH_TOKEN: tokenData.refresh_token || '',
|
|
32
34
|
LARK_UAT_SCOPE: tokenData.scope || '',
|
|
33
|
-
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + tokenData.expires_in)),
|
|
35
|
+
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + (typeof tokenData.expires_in === 'number' && tokenData.expires_in > 0 ? tokenData.expires_in : 7200))),
|
|
34
36
|
};
|
|
35
37
|
for (const [key, val] of Object.entries(updates)) {
|
|
36
38
|
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
@@ -161,29 +163,6 @@ async function run() {
|
|
|
161
163
|
console.log('scope:', tokenData.scope);
|
|
162
164
|
console.log('expires_in:', tokenData.expires_in, 's');
|
|
163
165
|
|
|
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
166
|
} catch (e) {
|
|
188
167
|
console.error('\nError:', e.message);
|
|
189
168
|
await page.screenshot({ path: '/tmp/feishu-oauth-error.png' }).catch(() => {});
|
package/src/oauth.js
CHANGED
|
@@ -2,33 +2,31 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* OAuth 授权脚本 — 获取带 IM 权限的 user_access_token
|
|
4
4
|
*
|
|
5
|
-
* 用法:
|
|
5
|
+
* 用法: npx feishu-user-plugin oauth
|
|
6
6
|
*
|
|
7
7
|
* 流程 (新版 End User Consent):
|
|
8
8
|
* 1. 查询应用信息,提示用户选择正确的飞书账号
|
|
9
9
|
* 2. 启动本地 HTTP 服务器 (端口 9997)
|
|
10
10
|
* 3. 打开 accounts.feishu.cn 授权页面 (新版 OAuth 2.0)
|
|
11
11
|
* 4. 用户点击"授权"后,用 /authen/v2/oauth/token 交换 token
|
|
12
|
-
* 5. 保存
|
|
12
|
+
* 5. 保存 token 到 MCP 配置文件
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const http = require('http');
|
|
16
16
|
const { execSync } = require('child_process');
|
|
17
|
-
const
|
|
18
|
-
const path = require('path');
|
|
19
|
-
const dotenv = require('dotenv');
|
|
17
|
+
const { readCredentials, persistToConfig } = require('./config');
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const APP_SECRET = process.env.LARK_APP_SECRET;
|
|
19
|
+
const creds = readCredentials();
|
|
20
|
+
const APP_ID = creds.LARK_APP_ID;
|
|
21
|
+
const APP_SECRET = creds.LARK_APP_SECRET;
|
|
25
22
|
const PORT = 9997;
|
|
26
23
|
const REDIRECT_URI = `http://127.0.0.1:${PORT}/callback`;
|
|
27
24
|
// offline_access is required to get refresh_token for auto-renewal
|
|
28
25
|
const SCOPES = 'offline_access im:message im:message:readonly im:chat:readonly contact:user.base:readonly';
|
|
29
26
|
|
|
30
27
|
if (!APP_ID || !APP_SECRET) {
|
|
31
|
-
console.error('Missing LARK_APP_ID or LARK_APP_SECRET
|
|
28
|
+
console.error('Missing LARK_APP_ID or LARK_APP_SECRET.');
|
|
29
|
+
console.error('Run "npx feishu-user-plugin setup" first to configure app credentials.');
|
|
32
30
|
process.exit(1);
|
|
33
31
|
}
|
|
34
32
|
|
|
@@ -100,52 +98,20 @@ async function exchangeCode(code) {
|
|
|
100
98
|
}
|
|
101
99
|
|
|
102
100
|
function saveToken(tokenData) {
|
|
103
|
-
const envPath = path.join(__dirname, '..', '.env');
|
|
104
|
-
let envContent = '';
|
|
105
|
-
try { envContent = fs.readFileSync(envPath, 'utf8'); } catch {}
|
|
106
|
-
|
|
107
101
|
const updates = {
|
|
108
102
|
LARK_USER_ACCESS_TOKEN: tokenData.access_token,
|
|
109
103
|
LARK_USER_REFRESH_TOKEN: tokenData.refresh_token || '',
|
|
110
104
|
LARK_UAT_SCOPE: tokenData.scope || '',
|
|
111
|
-
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + tokenData.expires_in)),
|
|
105
|
+
LARK_UAT_EXPIRES: String(Math.floor(Date.now() / 1000 + (typeof tokenData.expires_in === 'number' && tokenData.expires_in > 0 ? tokenData.expires_in : 7200))),
|
|
112
106
|
};
|
|
113
107
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
envContent += `\n${key}=${val}`;
|
|
108
|
+
const ok = persistToConfig(updates);
|
|
109
|
+
if (!ok) {
|
|
110
|
+
console.error('WARNING: Tokens could not be saved to config. Copy them manually:');
|
|
111
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
112
|
+
console.error(` ${k}=${v}`);
|
|
120
113
|
}
|
|
121
114
|
}
|
|
122
|
-
|
|
123
|
-
fs.writeFileSync(envPath, envContent.trim() + '\n');
|
|
124
|
-
|
|
125
|
-
// Also persist to ~/.claude.json MCP config so MCP restart picks up tokens immediately
|
|
126
|
-
_persistToClaudeJson(updates);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function _persistToClaudeJson(updates) {
|
|
130
|
-
const claudeJsonPaths = [
|
|
131
|
-
path.join(process.env.HOME || '', '.claude.json'),
|
|
132
|
-
path.join(process.env.HOME || '', '.claude', '.claude.json'),
|
|
133
|
-
];
|
|
134
|
-
for (const cjPath of claudeJsonPaths) {
|
|
135
|
-
try {
|
|
136
|
-
const raw = fs.readFileSync(cjPath, 'utf8');
|
|
137
|
-
const config = JSON.parse(raw);
|
|
138
|
-
const servers = config.mcpServers || {};
|
|
139
|
-
for (const name of ['feishu-user-plugin', 'feishu']) {
|
|
140
|
-
if (servers[name]?.env) {
|
|
141
|
-
Object.assign(servers[name].env, updates);
|
|
142
|
-
fs.writeFileSync(cjPath, JSON.stringify(config, null, 2) + '\n');
|
|
143
|
-
console.log(`[feishu-user-plugin] OAuth tokens persisted to ${cjPath}`);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
} catch {}
|
|
148
|
-
}
|
|
149
115
|
}
|
|
150
116
|
|
|
151
117
|
const server = http.createServer(async (req, res) => {
|
|
@@ -170,7 +136,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
170
136
|
<p>scope: ${tokenData.scope}</p>
|
|
171
137
|
<p>expires_in: ${tokenData.expires_in}s</p>
|
|
172
138
|
<p>refresh_token: ${hasRefresh ? '✅ 已获取(30天有效,支持自动续期)' : '❌ 未返回(token 将在 2 小时后过期,需重新授权)'}</p>
|
|
173
|
-
<p>已保存到
|
|
139
|
+
<p>已保存到 MCP 配置文件,可以关闭此页面。</p>`);
|
|
174
140
|
|
|
175
141
|
console.log('\n=== OAuth 授权成功 ===');
|
|
176
142
|
console.log('scope:', tokenData.scope);
|
|
@@ -182,7 +148,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
182
148
|
console.log(' - 授权时 scope 中未包含 offline_access');
|
|
183
149
|
console.log(' Token 将在 2 小时后过期,届时需要重新运行此脚本。');
|
|
184
150
|
}
|
|
185
|
-
console.log('token 已保存到
|
|
151
|
+
console.log('token 已保存到 MCP 配置文件');
|
|
186
152
|
|
|
187
153
|
setTimeout(() => { server.close(); process.exit(0); }, 1000);
|
|
188
154
|
} catch (e) {
|
|
@@ -197,6 +163,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
197
163
|
res.end('Not found');
|
|
198
164
|
});
|
|
199
165
|
|
|
166
|
+
server.on('error', (e) => {
|
|
167
|
+
if (e.code === 'EADDRINUSE') {
|
|
168
|
+
console.error(`\nPort ${PORT} is already in use. Another OAuth process may be running.`);
|
|
169
|
+
console.error('Wait a minute and try again, or kill the process using the port.');
|
|
170
|
+
} else {
|
|
171
|
+
console.error('Server error:', e.message);
|
|
172
|
+
}
|
|
173
|
+
process.exit(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
200
176
|
server.listen(PORT, '127.0.0.1', async () => {
|
|
201
177
|
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)}`;
|
|
202
178
|
|
|
@@ -226,7 +202,8 @@ server.listen(PORT, '127.0.0.1', async () => {
|
|
|
226
202
|
console.log('授权 URL:', authUrl);
|
|
227
203
|
|
|
228
204
|
try {
|
|
229
|
-
|
|
205
|
+
const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
206
|
+
execSync(`${openCmd} "${authUrl}"`);
|
|
230
207
|
} catch {
|
|
231
208
|
console.log('\n请手动在浏览器中打开上面的 URL');
|
|
232
209
|
}
|
package/src/official.js
CHANGED
|
@@ -58,84 +58,31 @@ class LarkOfficialClient {
|
|
|
58
58
|
|
|
59
59
|
this._uat = tokenData.access_token;
|
|
60
60
|
this._uatRefresh = tokenData.refresh_token || this._uatRefresh;
|
|
61
|
-
|
|
61
|
+
const expiresIn = typeof tokenData.expires_in === 'number' && tokenData.expires_in > 0 ? tokenData.expires_in : 7200;
|
|
62
|
+
this._uatExpires = Math.floor(Date.now() / 1000) + expiresIn;
|
|
62
63
|
this._persistUAT();
|
|
63
64
|
console.error('[feishu-user-plugin] UAT refreshed successfully');
|
|
64
65
|
return this._uat;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
_persistUAT() {
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
69
|
+
// Lazy require to avoid circular dependency at module load time
|
|
70
|
+
const { persistToConfig } = require('./config');
|
|
71
|
+
persistToConfig({
|
|
71
72
|
LARK_USER_ACCESS_TOKEN: this._uat,
|
|
72
73
|
LARK_USER_REFRESH_TOKEN: this._uatRefresh,
|
|
73
74
|
LARK_UAT_EXPIRES: String(this._uatExpires),
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Strategy 1: Update ~/.claude.json MCP config (works for npx users)
|
|
77
|
-
const claudeJsonPaths = [
|
|
78
|
-
path.join(process.env.HOME || '', '.claude.json'),
|
|
79
|
-
path.join(process.env.HOME || '', '.claude', '.claude.json'),
|
|
80
|
-
];
|
|
81
|
-
for (const cjPath of claudeJsonPaths) {
|
|
82
|
-
try {
|
|
83
|
-
const raw = fs.readFileSync(cjPath, 'utf8');
|
|
84
|
-
const config = JSON.parse(raw);
|
|
85
|
-
const servers = config.mcpServers || {};
|
|
86
|
-
// Find our server entry by name
|
|
87
|
-
for (const name of ['feishu-user-plugin', 'feishu']) {
|
|
88
|
-
if (servers[name]?.env) {
|
|
89
|
-
Object.assign(servers[name].env, updates);
|
|
90
|
-
fs.writeFileSync(cjPath, JSON.stringify(config, null, 2) + '\n');
|
|
91
|
-
console.error('[feishu-user-plugin] UAT persisted to', cjPath);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
} catch {}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Strategy 2: Update project .mcp.json
|
|
99
|
-
const mcpJsonPaths = [
|
|
100
|
-
path.join(process.cwd(), '.mcp.json'),
|
|
101
|
-
];
|
|
102
|
-
for (const mjPath of mcpJsonPaths) {
|
|
103
|
-
try {
|
|
104
|
-
const raw = fs.readFileSync(mjPath, 'utf8');
|
|
105
|
-
const config = JSON.parse(raw);
|
|
106
|
-
const servers = config.mcpServers || config;
|
|
107
|
-
for (const name of ['feishu-user-plugin', 'feishu']) {
|
|
108
|
-
if (servers[name]?.env) {
|
|
109
|
-
Object.assign(servers[name].env, updates);
|
|
110
|
-
fs.writeFileSync(mjPath, JSON.stringify(config, null, 2) + '\n');
|
|
111
|
-
console.error('[feishu-user-plugin] UAT persisted to', mjPath);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} catch {}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Strategy 3: Fallback to .env in project root (for local dev)
|
|
119
|
-
const envPath = path.join(__dirname, '..', '.env');
|
|
120
|
-
try {
|
|
121
|
-
let env = '';
|
|
122
|
-
try { env = fs.readFileSync(envPath, 'utf8'); } catch {}
|
|
123
|
-
for (const [key, val] of Object.entries(updates)) {
|
|
124
|
-
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
125
|
-
if (regex.test(env)) env = env.replace(regex, `${key}=${val}`);
|
|
126
|
-
else env += `\n${key}=${val}`;
|
|
127
|
-
}
|
|
128
|
-
fs.writeFileSync(envPath, env.trim() + '\n');
|
|
129
|
-
} catch {}
|
|
75
|
+
});
|
|
130
76
|
}
|
|
131
77
|
|
|
132
78
|
// --- UAT-based IM operations (for P2P chats) ---
|
|
133
79
|
|
|
134
|
-
// Wrapper: call fn with UAT, retry once after refresh if auth fails
|
|
80
|
+
// Wrapper: call fn with UAT, retry once after refresh if auth fails
|
|
135
81
|
async _withUAT(fn) {
|
|
136
82
|
let uat = await this._getValidUAT();
|
|
137
83
|
const data = await fn(uat);
|
|
138
|
-
|
|
84
|
+
// Known auth error codes: 99991668 (invalid), 99991663 (expired), 99991677 (auth_expired)
|
|
85
|
+
if (data.code === 99991668 || data.code === 99991663 || data.code === 99991677) {
|
|
139
86
|
// Token invalid/expired — try refresh once
|
|
140
87
|
uat = await this._refreshUAT();
|
|
141
88
|
return fn(uat);
|
|
@@ -230,6 +177,36 @@ class LarkOfficialClient {
|
|
|
230
177
|
return { messageId: res.data.message_id };
|
|
231
178
|
}
|
|
232
179
|
|
|
180
|
+
// --- Upload ---
|
|
181
|
+
|
|
182
|
+
async uploadImage(imagePath, imageType = 'message') {
|
|
183
|
+
const fs = require('fs');
|
|
184
|
+
const res = await this._safeSDKCall(
|
|
185
|
+
() => this.client.im.image.create({
|
|
186
|
+
data: { image_type: imageType, image: fs.createReadStream(imagePath) },
|
|
187
|
+
}),
|
|
188
|
+
'uploadImage'
|
|
189
|
+
);
|
|
190
|
+
return { imageKey: res.data.image_key };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async uploadFile(filePath, fileType = 'stream', fileName) {
|
|
194
|
+
const fs = require('fs');
|
|
195
|
+
const path = require('path');
|
|
196
|
+
if (!fileName) fileName = path.basename(filePath);
|
|
197
|
+
const res = await this._safeSDKCall(
|
|
198
|
+
() => this.client.im.file.create({
|
|
199
|
+
data: {
|
|
200
|
+
file_type: fileType,
|
|
201
|
+
file_name: fileName,
|
|
202
|
+
file: fs.createReadStream(filePath),
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
'uploadFile'
|
|
206
|
+
);
|
|
207
|
+
return { fileKey: res.data.file_key };
|
|
208
|
+
}
|
|
209
|
+
|
|
233
210
|
// --- Docs ---
|
|
234
211
|
|
|
235
212
|
async searchDocs(query, { pageSize = 10, pageToken } = {}) {
|
|
@@ -441,14 +418,14 @@ class LarkOfficialClient {
|
|
|
441
418
|
unknownIds.add(item.senderId);
|
|
442
419
|
}
|
|
443
420
|
}
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
await this.getUserById(id);
|
|
421
|
+
// Parallel resolve via official contact API (instead of sequential N calls)
|
|
422
|
+
if (unknownIds.size > 0) {
|
|
423
|
+
await Promise.allSettled([...unknownIds].map(id => this.getUserById(id)));
|
|
447
424
|
}
|
|
448
425
|
// Fallback: resolve remaining unknowns via cookie-based user identity client
|
|
449
426
|
if (userClient) {
|
|
450
427
|
for (const id of unknownIds) {
|
|
451
|
-
if (!this._userNameCache.has(id)
|
|
428
|
+
if (!this._userNameCache.has(id)) {
|
|
452
429
|
try {
|
|
453
430
|
const name = await userClient.getUserName(id);
|
|
454
431
|
if (name) this._userNameCache.set(id, name);
|
package/src/setup.js
CHANGED
|
@@ -1,67 +1,92 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Setup wizard for feishu-user-plugin
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Two modes:
|
|
6
|
+
* Interactive: npx feishu-user-plugin setup
|
|
7
|
+
* Non-interactive: npx feishu-user-plugin setup --app-id xxx --app-secret yyy
|
|
8
|
+
*
|
|
9
|
+
* Writes MCP config to ~/.claude.json top-level mcpServers (global).
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
12
|
const readline = require('readline');
|
|
13
|
+
const { findMcpConfig, writeNewConfig } = require('./config');
|
|
14
|
+
|
|
15
|
+
// Parse CLI args: --app-id, --app-secret, --cookie
|
|
16
|
+
function parseArgs() {
|
|
17
|
+
const args = {};
|
|
18
|
+
const argv = process.argv.slice(2);
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
if (argv[i] === '--app-id' && argv[i + 1]) args.appId = argv[++i];
|
|
21
|
+
else if (argv[i] === '--app-secret' && argv[i + 1]) args.appSecret = argv[++i];
|
|
22
|
+
else if (argv[i] === '--cookie' && argv[i + 1]) args.cookie = argv[++i];
|
|
23
|
+
}
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
const
|
|
27
|
+
async function main() {
|
|
28
|
+
const cliArgs = parseArgs();
|
|
29
|
+
const nonInteractive = !!(cliArgs.appId && cliArgs.appSecret);
|
|
15
30
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
31
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
+
const ask = (q) => new Promise(resolve => rl.question(q, resolve));
|
|
18
33
|
|
|
19
|
-
async function main() {
|
|
20
34
|
console.log('='.repeat(60));
|
|
21
|
-
console.log(' feishu-user-plugin Setup
|
|
35
|
+
console.log(' feishu-user-plugin Setup');
|
|
22
36
|
console.log('='.repeat(60));
|
|
23
|
-
console.log('');
|
|
24
37
|
|
|
25
38
|
// Check existing config
|
|
26
|
-
let config = {};
|
|
27
39
|
let existingEnv = {};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
console.log('
|
|
34
|
-
|
|
40
|
+
const found = findMcpConfig();
|
|
41
|
+
if (found) {
|
|
42
|
+
existingEnv = found.serverEnv;
|
|
43
|
+
if (found.projectPath) {
|
|
44
|
+
console.log(`\nFound project-level config in ${found.configPath} (project: ${found.projectPath})`);
|
|
45
|
+
console.log('This setup will write to global config instead (recommended).');
|
|
46
|
+
console.log('You can remove the project-level entry later to avoid conflicts.');
|
|
47
|
+
} else {
|
|
48
|
+
console.log(`\nFound existing config in ${found.configPath}`);
|
|
49
|
+
}
|
|
50
|
+
if (!nonInteractive) {
|
|
51
|
+
const update = await ask('Update config? (Y/n): ');
|
|
35
52
|
if (update.toLowerCase() === 'n') {
|
|
36
53
|
console.log('Cancelled.');
|
|
37
54
|
rl.close();
|
|
38
55
|
return;
|
|
39
56
|
}
|
|
40
57
|
}
|
|
41
|
-
} catch {}
|
|
42
|
-
|
|
43
|
-
// Collect credentials
|
|
44
|
-
console.log('\n--- App Credentials ---');
|
|
45
|
-
console.log('Team members: press Enter to use the shared defaults.');
|
|
46
|
-
console.log('External users: get these from https://open.feishu.cn/app\n');
|
|
47
|
-
|
|
48
|
-
const defaultAppId = existingEnv.LARK_APP_ID || '';
|
|
49
|
-
const defaultAppSecret = existingEnv.LARK_APP_SECRET || '';
|
|
50
|
-
|
|
51
|
-
let appId = await ask(`LARK_APP_ID [${defaultAppId || 'required'}]: `);
|
|
52
|
-
appId = appId.trim() || defaultAppId;
|
|
53
|
-
if (!appId) {
|
|
54
|
-
console.error('Error: LARK_APP_ID is required.');
|
|
55
|
-
rl.close();
|
|
56
|
-
process.exit(1);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
// Resolve App credentials
|
|
61
|
+
let appId, appSecret;
|
|
62
|
+
|
|
63
|
+
if (nonInteractive) {
|
|
64
|
+
// CLI args provided — no prompting
|
|
65
|
+
appId = cliArgs.appId;
|
|
66
|
+
appSecret = cliArgs.appSecret;
|
|
67
|
+
console.log(`\nApp ID: ${appId}`);
|
|
68
|
+
console.log('App Secret: ***');
|
|
69
|
+
} else {
|
|
70
|
+
// Interactive mode
|
|
71
|
+
console.log('\n--- App Credentials ---');
|
|
72
|
+
console.log('Get these from https://open.feishu.cn/app\n');
|
|
73
|
+
|
|
74
|
+
const defaultAppId = existingEnv.LARK_APP_ID || '';
|
|
75
|
+
const defaultAppSecret = existingEnv.LARK_APP_SECRET || '';
|
|
76
|
+
|
|
77
|
+
appId = (await ask(`LARK_APP_ID [${defaultAppId || 'required'}]: `)).trim() || defaultAppId;
|
|
78
|
+
if (!appId) {
|
|
79
|
+
console.error('Error: LARK_APP_ID is required.');
|
|
80
|
+
rl.close();
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
appSecret = (await ask(`LARK_APP_SECRET [${defaultAppSecret ? '***' : 'required'}]: `)).trim() || defaultAppSecret;
|
|
85
|
+
if (!appSecret) {
|
|
86
|
+
console.error('Error: LARK_APP_SECRET is required.');
|
|
87
|
+
rl.close();
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
// Validate app credentials
|
|
@@ -77,7 +102,6 @@ async function main() {
|
|
|
77
102
|
console.log('App credentials: VALID');
|
|
78
103
|
} else {
|
|
79
104
|
console.error(`App credentials: INVALID — ${data.msg || JSON.stringify(data)}`);
|
|
80
|
-
console.error('Please check your LARK_APP_ID and LARK_APP_SECRET.');
|
|
81
105
|
rl.close();
|
|
82
106
|
process.exit(1);
|
|
83
107
|
}
|
|
@@ -85,86 +109,57 @@ async function main() {
|
|
|
85
109
|
console.warn(`Could not validate: ${e.message}. Continuing anyway.`);
|
|
86
110
|
}
|
|
87
111
|
|
|
88
|
-
// Cookie
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const existingCookie = existingEnv.LARK_COOKIE;
|
|
94
|
-
const hasCookie = existingCookie && existingCookie !== 'PLACEHOLDER' && existingCookie.includes('session=');
|
|
95
|
-
if (hasCookie) {
|
|
96
|
-
console.log('Existing cookie found (has session token).');
|
|
97
|
-
const keepCookie = await ask('Keep existing cookie? (Y/n): ');
|
|
98
|
-
if (keepCookie.toLowerCase() === 'n') {
|
|
99
|
-
console.log('You can update it later or use Playwright extraction.');
|
|
100
|
-
}
|
|
112
|
+
// Resolve Cookie
|
|
113
|
+
let cookie;
|
|
114
|
+
if (cliArgs.cookie) {
|
|
115
|
+
cookie = cliArgs.cookie;
|
|
101
116
|
} else {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
117
|
+
const existingCookie = existingEnv.LARK_COOKIE;
|
|
118
|
+
const hasCookie = existingCookie && existingCookie !== 'SETUP_NEEDED' && existingCookie.includes('session=');
|
|
119
|
+
if (hasCookie) {
|
|
120
|
+
cookie = existingCookie;
|
|
121
|
+
console.log('\nKeeping existing cookie (has session token).');
|
|
122
|
+
} else {
|
|
123
|
+
cookie = 'SETUP_NEEDED';
|
|
124
|
+
if (!nonInteractive) {
|
|
125
|
+
console.log('\n--- Cookie ---');
|
|
126
|
+
console.log('No valid cookie found. After setup:');
|
|
127
|
+
console.log(' Tell Claude Code: "帮我设置飞书 Cookie" (with Playwright MCP)');
|
|
128
|
+
console.log(' Or manually copy from DevTools → Network → Cookie header');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
105
131
|
}
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// UAT
|
|
133
|
+
// Resolve UAT
|
|
110
134
|
const existingUAT = existingEnv.LARK_USER_ACCESS_TOKEN;
|
|
111
135
|
const existingRT = existingEnv.LARK_USER_REFRESH_TOKEN;
|
|
112
|
-
const hasUAT = existingUAT && existingUAT !== '
|
|
113
|
-
|
|
114
|
-
if (!hasUAT) {
|
|
115
|
-
console.log('\n--- OAuth UAT ---');
|
|
116
|
-
console.log('UAT not configured. After setup, run:');
|
|
117
|
-
console.log(' npx feishu-user-plugin oauth');
|
|
118
|
-
console.log('This will open a browser for OAuth consent.');
|
|
119
|
-
}
|
|
136
|
+
const hasUAT = existingUAT && existingUAT !== 'SETUP_NEEDED' && existingUAT.length > 20;
|
|
120
137
|
|
|
121
138
|
// Write config
|
|
122
139
|
console.log('\n--- Writing Config ---');
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
LARK_APP_ID: appId,
|
|
131
|
-
LARK_APP_SECRET: appSecret,
|
|
132
|
-
LARK_USER_ACCESS_TOKEN: hasUAT ? existingUAT : 'SETUP_NEEDED',
|
|
133
|
-
LARK_USER_REFRESH_TOKEN: hasUAT ? (existingRT || '') : '',
|
|
134
|
-
},
|
|
141
|
+
const env = {
|
|
142
|
+
LARK_COOKIE: cookie,
|
|
143
|
+
LARK_APP_ID: appId,
|
|
144
|
+
LARK_APP_SECRET: appSecret,
|
|
145
|
+
LARK_USER_ACCESS_TOKEN: hasUAT ? existingUAT : 'SETUP_NEEDED',
|
|
146
|
+
LARK_USER_REFRESH_TOKEN: hasUAT ? (existingRT || '') : '',
|
|
135
147
|
};
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
delete config.mcpServers.feishu;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
fs.writeFileSync(CLAUDE_JSON_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
143
|
-
console.log(`Written to ${CLAUDE_JSON_PATH}`);
|
|
144
|
-
|
|
145
|
-
// Also write .env for oauth.js to use
|
|
146
|
-
const envPath = path.join(__dirname, '..', '.env');
|
|
147
|
-
const envContent = [
|
|
148
|
-
`LARK_APP_ID=${appId}`,
|
|
149
|
-
`LARK_APP_SECRET=${appSecret}`,
|
|
150
|
-
cookie !== 'SETUP_NEEDED' ? `LARK_COOKIE=${cookie}` : '',
|
|
151
|
-
hasUAT ? `LARK_USER_ACCESS_TOKEN=${existingUAT}` : '',
|
|
152
|
-
hasUAT && existingRT ? `LARK_USER_REFRESH_TOKEN=${existingRT}` : '',
|
|
153
|
-
].filter(Boolean).join('\n') + '\n';
|
|
154
|
-
fs.writeFileSync(envPath, envContent);
|
|
149
|
+
const result = writeNewConfig(env);
|
|
150
|
+
console.log(`Written to ${result.configPath} (global)`);
|
|
155
151
|
|
|
156
152
|
// Summary
|
|
157
153
|
console.log('\n' + '='.repeat(60));
|
|
158
154
|
console.log(' Setup Complete!');
|
|
159
155
|
console.log('='.repeat(60));
|
|
160
|
-
console.log('');
|
|
161
156
|
|
|
162
157
|
const todo = [];
|
|
163
158
|
if (cookie === 'SETUP_NEEDED') todo.push('Get Cookie: tell Claude Code "帮我设置飞书 Cookie"');
|
|
164
159
|
if (!hasUAT) todo.push('Get UAT: run "npx feishu-user-plugin oauth"');
|
|
165
160
|
todo.push('Restart Claude Code');
|
|
166
161
|
|
|
167
|
-
console.log('
|
|
162
|
+
console.log('\nNext steps:');
|
|
168
163
|
todo.forEach((t, i) => console.log(` ${i + 1}. ${t}`));
|
|
169
164
|
console.log('');
|
|
170
165
|
|
|
@@ -173,6 +168,5 @@ async function main() {
|
|
|
173
168
|
|
|
174
169
|
main().catch(e => {
|
|
175
170
|
console.error('Setup failed:', e.message);
|
|
176
|
-
rl.close();
|
|
177
171
|
process.exit(1);
|
|
178
172
|
});
|
|
@@ -274,7 +274,7 @@ async function testUAT() {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
async function main() {
|
|
277
|
-
console.log('=== feishu-user-plugin v1.1.
|
|
277
|
+
console.log('=== feishu-user-plugin v1.1.3 — Comprehensive Test ===\n');
|
|
278
278
|
|
|
279
279
|
await testUserIdentity();
|
|
280
280
|
console.log('');
|