@bamdra/bamdra-user-bind 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -425,6 +425,7 @@ var UserBindRuntime = class {
425
425
  bindingCache = /* @__PURE__ */ new Map();
426
426
  feishuScopeStatus = null;
427
427
  bitableMirror = null;
428
+ feishuTokenCache = /* @__PURE__ */ new Map();
428
429
  close() {
429
430
  this.store.close();
430
431
  }
@@ -707,77 +708,162 @@ var UserBindRuntime = class {
707
708
  return identity;
708
709
  }
709
710
  async tryResolveFeishuUser(openId) {
710
- const executor = this.host.callTool ?? this.host.invokeTool;
711
- if (typeof executor !== "function") {
712
- logUserBindEvent("feishu-resolution-skipped", { reason: "tool-executor-unavailable" });
711
+ logUserBindEvent("feishu-resolution-start", { openId });
712
+ const accounts = readFeishuAccountsFromOpenClawConfig();
713
+ if (accounts.length === 0) {
714
+ logUserBindEvent("feishu-resolution-skipped", { reason: "no-feishu-accounts-configured" });
713
715
  return null;
714
716
  }
715
- try {
716
- logUserBindEvent("feishu-resolution-start", { openId });
717
- const result = await executor.call(this.host, "feishu_user_get", {
718
- user_id_type: "open_id",
719
- user_id: openId
720
- });
721
- const candidate = extractDeepString(result, [
722
- ["data", "user", "user_id"],
723
- ["user", "user_id"],
724
- ["data", "user_id"]
725
- ]);
726
- if (!candidate) {
727
- logUserBindEvent("feishu-resolution-empty", { openId });
728
- return null;
717
+ for (const account of accounts) {
718
+ try {
719
+ const token = await this.getFeishuAppAccessToken(account);
720
+ const result = await feishuJsonRequest(
721
+ account,
722
+ `/open-apis/contact/v3/users/${encodeURIComponent(openId)}?user_id_type=open_id`,
723
+ token
724
+ );
725
+ const candidate = extractDeepString(result, [
726
+ ["data", "user", "user_id"],
727
+ ["user", "user_id"],
728
+ ["data", "user_id"]
729
+ ]);
730
+ if (!candidate) {
731
+ continue;
732
+ }
733
+ logUserBindEvent("feishu-resolution-success", {
734
+ accountId: account.accountId,
735
+ openId,
736
+ userId: candidate
737
+ });
738
+ return {
739
+ userId: candidate,
740
+ source: `feishu-api:${account.accountId}`,
741
+ profilePatch: {
742
+ name: extractDeepString(result, [["data", "user", "name"], ["user", "name"]]),
743
+ email: extractDeepString(result, [["data", "user", "email"], ["user", "email"]]),
744
+ avatar: extractDeepString(result, [["data", "user", "avatar", "avatar_origin"], ["user", "avatar", "avatar_origin"]])
745
+ }
746
+ };
747
+ } catch (error) {
748
+ const message = error instanceof Error ? error.message : String(error);
749
+ logUserBindEvent("feishu-resolution-attempt-failed", {
750
+ accountId: account.accountId,
751
+ openId,
752
+ message
753
+ });
729
754
  }
730
- logUserBindEvent("feishu-resolution-success", { openId, userId: candidate });
731
- return {
732
- userId: candidate,
733
- source: "feishu-api",
734
- profilePatch: {
735
- name: extractDeepString(result, [["data", "user", "name"], ["user", "name"]]),
736
- email: extractDeepString(result, [["data", "user", "email"], ["user", "email"]]),
737
- avatar: extractDeepString(result, [["data", "user", "avatar", "avatar_origin"], ["user", "avatar", "avatar_origin"]])
755
+ }
756
+ const executor = this.host.callTool ?? this.host.invokeTool;
757
+ if (typeof executor === "function") {
758
+ try {
759
+ const result = await executor.call(this.host, "feishu_user_get", {
760
+ user_id_type: "open_id",
761
+ user_id: openId
762
+ });
763
+ const candidate = extractDeepString(result, [
764
+ ["data", "user", "user_id"],
765
+ ["user", "user_id"],
766
+ ["data", "user_id"]
767
+ ]);
768
+ if (candidate) {
769
+ return {
770
+ userId: candidate,
771
+ source: "feishu-tool-fallback",
772
+ profilePatch: {
773
+ name: extractDeepString(result, [["data", "user", "name"], ["user", "name"]])
774
+ }
775
+ };
738
776
  }
739
- };
740
- } catch (error) {
741
- const message = error instanceof Error ? error.message : String(error);
742
- logUserBindEvent("feishu-resolution-failed", { openId, message });
743
- this.store.recordIssue("feishu-resolution", message);
744
- return null;
777
+ } catch {
778
+ }
745
779
  }
780
+ logUserBindEvent("feishu-resolution-empty", { openId });
781
+ return null;
746
782
  }
747
783
  async ensureFeishuScopeStatus() {
748
784
  if (this.feishuScopeStatus) {
749
785
  return this.feishuScopeStatus;
750
786
  }
787
+ const accounts = readFeishuAccountsFromOpenClawConfig();
788
+ for (const account of accounts) {
789
+ try {
790
+ const token = await this.getFeishuAppAccessToken(account);
791
+ const result = await feishuJsonRequest(
792
+ account,
793
+ "/open-apis/application/v6/scopes",
794
+ token
795
+ );
796
+ const scopes = extractScopes(result);
797
+ this.feishuScopeStatus = {
798
+ scopes,
799
+ missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
800
+ hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
801
+ };
802
+ logUserBindEvent("feishu-scopes-read", {
803
+ accountId: account.accountId,
804
+ ...this.feishuScopeStatus
805
+ });
806
+ return this.feishuScopeStatus;
807
+ } catch (error) {
808
+ const message = error instanceof Error ? error.message : String(error);
809
+ logUserBindEvent("feishu-scopes-attempt-failed", { accountId: account.accountId, message });
810
+ }
811
+ }
751
812
  const executor = this.host.callTool ?? this.host.invokeTool;
752
- if (typeof executor !== "function") {
753
- this.feishuScopeStatus = {
754
- scopes: [],
755
- missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
756
- hasDocumentAccess: false
757
- };
758
- return this.feishuScopeStatus;
813
+ if (typeof executor === "function") {
814
+ try {
815
+ const result = await executor.call(this.host, "feishu_app_scopes", {});
816
+ const scopes = extractScopes(result);
817
+ this.feishuScopeStatus = {
818
+ scopes,
819
+ missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
820
+ hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
821
+ };
822
+ logUserBindEvent("feishu-scopes-read", this.feishuScopeStatus);
823
+ return this.feishuScopeStatus;
824
+ } catch (error) {
825
+ const message = error instanceof Error ? error.message : String(error);
826
+ logUserBindEvent("feishu-scopes-failed", { message });
827
+ this.store.recordIssue("feishu-scope-read", message);
828
+ }
759
829
  }
760
- try {
761
- const result = await executor.call(this.host, "feishu_app_scopes", {});
762
- const scopes = extractScopes(result);
763
- this.feishuScopeStatus = {
764
- scopes,
765
- missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
766
- hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
767
- };
768
- logUserBindEvent("feishu-scopes-read", this.feishuScopeStatus);
769
- return this.feishuScopeStatus;
770
- } catch (error) {
771
- const message = error instanceof Error ? error.message : String(error);
772
- logUserBindEvent("feishu-scopes-failed", { message });
773
- this.store.recordIssue("feishu-scope-read", message);
774
- this.feishuScopeStatus = {
775
- scopes: [],
776
- missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
777
- hasDocumentAccess: false
778
- };
779
- return this.feishuScopeStatus;
830
+ this.feishuScopeStatus = {
831
+ scopes: [],
832
+ missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
833
+ hasDocumentAccess: false
834
+ };
835
+ return this.feishuScopeStatus;
836
+ }
837
+ async getFeishuAppAccessToken(account) {
838
+ const cached = this.feishuTokenCache.get(account.accountId);
839
+ if (cached && cached.expiresAt > Date.now()) {
840
+ return cached.token;
841
+ }
842
+ const base = resolveFeishuOpenApiBase(account.domain);
843
+ const response = await fetch(`${base}/open-apis/auth/v3/app_access_token/internal`, {
844
+ method: "POST",
845
+ headers: {
846
+ "content-type": "application/json; charset=utf-8"
847
+ },
848
+ body: JSON.stringify({
849
+ app_id: account.appId,
850
+ app_secret: account.appSecret
851
+ })
852
+ });
853
+ const payload = await response.json();
854
+ if (!response.ok || Number(payload.code ?? 0) !== 0) {
855
+ throw new Error(`Failed to get Feishu app access token for ${account.accountId}: ${JSON.stringify(payload)}`);
780
856
  }
857
+ const token = asNullableString(payload.app_access_token);
858
+ if (!token) {
859
+ throw new Error(`Feishu app access token missing for ${account.accountId}`);
860
+ }
861
+ const expire = Number(payload.expire ?? 7200);
862
+ this.feishuTokenCache.set(account.accountId, {
863
+ token,
864
+ expiresAt: Date.now() + Math.max(60, expire - 120) * 1e3
865
+ });
866
+ return token;
781
867
  }
782
868
  async syncFeishuMirror(identity) {
783
869
  const scopeStatus = await this.ensureFeishuScopeStatus();
@@ -1070,14 +1156,17 @@ function parseIdentityContext(context) {
1070
1156
  const message = findNestedRecord(record, ["message"], ["event", "message"], ["payload", "message"]);
1071
1157
  const session = findNestedRecord(record, ["session"], ["context", "session"]);
1072
1158
  const channel = findNestedRecord(record, ["channel"], ["message", "channel"], ["event", "channel"], ["payload", "channel"]);
1073
- const metadataText = asNullableString(record.text) ?? asNullableString(message.text) ?? asNullableString(record.content) ?? asNullableString(findNestedValue(record, ["message", "content", "text"]));
1159
+ const metadata = findNestedRecord(record, ["metadata"]);
1160
+ const input = findNestedRecord(record, ["input"]);
1161
+ const conversation = findNestedRecord(record, ["conversation"]);
1162
+ const metadataText = asNullableString(record.text) ?? asNullableString(message.text) ?? asNullableString(record.content) ?? asNullableString(metadata.text) ?? asNullableString(input.text) ?? asNullableString(findNestedValue(record, ["message", "content", "text"]));
1074
1163
  const conversationInfo = metadataText ? extractTaggedJsonBlock(metadataText, "Conversation info (untrusted metadata)") : null;
1075
1164
  const senderInfo = metadataText ? extractTaggedJsonBlock(metadataText, "Sender (untrusted metadata)") : null;
1076
1165
  const senderIdFromText = metadataText ? extractRegexValue(metadataText, /"sender_id"\s*:\s*"([^"]+)"/) : null;
1077
1166
  const senderNameFromText = metadataText ? extractRegexValue(metadataText, /"sender"\s*:\s*"([^"]+)"/) : null;
1078
1167
  const senderNameFromMessageLine = metadataText ? extractRegexValue(metadataText, /\]\s*([^\n::]{1,40})\s*[::]/) : null;
1079
- const sessionId = asNullableString(record.sessionId) ?? asNullableString(record.sessionKey) ?? asNullableString(session.id) ?? asNullableString(record.context?.sessionId) ?? asNullableString(conversationInfo?.session_id) ?? asNullableString(conversationInfo?.message_id);
1080
- const channelType = asNullableString(record.channelType) ?? asNullableString(channel.type) ?? asNullableString(record.provider) ?? asNullableString(conversationInfo?.provider) ?? inferChannelTypeFromSessionId(sessionId);
1168
+ const sessionId = asNullableString(record.sessionKey) ?? asNullableString(record.sessionId) ?? asNullableString(session.id) ?? asNullableString(conversation.id) ?? asNullableString(metadata.sessionId) ?? asNullableString(input.sessionId) ?? asNullableString(input.session?.id) ?? asNullableString(record.context?.sessionId) ?? asNullableString(conversationInfo?.session_id) ?? asNullableString(conversationInfo?.message_id);
1169
+ const channelType = asNullableString(record.channelType) ?? asNullableString(channel.type) ?? asNullableString(metadata.channelType) ?? asNullableString(conversation?.provider) ?? asNullableString(record.provider) ?? asNullableString(conversationInfo?.provider) ?? inferChannelTypeFromSessionId(sessionId);
1081
1170
  const openId = asNullableString(sender.id) ?? asNullableString(sender.open_id) ?? asNullableString(sender.openId) ?? asNullableString(sender.user_id) ?? asNullableString(senderInfo?.id) ?? asNullableString(conversationInfo?.sender_id) ?? senderIdFromText ?? extractOpenIdFromSessionId(sessionId);
1082
1171
  const senderName = asNullableString(sender.name) ?? asNullableString(sender.display_name) ?? asNullableString(senderInfo?.name) ?? asNullableString(conversationInfo?.sender) ?? senderNameFromText ?? senderNameFromMessageLine;
1083
1172
  return {
@@ -1445,6 +1534,65 @@ function findBitableRecordId(result, userId) {
1445
1534
  }
1446
1535
  return null;
1447
1536
  }
1537
+ function readFeishuAccountsFromOpenClawConfig() {
1538
+ const openclawConfigPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "openclaw.json");
1539
+ if (!(0, import_node_fs.existsSync)(openclawConfigPath)) {
1540
+ return [];
1541
+ }
1542
+ try {
1543
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(openclawConfigPath, "utf8"));
1544
+ const channels = parsed.channels && typeof parsed.channels === "object" ? parsed.channels : {};
1545
+ const feishu = channels.feishu && typeof channels.feishu === "object" ? channels.feishu : {};
1546
+ const accounts = feishu.accounts && typeof feishu.accounts === "object" ? feishu.accounts : {};
1547
+ const topLevel = normalizeFeishuAccount("default", feishu, feishu);
1548
+ const values = Object.entries(accounts).map(([accountId, value]) => normalizeFeishuAccount(accountId, value, feishu)).filter((item) => item != null);
1549
+ if (topLevel && !values.some((item) => item.accountId === topLevel.accountId)) {
1550
+ values.unshift(topLevel);
1551
+ }
1552
+ return values;
1553
+ } catch (error) {
1554
+ logUserBindEvent("feishu-config-read-failed", {
1555
+ message: error instanceof Error ? error.message : String(error)
1556
+ });
1557
+ return [];
1558
+ }
1559
+ }
1560
+ function normalizeFeishuAccount(accountId, input, fallback) {
1561
+ const record = input && typeof input === "object" ? input : {};
1562
+ const enabled = record.enabled !== false && fallback.enabled !== false;
1563
+ const appId = asNullableString(record.appId) ?? asNullableString(fallback.appId);
1564
+ const appSecret = asNullableString(record.appSecret) ?? asNullableString(fallback.appSecret);
1565
+ const domain = asNullableString(record.domain) ?? asNullableString(fallback.domain) ?? "feishu";
1566
+ if (!enabled || !appId || !appSecret) {
1567
+ return null;
1568
+ }
1569
+ return { accountId, appId, appSecret, domain };
1570
+ }
1571
+ function resolveFeishuOpenApiBase(domain) {
1572
+ if (domain === "lark") {
1573
+ return "https://open.larksuite.com";
1574
+ }
1575
+ if (domain === "feishu") {
1576
+ return "https://open.feishu.cn";
1577
+ }
1578
+ return domain.replace(/\/+$/, "");
1579
+ }
1580
+ async function feishuJsonRequest(account, path, appAccessToken, init) {
1581
+ const base = resolveFeishuOpenApiBase(account.domain);
1582
+ const response = await fetch(`${base}${path}`, {
1583
+ ...init,
1584
+ headers: {
1585
+ authorization: `Bearer ${appAccessToken}`,
1586
+ "content-type": "application/json; charset=utf-8",
1587
+ ...init?.headers ?? {}
1588
+ }
1589
+ });
1590
+ const payload = await response.json();
1591
+ if (!response.ok || Number(payload.code ?? 0) !== 0) {
1592
+ throw new Error(JSON.stringify(payload));
1593
+ }
1594
+ return payload;
1595
+ }
1448
1596
  function getConfiguredAgentId(agent) {
1449
1597
  return asNullableString(agent.id) ?? asNullableString(agent.name) ?? asNullableString(agent.agentId);
1450
1598
  }
@@ -3,7 +3,7 @@
3
3
  "type": "tool",
4
4
  "name": "Bamdra User Bind",
5
5
  "description": "Identity resolution, user profile binding, and admin profile tools for OpenClaw channels.",
6
- "version": "0.1.0",
6
+ "version": "0.1.6",
7
7
  "main": "./dist/index.js",
8
8
  "skills": ["./skills"],
9
9
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamdra/bamdra-user-bind",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Identity resolution, user profile binding, and admin-safe profile tools for OpenClaw channels.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.bamdra.com",