feishu-user-plugin 1.3.10 → 1.3.12
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 +2 -2
- package/.cursor-plugin/plugin.json +27 -0
- package/.mcpb/manifest.json +91 -0
- package/CHANGELOG.md +118 -0
- package/PRIVACY.md +105 -0
- package/README.en.md +130 -413
- package/README.md +88 -258
- package/package.json +5 -3
- package/scripts/build-mcpb.js +119 -0
- package/scripts/check-description-drift.js +73 -0
- package/scripts/check-docs-sync.js +7 -16
- package/scripts/check-mcp-registry-version.js +43 -0
- package/scripts/check-mcpb-version.js +33 -0
- package/scripts/check-scopes.js +99 -0
- package/scripts/check-tool-count.js +4 -3
- package/scripts/check-version.js +5 -0
- package/scripts/sync-claude-md.sh +3 -4
- package/scripts/sync-team-skills.sh +72 -57
- package/scripts/verify-app-name.js +64 -0
- package/skills/feishu-user-plugin/SKILL.md +3 -3
- package/skills/feishu-user-plugin/references/search.md +3 -3
- package/src/auth/credentials-monitor.js +185 -0
- package/src/auth/credentials.js +49 -0
- package/src/auth/identity-state.js +204 -0
- package/src/auth/lark-desktop.js +135 -0
- package/src/auth/uat.js +49 -35
- package/src/cli.js +87 -0
- package/src/clients/official/base.js +145 -14
- package/src/clients/official/calendar.js +3 -1
- package/src/clients/official/im.js +76 -2
- package/src/clients/official/okr.js +2 -1
- package/src/error-codes.js +40 -0
- package/src/events/lockfile.js +40 -4
- package/src/events/owner.js +11 -2
- package/src/index.js +1 -1
- package/src/logger.js +11 -5
- package/src/oauth.js +46 -10
- package/src/server.js +102 -37
- package/src/setup.js +44 -0
- package/src/test-all.js +40 -0
- package/src/test-cli-tool.js +87 -0
- package/src/test-credentials-monitor.js +124 -0
- package/src/test-display-label.js +88 -0
- package/src/test-error-codes.js +85 -0
- package/src/test-identity-state.js +172 -0
- package/src/test-lark-desktop.js +300 -0
- package/src/test-lockfile-pid.js +90 -0
- package/src/test-lru-cache.js +145 -0
- package/src/test-negative-cache.js +85 -0
- package/src/test-populate-sender-names.js +98 -0
- package/src/test-search-messages.js +101 -0
- package/src/test-send-shape.js +115 -0
- package/src/test-via-user.js +94 -0
- package/src/test-with-uat-retry.js +135 -0
- package/src/tools/_registry.js +24 -1
- package/src/tools/calendar.js +5 -5
- package/src/tools/im-read.js +52 -4
- package/src/tools/messaging-user.js +1 -1
- package/src/utils.js +83 -0
- package/scripts/generate-og-image.js +0 -39
- package/skills/feishu-user-plugin/references/CLAUDE.md +0 -523
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Verifies every manifest description / long_description references only
|
|
4
|
+
// the current package.json::version (or no version at all).
|
|
5
|
+
//
|
|
6
|
+
// Catches the "plugin.json description stuck at v1.3.8 for 3 releases"
|
|
7
|
+
// class of bug: a CI gate would have flagged it on the v1.3.9 release PR.
|
|
8
|
+
//
|
|
9
|
+
// Rule: every `vX.Y.Z` token inside the listed description fields must
|
|
10
|
+
// equal the current package.json::version. To keep a description across
|
|
11
|
+
// releases without churn, drop the version reference entirely.
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const ROOT = path.join(__dirname, '..');
|
|
17
|
+
const VERSION = require(path.join(ROOT, 'package.json')).version;
|
|
18
|
+
|
|
19
|
+
// Match `vX.Y.Z` only (must have leading `v`) — avoids false positives on
|
|
20
|
+
// schema versions like "0.3" or random numbers.
|
|
21
|
+
const VERSION_PATTERN = /v(\d+\.\d+\.\d+)/g;
|
|
22
|
+
|
|
23
|
+
const SOURCES = [
|
|
24
|
+
{ label: 'package.json::description', file: 'package.json', extract: (raw) => JSON.parse(raw).description },
|
|
25
|
+
{ label: '.claude-plugin/plugin.json::description', file: '.claude-plugin/plugin.json', extract: (raw) => JSON.parse(raw).description },
|
|
26
|
+
{ label: '.cursor-plugin/plugin.json::description', file: '.cursor-plugin/plugin.json', extract: (raw) => JSON.parse(raw).description },
|
|
27
|
+
{ label: 'mcp-registry.json::description', file: 'mcp-registry.json', extract: (raw) => JSON.parse(raw).description },
|
|
28
|
+
{ label: '.mcpb/manifest.json::description', file: '.mcpb/manifest.json', extract: (raw) => JSON.parse(raw).description },
|
|
29
|
+
{ label: '.mcpb/manifest.json::long_description', file: '.mcpb/manifest.json', extract: (raw) => JSON.parse(raw).long_description },
|
|
30
|
+
{ label: 'skills/feishu-user-plugin/SKILL.md description', file: 'skills/feishu-user-plugin/SKILL.md', extract: extractSkillDescription },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function extractSkillDescription(raw) {
|
|
34
|
+
// SKILL.md frontmatter has description: "..." on a single line.
|
|
35
|
+
const m = raw.match(/^description:\s*"((?:[^"\\]|\\.)*)"/m);
|
|
36
|
+
return m ? m[1].replace(/\\"/g, '"') : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const failures = [];
|
|
40
|
+
|
|
41
|
+
for (const src of SOURCES) {
|
|
42
|
+
const fullPath = path.join(ROOT, src.file);
|
|
43
|
+
if (!fs.existsSync(fullPath)) {
|
|
44
|
+
failures.push(`${src.label}: source file ${src.file} does not exist`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let description;
|
|
49
|
+
try {
|
|
50
|
+
description = src.extract(fs.readFileSync(fullPath, 'utf8'));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
failures.push(`${src.label}: parse error — ${e.message}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!description) continue; // Field absent — nothing to check.
|
|
57
|
+
|
|
58
|
+
for (const m of description.matchAll(VERSION_PATTERN)) {
|
|
59
|
+
const found = m[1];
|
|
60
|
+
if (found !== VERSION) {
|
|
61
|
+
failures.push(`${src.label}: references v${found}, but package.json is v${VERSION}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (failures.length) {
|
|
67
|
+
console.error('description drift detected:');
|
|
68
|
+
for (const f of failures) console.error(` ${f}`);
|
|
69
|
+
console.error(`\nFix: update each description to reference v${VERSION}, or remove the version reference entirely (e.g. drop "v1.3.8: feature X" → "feature X").`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log(`OK: all manifest descriptions reference v${VERSION} (or no version reference)`);
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// Verifies CLAUDE.md is in sync with AGENTS.md (Codex)
|
|
4
|
-
// skills/feishu-user-plugin/references/CLAUDE.md (skill reference copy).
|
|
3
|
+
// Verifies CLAUDE.md is in sync with AGENTS.md (Codex).
|
|
5
4
|
//
|
|
6
|
-
// Pre-commit hook (scripts/sync-claude-md.sh) already auto-regenerates
|
|
5
|
+
// Pre-commit hook (scripts/sync-claude-md.sh) already auto-regenerates AGENTS.md
|
|
7
6
|
// from CLAUDE.md, but this script gives prepublishOnly + CI a hard gate.
|
|
8
7
|
//
|
|
9
|
-
// Match logic mirrors validate.yml's diff
|
|
10
|
-
// AGENTS.md = "# feishu-user-plugin — Codex
|
|
11
|
-
// skills/.../CLAUDE.md = identical to CLAUDE.md
|
|
8
|
+
// Match logic mirrors validate.yml's diff step:
|
|
9
|
+
// AGENTS.md = "# feishu-user-plugin — Codex 指令\n" + tail -n +2 CLAUDE.md
|
|
12
10
|
|
|
13
11
|
const fs = require('fs');
|
|
14
12
|
const path = require('path');
|
|
@@ -16,9 +14,9 @@ const path = require('path');
|
|
|
16
14
|
const ROOT = path.join(__dirname, '..');
|
|
17
15
|
const claude = fs.readFileSync(path.join(ROOT, 'CLAUDE.md'), 'utf8');
|
|
18
16
|
|
|
19
|
-
// AGENTS.md: header replaced with "# feishu-user-plugin — Codex
|
|
17
|
+
// AGENTS.md: header replaced with "# feishu-user-plugin — Codex 指令"
|
|
20
18
|
const claudeBody = claude.split('\n').slice(1).join('\n'); // drop first line
|
|
21
|
-
const expectedAgents = '# feishu-user-plugin — Codex
|
|
19
|
+
const expectedAgents = '# feishu-user-plugin — Codex 指令\n' + claudeBody;
|
|
22
20
|
const actualAgents = fs.readFileSync(path.join(ROOT, 'AGENTS.md'), 'utf8');
|
|
23
21
|
|
|
24
22
|
const failures = [];
|
|
@@ -27,15 +25,8 @@ if (actualAgents !== expectedAgents) {
|
|
|
27
25
|
failures.push('Fix: bash scripts/sync-claude-md.sh (or edit CLAUDE.md and re-stage)');
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
const skillRef = path.join(ROOT, 'skills', 'feishu-user-plugin', 'references', 'CLAUDE.md');
|
|
31
|
-
const actualSkillRef = fs.readFileSync(skillRef, 'utf8');
|
|
32
|
-
if (actualSkillRef !== claude) {
|
|
33
|
-
failures.push('skills/feishu-user-plugin/references/CLAUDE.md is out of sync with CLAUDE.md');
|
|
34
|
-
failures.push('Fix: bash scripts/sync-claude-md.sh (or edit CLAUDE.md and re-stage)');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
28
|
if (failures.length) {
|
|
38
29
|
for (const f of failures) console.error(f);
|
|
39
30
|
process.exit(1);
|
|
40
31
|
}
|
|
41
|
-
console.log('OK: CLAUDE.md / AGENTS.md
|
|
32
|
+
console.log('OK: CLAUDE.md / AGENTS.md in sync');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Verifies mcp-registry.json::version + packages[0].version == package.json::version.
|
|
4
|
+
// Wired into:
|
|
5
|
+
// - .github/workflows/publish.yml — pre-publish gate so CI never publishes to the
|
|
6
|
+
// official MCP Registry with a stale version string.
|
|
7
|
+
// - .github/workflows/validate.yml — PR-time gate so any version bump on
|
|
8
|
+
// package.json without a matching bump on mcp-registry.json fails before merge.
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const ROOT = path.join(__dirname, '..');
|
|
13
|
+
|
|
14
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
15
|
+
const pkgVersion = pkg.version;
|
|
16
|
+
|
|
17
|
+
const registryPath = path.join(ROOT, 'mcp-registry.json');
|
|
18
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
19
|
+
const registryVersion = registry.version;
|
|
20
|
+
|
|
21
|
+
if (!Array.isArray(registry.packages) || registry.packages.length === 0) {
|
|
22
|
+
console.error('ERROR: mcp-registry.json has no packages[] entries');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const pkgEntryVersion = registry.packages[0].version;
|
|
26
|
+
|
|
27
|
+
const sources = [
|
|
28
|
+
{ label: 'package.json', version: pkgVersion, path: 'package.json' },
|
|
29
|
+
{ label: 'mcp-registry.json::version', version: registryVersion, path: 'mcp-registry.json' },
|
|
30
|
+
{ label: 'mcp-registry.json::packages[0].version', version: pkgEntryVersion, path: 'mcp-registry.json' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const allEqual = sources.every((s) => s.version === sources[0].version);
|
|
34
|
+
|
|
35
|
+
if (!allEqual) {
|
|
36
|
+
console.error('ERROR: mcp-registry.json version mismatch with package.json!');
|
|
37
|
+
sources.forEach((s) => console.error(` ${s.label}: ${s.version}`));
|
|
38
|
+
console.error('Fix: bump mcp-registry.json::version AND mcp-registry.json::packages[0].version');
|
|
39
|
+
console.error(` to match package.json (${pkgVersion}).`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`OK: mcp-registry.json version ${pkgVersion}`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.join(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
// Source 1: package.json
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
10
|
+
const pkgVersion = pkg.version;
|
|
11
|
+
|
|
12
|
+
// Source 2: .mcpb/manifest.json
|
|
13
|
+
const manifestPath = path.join(ROOT, '.mcpb', 'manifest.json');
|
|
14
|
+
if (!fs.existsSync(manifestPath)) {
|
|
15
|
+
console.error('ERROR: .mcpb/manifest.json not found');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
19
|
+
const manifestVersion = manifest.version;
|
|
20
|
+
|
|
21
|
+
if (!manifestVersion) {
|
|
22
|
+
console.error('ERROR: .mcpb/manifest.json is missing the `version` field');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (pkgVersion !== manifestVersion) {
|
|
27
|
+
console.error('ERROR: .mcpb manifest version mismatch!');
|
|
28
|
+
console.error(` package.json: ${pkgVersion}`);
|
|
29
|
+
console.error(` .mcpb/manifest.json: ${manifestVersion}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`OK: .mcpb manifest version ${pkgVersion}`);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Validates src/oauth.js::SCOPES against:
|
|
5
|
+
// 1) BANLIST — scope names we've confirmed do NOT exist in Feishu's catalog
|
|
6
|
+
// (caused OAuth 422 / runtime 20043). Add new ones here when discovered.
|
|
7
|
+
// 2) docs/AUTH-SETUP.md mentions — every scope in SCOPES must appear at least
|
|
8
|
+
// once in AUTH-SETUP.md, so the doc never drifts behind the code.
|
|
9
|
+
//
|
|
10
|
+
// Why this gate exists: Feishu's OAuth server SILENTLY accepted some malformed
|
|
11
|
+
// scope names pre-2026-05 (they were ignored, UAT just lacked the scope); from
|
|
12
|
+
// May 2026 it started rejecting the whole authorize request with 422 +
|
|
13
|
+
// "scope <name> 有误". A single bad name in SCOPES locks every user out of
|
|
14
|
+
// `npx oauth`. This script catches it in CI before merge.
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
|
|
19
|
+
const repoRoot = path.join(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
// --- Step 1: extract SCOPES constant from src/oauth.js ---
|
|
22
|
+
const oauthSrc = fs.readFileSync(path.join(repoRoot, 'src', 'oauth.js'), 'utf8');
|
|
23
|
+
const m = oauthSrc.match(/const\s+SCOPES\s*=\s*'([^']+)'/);
|
|
24
|
+
if (!m) {
|
|
25
|
+
console.error('check-scopes: could not find `const SCOPES = \'...\'` in src/oauth.js');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const scopes = m[1].split(/\s+/).filter(Boolean);
|
|
29
|
+
|
|
30
|
+
// --- Step 1b: ADDITIONAL_APP_SCOPES — tenant-side scopes that the plugin requires
|
|
31
|
+
// but that don't live in SCOPES (because they can't be granted via OAuth — only
|
|
32
|
+
// in the Feishu app console "应用身份" tab). Validated against AUTH-SETUP.md only.
|
|
33
|
+
const ADDITIONAL_APP_SCOPES = [
|
|
34
|
+
// Used by LarkOfficialClient.getAppName() to resolve self-app display label.
|
|
35
|
+
// Without it, `senderType=app` messages fall back to "[Bot] (cli_xxx)".
|
|
36
|
+
// Feishu marks this scope as 免审权限 (no admin review needed).
|
|
37
|
+
'application:application:self_manage',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// --- Step 2: BANLIST of known-bad scope names ---
|
|
41
|
+
//
|
|
42
|
+
// Each entry: { bad: '<name>', reason: '<why>', replacement: '<correct names>' }
|
|
43
|
+
// Append-only — never remove an entry (it's a regression guard).
|
|
44
|
+
const BANLIST = [
|
|
45
|
+
{
|
|
46
|
+
bad: 'calendar:calendar.event:write',
|
|
47
|
+
reason: 'Feishu catalog has no such scope. The catalog splits write into 4 verbs.',
|
|
48
|
+
replacement: 'calendar:calendar.event:create + calendar:calendar.event:update + calendar:calendar.event:delete + calendar:calendar.event:reply',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
bad: 'okr:okr.content:write',
|
|
52
|
+
reason: 'Feishu catalog uses :writeonly (one word) not :write.',
|
|
53
|
+
replacement: 'okr:okr.content:writeonly',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// --- Step 3: validate ---
|
|
58
|
+
const failures = [];
|
|
59
|
+
|
|
60
|
+
for (const entry of BANLIST) {
|
|
61
|
+
if (scopes.includes(entry.bad)) {
|
|
62
|
+
failures.push(
|
|
63
|
+
`BANLIST hit: SCOPES contains \`${entry.bad}\`.\n` +
|
|
64
|
+
` Reason: ${entry.reason}\n` +
|
|
65
|
+
` Replace with: ${entry.replacement}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// docs/AUTH-SETUP.md must mention every scope. Catches silent additions
|
|
71
|
+
// to SCOPES that never made it into the OAuth setup docs.
|
|
72
|
+
const authSetupPath = path.join(repoRoot, 'docs', 'AUTH-SETUP.md');
|
|
73
|
+
const authSetup = fs.readFileSync(authSetupPath, 'utf8');
|
|
74
|
+
const missingFromDocs = scopes.filter(s => s !== 'offline_access' && !authSetup.includes(s));
|
|
75
|
+
if (missingFromDocs.length) {
|
|
76
|
+
failures.push(
|
|
77
|
+
`${missingFromDocs.length} scope(s) in SCOPES not mentioned in docs/AUTH-SETUP.md:\n` +
|
|
78
|
+
missingFromDocs.map(s => ` - ${s}`).join('\n') +
|
|
79
|
+
`\n Add them to the scope table around line 117 (\`## OAuth Scopes\` section).`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Same enforcement for tenant-only scopes.
|
|
84
|
+
const missingAppScopes = ADDITIONAL_APP_SCOPES.filter(s => !authSetup.includes(s));
|
|
85
|
+
if (missingAppScopes.length) {
|
|
86
|
+
failures.push(
|
|
87
|
+
`${missingAppScopes.length} tenant-side scope(s) not in docs/AUTH-SETUP.md:\n` +
|
|
88
|
+
missingAppScopes.map(s => ` - ${s}`).join('\n') +
|
|
89
|
+
`\n Add them to the "应用身份额外 scope" section.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (failures.length) {
|
|
94
|
+
console.error('check-scopes: FAIL\n');
|
|
95
|
+
for (const f of failures) console.error(f + '\n');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(`check-scopes: OK (${scopes.length} OAuth + ${ADDITIONAL_APP_SCOPES.length} tenant-only scopes, ${BANLIST.length} banned names guarded)`);
|
|
@@ -6,11 +6,12 @@ const { TOOLS } = require(path.join(__dirname, '..', 'src', 'server'));
|
|
|
6
6
|
|
|
7
7
|
const failures = [];
|
|
8
8
|
|
|
9
|
-
// Source 1: README.md "N tools"
|
|
9
|
+
// Source 1: README.md tool count — accepts "N tools" (English) or "N 工具" (Chinese)
|
|
10
|
+
// since README.md is Chinese-primary while README.en.md mirrors in English.
|
|
10
11
|
const readme = fs.readFileSync(path.join(__dirname, '..', 'README.md'), 'utf8');
|
|
11
|
-
const readmeMatch = readme.match(/(\d+)\s
|
|
12
|
+
const readmeMatch = readme.match(/(\d+)\s*(?:tools|工具)/);
|
|
12
13
|
if (!readmeMatch) {
|
|
13
|
-
failures.push('No "N tools"
|
|
14
|
+
failures.push('No "N tools" / "N 工具" marker in README.md');
|
|
14
15
|
} else if (parseInt(readmeMatch[1], 10) !== TOOLS.length) {
|
|
15
16
|
failures.push(`README.md claims ${readmeMatch[1]} tools, src/server.js has ${TOOLS.length}`);
|
|
16
17
|
}
|
package/scripts/check-version.js
CHANGED
|
@@ -22,10 +22,15 @@ if (!skillMatch) {
|
|
|
22
22
|
}
|
|
23
23
|
const skillVersion = skillMatch[1];
|
|
24
24
|
|
|
25
|
+
// Source 4: .cursor-plugin/plugin.json
|
|
26
|
+
const cursorPlugin = JSON.parse(fs.readFileSync(path.join(ROOT, '.cursor-plugin', 'plugin.json'), 'utf8'));
|
|
27
|
+
const cursorVersion = cursorPlugin.version;
|
|
28
|
+
|
|
25
29
|
const sources = [
|
|
26
30
|
{ label: 'package.json', version: pkgVersion, path: 'package.json' },
|
|
27
31
|
{ label: '.claude-plugin/plugin.json', version: pluginVersion, path: '.claude-plugin/plugin.json' },
|
|
28
32
|
{ label: 'skills/feishu-user-plugin/SKILL.md', version: skillVersion, path: 'skills/feishu-user-plugin/SKILL.md' },
|
|
33
|
+
{ label: '.cursor-plugin/plugin.json', version: cursorVersion, path: '.cursor-plugin/plugin.json' },
|
|
29
34
|
];
|
|
30
35
|
|
|
31
36
|
const versions = sources.map((s) => s.version);
|
|
@@ -4,9 +4,8 @@ ROOT="$(git rev-parse --show-toplevel)"
|
|
|
4
4
|
cd "$ROOT"
|
|
5
5
|
if git diff --cached --name-only | grep -qx "CLAUDE.md"; then
|
|
6
6
|
tail -n +2 CLAUDE.md > /tmp/feishu-claude-body.$$
|
|
7
|
-
{ echo "# feishu-user-plugin — Codex
|
|
7
|
+
{ echo "# feishu-user-plugin — Codex 指令"; cat /tmp/feishu-claude-body.$$; } > AGENTS.md
|
|
8
8
|
rm -f /tmp/feishu-claude-body.$$
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
echo "[hook] CLAUDE.md → AGENTS.md + skill reference synced"
|
|
9
|
+
git add AGENTS.md
|
|
10
|
+
echo "[hook] CLAUDE.md → AGENTS.md synced"
|
|
12
11
|
fi
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# scripts/sync-team-skills.sh — post-merge hook on main.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# 2. Run team-skills' generate-catalog.py (forced manual-yaml path for byte
|
|
7
|
-
# parity with CI)
|
|
8
|
-
# 3. Run our scripts/generate-release-artifacts.js to produce
|
|
9
|
-
# changelog + readme-row from CHANGELOG.md
|
|
10
|
-
# 4. Inject the changelog block into team-skills child README before the
|
|
11
|
-
# previous version's heading
|
|
12
|
-
# 5. Replace the team-skills root README catalog row matching feishu-user-plugin
|
|
13
|
-
# 6. Commit + push branch + open PR
|
|
14
|
-
# 7. Auto-merge: --admin --squash (we have admin on team-skills repo;
|
|
15
|
-
# org-level setting blocks repo PATCH for allow_auto_merge so we use
|
|
16
|
-
# --admin to bypass review wait. CI is non-blocking via "Check catalog"
|
|
17
|
-
# drift never happening since step 2 produced byte-identical output.)
|
|
4
|
+
# Idempotent + conflict-resilient sync from feishu-user-plugin's main into
|
|
5
|
+
# zhuzhen-team/team-skills. Designed so retries always converge:
|
|
18
6
|
#
|
|
19
|
-
#
|
|
7
|
+
# Flow:
|
|
8
|
+
# 1. Generate release artifacts in feishu repo (changelog block + readme row).
|
|
9
|
+
# 2. cd team-skills repo, fetch origin main.
|
|
10
|
+
# 3. Close any stale OPEN sync PRs whose branch is for an older version
|
|
11
|
+
# (so v1.3.10 sync doesn't pile up behind a never-merged v1.3.9 sync).
|
|
12
|
+
# 4. Delete any local stale sync/feishu-v$VERSION branch and recreate from
|
|
13
|
+
# origin/main — always starts fresh, never carries leftover commits.
|
|
14
|
+
# 5. Copy plugin tree + inject changelog + replace catalog row + regen catalog.
|
|
15
|
+
# 6. If nothing changed → exit 0 (already in sync for v$VERSION).
|
|
16
|
+
# 7. Commit + push --force-with-lease (safe: only this script writes to sync/* branches).
|
|
17
|
+
# 8. Open PR if not exists; merge --admin --squash.
|
|
18
|
+
#
|
|
19
|
+
# Failure modes:
|
|
20
20
|
# - team-skills repo not cloned at expected path → clean skip
|
|
21
|
-
# -
|
|
22
|
-
# -
|
|
23
|
-
#
|
|
21
|
+
# - generate-release-artifacts.js fails → exit non-zero (visible in stderr)
|
|
22
|
+
# - PR merge fails (rare; should be impossible after force-recreate from origin/main)
|
|
23
|
+
# → exit non-zero, post-merge wrapper labels as "non-fatal" but user sees stderr
|
|
24
24
|
set -e
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
TEAM_SKILLS_REPO="/Users/abble/team-skills"
|
|
27
|
+
TEAM_SKILLS="$TEAM_SKILLS_REPO/plugins/feishu-user-plugin"
|
|
26
28
|
if [ ! -d "$TEAM_SKILLS" ]; then echo "[hook] team-skills not present, skip"; exit 0; fi
|
|
27
29
|
|
|
28
30
|
ROOT="$(git rev-parse --show-toplevel)"
|
|
@@ -30,25 +32,43 @@ cd "$ROOT"
|
|
|
30
32
|
|
|
31
33
|
VERSION=$(node -e "console.log(require('./package.json').version)")
|
|
32
34
|
ARTIFACTS="/tmp/feishu-release/v${VERSION}"
|
|
35
|
+
BRANCH="sync/feishu-v$VERSION"
|
|
33
36
|
|
|
34
|
-
# Generate release artifacts FIRST so we can inject them into team-skills.
|
|
35
|
-
# This reads CHANGELOG.md for the v$VERSION section and emits team-skills
|
|
36
|
-
# changelog markdown + root readme row + announcement card JSON.
|
|
37
|
+
# 1. Generate release artifacts FIRST so we can inject them into team-skills.
|
|
37
38
|
node scripts/generate-release-artifacts.js "$VERSION" >/dev/null
|
|
38
39
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
# 2. cd team-skills, fetch origin main.
|
|
41
|
+
cd "$TEAM_SKILLS_REPO"
|
|
42
|
+
git fetch origin main --quiet
|
|
43
|
+
|
|
44
|
+
# 3. Close any stale OPEN sync PRs (different version branch). Idempotent —
|
|
45
|
+
# any matching PR for this same $VERSION is preserved (we'll force-update
|
|
46
|
+
# its branch in step 7 instead).
|
|
47
|
+
STALE_PRS=$(gh pr list --state open --search "Sync feishu-user-plugin in:title" \
|
|
48
|
+
--json number,headRefName --jq ".[] | select(.headRefName != \"$BRANCH\") | .number")
|
|
49
|
+
if [ -n "$STALE_PRS" ]; then
|
|
50
|
+
for stale_num in $STALE_PRS; do
|
|
51
|
+
gh pr close "$stale_num" \
|
|
52
|
+
--comment "Superseded by sync/feishu-v$VERSION (auto-closed by sync-team-skills.sh)" \
|
|
53
|
+
--delete-branch 2>&1 | tail -1 || true
|
|
54
|
+
echo "[hook] closed stale sync PR #$stale_num"
|
|
55
|
+
done
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# 4. Delete any local stale sync branch + recreate from origin/main.
|
|
59
|
+
# `git checkout -B` is "create or reset". We always start from latest main
|
|
60
|
+
# so there are no inherited commits from older sync attempts.
|
|
61
|
+
git checkout -B "$BRANCH" origin/main
|
|
62
|
+
|
|
63
|
+
# 5. Copy plugin tree from feishu repo, inject changelog, regen catalog.
|
|
64
|
+
cp -r "$ROOT/skills/." "$TEAM_SKILLS/skills/"
|
|
65
|
+
cp "$ROOT/.claude-plugin/plugin.json" "$TEAM_SKILLS/.claude-plugin/"
|
|
42
66
|
|
|
43
|
-
# Inject changelog block into team-skills
|
|
44
|
-
# Insert just before the existing first "### vX.Y.Z" heading, OR after
|
|
45
|
-
# "## 更新日志" if no prior version exists.
|
|
67
|
+
# 5a. Inject changelog block into team-skills child README (idempotent).
|
|
46
68
|
README="$TEAM_SKILLS/README.md"
|
|
47
69
|
if grep -q "^### v${VERSION} " "$README"; then
|
|
48
70
|
echo "[hook] team-skills child README already has v${VERSION} section, skipping inject"
|
|
49
71
|
else
|
|
50
|
-
# awk: print everything; when we hit the FIRST `### vX.Y.Z (date)` heading,
|
|
51
|
-
# insert the new block before it.
|
|
52
72
|
awk -v block_file="$ARTIFACTS/team-skills-changelog.md" '
|
|
53
73
|
BEGIN { inserted = 0 }
|
|
54
74
|
/^### v[0-9]+\.[0-9]+\.[0-9]+ \(/ && !inserted {
|
|
@@ -61,14 +81,12 @@ else
|
|
|
61
81
|
echo "[hook] injected v${VERSION} changelog block into child README"
|
|
62
82
|
fi
|
|
63
83
|
|
|
64
|
-
# Replace
|
|
65
|
-
ROOT_README="$
|
|
84
|
+
# 5b. Replace root README catalog row matching feishu-user-plugin.
|
|
85
|
+
ROOT_README="$TEAM_SKILLS_REPO/README.md"
|
|
66
86
|
NEW_ROW=$(cat "$ARTIFACTS/team-skills-readme-row.md")
|
|
67
87
|
if grep -q "^| \\*\\*feishu-user-plugin\\*\\* |" "$ROOT_README"; then
|
|
68
|
-
# Replace the line in-place. Use Python (sed regex with table chars + |
|
|
69
|
-
# quotes is brittle across BSD/GNU).
|
|
70
88
|
python3 -c "
|
|
71
|
-
import
|
|
89
|
+
import re
|
|
72
90
|
p = '$ROOT_README'
|
|
73
91
|
new_row = '''$NEW_ROW'''.strip()
|
|
74
92
|
text = open(p, 'r', encoding='utf-8').read()
|
|
@@ -78,47 +96,44 @@ open(p, 'w', encoding='utf-8').write(text)
|
|
|
78
96
|
echo "[hook] updated root README catalog row to v${VERSION}"
|
|
79
97
|
fi
|
|
80
98
|
|
|
81
|
-
#
|
|
82
|
-
cd "$TEAM_SKILLS/../.."
|
|
83
|
-
|
|
84
|
-
BRANCH="sync/feishu-v$VERSION"
|
|
85
|
-
if git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then
|
|
86
|
-
echo "[hook] branch $BRANCH already exists locally, skipping"; exit 0
|
|
87
|
-
fi
|
|
88
|
-
git checkout -b "$BRANCH"
|
|
89
|
-
|
|
90
|
-
# team-skills CI runs generate-catalog.py without PyYAML; force the same path
|
|
91
|
-
# locally for byte-identical output. Verified in PR #36.
|
|
99
|
+
# 5c. Regenerate catalog.yaml (force PyYAML-less path for byte parity with CI).
|
|
92
100
|
if [ -f "scripts/generate-catalog.py" ]; then
|
|
93
101
|
python3 -c "import sys, runpy; sys.modules['yaml']=None; runpy.run_path('scripts/generate-catalog.py', run_name='__main__')" >/dev/null 2>&1
|
|
94
102
|
fi
|
|
95
103
|
|
|
96
|
-
# Stage
|
|
97
|
-
# on already-clean files, so unchanged ones stage as no-op. Files that don't
|
|
98
|
-
# exist (e.g., catalog.yaml when team-skills repo doesn't have the generator)
|
|
99
|
-
# would fail under `set -e`, so guard explicitly.
|
|
104
|
+
# 6. Stage everything the hook touched.
|
|
100
105
|
git add "plugins/feishu-user-plugin/"
|
|
101
106
|
[ -f "README.md" ] && git add README.md
|
|
102
107
|
[ -f "catalog.yaml" ] && git add catalog.yaml
|
|
103
108
|
|
|
104
|
-
# If nothing actually changed, exit 0 — the v$VERSION sync was already done.
|
|
105
109
|
if git diff --cached --quiet; then
|
|
106
110
|
echo "[hook] nothing to sync (working tree clean for v$VERSION)"; exit 0
|
|
107
111
|
fi
|
|
108
112
|
|
|
109
113
|
git commit -m "chore: sync feishu-user-plugin v$VERSION (skills + plugin.json + README changelog + catalog)"
|
|
110
|
-
git push -u origin "$BRANCH"
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
# 7. Push (force-with-lease since this branch is exclusively written by this
|
|
116
|
+
# script — safe even if a previous run pushed something we just rebuilt).
|
|
117
|
+
git push --force-with-lease -u origin "$BRANCH"
|
|
118
|
+
|
|
119
|
+
# 8. Open PR if not exists, then merge.
|
|
120
|
+
PR_NUM=$(gh pr list --head "$BRANCH" --state open --json number --jq ".[0].number // empty")
|
|
121
|
+
if [ -z "$PR_NUM" ]; then
|
|
122
|
+
gh pr create --title "Sync feishu-user-plugin v$VERSION" --body "Auto-sync from feishu-user-plugin main. Includes:
|
|
113
123
|
- plugins/feishu-user-plugin/.claude-plugin/plugin.json bumped to v$VERSION
|
|
114
124
|
- plugins/feishu-user-plugin/skills/ regenerated
|
|
115
125
|
- plugins/feishu-user-plugin/README.md: v$VERSION changelog section auto-generated from feishu-user-plugin's CHANGELOG.md
|
|
116
126
|
- README.md (root): catalog row updated
|
|
117
127
|
- catalog.yaml regenerated"
|
|
128
|
+
PR_NUM=$(gh pr view "$BRANCH" --json number --jq .number)
|
|
129
|
+
echo "[hook] opened sync PR #$PR_NUM"
|
|
130
|
+
else
|
|
131
|
+
echo "[hook] reusing existing sync PR #$PR_NUM (branch force-updated)"
|
|
132
|
+
fi
|
|
118
133
|
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
134
|
+
# Use --admin --squash: we have admin permissions on team-skills (verified).
|
|
135
|
+
# After step 4's force-recreate from origin/main, this PR is always cleanly
|
|
136
|
+
# mergeable (no carried conflicts). --admin bypasses required reviews; CI
|
|
137
|
+
# is informational since step 5c produced byte-identical catalog output.
|
|
123
138
|
gh pr merge "$PR_NUM" --admin --squash
|
|
124
139
|
echo "[hook] team-skills sync PR #$PR_NUM merged"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/verify-app-name.js — diagnostic: does the current app have the
|
|
3
|
+
// tenant-side `application:application:self_manage` scope?
|
|
4
|
+
//
|
|
5
|
+
// Hits Feishu's app-info endpoint with the current credentials' APP_ID/SECRET
|
|
6
|
+
// and prints either the resolved app name (scope is granted) or the error
|
|
7
|
+
// code (with remediation pointing at docs/AUTH-SETUP.md).
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// node scripts/verify-app-name.js
|
|
11
|
+
//
|
|
12
|
+
// Exit codes:
|
|
13
|
+
// 0 scope works, displayLabel will say "[Bot] AppName"
|
|
14
|
+
// 1 99991672 — scope missing, displayLabel will fall back to "[Bot] (cli_xxx)"
|
|
15
|
+
// 2 other auth failure (wrong APP_ID/SECRET, network, etc.)
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const { readCredentials } = require('../src/auth/credentials');
|
|
20
|
+
|
|
21
|
+
async function main() {
|
|
22
|
+
const creds = readCredentials() || {};
|
|
23
|
+
const appId = creds.LARK_APP_ID;
|
|
24
|
+
const appSecret = creds.LARK_APP_SECRET;
|
|
25
|
+
if (!appId || !appSecret) {
|
|
26
|
+
console.error('No LARK_APP_ID/SECRET in credentials. Run `npx feishu-user-plugin setup` first.');
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
console.error(`Probing app info for APP_ID=${appId}…`);
|
|
30
|
+
|
|
31
|
+
const tokenRes = await fetch('https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
35
|
+
});
|
|
36
|
+
const tokenData = await tokenRes.json();
|
|
37
|
+
if (!tokenData.app_access_token) {
|
|
38
|
+
console.error(`app_access_token request failed: ${JSON.stringify(tokenData)}`);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const infoRes = await fetch(`https://open.feishu.cn/open-apis/application/v6/applications/${appId}?lang=zh_cn`, {
|
|
43
|
+
headers: { 'Authorization': `Bearer ${tokenData.app_access_token}` },
|
|
44
|
+
});
|
|
45
|
+
const info = await infoRes.json();
|
|
46
|
+
|
|
47
|
+
if (info.code === 0 && info.data?.app?.app_name) {
|
|
48
|
+
console.error(`OK — app name resolves to "${info.data.app.app_name}". displayLabel will read "[Bot] ${info.data.app.app_name}".`);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
if (info.code === 99991672) {
|
|
52
|
+
console.error('FAIL — code 99991672. The tenant-side scope `application:application:self_manage` is not granted.');
|
|
53
|
+
console.error('Fix:');
|
|
54
|
+
console.error(' 1. Open https://open.feishu.cn/app/<appId>/safe — "应用身份" tab');
|
|
55
|
+
console.error(' 2. Add scope `application:application:self_manage` (marked 免审权限 — no admin review needed)');
|
|
56
|
+
console.error(' 3. Save; no re-publish required');
|
|
57
|
+
console.error(' 4. Re-run this script to confirm');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
console.error(`FAIL — unexpected response: code=${info.code} msg=${info.msg || JSON.stringify(info)}`);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch((e) => { console.error(`Threw: ${e.message}`); process.exit(2); });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feishu-user-plugin
|
|
3
|
-
version: "1.3.
|
|
4
|
-
description: "All-in-one Feishu
|
|
5
|
-
allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_as_user, batch_send, send_card_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, list_profiles, switch_profile, manage_profile_hints, read_p2p_messages, list_user_chats, list_chats, read_messages, send_message_as_bot, reply_message, forward_message, delete_message, update_message, add_reaction, delete_reaction, pin_message, create_group, update_group, list_members, manage_members, search_docs, read_doc, get_doc_blocks, create_doc, manage_doc_block, read_doc_markdown, manage_bitable_app, manage_bitable_table, manage_bitable_field, manage_bitable_view, manage_bitable_record, upload_bitable_attachment, list_wiki_spaces, search_wiki, list_wiki_nodes, get_wiki_node, create_wiki_node, update_wiki_node, move_wiki_node, copy_wiki_node, delete_wiki_node, list_files, create_folder, upload_drive_file, manage_drive_file, upload_image, upload_file, download_message_resource, download_doc_image, list_user_okrs, get_okrs, list_okr_periods, create_okr_progress_record, list_okr_progress_records, delete_okr_progress_record, list_calendars, list_calendar_events, get_calendar_event, create_calendar_event, update_calendar_event, delete_calendar_event, respond_calendar_event, get_freebusy, list_tasks, get_task, create_task, update_task, complete_task, delete_task, manage_task_members, get_new_events, manage_ws_status
|
|
3
|
+
version: "1.3.12"
|
|
4
|
+
description: "All-in-one Feishu MCP server + CLI tool — send messages as yourself (incl. batch_send), read group/P2P chats (auto-expands merge_forward), manage docs/tables/wiki (full CRUD)/drive, OKR (with progress writes), calendar (read+write), Tasks v2, multi-profile auto-switch, real-time WS events. v1.3.12: search_messages tool (Protobuf phase 2 B.5, UAT-only), CLI tool mode (`tool list` / `tool help <name>` / `tool <name> '<json>'`), IdentityState state machine + credentials hot-reload (no-restart UAT reload), displayLabel + sender semantics pack for LLM consumption, WS owner PID liveness check, gitleaks secret scan."
|
|
5
|
+
allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_as_user, batch_send, send_card_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, list_profiles, switch_profile, manage_profile_hints, read_p2p_messages, list_user_chats, list_chats, read_messages, search_messages, send_message_as_bot, reply_message, forward_message, delete_message, update_message, add_reaction, delete_reaction, pin_message, create_group, update_group, list_members, manage_members, search_docs, read_doc, get_doc_blocks, create_doc, manage_doc_block, read_doc_markdown, manage_bitable_app, manage_bitable_table, manage_bitable_field, manage_bitable_view, manage_bitable_record, upload_bitable_attachment, list_wiki_spaces, search_wiki, list_wiki_nodes, get_wiki_node, create_wiki_node, update_wiki_node, move_wiki_node, copy_wiki_node, delete_wiki_node, list_files, create_folder, upload_drive_file, manage_drive_file, upload_image, upload_file, download_message_resource, download_doc_image, list_user_okrs, get_okrs, list_okr_periods, create_okr_progress_record, list_okr_progress_records, delete_okr_progress_record, list_calendars, list_calendar_events, get_calendar_event, create_calendar_event, update_calendar_event, delete_calendar_event, respond_calendar_event, get_freebusy, list_tasks, get_task, create_task, update_task, complete_task, delete_task, manage_task_members, get_new_events, manage_ws_status
|
|
6
6
|
user_invocable: true
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
- `/digest 群名` 整理聊天摘要
|
|
16
16
|
|
|
17
17
|
## 通过邮箱或手机号查找
|
|
18
|
-
|
|
18
|
+
邮箱、手机号、姓名都可以作为 `query` 直接传给 `search_contacts`,不需要单独的工具:
|
|
19
19
|
```
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
search_contacts({ query: "xxx@xxx.com" })
|
|
21
|
+
search_contacts({ query: "+86xxx" })
|
|
22
22
|
```
|