chenbhao commited on
Commit
73bc263
·
1 Parent(s): 3a0e256

feat: feishu connect

Browse files
bun.lock CHANGED
@@ -21,6 +21,7 @@
21
  "@azure/identity": "^4.13.1",
22
  "@commander-js/extra-typings": "^14.0.0",
23
  "@growthbook/growthbook": "^1.6.5",
 
24
  "@modelcontextprotocol/sdk": "^1.29.0",
25
  "@opentelemetry/api": "^1.9.1",
26
  "@opentelemetry/api-logs": "^0.214.0",
@@ -324,6 +325,8 @@
324
 
325
  "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
326
 
 
 
327
  "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
328
 
329
  "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
@@ -810,6 +813,8 @@
810
 
811
  "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
812
 
 
 
813
  "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
814
 
815
  "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
@@ -822,8 +827,12 @@
822
 
823
  "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
824
 
 
 
825
  "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
826
 
 
 
827
  "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
828
 
829
  "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
@@ -926,7 +935,7 @@
926
 
927
  "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
928
 
929
- "protobufjs": ["protobufjs@8.6.3", "", { "dependencies": { "long": "^5.3.2" } }, "sha512-alQyzT0j401LGBtwsqu6uprjR6pfNH1UJf9N6GBFMjIcd+HzTe0/HrjAbFCqun+zvnfLarrxAtMM2xvZ+kFZ5A=="],
930
 
931
  "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
932
 
@@ -1202,8 +1211,6 @@
1202
 
1203
  "@aws-sdk/xml-builder/@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="],
1204
 
1205
- "@grpc/proto-loader/protobufjs": ["protobufjs@7.6.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw=="],
1206
-
1207
  "@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
1208
 
1209
  "@inquirer/checkbox/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
@@ -1220,6 +1227,8 @@
1220
 
1221
  "@inquirer/select/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
1222
 
 
 
1223
  "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
1224
 
1225
  "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
@@ -1284,8 +1293,6 @@
1284
 
1285
  "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
1286
 
1287
- "@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.6.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw=="],
1288
-
1289
  "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
1290
 
1291
  "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
@@ -1340,6 +1347,8 @@
1340
 
1341
  "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
1342
 
 
 
1343
  "external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
1344
 
1345
  "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
@@ -1418,6 +1427,8 @@
1418
 
1419
  "@inquirer/select/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
1420
 
 
 
1421
  "@smithy/eventstream-codec/@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="],
1422
 
1423
  "@smithy/eventstream-codec/@aws-crypto/crc32/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
 
21
  "@azure/identity": "^4.13.1",
22
  "@commander-js/extra-typings": "^14.0.0",
23
  "@growthbook/growthbook": "^1.6.5",
24
+ "@larksuiteoapi/node-sdk": "^1.60.0",
25
  "@modelcontextprotocol/sdk": "^1.29.0",
26
  "@opentelemetry/api": "^1.9.1",
27
  "@opentelemetry/api-logs": "^0.214.0",
 
325
 
326
  "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
327
 
328
+ "@larksuiteoapi/node-sdk": ["@larksuiteoapi/node-sdk@1.66.1", "", { "dependencies": { "axios": "~1.13.3", "lodash.identity": "^3.0.0", "lodash.merge": "^4.6.2", "lodash.pickby": "^4.6.0", "protobufjs": "^7.2.6", "qs": "^6.14.2", "ws": "^8.19.0" } }, "sha512-W1rIAs/8Oc/rEYuWc0sxGvR8iLwd8p5D2RS4ODMqs8htIxK8yIa8sb22EDv/OEBqUpKXZaNLydTz7Oq8HOQROg=="],
329
+
330
  "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
331
 
332
  "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
 
813
 
814
  "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
815
 
816
+ "lodash.identity": ["lodash.identity@3.0.0", "", {}, "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q=="],
817
+
818
  "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
819
 
820
  "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
 
827
 
828
  "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
829
 
830
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
831
+
832
  "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
833
 
834
+ "lodash.pickby": ["lodash.pickby@4.6.0", "", {}, "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q=="],
835
+
836
  "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
837
 
838
  "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
 
935
 
936
  "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
937
 
938
+ "protobufjs": ["protobufjs@7.6.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw=="],
939
 
940
  "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
941
 
 
1211
 
1212
  "@aws-sdk/xml-builder/@smithy/types": ["@smithy/types@4.14.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ=="],
1213
 
 
 
1214
  "@grpc/proto-loader/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
1215
 
1216
  "@inquirer/checkbox/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
 
1227
 
1228
  "@inquirer/select/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
1229
 
1230
+ "@larksuiteoapi/node-sdk/axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
1231
+
1232
  "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
1233
 
1234
  "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
 
1293
 
1294
  "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
1295
 
 
 
1296
  "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
1297
 
1298
  "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
 
1347
 
1348
  "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
1349
 
1350
+ "doubaoime-asr/protobufjs": ["protobufjs@8.6.3", "", { "dependencies": { "long": "^5.3.2" } }, "sha512-alQyzT0j401LGBtwsqu6uprjR6pfNH1UJf9N6GBFMjIcd+HzTe0/HrjAbFCqun+zvnfLarrxAtMM2xvZ+kFZ5A=="],
1351
+
1352
  "external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
1353
 
1354
  "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
1427
 
1428
  "@inquirer/select/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
1429
 
1430
+ "@larksuiteoapi/node-sdk/axios/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
1431
+
1432
  "@smithy/eventstream-codec/@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="],
1433
 
1434
  "@smithy/eventstream-codec/@aws-crypto/crc32/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
package.json CHANGED
@@ -39,6 +39,7 @@
39
  "@azure/identity": "^4.13.1",
