@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 +209 -61
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
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
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
|
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.
|
|
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
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
6
|
+
"version": "0.1.6",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"skills": ["./skills"],
|
|
9
9
|
"configSchema": {
|
package/package.json
CHANGED