@bamdra/bamdra-user-bind 0.1.4 → 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 +203 -58
- 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();
|
|
@@ -1445,6 +1531,65 @@ function findBitableRecordId(result, userId) {
|
|
|
1445
1531
|
}
|
|
1446
1532
|
return null;
|
|
1447
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
|
+
}
|
|
1448
1593
|
function getConfiguredAgentId(agent) {
|
|
1449
1594
|
return asNullableString(agent.id) ?? asNullableString(agent.name) ?? asNullableString(agent.agentId);
|
|
1450
1595
|
}
|
package/package.json
CHANGED