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.
Files changed (61) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/.cursor-plugin/plugin.json +27 -0
  3. package/.mcpb/manifest.json +91 -0
  4. package/CHANGELOG.md +118 -0
  5. package/PRIVACY.md +105 -0
  6. package/README.en.md +130 -413
  7. package/README.md +88 -258
  8. package/package.json +5 -3
  9. package/scripts/build-mcpb.js +119 -0
  10. package/scripts/check-description-drift.js +73 -0
  11. package/scripts/check-docs-sync.js +7 -16
  12. package/scripts/check-mcp-registry-version.js +43 -0
  13. package/scripts/check-mcpb-version.js +33 -0
  14. package/scripts/check-scopes.js +99 -0
  15. package/scripts/check-tool-count.js +4 -3
  16. package/scripts/check-version.js +5 -0
  17. package/scripts/sync-claude-md.sh +3 -4
  18. package/scripts/sync-team-skills.sh +72 -57
  19. package/scripts/verify-app-name.js +64 -0
  20. package/skills/feishu-user-plugin/SKILL.md +3 -3
  21. package/skills/feishu-user-plugin/references/search.md +3 -3
  22. package/src/auth/credentials-monitor.js +185 -0
  23. package/src/auth/credentials.js +49 -0
  24. package/src/auth/identity-state.js +204 -0
  25. package/src/auth/lark-desktop.js +135 -0
  26. package/src/auth/uat.js +49 -35
  27. package/src/cli.js +87 -0
  28. package/src/clients/official/base.js +145 -14
  29. package/src/clients/official/calendar.js +3 -1
  30. package/src/clients/official/im.js +76 -2
  31. package/src/clients/official/okr.js +2 -1
  32. package/src/error-codes.js +40 -0
  33. package/src/events/lockfile.js +40 -4
  34. package/src/events/owner.js +11 -2
  35. package/src/index.js +1 -1
  36. package/src/logger.js +11 -5
  37. package/src/oauth.js +46 -10
  38. package/src/server.js +102 -37
  39. package/src/setup.js +44 -0
  40. package/src/test-all.js +40 -0
  41. package/src/test-cli-tool.js +87 -0
  42. package/src/test-credentials-monitor.js +124 -0
  43. package/src/test-display-label.js +88 -0
  44. package/src/test-error-codes.js +85 -0
  45. package/src/test-identity-state.js +172 -0
  46. package/src/test-lark-desktop.js +300 -0
  47. package/src/test-lockfile-pid.js +90 -0
  48. package/src/test-lru-cache.js +145 -0
  49. package/src/test-negative-cache.js +85 -0
  50. package/src/test-populate-sender-names.js +98 -0
  51. package/src/test-search-messages.js +101 -0
  52. package/src/test-send-shape.js +115 -0
  53. package/src/test-via-user.js +94 -0
  54. package/src/test-with-uat-retry.js +135 -0
  55. package/src/tools/_registry.js +24 -1
  56. package/src/tools/calendar.js +5 -5
  57. package/src/tools/im-read.js +52 -4
  58. package/src/tools/messaging-user.js +1 -1
  59. package/src/utils.js +83 -0
  60. package/scripts/generate-og-image.js +0 -39
  61. 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) and
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 these
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 steps:
10
- // AGENTS.md = "# feishu-user-plugin — Codex Instructions\n" + tail -n +2 CLAUDE.md
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 Instructions"
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 Instructions\n' + claudeBody;
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 / skill reference all in sync');
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" badge
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+tools/);
12
+ const readmeMatch = readme.match(/(\d+)\s*(?:tools|工具)/);
12
13
  if (!readmeMatch) {
13
- failures.push('No "N tools" badge in README.md');
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
  }
@@ -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 Instructions"; cat /tmp/feishu-claude-body.$$; } > AGENTS.md
7
+ { echo "# feishu-user-plugin — Codex 指令"; cat /tmp/feishu-claude-body.$$; } > AGENTS.md
8
8
  rm -f /tmp/feishu-claude-body.$$
9
- cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md
10
- git add AGENTS.md skills/feishu-user-plugin/references/CLAUDE.md
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
- # What this does (zero manual steps, no degradation):
5
- # 1. Copy skills/ + .claude-plugin/plugin.json into team-skills repo
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
- # Failure modes are now narrow:
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
- # - branch already exists from previous attempt clean skip
22
- # - generate-release-artifacts.js fails exit non-zero (visible to user
23
- # via post-merge stderr; user fixes CHANGELOG and re-pushes)
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
- TEAM_SKILLS="/Users/abble/team-skills/plugins/feishu-user-plugin"
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
- # Copy plugin tree.
40
- cp -r skills/. "$TEAM_SKILLS/skills/"
41
- cp .claude-plugin/plugin.json "$TEAM_SKILLS/.claude-plugin/"
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/plugins/feishu-user-plugin/README.md.
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 the team-skills root README catalog row matching feishu-user-plugin.
65
- ROOT_README="$TEAM_SKILLS/../../README.md"
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 sys, re
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
- # Switch into team-skills repo root (two parents up from $TEAM_SKILLS).
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 every file the hook might have touched. Each `git add` is idempotent
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
- gh pr create --title "Sync feishu-user-plugin v$VERSION" --body "Auto-sync from feishu-user-plugin main. Includes:
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
- PR_NUM=$(gh pr view --json number --jq .number)
120
- # Use --admin --squash: we have admin permissions on team-skills (verified) and
121
- # the team-skills org has auto-merge disabled at org level. --admin merges
122
- # without waiting for required reviews. CI is informational only here.
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.10"
4
- description: "All-in-one Feishu plugin — 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.8: multi-profile auto-switch on read errors (B), WebSocket realtime im.message events via get_new_events (C), credential pointer-only mode (E), CI gates (F), auth/uat.js + auth/cookie.js extracts (D)."
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
- 如果用户提供了邮箱或手机号,改用 `find_user`:
18
+ 邮箱、手机号、姓名都可以作为 `query` 直接传给 `search_contacts`,不需要单独的工具:
19
19
  ```
20
- find_user({ email: "xxx@xxx.com" })
21
- find_user({ mobile: "+86xxx" })
20
+ search_contacts({ query: "xxx@xxx.com" })
21
+ search_contacts({ query: "+86xxx" })
22
22
  ```