feishu-user-plugin 1.3.8 → 1.3.10

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.
Files changed (42) hide show
  1. package/.claude-plugin/plugin.json +12 -2
  2. package/CHANGELOG.md +100 -12
  3. package/README.en.md +610 -0
  4. package/README.md +292 -532
  5. package/package.json +12 -5
  6. package/proto/lark.proto +10 -0
  7. package/scripts/explore-card-protobuf.js +144 -0
  8. package/scripts/explore-image-minimize.js +163 -0
  9. package/scripts/generate-og-image.js +39 -0
  10. package/scripts/generate-release-artifacts.js +318 -0
  11. package/scripts/probe-feishu-docx.js +203 -0
  12. package/scripts/sync-team-skills.sh +109 -7
  13. package/skills/feishu-user-plugin/SKILL.md +76 -4
  14. package/skills/feishu-user-plugin/references/CLAUDE.md +74 -54
  15. package/src/auth/credentials.js +36 -0
  16. package/src/cli.js +86 -45
  17. package/src/clients/user.js +15 -13
  18. package/src/events/cursor.js +103 -0
  19. package/src/events/event-buffer.js +8 -5
  20. package/src/events/event-log.js +151 -0
  21. package/src/events/index.js +8 -1
  22. package/src/events/lockfile.js +126 -0
  23. package/src/events/owner.js +73 -0
  24. package/src/events/ws-server.js +95 -25
  25. package/src/oauth.js +48 -7
  26. package/src/resolver.js +10 -0
  27. package/src/server.js +248 -29
  28. package/src/setup.js +99 -25
  29. package/src/test-all.js +12 -9
  30. package/src/test-events-cursor.js +56 -0
  31. package/src/test-events-lockfile.js +36 -0
  32. package/src/test-events-log.js +67 -0
  33. package/src/test-events-owner.js +64 -0
  34. package/src/test-fixtures/doc-blocks/sample-1.json +1256 -0
  35. package/src/test-read-doc-markdown.js +61 -0
  36. package/src/test-switch-profile.js +171 -0
  37. package/src/tools/diagnostics.js +10 -3
  38. package/src/tools/docs.js +93 -3
  39. package/src/tools/events.js +143 -33
  40. package/src/tools/messaging-bot.js +2 -3
  41. package/src/tools/messaging-user.js +23 -14
  42. package/src/tools/profile.js +12 -7
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "feishu-user-plugin",
3
- "version": "1.3.8",
4
- "description": "All-in-one Feishu plugin for Claude Code & Codex — messaging (with merge_forward expansion + batch_send), docs (image + file blocks read/write), bitable, wiki (full CRUD), drive, OKR (with progress writes), calendar (read+write), Tasks v2, multi-profile auto-switch, real-time WS events. 82 tools + 9 prompts, 3 auth layers.",
3
+ "mcpName": "io.github.EthanQC/feishu-user-plugin",
4
+ "version": "1.3.10",
5
+ "description": "All-in-one Feishu MCP server for Claude Code & Codex — 84 tools across 3 auth layers (cookie / app / OAuth). Send as you, read groups, manage docs / bitable / wiki / drive / calendar / tasks / OKR.",
5
6
  "main": "src/index.js",
6
7
  "bin": {
7
8
  "feishu-user-plugin": "src/cli.js"
@@ -49,18 +50,24 @@
49
50
  ".env.example",
50
51
  "CHANGELOG.md",
51
52
  "README.md",
53
+ "README.en.md",
52
54
  "LICENSE"
53
55
  ],
54
56
  "engines": {
55
57
  "node": ">=18.0.0"
56
58
  },
57
59
  "dependencies": {
58
- "@larksuiteoapi/node-sdk": "^1.59.0",
59
- "@modelcontextprotocol/sdk": "^1.12.1",
60
+ "@larksuiteoapi/node-sdk": "^1.63.1",
61
+ "@modelcontextprotocol/sdk": "^1.29.0",
60
62
  "dotenv": "^16.4.7",
61
- "protobufjs": "^7.4.0"
63
+ "feishu-docx": "^0.7.0",
64
+ "protobufjs": "^7.5.6"
62
65
  },
63
66
  "devDependencies": {
67
+ "@resvg/resvg-js": "^2.6.2",
64
68
  "husky": "^9.1.7"
69
+ },
70
+ "overrides": {
71
+ "axios": "^1.16.0"
65
72
  }
66
73
  }
