feishu-user-plugin 1.1.3 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +2 -2
- package/src/cli.js +85 -12
- package/src/client.js +6 -2
- package/src/config.js +188 -0
- package/src/index.js +267 -23
- package/src/oauth-auto.js +4 -25
- package/src/oauth.js +27 -53
- package/src/official.js +145 -65
- package/src/setup.js +96 -102
|
@@ -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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki.
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "All-in-one Feishu plugin for Claude Code — send messages as yourself, read chats, manage docs/tables/wiki. 46 tools + 9 skills, 3 auth layers.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"feishu-user-plugin": "src/cli.js"
|
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 };
|