@bamdra/bamdra-user-bind 0.1.3 → 0.1.5

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 (2) hide show
  1. package/dist/index.js +474 -25
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -59,6 +59,17 @@ var TABLES = {
59
59
  issues: "bamdra_user_bind_issues",
60
60
  audits: "bamdra_user_bind_audits"
61
61
  };
62
+ var REQUIRED_FEISHU_IDENTITY_SCOPES = [
63
+ "contact:user.employee_id:readonly",
64
+ "contact:user.base:readonly"
65
+ ];
66
+ function logUserBindEvent(event, details = {}) {
67
+ try {
68
+ console.info("[bamdra-user-bind]", event, JSON.stringify(details));
69
+ } catch {
70
+ console.info("[bamdra-user-bind]", event);
71
+ }
72
+ }
62
73
  var UserBindStore = class {
63
74
  constructor(dbPath, exportPath, profileMarkdownRoot) {
64
75
  this.dbPath = dbPath;
@@ -412,6 +423,9 @@ var UserBindRuntime = class {
412
423
  config;
413
424
  sessionCache = /* @__PURE__ */ new Map();
414
425
  bindingCache = /* @__PURE__ */ new Map();
426
+ feishuScopeStatus = null;
427
+ bitableMirror = null;
428
+ feishuTokenCache = /* @__PURE__ */ new Map();
415
429
  close() {
416
430
  this.store.close();
417
431
  }
@@ -446,13 +460,26 @@ var UserBindRuntime = class {
446
460
  const binding = this.store.findBinding(parsed.channelType, parsed.openId);
447
461
  let userId = binding?.userId ?? null;
448
462
  let source = binding?.source ?? "local";
449
- if (!userId && parsed.channelType === "feishu" && parsed.openId) {
450
- const remote = await this.tryResolveFeishuUser(parsed.openId);
451
- if (remote?.userId) {
452
- userId = remote.userId;
453
- source = remote.source;
454
- } else {
455
- this.store.recordIssue("feishu-resolution", `Failed to resolve real user id for ${parsed.openId}`);
463
+ let remoteProfilePatch = {};
464
+ if (parsed.channelType === "feishu" && parsed.openId) {
465
+ const scopeStatus = await this.ensureFeishuScopeStatus();
466
+ if (scopeStatus.missingIdentityScopes.length > 0) {
467
+ const details = `Missing Feishu scopes: ${scopeStatus.missingIdentityScopes.join(", ")}`;
468
+ logUserBindEvent("feishu-scope-missing", {
469
+ openId: parsed.openId,
470
+ missingScopes: scopeStatus.missingIdentityScopes
471
+ });
472
+ this.store.recordIssue("feishu-scope-missing", details);
473
+ }
474
+ if (!userId) {
475
+ const remote = await this.tryResolveFeishuUser(parsed.openId);
476
+ if (remote?.userId) {
477
+ userId = remote.userId;
478
+ source = remote.source;
479
+ remoteProfilePatch = remote.profilePatch;
480
+ } else {
481
+ this.store.recordIssue("feishu-resolution", `Failed to resolve real user id for ${parsed.openId}`);
482
+ }
456
483
  }
457
484
  }
458
485
  if (!userId) {
@@ -465,7 +492,8 @@ var UserBindRuntime = class {
465
492
  openId: parsed.openId,
466
493
  source,
467
494
  profilePatch: {
468
- name: parsed.senderName
495
+ name: parsed.senderName,
496
+ ...remoteProfilePatch
469
497
  }
470
498
  });
471
499
  const identity = {
@@ -483,6 +511,9 @@ var UserBindRuntime = class {
483
511
  expiresAt: Date.now() + this.config.cacheTtlMs,
484
512
  identity
485
513
  });
514
+ if (parsed.channelType === "feishu") {
515
+ await this.syncFeishuMirror(identity);
516
+ }
486
517
  return identity;
487
518
  }
488
519
  async getMyProfile(context) {
@@ -677,24 +708,260 @@ var UserBindRuntime = class {
677
708
  return identity;
678
709
  }
679
710
  async tryResolveFeishuUser(openId) {
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" });
715
+ return null;
716
+ }
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
+ });
754
+ }
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
+ };
776
+ }
777
+ } catch {
778
+ }
779
+ }
780
+ logUserBindEvent("feishu-resolution-empty", { openId });
781
+ return null;
782
+ }
783
+ async ensureFeishuScopeStatus() {
784
+ if (this.feishuScopeStatus) {
785
+ return this.feishuScopeStatus;
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
+ }
812
+ const executor = this.host.callTool ?? this.host.invokeTool;
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
+ }
829
+ }
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)}`);
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;
867
+ }
868
+ async syncFeishuMirror(identity) {
869
+ const scopeStatus = await this.ensureFeishuScopeStatus();
870
+ if (!scopeStatus.hasDocumentAccess) {
871
+ return;
872
+ }
680
873
  const executor = this.host.callTool ?? this.host.invokeTool;
681
874
  if (typeof executor !== "function") {
682
- return null;
875
+ return;
683
876
  }
684
877
  try {
685
- const result = await executor.call(this.host, "feishu_user_get", {
686
- user_id_type: "open_id",
687
- user_id: openId
878
+ const mirror = await this.ensureFeishuBitableMirror(executor.bind(this.host));
879
+ if (!mirror.appToken || !mirror.tableId) {
880
+ return;
881
+ }
882
+ const existing = await executor.call(this.host, "feishu_bitable_list_records", {
883
+ app_token: mirror.appToken,
884
+ table_id: mirror.tableId
688
885
  });
689
- const candidate = extractDeepString(result, [
690
- ["data", "user", "user_id"],
691
- ["user", "user_id"],
692
- ["data", "user_id"]
886
+ const recordId = findBitableRecordId(existing, identity.userId);
887
+ const fields = {
888
+ user_id: identity.userId,
889
+ channel_type: identity.channelType,
890
+ open_id: identity.senderOpenId,
891
+ name: identity.profile.name,
892
+ nickname: identity.profile.nickname,
893
+ preferences: identity.profile.preferences,
894
+ personality: identity.profile.personality,
895
+ role: identity.profile.role,
896
+ timezone: identity.profile.timezone,
897
+ email: identity.profile.email,
898
+ avatar: identity.profile.avatar
899
+ };
900
+ if (recordId) {
901
+ await executor.call(this.host, "feishu_bitable_update_record", {
902
+ app_token: mirror.appToken,
903
+ table_id: mirror.tableId,
904
+ record_id: recordId,
905
+ fields
906
+ });
907
+ } else {
908
+ await executor.call(this.host, "feishu_bitable_create_record", {
909
+ app_token: mirror.appToken,
910
+ table_id: mirror.tableId,
911
+ fields
912
+ });
913
+ }
914
+ logUserBindEvent("feishu-bitable-sync-success", { userId: identity.userId });
915
+ } catch (error) {
916
+ const message = error instanceof Error ? error.message : String(error);
917
+ logUserBindEvent("feishu-bitable-sync-failed", { userId: identity.userId, message });
918
+ this.store.recordIssue("feishu-bitable-sync", message, identity.userId);
919
+ }
920
+ }
921
+ async ensureFeishuBitableMirror(executor) {
922
+ if (this.bitableMirror?.appToken && this.bitableMirror?.tableId) {
923
+ return this.bitableMirror;
924
+ }
925
+ try {
926
+ const app = await executor("feishu_bitable_create_app", { name: "Bamdra User Bind" });
927
+ const appToken = extractDeepString(app, [
928
+ ["data", "app", "app_token"],
929
+ ["data", "app_token"],
930
+ ["app", "app_token"],
931
+ ["app_token"]
932
+ ]);
933
+ if (!appToken) {
934
+ return { appToken: null, tableId: null };
935
+ }
936
+ const meta = await executor("feishu_bitable_get_meta", { app_token: appToken });
937
+ const tableId = extractDeepString(meta, [
938
+ ["data", "tables", "0", "table_id"],
939
+ ["data", "items", "0", "table_id"],
940
+ ["tables", "0", "table_id"]
693
941
  ]);
694
- return candidate ? { userId: candidate, source: "feishu-api" } : null;
942
+ if (!tableId) {
943
+ this.store.recordIssue("feishu-bitable-init", "Unable to determine users table id from Feishu bitable metadata");
944
+ return { appToken, tableId: null };
945
+ }
946
+ for (const fieldName of ["user_id", "channel_type", "open_id", "name", "nickname", "preferences", "personality", "role", "timezone", "email", "avatar"]) {
947
+ try {
948
+ await executor("feishu_bitable_create_field", {
949
+ app_token: appToken,
950
+ table_id: tableId,
951
+ field_name: fieldName,
952
+ type: 1
953
+ });
954
+ } catch {
955
+ }
956
+ }
957
+ this.bitableMirror = { appToken, tableId };
958
+ logUserBindEvent("feishu-bitable-ready", this.bitableMirror);
959
+ return this.bitableMirror;
695
960
  } catch (error) {
696
- this.store.recordIssue("feishu-resolution", error instanceof Error ? error.message : String(error));
697
- return null;
961
+ const message = error instanceof Error ? error.message : String(error);
962
+ logUserBindEvent("feishu-bitable-init-failed", { message });
963
+ this.store.recordIssue("feishu-bitable-init", message);
964
+ return { appToken: null, tableId: null };
698
965
  }
699
966
  }
700
967
  };
@@ -885,12 +1152,20 @@ function mapProfileRow(row) {
885
1152
  }
886
1153
  function parseIdentityContext(context) {
887
1154
  const record = context && typeof context === "object" ? context : {};
888
- const sender = record.sender && typeof record.sender === "object" ? record.sender : {};
889
- const message = record.message && typeof record.message === "object" ? record.message : {};
890
- const sessionId = asNullableString(record.sessionId) ?? asNullableString(record.sessionKey) ?? asNullableString(record.session?.id) ?? asNullableString(record.context?.sessionId);
891
- const channelType = asNullableString(record.channelType) ?? asNullableString(record.channel?.type) ?? asNullableString(message.channel?.type) ?? asNullableString(record.provider);
892
- const openId = asNullableString(sender.id) ?? asNullableString(sender.open_id) ?? asNullableString(sender.openId) ?? asNullableString(message.sender?.id);
893
- const senderName = asNullableString(sender.name) ?? asNullableString(sender.display_name) ?? asNullableString(message.sender?.name);
1155
+ const sender = findNestedRecord(record, ["sender"], ["message", "sender"], ["event", "sender"], ["payload", "sender"]);
1156
+ const message = findNestedRecord(record, ["message"], ["event", "message"], ["payload", "message"]);
1157
+ const session = findNestedRecord(record, ["session"], ["context", "session"]);
1158
+ const channel = findNestedRecord(record, ["channel"], ["message", "channel"], ["event", "channel"], ["payload", "channel"]);
1159
+ const metadataText = asNullableString(record.text) ?? asNullableString(message.text) ?? asNullableString(record.content) ?? asNullableString(findNestedValue(record, ["message", "content", "text"]));
1160
+ const conversationInfo = metadataText ? extractTaggedJsonBlock(metadataText, "Conversation info (untrusted metadata)") : null;
1161
+ const senderInfo = metadataText ? extractTaggedJsonBlock(metadataText, "Sender (untrusted metadata)") : null;
1162
+ const senderIdFromText = metadataText ? extractRegexValue(metadataText, /"sender_id"\s*:\s*"([^"]+)"/) : null;
1163
+ const senderNameFromText = metadataText ? extractRegexValue(metadataText, /"sender"\s*:\s*"([^"]+)"/) : null;
1164
+ const senderNameFromMessageLine = metadataText ? extractRegexValue(metadataText, /\]\s*([^\n::]{1,40})\s*[::]/) : null;
1165
+ const sessionId = asNullableString(record.sessionId) ?? asNullableString(record.sessionKey) ?? asNullableString(session.id) ?? asNullableString(record.context?.sessionId) ?? asNullableString(conversationInfo?.session_id) ?? asNullableString(conversationInfo?.message_id);
1166
+ const channelType = asNullableString(record.channelType) ?? asNullableString(channel.type) ?? asNullableString(record.provider) ?? asNullableString(conversationInfo?.provider) ?? inferChannelTypeFromSessionId(sessionId);
1167
+ 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);
1168
+ const senderName = asNullableString(sender.name) ?? asNullableString(sender.display_name) ?? asNullableString(senderInfo?.name) ?? asNullableString(conversationInfo?.sender) ?? senderNameFromText ?? senderNameFromMessageLine;
894
1169
  return {
895
1170
  sessionId,
896
1171
  channelType,
@@ -898,6 +1173,67 @@ function parseIdentityContext(context) {
898
1173
  senderName
899
1174
  };
900
1175
  }
1176
+ function findNestedRecord(root, ...paths) {
1177
+ for (const path of paths) {
1178
+ const value = findNestedValue(root, path);
1179
+ if (value && typeof value === "object") {
1180
+ return value;
1181
+ }
1182
+ }
1183
+ return {};
1184
+ }
1185
+ function findNestedValue(root, path) {
1186
+ let current = root;
1187
+ for (const part of path) {
1188
+ if (!current || typeof current !== "object") {
1189
+ return null;
1190
+ }
1191
+ current = current[part];
1192
+ }
1193
+ return current;
1194
+ }
1195
+ function extractTaggedJsonBlock(text, label) {
1196
+ const start = text.indexOf(label);
1197
+ if (start < 0) {
1198
+ return null;
1199
+ }
1200
+ const block = text.slice(start).match(/```json\s*([\s\S]*?)\s*```/i);
1201
+ if (!block) {
1202
+ return null;
1203
+ }
1204
+ try {
1205
+ const parsed = JSON.parse(block[1]);
1206
+ return parsed && typeof parsed === "object" ? parsed : null;
1207
+ } catch {
1208
+ return null;
1209
+ }
1210
+ }
1211
+ function inferChannelTypeFromSessionId(sessionId) {
1212
+ if (!sessionId) {
1213
+ return null;
1214
+ }
1215
+ if (sessionId.includes(":feishu:")) {
1216
+ return "feishu";
1217
+ }
1218
+ if (sessionId.includes(":telegram:")) {
1219
+ return "telegram";
1220
+ }
1221
+ if (sessionId.includes(":whatsapp:")) {
1222
+ return "whatsapp";
1223
+ }
1224
+ return null;
1225
+ }
1226
+ function extractRegexValue(text, pattern) {
1227
+ const match = text.match(pattern);
1228
+ return match?.[1]?.trim() || null;
1229
+ }
1230
+ function extractOpenIdFromSessionId(sessionId) {
1231
+ if (!sessionId) {
1232
+ return null;
1233
+ }
1234
+ const match = sessionId.match(/:([A-Za-z0-9_-]+)$/);
1235
+ return match?.[1] ?? null;
1236
+ }
901
1237
  function getAgentIdFromContext(context) {
902
1238
  const record = context && typeof context === "object" ? context : {};
903
1239
  return asNullableString(record.agentId) ?? asNullableString(record.agent?.id) ?? asNullableString(record.agent?.name);
@@ -1141,6 +1477,119 @@ function ensureArrayIncludes(parent, key, value) {
1141
1477
  parent[key] = current;
1142
1478
  return true;
1143
1479
  }
1480
+ function extractScopes(result) {
1481
+ const candidates = [
1482
+ findNestedValue(result, ["data", "scopes"]),
1483
+ findNestedValue(result, ["scopes"]),
1484
+ findNestedValue(result, ["data", "items"])
1485
+ ];
1486
+ for (const candidate of candidates) {
1487
+ if (!Array.isArray(candidate)) {
1488
+ continue;
1489
+ }
1490
+ const scopes = candidate.map((item) => {
1491
+ if (typeof item === "string") {
1492
+ return item;
1493
+ }
1494
+ if (item && typeof item === "object") {
1495
+ const record = item;
1496
+ const scope = record.scope ?? record.name;
1497
+ return typeof scope === "string" ? scope : "";
1498
+ }
1499
+ return "";
1500
+ }).filter(Boolean);
1501
+ if (scopes.length > 0) {
1502
+ return scopes;
1503
+ }
1504
+ }
1505
+ return [];
1506
+ }
1507
+ function findBitableRecordId(result, userId) {
1508
+ const candidates = [
1509
+ findNestedValue(result, ["data", "items"]),
1510
+ findNestedValue(result, ["items"]),
1511
+ findNestedValue(result, ["data", "records"])
1512
+ ];
1513
+ for (const candidate of candidates) {
1514
+ if (!Array.isArray(candidate)) {
1515
+ continue;
1516
+ }
1517
+ for (const item of candidate) {
1518
+ if (!item || typeof item !== "object") {
1519
+ continue;
1520
+ }
1521
+ const record = item;
1522
+ const fields = record.fields && typeof record.fields === "object" ? record.fields : {};
1523
+ if (String(fields.user_id ?? "") !== userId) {
1524
+ continue;
1525
+ }
1526
+ const recordId = record.record_id ?? record.recordId ?? record.id;
1527
+ if (typeof recordId === "string" && recordId.trim()) {
1528
+ return recordId;
1529
+ }
1530
+ }
1531
+ }
1532
+ return null;
1533
+ }
1534
+ function readFeishuAccountsFromOpenClawConfig() {
1535
+ const openclawConfigPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "openclaw.json");
1536
+ if (!(0, import_node_fs.existsSync)(openclawConfigPath)) {
1537
+ return [];
1538
+ }
1539
+ try {
1540
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(openclawConfigPath, "utf8"));
1541
+ const channels = parsed.channels && typeof parsed.channels === "object" ? parsed.channels : {};
1542
+ const feishu = channels.feishu && typeof channels.feishu === "object" ? channels.feishu : {};
1543
+ const accounts = feishu.accounts && typeof feishu.accounts === "object" ? feishu.accounts : {};
1544
+ const topLevel = normalizeFeishuAccount("default", feishu, feishu);
1545
+ const values = Object.entries(accounts).map(([accountId, value]) => normalizeFeishuAccount(accountId, value, feishu)).filter((item) => item != null);
1546
+ if (topLevel && !values.some((item) => item.accountId === topLevel.accountId)) {
1547
+ values.unshift(topLevel);
1548
+ }
1549
+ return values;
1550
+ } catch (error) {
1551
+ logUserBindEvent("feishu-config-read-failed", {
1552
+ message: error instanceof Error ? error.message : String(error)
1553
+ });
1554
+ return [];
1555
+ }
1556
+ }
1557
+ function normalizeFeishuAccount(accountId, input, fallback) {
1558
+ const record = input && typeof input === "object" ? input : {};
1559
+ const enabled = record.enabled !== false && fallback.enabled !== false;
1560
+ const appId = asNullableString(record.appId) ?? asNullableString(fallback.appId);
1561
+ const appSecret = asNullableString(record.appSecret) ?? asNullableString(fallback.appSecret);
1562
+ const domain = asNullableString(record.domain) ?? asNullableString(fallback.domain) ?? "feishu";
1563
+ if (!enabled || !appId || !appSecret) {
1564
+ return null;
1565
+ }
1566
+ return { accountId, appId, appSecret, domain };
1567
+ }
1568
+ function resolveFeishuOpenApiBase(domain) {
1569
+ if (domain === "lark") {
1570
+ return "https://open.larksuite.com";
1571
+ }
1572
+ if (domain === "feishu") {
1573
+ return "https://open.feishu.cn";
1574
+ }
1575
+ return domain.replace(/\/+$/, "");
1576
+ }
1577
+ async function feishuJsonRequest(account, path, appAccessToken, init) {
1578
+ const base = resolveFeishuOpenApiBase(account.domain);
1579
+ const response = await fetch(`${base}${path}`, {
1580
+ ...init,
1581
+ headers: {
1582
+ authorization: `Bearer ${appAccessToken}`,
1583
+ "content-type": "application/json; charset=utf-8",
1584
+ ...init?.headers ?? {}
1585
+ }
1586
+ });
1587
+ const payload = await response.json();
1588
+ if (!response.ok || Number(payload.code ?? 0) !== 0) {
1589
+ throw new Error(JSON.stringify(payload));
1590
+ }
1591
+ return payload;
1592
+ }
1144
1593
  function getConfiguredAgentId(agent) {
1145
1594
  return asNullableString(agent.id) ?? asNullableString(agent.name) ?? asNullableString(agent.agentId);
1146
1595
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamdra/bamdra-user-bind",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",