40
  "@commander-js/extra-typings": "^14.0.0",
41
  "@growthbook/growthbook": "^1.6.5",
 
42
  "@modelcontextprotocol/sdk": "^1.29.0",
43
  "@opentelemetry/api": "^1.9.1",
44
  "@opentelemetry/api-logs": "^0.214.0",
 
39
  "@azure/identity": "^4.13.1",
40
  "@commander-js/extra-typings": "^14.0.0",
41
  "@growthbook/growthbook": "^1.6.5",
42
+ "@larksuiteoapi/node-sdk": "^1.60.0",
43
  "@modelcontextprotocol/sdk": "^1.29.0",
44
  "@opentelemetry/api": "^1.9.1",
45
  "@opentelemetry/api-logs": "^0.214.0",
src/commands.ts CHANGED
@@ -43,6 +43,7 @@ import share from './commands/share/index.js'
43
  import skills from './commands/skills/index.js'
44
  import status from './commands/status/index.js'
45
  import tasks from './commands/tasks/index.js'
 
46
  import telegram from './commands/telegram/index.js'
47
  import teleport from './commands/teleport/index.js'
48
  /* eslint-disable @typescript-eslint/no-require-imports */
@@ -306,6 +307,7 @@ const COMMANDS = memoize((): Command[] => [
306
  telegram,
307
  theme,
308
  feedback,
 
309
  review,
310
  ultrareview,
311
  rewind,
 
43
  import skills from './commands/skills/index.js'
44
  import status from './commands/status/index.js'
45
  import tasks from './commands/tasks/index.js'
46
+ import feishu from './commands/feishu/index.js'
47
  import telegram from './commands/telegram/index.js'
48
  import teleport from './commands/teleport/index.js'
49
  /* eslint-disable @typescript-eslint/no-require-imports */
 
307
  telegram,
308
  theme,
309
  feedback,
310
+ feishu,
311
  review,
312
  ultrareview,
313
  rewind,
src/commands/feishu/feishu.tsx ADDED
@@ -0,0 +1,551 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from 'react'
2
+ import { Box, Text, useInput } from '../../ink.js'
3
+ import { Dialog } from '../../components/design-system/Dialog.js'
4
+ import { ListItem } from '../../components/design-system/ListItem.js'
5
+ import type { LocalJSXCommandContext } from '../../commands.js'
6
+ import type { LocalJSXCommandOnDone } from '../../types/command.js'
7
+ import { feishuService } from '../../services/feishu/FeishuService.js'
8
+ import {
9
+ clearFeishuConfig,
10
+ getFeishuConfig,
11
+ maskFeishuAppId,
12
+ maskFeishuAppSecret,
13
+ saveFeishuConfig,
14
+ } from '../../services/feishu/feishuConfig.js'
15
+
16
+ type Step =
17
+ | { type: 'menu' }
18
+ | { type: 'edit-app-id' }
19
+ | { type: 'edit-app-secret' }
20
+ | { type: 'edit-encrypt-key' }
21
+ | { type: 'edit-verification-token' }
22
+ | { type: 'edit-allowed-users' }
23
+ | { type: 'confirm-clear' }
24
+
25
+ type Notice = {
26
+ text: string
27
+ tone: 'success' | 'error' | 'info'
28
+ }
29
+
30
+ function TextInput({
31
+ title,
32
+ hint,
33
+ initialValue,
34
+ masked = false,
35
+ onSubmit,
36
+ onCancel,
37
+ }: {
38
+ title: string
39
+ hint: string
40
+ initialValue?: string
41
+ masked?: boolean
42
+ onSubmit: (value: string) => void
43
+ onCancel: () => void
44
+ }) {
45
+ const [value, setValue] = React.useState(initialValue ?? '')
46
+ const [cursorOffset, setCursorOffset] = React.useState(
47
+ (initialValue ?? '').length,
48
+ )
49
+
50
+ useInput((input, key) => {
51
+ if (key.escape) {
52
+ onCancel()
53
+ return
54
+ }
55
+
56
+ if (key.return) {
57
+ onSubmit(value.trim())
58
+ return
59
+ }
60
+
61
+ if (key.leftArrow) {
62
+ setCursorOffset(current => Math.max(0, current - 1))
63
+ return
64
+ }
65
+
66
+ if (key.rightArrow) {
67
+ setCursorOffset(current => Math.min(value.length, current + 1))
68
+ return
69
+ }
70
+
71
+ if (key.backspace || key.delete) {
72
+ if (cursorOffset === 0) return
73
+ const nextValue =
74
+ value.slice(0, cursorOffset - 1) + value.slice(cursorOffset)
75
+ setValue(nextValue)
76
+ setCursorOffset(current => Math.max(0, current - 1))
77
+ return
78
+ }
79
+
80
+ if (input) {
81
+ const nextValue =
82
+ value.slice(0, cursorOffset) + input + value.slice(cursorOffset)
83
+ setValue(nextValue)
84
+ setCursorOffset(current => current + input.length)
85
+ }
86
+ })
87
+
88
+ const displayValue = masked ? '*'.repeat(value.length) : value
89
+ const cursorChar =
90
+ cursorOffset < displayValue.length ? displayValue[cursorOffset] : ' '
91
+
92
+ return (
93
+ <Box flexDirection="column" gap={1}>
94
+ <Text bold>{title}</Text>
95
+ <Text dimColor>{hint}</Text>
96
+ <Box flexDirection="column" marginTop={1}>
97
+ <Text dimColor>Value:</Text>
98
+ <Box>
99
+ <Text>{displayValue.slice(0, cursorOffset)}</Text>
100
+ <Text backgroundColor="white" color="black">
101
+ {cursorChar}
102
+ </Text>
103
+ <Text>{displayValue.slice(cursorOffset + 1)}</Text>
104
+ </Box>
105
+ </Box>
106
+ <Text dimColor>[Enter] 保存 · [Esc] 返回</Text>
107
+ </Box>
108
+ )
109
+ }
110
+
111
+ function ConfirmClear({
112
+ onConfirm,
113
+ onCancel,
114
+ }: {
115
+ onConfirm: () => void
116
+ onCancel: () => void
117
+ }) {
118
+ useInput((_, key) => {
119
+ if (key.escape) {
120
+ onCancel()
121
+ return
122
+ }
123
+
124
+ if (key.return) {
125
+ onConfirm()
126
+ }
127
+ })
128
+
129
+ return (
130
+ <Box flexDirection="column" gap={1}>
131
+ <Text color="yellow">这会清空飞书 App ID / App Secret 以及所有白名单配置。</Text>
132
+ <Text dimColor>
133
+ 如果当前 bot 正在运行,会一并停止。按 Enter 确认,Esc 返回。
134
+ </Text>
135
+ </Box>
136
+ )
137
+ }
138
+
139
+ function FeishuDialog({
140
+ onDone,
141
+ }: {
142
+ onDone: LocalJSXCommandOnDone
143
+ }) {
144
+ const serviceState = React.useSyncExternalStore(
145
+ feishuService.subscribe,
146
+ feishuService.getStateSnapshot,
147
+ )
148
+ const [config, setConfig] = React.useState(() => getFeishuConfig())
149
+ const [step, setStep] = React.useState<Step>({ type: 'menu' })
150
+ const [notice, setNotice] = React.useState<Notice | null>(null)
151
+ const [focusIndex, setFocusIndex] = React.useState(0)
152
+ const [busy, setBusy] = React.useState(false)
153
+
154
+ const refreshConfig = React.useCallback(() => {
155
+ setConfig(getFeishuConfig())
156
+ }, [])
157
+
158
+ const menuItems = React.useMemo(
159
+ () => [
160
+ {
161
+ label: '设置 App ID',
162
+ action: () => setStep({ type: 'edit-app-id' }),
163
+ },
164
+ {
165
+ label: '设置 App Secret',
166
+ action: () => setStep({ type: 'edit-app-secret' }),
167
+ },
168
+ {
169
+ label: '设置加密密钥(可选)',
170
+ action: () => setStep({ type: 'edit-encrypt-key' }),
171
+ },
172
+ {
173
+ label: '设置验证 Token(可选)',
174
+ action: () => setStep({ type: 'edit-verification-token' }),
175
+ },
176
+ {
177
+ label: '设置允许访问的用户',
178
+ action: () => setStep({ type: 'edit-allowed-users' }),
179
+ },
180
+ {
181
+ label:
182
+ serviceState.status === 'running' ? '停止当前飞书 Bot' : '启动当前飞书 Bot',
183
+ action: async () => {
184
+ setBusy(true)
185
+ try {
186
+ if (serviceState.status === 'running') {
187
+ await feishuService.stop()
188
+ setNotice({ text: '飞书 Bot 已停止。', tone: 'success' })
189
+ } else {
190
+ await feishuService.startFromSavedConfig()
191
+ setNotice({
192
+ text: '飞书 Bot 已启动,请在飞书中私聊机器人验证连接。',
193
+ tone: 'success',
194
+ })
195
+ }
196
+ } catch (error) {
197
+ setNotice({
198
+ text: error instanceof Error ? error.message : '启动飞书 Bot 失败',
199
+ tone: 'error',
200
+ })
201
+ } finally {
202
+ setBusy(false)
203
+ }
204
+ },
205
+ },
206
+ {
207
+ label: '清空飞书配置',
208
+ action: () => setStep({ type: 'confirm-clear' }),
209
+ },
210
+ {
211
+ label: '关闭',
212
+ action: () => onDone(undefined, { display: 'skip' }),
213
+ },
214
+ ],
215
+ [onDone, serviceState.status],
216
+ )
217
+
218
+ React.useEffect(() => {
219
+ setFocusIndex(current => Math.min(current, menuItems.length - 1))
220
+ }, [menuItems.length])
221
+
222
+ const saveAppId = React.useCallback(
223
+ async (value: string) => {
224
+ if (!value) {
225
+ setNotice({ text: 'App ID 不能为空。', tone: 'error' })
226
+ setStep({ type: 'menu' })
227
+ return
228
+ }
229
+
230
+ setBusy(true)
231
+ try {
232
+ const current = getFeishuConfig()
233
+ saveFeishuConfig({ ...current, appId: value })
234
+ refreshConfig()
235
+ if (serviceState.status === 'running') {
236
+ await feishuService.stop()
237
+ await feishuService.startFromSavedConfig()
238
+ }
239
+ setNotice({ text: 'App ID 已保存。', tone: 'success' })
240
+ } catch (error) {
241
+ setNotice({
242
+ text: error instanceof Error ? error.message : '保存 App ID 失败',
243
+ tone: 'error',
244
+ })
245
+ } finally {
246
+ setBusy(false)
247
+ setStep({ type: 'menu' })
248
+ }
249
+ },
250
+ [refreshConfig, serviceState.status],
251
+ )
252
+
253
+ const saveAppSecret = React.useCallback(
254
+ async (value: string) => {
255
+ if (!value) {
256
+ setNotice({ text: 'App Secret 不能为空。', tone: 'error' })
257
+ setStep({ type: 'menu' })
258
+ return
259
+ }
260
+
261
+ setBusy(true)
262
+ try {
263
+ const current = getFeishuConfig()
264
+ saveFeishuConfig({ ...current, appSecret: value })
265
+ refreshConfig()
266
+ if (serviceState.status === 'running') {
267
+ await feishuService.stop()
268
+ await feishuService.startFromSavedConfig()
269
+ }
270
+ setNotice({ text: 'App Secret 已保存。', tone: 'success' })
271
+ } catch (error) {
272
+ setNotice({
273
+ text: error instanceof Error ? error.message : '保存 App Secret 失败',
274
+ tone: 'error',
275
+ })
276
+ } finally {
277
+ setBusy(false)
278
+ setStep({ type: 'menu' })
279
+ }
280
+ },
281
+ [refreshConfig, serviceState.status],
282
+ )
283
+
284
+ const saveEncryptKey = React.useCallback(
285
+ async (value: string) => {
286
+ setBusy(true)
287
+ try {
288
+ const current = getFeishuConfig()
289
+ saveFeishuConfig({ ...current, encryptKey: value || undefined })
290
+ refreshConfig()
291
+ if (serviceState.status === 'running') {
292
+ await feishuService.stop()
293
+ await feishuService.startFromSavedConfig()
294
+ }
295
+ setNotice({ text: '加密密钥已保存。', tone: 'success' })
296
+ } catch (error) {
297
+ setNotice({
298
+ text: error instanceof Error ? error.message : '保存失败',
299
+ tone: 'error',
300
+ })
301
+ } finally {
302
+ setBusy(false)
303
+ setStep({ type: 'menu' })
304
+ }
305
+ },
306
+ [refreshConfig, serviceState.status],
307
+ )
308
+
309
+ const saveVerificationToken = React.useCallback(
310
+ async (value: string) => {
311
+ setBusy(true)
312
+ try {
313
+ const current = getFeishuConfig()
314
+ saveFeishuConfig({ ...current, verificationToken: value || undefined })
315
+ refreshConfig()
316
+ if (serviceState.status === 'running') {
317
+ await feishuService.stop()
318
+ await feishuService.startFromSavedConfig()
319
+ }
320
+ setNotice({ text: '验证 Token 已保存。', tone: 'success' })
321
+ } catch (error) {
322
+ setNotice({
323
+ text: error instanceof Error ? error.message : '保存失败',
324
+ tone: 'error',
325
+ })
326
+ } finally {
327
+ setBusy(false)
328
+ setStep({ type: 'menu' })
329
+ }
330
+ },
331
+ [refreshConfig, serviceState.status],
332
+ )
333
+
334
+ const saveAllowedUsers = React.useCallback(
335
+ async (value: string) => {
336
+ const users = value.split(/[,\s]+/).map(s => s.trim()).filter(Boolean)
337
+ setBusy(true)
338
+ try {
339
+ const current = getFeishuConfig()
340
+ saveFeishuConfig({ ...current, allowedUsers: users.length > 0 ? users : undefined })
341
+ refreshConfig()
342
+ setNotice({ text: '允许用户列表已保存。', tone: 'success' })
343
+ } catch (error) {
344
+ setNotice({
345
+ text: error instanceof Error ? error.message : '保存失败',
346
+ tone: 'error',
347
+ })
348
+ } finally {
349
+ setBusy(false)
350
+ setStep({ type: 'menu' })
351
+ }
352
+ },
353
+ [refreshConfig],
354
+ )
355
+
356
+ const clearConfigAndStop = React.useCallback(async () => {
357
+ setBusy(true)
358
+ try {
359
+ await feishuService.stop()
360
+ clearFeishuConfig()
361
+ refreshConfig()
362
+ setNotice({ text: '飞书配置已清空。', tone: 'success' })
363
+ } catch (error) {
364
+ setNotice({
365
+ text: error instanceof Error ? error.message : '清空配置失败',
366
+ tone: 'error',
367
+ })
368
+ } finally {
369
+ setBusy(false)
370
+ setStep({ type: 'menu' })
371
+ }
372
+ }, [refreshConfig])
373
+
374
+ useInput((_, key) => {
375
+ if (step.type !== 'menu' || busy) return
376
+
377
+ if (key.escape) {
378
+ onDone(undefined, { display: 'skip' })
379
+ return
380
+ }
381
+
382
+ if (key.upArrow) {
383
+ setFocusIndex(current => (current - 1 + menuItems.length) % menuItems.length)
384
+ return
385
+ }
386
+
387
+ if (key.downArrow) {
388
+ setFocusIndex(current => (current + 1) % menuItems.length)
389
+ return
390
+ }
391
+
392
+ if (key.return) {
393
+ void menuItems[focusIndex]?.action()
394
+ }
395
+ })
396
+
397
+ const noticeColor =
398
+ notice?.tone === 'error'
399
+ ? 'red'
400
+ : notice?.tone === 'success'
401
+ ? 'green'
402
+ : 'cyan'
403
+
404
+ // Step rendering
405
+ if (step.type === 'edit-app-id') {
406
+ return (
407
+ <Dialog title="飞书 App ID" onCancel={() => setStep({ type: 'menu' })}>
408
+ <TextInput
409
+ title="App ID"
410
+ hint="从飞书开放平台 > 凭证与基础信息 获取。"
411
+ initialValue={config?.appId}
412
+ onSubmit={value => { void saveAppId(value) }}
413
+ onCancel={() => setStep({ type: 'menu' })}
414
+ />
415
+ </Dialog>
416
+ )
417
+ }
418
+
419
+ if (step.type === 'edit-app-secret') {
420
+ return (
421
+ <Dialog title="飞书 App Secret" onCancel={() => setStep({ type: 'menu' })}>
422
+ <TextInput
423
+ title="App Secret"
424
+ hint="从飞书开放平台 > 凭证与基础信息 获取。"
425
+ initialValue={config?.appSecret}
426
+ masked={true}
427
+ onSubmit={value => { void saveAppSecret(value) }}
428
+ onCancel={() => setStep({ type: 'menu' })}
429
+ />
430
+ </Dialog>
431
+ )
432
+ }
433
+
434
+ if (step.type === 'edit-encrypt-key') {
435
+ return (
436
+ <Dialog title="飞书加密密钥" onCancel={() => setStep({ type: 'menu' })}>
437
+ <TextInput
438
+ title="加密密钥(可选)"
439
+ hint="如果启用了「订阅事件请求 URL 加密模式」才需要填写。"
440
+ initialValue={config?.encryptKey}
441
+ masked={true}
442
+ onSubmit={value => { void saveEncryptKey(value) }}
443
+ onCancel={() => setStep({ type: 'menu' })}
444
+ />
445
+ </Dialog>
446
+ )
447
+ }
448
+
449
+ if (step.type === 'edit-verification-token') {
450
+ return (
451
+ <Dialog title="飞书验证 Token" onCancel={() => setStep({ type: 'menu' })}>
452
+ <TextInput
453
+ title="验证 Token(可选)"
454
+ hint="如果启用了「订阅事件请求 URL 验证」才需要填写。"
455
+ initialValue={config?.verificationToken}
456
+ masked={true}
457
+ onSubmit={value => { void saveVerificationToken(value) }}
458
+ onCancel={() => setStep({ type: 'menu' })}
459
+ />
460
+ </Dialog>
461
+ )
462
+ }
463
+
464
+ if (step.type === 'edit-allowed-users') {
465
+ return (
466
+ <Dialog title="允许访问的用户" onCancel={() => setStep({ type: 'menu' })}>
467
+ <TextInput
468
+ title="允许访问的飞书用户 open_id"
469
+ hint="可输入一个或多个 open_id,支持空格或逗号分隔。配对成功后可发送配对码完成授权。"
470
+ initialValue={config?.allowedUsers?.join(', ')}
471
+ onSubmit={value => { void saveAllowedUsers(value) }}
472
+ onCancel={() => setStep({ type: 'menu' })}
473
+ />
474
+ </Dialog>
475
+ )
476
+ }
477
+
478
+ if (step.type === 'confirm-clear') {
479
+ return (
480
+ <Dialog title="清空飞书配置" onCancel={() => setStep({ type: 'menu' })}>
481
+ <ConfirmClear
482
+ onConfirm={() => { void clearConfigAndStop() }}
483
+ onCancel={() => setStep({ type: 'menu' })}
484
+ />
485
+ </Dialog>
486
+ )
487
+ }
488
+
489
+ return (
490
+ <Dialog title="飞书 (Feishu)" onCancel={() => onDone(undefined, { display: 'skip' })}>
491
+ <Box flexDirection="column" gap={1}>
492
+ <Text dimColor>
493
+ 启动飞书长连接机器人,并把私聊消息接入当前会话。
494
+ </Text>
495
+ <Text>
496
+ 状态:
497
+ {' '}
498
+ <Text color={serviceState.status === 'running' ? 'green' : 'yellow'}>
499
+ {serviceState.status === 'running'
500
+ ? '运行中'
501
+ : serviceState.status === 'starting'
502
+ ? '启动中'
503
+ : '未启动'}
504
+ </Text>
505
+ </Text>
506
+ <Text>App ID: {maskFeishuAppId(config?.appId)}</Text>
507
+ <Text>App Secret: {maskFeishuAppSecret(config?.appSecret)}</Text>
508
+ <Text>
509
+ 加密密钥: {config?.encryptKey ? '已配置' : '未配置'}
510
+ </Text>
511
+ <Text>
512
+ 验证 Token: {config?.verificationToken ? '已配置' : '未配置'}
513
+ </Text>
514
+ <Text>
515
+ 允许用户:
516
+ {' '}
517
+ {config?.allowedUsers?.length
518
+ ? config.allowedUsers.join(', ')
519
+ : '未配置(使用配对码授权)'}
520
+ </Text>
521
+ {config?.pairedUsers?.length ? (
522
+ <Text>
523
+ 已配对用户: {config.pairedUsers.map(u => u.displayName || u.userId).join(', ')}
524
+ </Text>
525
+ ) : null}
526
+ {serviceState.lastError ? (
527
+ <Text color="red">最近错误: {serviceState.lastError}</Text>
528
+ ) : null}
529
+ {notice ? <Text color={noticeColor}>{notice.text}</Text> : null}
530
+ {busy ? <Text dimColor>处理中,请稍候...</Text> : null}
531
+ <Box flexDirection="column" marginTop={1}>
532
+ {menuItems.map((item, index) => (
533
+ <ListItem key={item.label} isFocused={focusIndex === index}>
534
+ <Text>{item.label}</Text>
535
+ </ListItem>
536
+ ))}
537
+ </Box>
538
+ <Text dimColor>上下箭头选择,Enter 执行,Esc 关闭</Text>
539
+ </Box>
540
+ </Dialog>
541
+ )
542
+ }
543
+
544
+ export async function call(
545
+ onDone: LocalJSXCommandOnDone,
546
+ _context: LocalJSXCommandContext,
547
+ ): Promise<React.ReactNode> {
548
+ return <FeishuDialog onDone={onDone} />
549
+ }
550
+
551
+ export default call
src/commands/feishu/index.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Command } from '../../commands.js'
2
+
3
+ const feishu = {
4
+ type: 'local-jsx',
5
+ name: 'feishu',
6
+ description: 'Connect a Feishu bot to this session',
7
+ load: () => import('./feishu.js'),
8
+ } satisfies Command
9
+
10
+ export default feishu
src/components/PromptInput/PromptInputFooter.tsx CHANGED
@@ -11,6 +11,7 @@ import { useTerminalSize } from '../../hooks/useTerminalSize.js';
11
  import { Box, Text } from '../../ink.js';
12
  import type { MCPServerConnection } from '../../services/mcp/types.js';
13
  import { telegramService } from '../../services/telegram/TelegramService.js';
 
14
  import { useAppState } from '../../state/AppState.js';
15
  import type { ToolPermissionContext } from '../../Tool.js';
16
  import type { Message } from '../../types/message.js';
@@ -146,6 +147,7 @@ function PromptInputFooter({
146
  {isFullscreen ? null : <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} isNarrow={isNarrow} />}
147
  {"external" === 'ant' && isUndercover() && <Text dimColor>undercover</Text>}
148
  <TelegramStatusIndicator />
 
149
  <BridgeStatusIndicator bridgeSelected={bridgeSelected} />
150
  </Box>
151
  </Box>
@@ -170,6 +172,22 @@ function TelegramStatusIndicator(): React.ReactNode {
170
  </Text>;
171
  }
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  type BridgeStatusProps = {
174
  bridgeSelected: boolean;
175
  };
 
11
  import { Box, Text } from '../../ink.js';
12
  import type { MCPServerConnection } from '../../services/mcp/types.js';
13
  import { telegramService } from '../../services/telegram/TelegramService.js';
14
+ import { feishuService } from '../../services/feishu/FeishuService.js';
15
  import { useAppState } from '../../state/AppState.js';
16
  import type { ToolPermissionContext } from '../../Tool.js';
17
  import type { Message } from '../../types/message.js';
 
147
  {isFullscreen ? null : <Notifications apiKeyStatus={apiKeyStatus} autoUpdaterResult={autoUpdaterResult} debug={debug} isAutoUpdating={isAutoUpdating} verbose={verbose} messages={messages} onAutoUpdaterResult={onAutoUpdaterResult} onChangeIsUpdating={onChangeIsUpdating} ideSelection={ideSelection} mcpClients={mcpClients} isInputWrapped={isInputWrapped} isNarrow={isNarrow} />}
148
  {"external" === 'ant' && isUndercover() && <Text dimColor>undercover</Text>}
149
  <TelegramStatusIndicator />
150
+ <FeishuStatusIndicator />
151
  <BridgeStatusIndicator bridgeSelected={bridgeSelected} />
152
  </Box>
153
  </Box>
 
172
  </Text>;
173
  }
174
 
175
+ function FeishuStatusIndicator(): React.ReactNode {
176
+ const state = React.useSyncExternalStore(
177
+ feishuService.subscribe,
178
+ feishuService.getStateSnapshot,
179
+ feishuService.getStateSnapshot,
180
+ );
181
+
182
+ if (state.status !== 'running' && state.status !== 'starting') {
183
+ return null;
184
+ }
185
+
186
+ return <Text color={state.status === 'running' ? 'notice' : 'warning'}>
187
+
188
+ </Text>;
189
+ }
190
+
191
  type BridgeStatusProps = {
192
  bridgeSelected: boolean;
193
  };
src/hooks/useFeishuBridge.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * useFeishuBridge — auto-starts Feishu service if configured.
3
+ */
4
+
5
+ import { useEffect, useRef } from 'react'
6
+ import { feishuService } from '../services/feishu/FeishuService.js'
7
+ import { getFeishuConfig } from '../services/feishu/feishuConfig.js'
8
+
9
+ export function useFeishuBridge(): void {
10
+ const attempted = useRef(false)
11
+
12
+ useEffect(() => {
13
+ if (attempted.current) return
14
+ attempted.current = true
15
+
16
+ const config = getFeishuConfig()
17
+ if (!config.appId || !config.appSecret) return
18
+
19
+ void feishuService.startFromSavedConfig().catch((e) => {
20
+ console.warn('[feishu] auto-start failed:', e instanceof Error ? e.message : String(e))
21
+ })
22
+ }, [])
23
+ }
src/services/feishu/FeishuService.ts ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * FeishuService — in-process Feishu bot using @larksuiteoapi/node-sdk
3
+ *
4
+ * Uses Lark.WSClient to receive events, enqueues messages to REPL,
5
+ * collects responses via polling AppState, sends back to Feishu.
6
+ */
7
+
8
+ import * as Lark from '@larksuiteoapi/node-sdk'
9
+ import { getFeishuConfig, type FeishuRuntimeConfig } from './feishuConfig.js'
10
+ import { logForDebugging } from '../../utils/debug.js'
11
+
12
+ type FeishuServiceState = {
13
+ status: 'stopped' | 'starting' | 'running' | 'error'
14
+ appId?: string
15
+ lastError?: string
16
+ }
17
+
18
+ type Listener = () => void
19
+
20
+ function normalizeError(error: unknown): string {
21
+ if (error instanceof Error) return error.message
22
+ return String(error)
23
+ }
24
+
25
+ class FeishuService {
26
+ private listeners = new Set<Listener>()
27
+ private state: FeishuServiceState = { status: 'stopped' }
28
+ private config?: FeishuRuntimeConfig
29
+ private client?: Lark.Client
30
+ private wsClient?: { stop?: () => void }
31
+ private runId = 0
32
+
33
+ subscribe = (listener: Listener): (() => void) => {
34
+ this.listeners.add(listener)
35
+ return () => this.listeners.delete(listener)
36
+ }
37
+
38
+ getStateSnapshot = (): FeishuServiceState => this.state
39
+
40
+ async start(config: FeishuRuntimeConfig): Promise<void> {
41
+ if (this.state.status === 'running' &&
42
+ this.config?.appId === config.appId &&
43
+ this.config?.appSecret === config.appSecret) {
44
+ return
45
+ }
46
+
47
+ await this.stop()
48
+ const runId = ++this.runId
49
+ this.config = config
50
+ this.client = new Lark.Client({
51
+ appId: config.appId,
52
+ appSecret: config.appSecret,
53
+ appType: Lark.AppType.SelfBuild,
54
+ domain: Lark.Domain.Feishu,
55
+ })
56
+
57
+ this.setState({ status: 'starting', appId: config.appId, lastError: undefined })
58
+
59
+ try {
60
+ // Verify credentials
61
+ await this.client.request({
62
+ method: 'GET',
63
+ url: '/open-apis/bot/v3/info',
64
+ })
65
+ if (runId !== this.runId) return
66
+
67
+ logForDebugging('[feishu] connected successfully')
68
+
69
+ const dispatcher = new Lark.EventDispatcher({
70
+ encryptKey: config.encryptKey,
71
+ verificationToken: config.verificationToken,
72
+ })
73
+
74
+ dispatcher.register({
75
+ 'im.message.receive_v1': async (data: any) => {
76
+ if (runId !== this.runId) return
77
+ try {
78
+ await this.handleInbound(data)
79
+ } catch (err) {
80
+ logForDebugging(`[feishu] inbound error: ${normalizeError(err)}`)
81
+ }
82
+ },
83
+ } as any)
84
+
85
+ // @ts-ignore - WSClient instance type varies
86
+ this.wsClient = new Lark.WSClient({
87
+ appId: config.appId,
88
+ appSecret: config.appSecret,
89
+ domain: Lark.Domain.Feishu,
90
+ loggerLevel: Lark.LoggerLevel.warn,
91
+ })
92
+
93
+ await this.wsClient.start?.({ eventDispatcher: dispatcher })
94
+ if (runId !== this.runId) return
95
+
96
+ this.setState({ status: 'running', appId: config.appId })
97
+ } catch (err) {
98
+ if (runId !== this.runId) return
99
+ this.setState({ status: 'error', appId: config.appId, lastError: normalizeError(err) })
100
+ throw err
101
+ }
102
+ }
103
+
104
+ async startFromSavedConfig(): Promise<void> {
105
+ const config = getFeishuConfig()
106
+ if (!config.appId || !config.appSecret) {
107
+ throw new Error('飞书 App ID 或 App Secret 未配置。')
108
+ }
109
+ await this.start(config)
110
+ }
111
+
112
+ async stop(): Promise<void> {
113
+ this.runId++
114
+ try { this.wsClient?.stop?.() } catch {}
115
+ this.wsClient = undefined
116
+ this.client = undefined
117
+ this.config = undefined
118
+ if (this.state.status !== 'stopped') {
119
+ this.setState({ ...this.state, status: 'stopped' })
120
+ }
121
+ }
122
+
123
+ async sendText(chatId: string, text: string): Promise<void> {
124
+ if (!this.client) return
125
+ const MAX = 4000
126
+ for (let i = 0; i < text.length; i += MAX) {
127
+ const chunk = text.slice(i, i + MAX)
128
+ await this.client.im.message.create({
129
+ data: {
130
+ receive_id: chatId,
131
+ receive_id_type: 'open_id',
132
+ msg_type: 'text',
133
+ content: JSON.stringify({ text: chunk }),
134
+ },
135
+ }).catch((e) => logForDebugging(`[feishu] send error: ${e}`))
136
+ }
137
+ }
138
+
139
+ /** Called from useFeishuBridge with REPL message state */
140
+ onMessagesChange(messages: any[], isLoading: boolean): void {
141
+ // Currently a no-op — responses are handled by the bridge pattern
142
+ // where messages go through enqueue → REPL → AppState → back to Feishu
143
+ }
144
+
145
+ private async handleInbound(data: any): Promise<void> {
146
+ // Debug: log what we receive
147
+ console.log('[feishu] raw inbound:', JSON.stringify(data).slice(0, 500))
148
+
149
+ const event = data.event as any
150
+ const sender = event?.sender as any
151
+ const message = event?.message as any
152
+ console.log('[feishu] event:', JSON.stringify(event)?.slice(0,200))
153
+ console.log('[feishu] sender:', JSON.stringify(sender)?.slice(0,200))
154
+ console.log('[feishu] message:', JSON.stringify(message)?.slice(0,200))
155
+
156
+ const chatId = sender?.sender_id?.open_id
157
+ const userId = sender?.sender_id?.open_id
158
+ let text = ''
159
+ try {
160
+ if (message.content) {
161
+ text = JSON.parse(message.content).text?.trim() ?? ''
162
+ }
163
+ } catch {}
164
+
165
+ if (!text || !chatId || !userId) return
166
+
167
+ // Check allowed users
168
+ if (this.config?.allowedUsers?.length && !this.config.allowedUsers.includes(userId)) {
169
+ await this.sendText(chatId, '你没有权限使用此飞书机器人。')
170
+ return
171
+ }
172
+
173
+ logForDebugging(`[feishu] inbound: ${text.slice(0, 50)}`)
174
+ console.log('[feishu] enqueueing:', text.slice(0, 50))
175
+
176
+ // Enqueue to REPL — will be picked up by the command queue
177
+ const { enqueue } = await import('../../utils/messageQueueManager.js')
178
+ enqueue({
179
+ value: text,
180
+ mode: 'prompt',
181
+ skipSlashCommands: true,
182
+ bridgeOrigin: true,
183
+ origin: { kind: 'channel', server: 'feishu' },
184
+ })
185
+ console.log('[feishu] enqueued, queue length now:', (await import('../../utils/messageQueueManager.js')).getCommandQueueSnapshot().length)
186
+ }
187
+
188
+ private setState(next: FeishuServiceState): void {
189
+ this.state = { ...this.state, ...next }
190
+ for (const listener of this.listeners) listener()
191
+ }
192
+ }
193
+
194
+ export const feishuService = new FeishuService()
src/services/feishu/feishuConfig.ts ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Feishu Config — read/write ~/.claude/adapters.json feishu section
3
+ */
4
+
5
+ import * as fs from 'node:fs'
6
+ import * as path from 'node:path'
7
+ import * as os from 'node:os'
8
+
9
+ export type PairedUser = {
10
+ userId: string | number
11
+ displayName: string
12
+ pairedAt: number
13
+ }
14
+
15
+ export type FeishuRuntimeConfig = {
16
+ appId?: string
17
+ appSecret?: string
18
+ encryptKey?: string
19
+ verificationToken?: string
20
+ allowedUsers?: string[]
21
+ pairedUsers?: PairedUser[]
22
+ streamingCard?: boolean
23
+ }
24
+
25
+ function getConfigPath(): string {
26
+ const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
27
+ return path.join(configDir, 'adapters.json')
28
+ }
29
+
30
+ function maskSecret(value: string | undefined): string | undefined {
31
+ if (!value) return value
32
+ if (value.length <= 4) return '****'
33
+ return '****' + value.slice(-4)
34
+ }
35
+
36
+ export function getFeishuConfig(): FeishuRuntimeConfig {
37
+ try {
38
+ const raw = fs.readFileSync(getConfigPath(), 'utf-8')
39
+ const config = JSON.parse(raw) as { feishu?: FeishuRuntimeConfig }
40
+ return config.feishu ?? {}
41
+ } catch {
42
+ return {}
43
+ }
44
+ }
45
+
46
+ export function saveFeishuConfig(patch: Partial<FeishuRuntimeConfig>): void {
47
+ const filePath = getConfigPath()
48
+ const dir = path.dirname(filePath)
49
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
50
+
51
+ let existing: Record<string, unknown> = {}
52
+ try {
53
+ const raw = fs.readFileSync(filePath, 'utf-8')
54
+ existing = JSON.parse(raw)
55
+ } catch {
56
+ // File doesn't exist yet
57
+ }
58
+
59
+ const current = ((existing.feishu as Record<string, unknown> | undefined) ?? {}) as Record<string, unknown>
60
+ // Don't overwrite masked secrets (frontend sends back masked values for unchanged fields)
61
+ const merged: Record<string, unknown> = { ...current, ...patch }
62
+ for (const key of Object.keys(merged)) {
63
+ const val = merged[key]
64
+ if (typeof val === 'string' && val.startsWith('****')) {
65
+ merged[key] = current[key]
66
+ }
67
+ }
68
+
69
+ const finalConfig = { ...existing, feishu: merged }
70
+ fs.writeFileSync(filePath, JSON.stringify(finalConfig, null, 2) + '\n', { mode: 0o600 })
71
+ }
72
+
73
+ export function clearFeishuConfig(): void {
74
+ try {
75
+ const filePath = getConfigPath()
76
+ const raw = fs.readFileSync(filePath, 'utf-8')
77
+ const config = JSON.parse(raw) as Record<string, unknown>
78
+ delete config.feishu
79
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 })
80
+ } catch {
81
+ // ignore
82
+ }
83
+ }
84
+
85
+ export function maskFeishuAppId(appId?: string): string {
86
+ if (!appId) return '未配置'
87
+ if (appId.length <= 6) return '****'
88
+ return appId.slice(0, 3) + '****' + appId.slice(-3)
89
+ }
90
+
91
+ export function maskFeishuAppSecret(secret?: string): string {
92
+ return maskSecret(secret) ?? '未配置'
93
+ }
94
+
95
+ export function isFeishuConfigured(): boolean {
96
+ const config = getFeishuConfig()
97
+ return Boolean(config.appId && config.appSecret)
98
+ }
test_feishu_ws.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Lark from '@larksuiteoapi/node-sdk'
2
+
3
+ const appId = 'cli_aaa6659174b85bec'
4
+ const appSecret = 'nMV7WvGm9Pbvtf0PA3RVLbqNujzeqP7I'
5
+
6
+ const client = new Lark.Client({
7
+ appId,
8
+ appSecret,
9
+ appType: Lark.AppType.SelfBuild,
10
+ domain: Lark.Domain.Feishu,
11
+ })
12
+
13
+ // Test bot info
14
+ try {
15
+ const info = await client.request({
16
+ method: 'GET',
17
+ url: '/open-apis/bot/v3/info',
18
+ })
19
+ console.log('Bot info:', JSON.stringify(info, null, 2))
20
+ } catch (e) {
21
+ console.error('Bot info failed:', e)
22
+ }
23
+
24
+ // Test WSClient
25
+ const dispatcher = new Lark.EventDispatcher({})
26
+
27
+ dispatcher.register({
28
+ 'im.message.receive_v1': async (data: any) => {
29
+ console.log('Received message:', JSON.stringify(data, null, 2))
30
+ },
31
+ } as any)
32
+
33
+ const ws = new Lark.WSClient({
34
+ appId,
35
+ appSecret,
36
+ domain: Lark.Domain.Feishu,
37
+ loggerLevel: Lark.LoggerLevel.debug,
38
+ })
39
+
40
+ await ws.start({ eventDispatcher: dispatcher })
41
+ console.log('[WS] started, waiting 10s...')
42
+ await new Promise(r => setTimeout(r, 10000))
43
+ console.log('[WS] done')
44
+ ws.stop?.()