feishu-user-plugin 1.3.8 → 1.3.9

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 (40) hide show
  1. package/.claude-plugin/plugin.json +12 -2
  2. package/CHANGELOG.md +50 -12
  3. package/README.md +4 -4
  4. package/package.json +9 -5
  5. package/proto/lark.proto +10 -0
  6. package/scripts/explore-card-protobuf.js +144 -0
  7. package/scripts/explore-image-minimize.js +163 -0
  8. package/scripts/generate-release-artifacts.js +318 -0
  9. package/scripts/probe-feishu-docx.js +203 -0
  10. package/scripts/sync-team-skills.sh +109 -7
  11. package/skills/feishu-user-plugin/SKILL.md +76 -4
  12. package/skills/feishu-user-plugin/references/CLAUDE.md +74 -54
  13. package/src/auth/credentials.js +36 -0
  14. package/src/cli.js +86 -45
  15. package/src/clients/user.js +15 -13
  16. package/src/events/cursor.js +103 -0
  17. package/src/events/event-buffer.js +8 -5
  18. package/src/events/event-log.js +151 -0
  19. package/src/events/index.js +8 -1
  20. package/src/events/lockfile.js +126 -0
  21. package/src/events/owner.js +73 -0
  22. package/src/events/ws-server.js +95 -25
  23. package/src/oauth.js +48 -7
  24. package/src/resolver.js +10 -0
  25. package/src/server.js +248 -29
  26. package/src/setup.js +99 -25
  27. package/src/test-all.js +12 -9
  28. package/src/test-events-cursor.js +56 -0
  29. package/src/test-events-lockfile.js +36 -0
  30. package/src/test-events-log.js +67 -0
  31. package/src/test-events-owner.js +64 -0
  32. package/src/test-fixtures/doc-blocks/sample-1.json +1256 -0
  33. package/src/test-read-doc-markdown.js +61 -0
  34. package/src/test-switch-profile.js +171 -0
  35. package/src/tools/diagnostics.js +10 -3
  36. package/src/tools/docs.js +93 -3
  37. package/src/tools/events.js +143 -33
  38. package/src/tools/messaging-bot.js +2 -3
  39. package/src/tools/messaging-user.js +23 -14
  40. package/src/tools/profile.js +12 -7