package/proto/lark.proto CHANGED
@@ -123,8 +123,18 @@ message Content {
123
123
  optional string text = 1;
124
124
  optional string imageKey = 2;
125
125
  optional string title = 3;
126
+ // Image metadata fields (v1.3.9 — reverse-engineered via brute-force probe;
127
+ // see scripts/explore-image-minimize.js). Required for IMAGE messages on the
128
+ // cookie-protobuf path: imageKey + thumbnailKey(10) is the minimum the
129
+ // gateway accepts. width(4) / height(5) / mimeType(8) / fileSize(9) are
130
+ // optional but commonly present in the real wire format.
131
+ optional int32 imageWidth = 4;
132
+ optional int32 imageHeight = 5;
126
133
  optional string fileKey = 6;
127
134
  optional string audioKey = 7;
135
+ optional string mimeType = 8;
136
+ optional int64 fileSize = 9;
137
+ optional string thumbnailKey = 10;
128
138
  optional string fileName = 11;
129
139
  optional RichText richText = 14;
130
140
  optional string stickerSetId = 24;
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // CARD (type=14) protobuf field exploration. Probe field numbers + types.
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const protobuf = require('protobufjs');
8
+
9
+ const claudeCfg = JSON.parse(fs.readFileSync(path.join(require('os').homedir(), '.claude.json'), 'utf8'));
10
+ const env = claudeCfg.mcpServers?.['feishu-user-plugin']?.env || {};
11
+ process.env.LARK_COOKIE = env.LARK_COOKIE;
12
+ process.env.LARK_APP_ID = env.LARK_APP_ID;
13
+ process.env.LARK_APP_SECRET = env.LARK_APP_SECRET;
14
+
15
+ const PLUGIN_ROOT = path.join(__dirname, '..');
16
+ const { LarkUserClient } = require(path.join(PLUGIN_ROOT, 'src/clients/user'));
17
+ const { generateCid, generateRequestId } = require(path.join(PLUGIN_ROOT, 'src/utils'));
18
+
19
+ const TEST_GROUP_NAME = '飞书plugin测试群';
20
+
21
+ const errProto = protobuf.parse(`
22
+ syntax = "proto3";
23
+ message ErrorResponse {
24
+ optional string message = 1;
25
+ optional int32 code = 2;
26
+ optional int32 subCode = 3;
27
+ optional string detail = 4;
28
+ optional string trace = 5;
29
+ optional string requestId = 6;
30
+ }
31
+ `).root;
32
+ const ErrorResponse = errProto.lookupType('ErrorResponse');
33
+
34
+ async function fetchProto(url, opts) {
35
+ const u = new URL(url);
36
+ return new Promise((resolve, reject) => {
37
+ const req = require('node:https').request({
38
+ hostname: u.hostname,
39
+ port: u.port || 443,
40
+ path: u.pathname + u.search,
41
+ method: opts.method || 'GET',
42
+ headers: opts.headers || {},
43
+ }, (res) => {
44
+ const chunks = [];
45
+ res.on('data', (c) => chunks.push(c));
46
+ res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks) }));
47
+ });
48
+ req.on('error', reject);
49
+ if (opts.body) req.write(opts.body);
50
+ req.end();
51
+ });
52
+ }
53
+
54
+ function encodeVarint(n) {
55
+ const bytes = [];
56
+ while (n >= 0x80) { bytes.push((n & 0x7f) | 0x80); n = Math.floor(n / 128); }
57
+ bytes.push(n & 0x7f);
58
+ return Buffer.from(bytes);
59
+ }
60
+
61
+ function encodeWireField(num, type, val) {
62
+ if (type === 'varint') return Buffer.concat([Buffer.from([(num << 3) | 0]), encodeVarint(val)]);
63
+ if (type === 'string' || type === 'bytes') {
64
+ const v = type === 'string' ? Buffer.from(val, 'utf8') : val;
65
+ const tag = (num << 3) | 2;
66
+ if (num < 16) return Buffer.concat([Buffer.from([tag]), encodeVarint(v.length), v]);
67
+ // For field numbers > 15, tag is multi-byte varint
68
+ return Buffer.concat([encodeVarint(tag), encodeVarint(v.length), v]);
69
+ }
70
+ throw new Error('unknown type');
71
+ }
72
+
73
+ async function trySendRaw(userClient, chatId, msgType, contentBytes, label) {
74
+ const proto = userClient.proto;
75
+ const PutMessageRequest = proto.lookupType('PutMessageRequest');
76
+ const Packet = proto.lookupType('Packet');
77
+
78
+ const restReq = PutMessageRequest.encode(PutMessageRequest.create({
79
+ type: msgType, chatId, cid: generateCid(), isNotified: true, version: 1,
80
+ })).finish();
81
+ const contentField = Buffer.concat([
82
+ Buffer.from([(2 << 3) | 2]),
83
+ encodeVarint(contentBytes.length),
84
+ contentBytes,
85
+ ]);
86
+ const reqBuf = Buffer.concat([contentField, restReq]);
87
+
88
+ const packetBuf = Packet.encode(Packet.create({
89
+ payloadType: 1,
90
+ cmd: 5,
91
+ cid: generateRequestId(),
92
+ payload: reqBuf,
93
+ })).finish();
94
+
95
+ const res = await fetchProto('https://internal-api-lark-api.feishu.cn/im/gateway/', {
96
+ method: 'POST',
97
+ headers: userClient._protoHeaders(5, '5.7.0'),
98
+ body: packetBuf,
99
+ });
100
+
101
+ let parsedErr = null;
102
+ try { parsedErr = ErrorResponse.toObject(ErrorResponse.decode(res.body)); } catch (_) {}
103
+
104
+ const status = res.status === 200 ? '✓ OK' : `✗ ${res.status}`;
105
+ const errMsg = parsedErr?.message?.slice(0, 120) || '';
106
+ console.log(`${status} [${label}] ${errMsg}`);
107
+ return { ok: res.status === 200, status: res.status, err: errMsg };
108
+ }
109
+
110
+ (async () => {
111
+ const userClient = new LarkUserClient(process.env.LARK_COOKIE);
112
+ await userClient.init();
113
+ const sr = await userClient.search(TEST_GROUP_NAME);
114
+ const chatId = sr.find(x => x.type === 'group' && x.title.includes(TEST_GROUP_NAME)).id;
115
+ console.log('chatId:', chatId);
116
+
117
+ const card = JSON.stringify({
118
+ config: { wide_screen_mode: true },
119
+ header: { title: { tag: 'plain_text', content: '[explore-card] please ignore' } },
120
+ elements: [{ tag: 'div', text: { tag: 'lark_md', content: 'Cookie protobuf test card' } }],
121
+ });
122
+ const cardBuf = Buffer.from(card, 'utf8');
123
+ console.log('card json size:', cardBuf.length);
124
+
125
+ console.log('\n=== Phase A: try type=14 with each unused field number ===');
126
+ // Known used: 1(text), 2(imageKey), 3(title), 6(fileKey), 7(audioKey), 11(fileName), 14(richText), 24(stickerSetId), 25(stickerId)
127
+ // Unused (potential card field): 4, 5, 8, 9, 10, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23
128
+ const candidates = [4, 5, 8, 9, 10, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23];
129
+ for (const fieldNum of candidates) {
130
+ // Try as string (JSON)
131
+ const ckS = encodeWireField(fieldNum, 'string', card);
132
+ const res = await trySendRaw(userClient, chatId, 14, ckS, `field ${fieldNum} string=cardJSON`);
133
+ if (res.ok) {
134
+ console.log(`\n🎯 SUCCESS at field ${fieldNum}!`);
135
+ }
136
+ }
137
+
138
+ console.log('\n=== Phase B: special — Content field 14 is richText. Try CARD via richText? ===');
139
+ // Skip — richText is for POST type. CARD is different.
140
+
141
+ console.log('\n=== Phase C: combos — maybe card needs type at multiple fields ===');
142
+ // Like image needed thumb. Try card at field 8 + 9 + 10 + 16 etc.
143
+ // Skip for now — Phase A may have answered.
144
+ })().catch(e => { console.error('FATAL:', e.message); process.exit(1); });
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Minimize IMAGE Content fields. Start with known-working combo, drop one field
4
+ // at a time and observe which omissions still pass.
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const protobuf = require('protobufjs');
9
+
10
+ const claudeCfg = JSON.parse(fs.readFileSync(path.join(require('os').homedir(), '.claude.json'), 'utf8'));
11
+ const env = claudeCfg.mcpServers?.['feishu-user-plugin']?.env || {};
12
+ process.env.LARK_COOKIE = env.LARK_COOKIE;
13
+ process.env.LARK_APP_ID = env.LARK_APP_ID;
14
+ process.env.LARK_APP_SECRET = env.LARK_APP_SECRET;
15
+
16
+ const PLUGIN_ROOT = path.join(__dirname, '..');
17
+ const { LarkUserClient } = require(path.join(PLUGIN_ROOT, 'src/clients/user'));
18
+ const { LarkOfficialClient } = require(path.join(PLUGIN_ROOT, 'src/clients/official'));
19
+ const { generateCid, generateRequestId } = require(path.join(PLUGIN_ROOT, 'src/utils'));
20
+
21
+ const TEST_IMAGE = path.join(PLUGIN_ROOT, '.playwright-mcp/captures/test-small.png');
22
+ const TEST_GROUP_NAME = '飞书plugin测试群';
23
+
24
+ const errProto = protobuf.parse(`
25
+ syntax = "proto3";
26
+ message ErrorResponse {
27
+ optional string message = 1;
28
+ optional int32 code = 2;
29
+ optional int32 subCode = 3;
30
+ optional string detail = 4;
31
+ optional string trace = 5;
32
+ optional string requestId = 6;
33
+ }
34
+ `).root;
35
+ const ErrorResponse = errProto.lookupType('ErrorResponse');
36
+
37
+ async function fetchProto(url, opts) {
38
+ const u = new URL(url);
39
+ return new Promise((resolve, reject) => {
40
+ const req = require('node:https').request({
41
+ hostname: u.hostname,
42
+ port: u.port || 443,
43
+ path: u.pathname + u.search,
44
+ method: opts.method || 'GET',
45
+ headers: opts.headers || {},
46
+ }, (res) => {
47
+ const chunks = [];
48
+ res.on('data', (c) => chunks.push(c));
49
+ res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks) }));
50
+ });
51
+ req.on('error', reject);
52
+ if (opts.body) req.write(opts.body);
53
+ req.end();
54
+ });
55
+ }
56
+
57
+ function encodeVarint(n) {
58
+ const bytes = [];
59
+ while (n >= 0x80) {
60
+ bytes.push((n & 0x7f) | 0x80);
61
+ n = Math.floor(n / 128);
62
+ }
63
+ bytes.push(n & 0x7f);
64
+ return Buffer.from(bytes);
65
+ }
66
+
67
+ function encodeWireField(num, type, val) {
68
+ if (type === 'varint') return Buffer.concat([Buffer.from([(num << 3) | 0]), encodeVarint(val)]);
69
+ if (type === 'string') {
70
+ const v = Buffer.from(val, 'utf8');
71
+ return Buffer.concat([Buffer.from([(num << 3) | 2]), encodeVarint(v.length), v]);
72
+ }
73
+ throw new Error('unknown type');
74
+ }
75
+
76
+ function buildContent(fields) {
77
+ return Buffer.concat(fields.map(([n, t, v]) => encodeWireField(n, t, v)));
78
+ }
79
+
80
+ async function trySendRaw(userClient, chatId, msgType, contentBytes, label) {
81
+ const proto = userClient.proto;
82
+ const PutMessageRequest = proto.lookupType('PutMessageRequest');
83
+ const Packet = proto.lookupType('Packet');
84
+
85
+ const restReq = PutMessageRequest.encode(PutMessageRequest.create({
86
+ type: msgType, chatId, cid: generateCid(), isNotified: true, version: 1,
87
+ })).finish();
88
+ const contentField = Buffer.concat([
89
+ Buffer.from([(2 << 3) | 2]),
90
+ encodeVarint(contentBytes.length),
91
+ contentBytes,
92
+ ]);
93
+ const reqBuf = Buffer.concat([contentField, restReq]);
94
+
95
+ const packetBuf = Packet.encode(Packet.create({
96
+ payloadType: 1,
97
+ cmd: 5,
98
+ cid: generateRequestId(),
99
+ payload: reqBuf,
100
+ })).finish();
101
+
102
+ const res = await fetchProto('https://internal-api-lark-api.feishu.cn/im/gateway/', {
103
+ method: 'POST',
104
+ headers: userClient._protoHeaders(5, '5.7.0'),
105
+ body: packetBuf,
106
+ });
107
+
108
+ let parsedErr = null;
109
+ try { parsedErr = ErrorResponse.toObject(ErrorResponse.decode(res.body)); } catch (_) {}
110
+
111
+ const status = res.status === 200 ? '✓ OK' : `✗ ${res.status}`;
112
+ const errMsg = parsedErr?.message?.slice(0, 100) || '';
113
+ console.log(`${status} [${label}] ${errMsg}`);
114
+ return { ok: res.status === 200, status: res.status, err: errMsg };
115
+ }
116
+
117
+ (async () => {
118
+ const oc = new LarkOfficialClient(process.env.LARK_APP_ID, process.env.LARK_APP_SECRET);
119
+ const r = await oc.uploadImage(TEST_IMAGE, 'message');
120
+ const imageKey = r.imageKey;
121
+ console.log('imageKey:', imageKey);
122
+
123
+ const userClient = new LarkUserClient(process.env.LARK_COOKIE);
124
+ await userClient.init();
125
+ const sr = await userClient.search(TEST_GROUP_NAME);
126
+ const chatId = sr.find(x => x.type === 'group' && x.title.includes(TEST_GROUP_NAME)).id;
127
+ console.log('chatId:', chatId);
128
+
129
+ // ALL fields baseline (known to work)
130
+ const allFields = [
131
+ [2, 'string', imageKey], // imageKey
132
+ [4, 'varint', 50], // ?
133
+ [5, 'varint', 50], // ?
134
+ [8, 'string', 'image/png'], // mime?
135
+ [9, 'varint', 141], // size?
136
+ [10, 'string', imageKey], // thumbnail?
137
+ ];
138
+
139
+ console.log('\n=== Verify baseline (all 6 fields) ===');
140
+ await trySendRaw(userClient, chatId, 5, buildContent(allFields), 'baseline all fields');
141
+
142
+ console.log('\n=== Drop each field individually ===');
143
+ // Field labels: imageKey is required; we test which of the OTHER 5 are required
144
+ const others = [4, 5, 8, 9, 10];
145
+ for (const dropField of others) {
146
+ const subset = allFields.filter(([n]) => n !== dropField);
147
+ const result = await trySendRaw(userClient, chatId, 5, buildContent(subset), `omit field ${dropField}`);
148
+ }
149
+
150
+ console.log('\n=== Drop pairs / minimize ===');
151
+ // Just imageKey alone (already known to fail)
152
+ await trySendRaw(userClient, chatId, 5, buildContent([allFields[0]]), 'just imageKey');
153
+ // imageKey + field 10 (thumb)
154
+ await trySendRaw(userClient, chatId, 5, buildContent([allFields[0], allFields[5]]), 'imageKey + thumb(10)');
155
+ // imageKey + 4 + 5 (dims)
156
+ await trySendRaw(userClient, chatId, 5, buildContent([allFields[0], allFields[1], allFields[2]]), 'imageKey + 4 + 5');
157
+ // imageKey + 4 + 5 + 10
158
+ await trySendRaw(userClient, chatId, 5, buildContent([allFields[0], allFields[1], allFields[2], allFields[5]]), 'imageKey + 4 + 5 + 10');
159
+ // imageKey + 8 + 10 (mime + thumb)
160
+ await trySendRaw(userClient, chatId, 5, buildContent([allFields[0], allFields[3], allFields[5]]), 'imageKey + mime(8) + thumb(10)');
161
+ // imageKey + 4 + 5 + 8 (no size, no thumb)
162
+ await trySendRaw(userClient, chatId, 5, buildContent([allFields[0], allFields[1], allFields[2], allFields[3]]), 'imageKey + 4 + 5 + mime(8)');
163
+ })().catch(e => { console.error('FATAL:', e.message); process.exit(1); });
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ // Render docs/og.svg → docs/og.png at 1200x630.
3
+ //
4
+ // Idempotent. Run `node scripts/generate-og-image.js` after editing
5
+ // docs/og.svg. Commit both the SVG (source) and the PNG (asset used
6
+ // by social-media unfurls and `<meta property="og:image">`).
7
+ //
8
+ // Why PNG: Twitter / WeChat / 飞书 unfurls don't render SVG `og:image`.
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { Resvg } = require('@resvg/resvg-js');
15
+
16
+ const svgPath = path.join(__dirname, '..', 'docs', 'og.svg');
17
+ const pngPath = path.join(__dirname, '..', 'docs', 'og.png');
18
+
19
+ const svg = fs.readFileSync(svgPath, 'utf8');
20
+
21
+ const resvg = new Resvg(svg, {
22
+ fitTo: { mode: 'width', value: 1200 },
23
+ // Try to use system fonts so Chinese characters render. resvg-js loads
24
+ // fonts from `font.fontDirs` and `font.defaultFontFamily`. On macOS
25
+ // /System/Library/Fonts has PingFang SC; on Linux CI, jekyll-seo-tag
26
+ // doesn't run this script anyway — only humans do, locally.
27
+ font: {
28
+ fontDirs: ['/System/Library/Fonts', '/Library/Fonts', '/usr/share/fonts'],
29
+ loadSystemFonts: true,
30
+ defaultFontFamily: 'PingFang SC',
31
+ },
32
+ background: '#0d1117',
33
+ });
34
+
35
+ const pngBuffer = resvg.render().asPng();
36
+ fs.writeFileSync(pngPath, pngBuffer);
37
+
38
+ const sizeKb = (pngBuffer.length / 1024).toFixed(1);
39
+ console.log(`OK: wrote ${pngPath} (${pngBuffer.length} bytes / ${sizeKb} KiB)`);