@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.
- package/dist/index.js +474 -25
- 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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
875
|
+
return;
|
|
683
876
|
}
|
|
684
877
|
try {
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
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
|
-
|
|
697
|
-
|
|
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
|
|
889
|
-
const message = record
|
|
890
|
-
const
|
|
891
|
-
const
|
|
892
|
-
const
|
|
893
|
-
const
|
|
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