@@ -0,0 +1,56 @@
1
+ // src/test-events-cursor.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const assert = require('node:assert/strict');
6
+ const { ensureLog, appendEvent } = require('./events/event-log');
7
+ const { drain, readSnapshot, resetCursorTo } = require('./events/cursor');
8
+
9
+ function run() {
10
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fish-cur-'));
11
+ const logPath = path.join(dir, 'events.jsonl');
12
+ ensureLog(logPath);
13
+
14
+ // No events → empty drain.
15
+ let r = drain(dir);
16
+ assert.equal(r.events.length, 0);
17
+
18
+ // Append 3 events, drain reads them.
19
+ appendEvent(logPath, { event_id: 'a', ts: 1, profile: 'default', payload: {} });
20
+ appendEvent(logPath, { event_id: 'b', ts: 2, profile: 'default', payload: {} });
21
+ appendEvent(logPath, { event_id: 'c', ts: 3, profile: 'default', payload: {} });
22
+
23
+ r = drain(dir);
24
+ assert.equal(r.events.length, 3);
25
+ assert.equal(r.advanced, true);
26
+
27
+ // Second drain returns nothing (cursor advanced).
28
+ r = drain(dir);
29
+ assert.equal(r.events.length, 0);
30
+
31
+ // Peek doesn't advance.
32
+ appendEvent(logPath, { event_id: 'd', ts: 4, profile: 'default', payload: {} });
33
+ r = drain(dir, { peek: true });
34
+ assert.equal(r.events.length, 1);
35
+ assert.equal(r.advanced, false);
36
+ // Real drain afterward still returns the same event.
37
+ r = drain(dir);
38
+ assert.equal(r.events.length, 1);
39
+ assert.equal(r.events[0].event_id, 'd');
40
+
41
+ // Snapshot.
42
+ const snap = readSnapshot(dir);
43
+ assert.equal(snap.pending, 0);
44
+ assert.ok(snap.cursor.offset > 0);
45
+
46
+ // Reset cursor to 0 → next drain returns all 4.
47
+ resetCursorTo(dir, 0);
48
+ r = drain(dir);
49
+ assert.equal(r.events.length, 4);
50
+
51
+ fs.rmSync(dir, { recursive: true, force: true });
52
+ console.log('cursor.js: PASS');
53
+ }
54
+
55
+ if (require.main === module) run();
56
+ module.exports = { run };
@@ -0,0 +1,36 @@
1
+ // src/test-events-lockfile.js (new file; require it from src/test-all.js)
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const assert = require('node:assert/strict');
6
+ const { acquireLongLived, withMutex } = require('./events/lockfile');
7
+
8
+ function run() {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fish-lock-'));
10
+ const lockPath = path.join(dir, 'test.lock');
11
+
12
+ // Long-lived: first acquire works, second returns null.
13
+ const handle1 = acquireLongLived(lockPath, { info: { test: 1 } });
14
+ assert.ok(handle1, 'first acquire should succeed');
15
+ const handle2 = acquireLongLived(lockPath, { info: { test: 2 } });
16
+ assert.equal(handle2, null, 'second acquire should fail (lock held)');
17
+ handle1.release();
18
+
19
+ // After release, can acquire again.
20
+ const handle3 = acquireLongLived(lockPath, { info: { test: 3 } });
21
+ assert.ok(handle3, 'third acquire after release should succeed');
22
+ handle3.release();
23
+
24
+ // withMutex serializes.
25
+ const mutexPath = path.join(dir, 'mutex.lock');
26
+ let counter = 0;
27
+ withMutex(mutexPath, () => { counter += 1; });
28
+ withMutex(mutexPath, () => { counter += 1; });
29
+ assert.equal(counter, 2);
30
+
31
+ fs.rmSync(dir, { recursive: true, force: true });
32
+ console.log('lockfile.js: PASS');
33
+ }
34
+
35
+ if (require.main === module) run();
36
+ module.exports = { run };
@@ -0,0 +1,67 @@
1
+ // src/test-events-log.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const assert = require('node:assert/strict');
6
+ const { ensureLog, appendEvent, readFrom, repairTail, maybeRotate, forceRotate } = require('./events/event-log');
7
+
8
+ function run() {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fish-log-'));
10
+ const logPath = path.join(dir, 'events.jsonl');
11
+
12
+ // Append + read
13
+ ensureLog(logPath);
14
+ appendEvent(logPath, { event_id: 'a', ts: 1, profile: 'default', payload: {} });
15
+ appendEvent(logPath, { event_id: 'b', ts: 2, profile: 'default', payload: {} });
16
+
17
+ let r = readFrom(logPath, 0);
18
+ assert.equal(r.events.length, 2);
19
+ assert.equal(r.events[0].event_id, 'a');
20
+ assert.equal(r.events[1].event_id, 'b');
21
+
22
+ // Read with offset
23
+ r = readFrom(logPath, r.events.length === 2 ? r.nextOffset : 0);
24
+ assert.equal(r.events.length, 0);
25
+
26
+ // Partial-line tolerance: append broken line
27
+ fs.appendFileSync(logPath, '{"event_id":"c","ts":3,"profile":"default","payload":{}}');
28
+ r = readFrom(logPath, 0);
29
+ assert.equal(r.events.length, 2, 'partial last line should not be consumed');
30
+
31
+ // Repair tail
32
+ const beforeSize = fs.statSync(logPath).size;
33
+ const repair = repairTail(logPath);
34
+ assert.equal(repair.repaired, true, 'repair should run');
35
+ assert.ok(repair.sizeAfter < beforeSize, 'truncation occurred');
36
+
37
+ // After repair, can append normally
38
+ appendEvent(logPath, { event_id: 'd', ts: 4, profile: 'default', payload: {} });
39
+ r = readFrom(logPath, 0);
40
+ assert.equal(r.events.length, 3, 'after repair + append, 3 full events');
41
+ assert.equal(r.events[2].event_id, 'd');
42
+
43
+ // Defer-rotate: size below threshold → no rotation
44
+ let rot = maybeRotate(logPath, r.nextOffset, 1024 * 1024);
45
+ assert.equal(rot.rotated, false);
46
+
47
+ // Defer-rotate: size above threshold + cursor caught up → rotation
48
+ // (we'll use a tiny threshold)
49
+ rot = maybeRotate(logPath, r.nextOffset, 1);
50
+ assert.equal(rot.rotated, true);
51
+ assert.ok(fs.existsSync(rot.droppedPath));
52
+ assert.equal(fs.statSync(logPath).size, 0, 'new log empty after rotate');
53
+
54
+ // Force rotate: even with cursor 0 (behind), drops + writes _rotated event
55
+ appendEvent(logPath, { event_id: 'e', ts: 5, profile: 'default', payload: {} });
56
+ forceRotate(logPath, fs.statSync(logPath).size);
57
+ r = readFrom(logPath, 0);
58
+ assert.equal(r.events.length, 1);
59
+ assert.equal(r.events[0].event_id, '_rotated');
60
+ assert.equal(r.events[0].profile, '_system');
61
+
62
+ fs.rmSync(dir, { recursive: true, force: true });
63
+ console.log('event-log.js: PASS');
64
+ }
65
+
66
+ if (require.main === module) run();
67
+ module.exports = { run };
@@ -0,0 +1,64 @@
1
+ // src/test-events-owner.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+ const assert = require('node:assert/strict');
6
+ const { tryClaim, readOwnerInfo, STALE_MS } = require('./events/owner');
7
+
8
+ function run() {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fish-own-'));
10
+
11
+ // Initial claim succeeds.
12
+ const h1 = tryClaim(dir, { info: { wsProfile: 'default' } });
13
+ assert.equal(h1.isOwner, true);
14
+
15
+ // Second claim fails (lock held).
16
+ const h2 = tryClaim(dir);
17
+ assert.equal(h2.isOwner, false);
18
+ assert.equal(h2.ownerInfo.pid, process.pid);
19
+
20
+ // readOwnerInfo reflects state.
21
+ const info = readOwnerInfo(dir);
22
+ assert.equal(info.exists, true);
23
+ assert.equal(info.alive, true);
24
+ assert.equal(info.pid, process.pid);
25
+
26
+ // Heartbeat updates mtime.
27
+ const beforeMtime = info.mtimeMs;
28
+ // Force a slight delay then heartbeat
29
+ const target = Date.now();
30
+ while (Date.now() - target < 50) {} // small busy wait
31
+ const ok = h1.heartbeat();
32
+ assert.equal(ok, true);
33
+ const afterMtime = readOwnerInfo(dir).mtimeMs;
34
+ assert.ok(afterMtime >= beforeMtime);
35
+
36
+ h1.release();
37
+
38
+ // After release, readOwnerInfo says no owner.
39
+ const info2 = readOwnerInfo(dir);
40
+ assert.equal(info2.exists, false);
41
+
42
+ // After release, can re-claim.
43
+ const h3 = tryClaim(dir);
44
+ assert.equal(h3.isOwner, true);
45
+ h3.release();
46
+
47
+ // Force claim over an existing lock.
48
+ const h4 = tryClaim(dir, { info: { wsProfile: 'A' } });
49
+ const h5 = tryClaim(dir, { info: { wsProfile: 'B' }, force: true });
50
+ assert.equal(h5.isOwner, true);
51
+ // h4's heartbeat should now fail because its lock file got stolen.
52
+ // (Best-effort — heartbeat returns false if file isn't there to utimes.)
53
+ // We don't assert this strictly because the file's still around (we renamed
54
+ // it, didn't delete). Instead just verify h5 holds the new lock.
55
+ const info3 = readOwnerInfo(dir);
56
+ assert.equal(info3.exists, true);
57
+
58
+ h5.release();
59
+ fs.rmSync(dir, { recursive: true, force: true });
60
+ console.log('owner.js: PASS');
61
+ }
62
+
63
+ if (require.main === module) run();
64
+ module.exports = { run };