oasis_test_v2 2.2.111 → 2.2.113

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
@@ -19611,405 +19611,6 @@ var init_host_isolation = __esm({
19611
19611
  }
19612
19612
  });
19613
19613
 
19614
- // ../connectors/src/feishu/index.ts
19615
- async function fetchFeishuTenantAccessToken(appId, appSecret) {
19616
- try {
19617
- const resp = await fetch(FEISHU_TENANT_TOKEN_URL, {
19618
- method: "POST",
19619
- headers: { "Content-Type": "application/json; charset=utf-8" },
19620
- body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
19621
- signal: AbortSignal.timeout(TENANT_TOKEN_TIMEOUT_MS)
19622
- });
19623
- const data = await resp.json();
19624
- if (!resp.ok || data.code !== 0 || typeof data.tenant_access_token !== "string" || !data.tenant_access_token) {
19625
- log("[feishu]", `\u6362\u4F01\u4E1A token \u5931\u8D25\uFF08\u672C\u8F6E bot \u8EAB\u4EFD\u4E0D\u53EF\u7528\uFF09: HTTP ${resp.status} code=${String(data.code)} ${String(data.msg ?? "")}`.trim());
19626
- return null;
19627
- }
19628
- return data.tenant_access_token;
19629
- } catch (err) {
19630
- log("[feishu]", `\u6362\u4F01\u4E1A token \u5931\u8D25\uFF08\u672C\u8F6E bot \u8EAB\u4EFD\u4E0D\u53EF\u7528\uFF09: ${String(err?.message ?? err).slice(0, 200)}`);
19631
- return null;
19632
- }
19633
- }
19634
- function previousTenantToken(credsFile, appId) {
19635
- if (!credsFile) return null;
19636
- try {
19637
- const prev = readSessionCredsFile(credsFile);
19638
- if (prev["LARKSUITE_CLI_APP_ID"] !== appId) return null;
19639
- return prev["LARKSUITE_CLI_TENANT_ACCESS_TOKEN"] || null;
19640
- } catch {
19641
- return null;
19642
- }
19643
- }
19644
- function parseFeishuTokenResponse(data, nowMs) {
19645
- const accessToken = String(data["access_token"] ?? "");
19646
- const refreshToken = String(data["refresh_token"] ?? "");
19647
- const expiresInSec = Number(data["expires_in"] ?? 7200);
19648
- const refreshExpiresInSec = Number(
19649
- data["refresh_token_expires_in"] ?? data["refresh_expires_in"] ?? 30 * 24 * 3600
19650
- );
19651
- const expiresAtMs = nowMs + expiresInSec * 1e3;
19652
- return {
19653
- credentials: {
19654
- ...accessToken ? { user_access_token: accessToken } : {},
19655
- ...refreshToken ? { refresh_token: refreshToken } : {},
19656
- user_token_expires_at: String(expiresAtMs),
19657
- refresh_token_expires_at: String(nowMs + refreshExpiresInSec * 1e3)
19658
- },
19659
- expiresAt: new Date(expiresAtMs)
19660
- };
19661
- }
19662
- var import_node_child_process13, import_node_util4, import_node_path12, import_node_url, import_promises5, execFileAsync2, __dirname2, FEISHU_WELL_KNOWN_SKILLS_BASE, FEISHU_TOKEN_URL, FEISHU_TENANT_TOKEN_URL, FEISHU_USER_OPEN_ID_VAR, FEISHU_AUTHORIZED_BY_VAR, FEISHU_USER_AUTH_INVALID_VAR, FEISHU_REFRESH_REVOKED_CODES, TENANT_TOKEN_TIMEOUT_MS, FeishuConnector;
19663
- var init_feishu = __esm({
19664
- "../connectors/src/feishu/index.ts"() {
19665
- "use strict";
19666
- import_node_child_process13 = require("node:child_process");
19667
- import_node_util4 = require("node:util");
19668
- import_node_path12 = require("node:path");
19669
- import_node_url = require("node:url");
19670
- import_promises5 = require("node:fs/promises");
19671
- init_base();
19672
- init_logger();
19673
- execFileAsync2 = (0, import_node_util4.promisify)(import_node_child_process13.execFile);
19674
- __dirname2 = (0, import_node_path12.dirname)((0, import_node_url.fileURLToPath)(__esm_import_meta_url));
19675
- FEISHU_WELL_KNOWN_SKILLS_BASE = "https://open.feishu.cn/.well-known/skills";
19676
- FEISHU_TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v2/oauth/token";
19677
- FEISHU_TENANT_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
19678
- FEISHU_USER_OPEN_ID_VAR = "FEISHU_USER_OPEN_ID";
19679
- FEISHU_AUTHORIZED_BY_VAR = "FEISHU_AUTHORIZED_BY";
19680
- FEISHU_USER_AUTH_INVALID_VAR = "FEISHU_USER_AUTH_INVALID";
19681
- FEISHU_REFRESH_REVOKED_CODES = /* @__PURE__ */ new Set([20024, 20026, 20037, 20064, 20073, 20074]);
19682
- TENANT_TOKEN_TIMEOUT_MS = 5e3;
19683
- FeishuConnector = class extends CliConnector {
19684
- config = {
19685
- slug: "feishu",
19686
- type: "cli",
19687
- // schema 要**声明本连接器实际消费的全部凭据**,不只是「让用户手填的那几个」。
19688
- // 历史上这里只写了 app_id,而 inject() 其实还要 app_secret / user_access_token
19689
- // (后两者由控制台 OAuth 流写进变量表,用户不手填)——于是 secret 标记缺失,
19690
- // 密钥黑名单只能在 serve.ts 另手写一份。现在黑名单从 sensitiveVars 推导,schema 必须诚实。
19691
- schema: [
19692
- { key: "app_id", description: "\u98DE\u4E66 App ID\uFF08\u7559\u7A7A\u5219\u81EA\u52A8\u521B\u5EFA\u65B0\u5E94\u7528\uFF09", secret: false, required: false },
19693
- { key: "app_secret", description: "\u98DE\u4E66 App Secret\uFF08\u63A7\u5236\u53F0\u6388\u6743\u6D41\u5199\u5165\uFF0C\u975E\u624B\u586B\uFF09", secret: true, required: true },
19694
- { key: "user_access_token", description: "\u98DE\u4E66\u7528\u6237 token\uFF08OAuth \u6388\u6743\u540E\u5199\u5165\uFF0C~2h \u8F6E\u6362\uFF09", secret: true, required: false }
19695
- ]
19696
- };
19697
- commandName = "lark-cli";
19698
- requiredCommands = ["lark-cli"];
19699
- envPrefix = "FEISHU_";
19700
- /** server 侧从 actor 变量表组装凭据的声明(见 provision.ts)。这些会随 job 下发到节点。 */
19701
- credentialSources = [
19702
- { key: "app_id", from: "FEISHU_APP_ID", required: true },
19703
- { key: "app_secret", from: "FEISHU_APP_SECRET", required: true },
19704
- { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false }
19705
- ];
19706
- /**
19707
- * 续期材料——**只在 server 侧读,不下发节点**。refresh_token 能换出新的 user token,
19708
- * 属于"长期材料",没有任何理由让它出现在 agent 所在的机器上。
19709
- */
19710
- refreshCredentialSources = [
19711
- // App 凭据是**组织级**共享的(个人变量优先,缺失回落全局默认);用户 token 类只认个人变量。
19712
- { key: "app_id", from: "FEISHU_APP_ID", orgDefault: true, required: true },
19713
- { key: "app_secret", from: "FEISHU_APP_SECRET", orgDefault: true, required: true },
19714
- { key: "refresh_token", from: "FEISHU_REFRESH_TOKEN", required: true },
19715
- { key: "user_token_expires_at", from: "FEISHU_USER_TOKEN_EXPIRES_AT", required: false },
19716
- { key: "refresh_token_expires_at", from: "FEISHU_REFRESH_TOKEN_EXPIRES_AT", required: false },
19717
- { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false }
19718
- ];
19719
- /** 过期判定只认这个键——必须与注入时消费的 token 同源(见接口注释里的历史坑)。 */
19720
- expiresAtCredentialKey = "user_token_expires_at";
19721
- /**
19722
- * 下发前 user token 至少还要剩 40 分钟,不够就换新(Bug 0201)。
19723
- *
19724
- * 必须 > `SESSION_CREDENTIAL_RENEW_INTERVAL_MS`(30 分钟):会话中途续期每 30 分钟问一次 server,
19725
- * 问的就是这条门槛。门槛若只有通用的 5 分钟,一张还剩 20 分钟的 token 会被原样发回,
19726
- * 20 分钟后失效、要再等 10 分钟才到下一拍——最坏断档近 25 分钟(`99991677 token expire`)。
19727
- * 取 40 = 30 分钟间隔 + 10 分钟调度余量:任一拍之后剩余 > 40,到下一拍仍 > 10。
19728
- *
19729
- * 不像 GitHub 那样取全寿命(每次都换):飞书每次刷新都会轮换 refresh_token,
19730
- * 换得越勤,「飞书已轮换、我们还没落库」的窗口就越常被碰上,所以只取刚好够用的值。
19731
- */
19732
- minRemainingLifetimeMs = 40 * 6e4;
19733
- /**
19734
- * 只由服务端定时任务续期(Bug 0318):refresh_token 一次性、每刷必轮换,派单与会话中途再各刷一次就是
19735
- * 多个写者并发打断续期链;懒续期又让闲置超过 refresh_token 寿命的员工悄悄失效。派单只读库里最新的 token。
19736
- */
19737
- scheduledRefreshOnly = true;
19738
- /** refresh_token 自身 ~30d 过期,过了只能重新授权。 */
19739
- refreshMaterialExpiresAtCredentialKey = "refresh_token_expires_at";
19740
- /**
19741
- * refresh_token 过期后该清掉的键——**只清这个员工的授权产物**。
19742
- * app_id / app_secret 刻意不在其中:它们代表组织级 App 凭据,与其他员工的连接无关。
19743
- */
19744
- clearOnExpiredCredentialKeys = [
19745
- "user_access_token",
19746
- "refresh_token",
19747
- "user_token_expires_at",
19748
- "refresh_token_expires_at"
19749
- ];
19750
- /**
19751
- * 绝不允许进入 agent 进程环境的原始变量。
19752
- * 含 APP_ID 与几个过期时间戳:它们本身不算密钥,但同属飞书凭据材料,一并挡在 agent 之外,
19753
- * 避免 agent 绕开 wrapper 自行拼凭据(沿用既有行为)。
19754
- */
19755
- sensitiveVars = [
19756
- "FEISHU_APP_ID",
19757
- "FEISHU_APP_SECRET",
19758
- "FEISHU_USER_ACCESS_TOKEN",
19759
- "FEISHU_REFRESH_TOKEN",
19760
- "FEISHU_USER_TOKEN_EXPIRES_AT",
19761
- "FEISHU_REFRESH_TOKEN_EXPIRES_AT",
19762
- FEISHU_USER_OPEN_ID_VAR,
19763
- FEISHU_AUTHORIZED_BY_VAR,
19764
- FEISHU_USER_AUTH_INVALID_VAR
19765
- ];
19766
- /** 宿主机登录态隔离,理由同 github(见接口注释):由通用装配层无条件设。 */
19767
- hostCredentialIsolationEnvKeys = ["LARKSUITE_CLI_CONFIG_DIR"];
19768
- /** inject() 写进 sessionEnv 的全部键——通用 cleanup() 按它逐个删。 */
19769
- injectedEnvKeys = [
19770
- "LARK_BIN",
19771
- "FEISHU_CREDS_FILE",
19772
- "FEISHU_RUN_TMPDIR",
19773
- "LARKSUITE_CLI_CONFIG_DIR"
19774
- ];
19775
- /**
19776
- * 自检:拿 user_access_token 问飞书「我是谁」,回 `{ok, identity, data:{name, open_id}}`。
19777
- *
19778
- * **不能用 `lark-cli auth status`**:注入外部凭据后整个 auth 命名空间会被拒
19779
- * ("credentials are provided externally and do not support interactive management"),
19780
- * 自检会恒为失败。实测 2026-08-03 踩到——`auth status` 只在**读宿主机 profile** 时才有输出,
19781
- * 恰恰是我们要隔离掉的那条路径。
19782
- */
19783
- verifyArgs = ["api", "GET", "/open-apis/authen/v1/user_info"];
19784
- /** 厂商技能包源——平台自动拉取飞书官方技能包时用。 */
19785
- wellKnownSkillsBase = FEISHU_WELL_KNOWN_SKILLS_BASE;
19786
- larkBin;
19787
- fetchTenantToken;
19788
- /** `fetchTenantToken` 只给测试替换(不打真飞书);生产一律用 {@link fetchFeishuTenantAccessToken}。 */
19789
- constructor(larkBin, opts = {}) {
19790
- super(__dirname2);
19791
- this.larkBin = larkBin ?? this.resolveLarkBin();
19792
- this.fetchTenantToken = opts.fetchTenantToken ?? fetchFeishuTenantAccessToken;
19793
- }
19794
- /**
19795
- * 解析 user_info 的 JSON。lark-cli 的 `ok:false` 也是**退出码 0** 的正常输出,
19796
- * 所以必须在这里判 ok——只看子进程退出码会把「凭据失效」当成自检通过。
19797
- */
19798
- parseIdentity(stdout) {
19799
- let parsed;
19800
- try {
19801
- parsed = JSON.parse(stdout);
19802
- } catch {
19803
- return null;
19804
- }
19805
- const obj2 = parsed;
19806
- if (obj2.ok !== true) return null;
19807
- const openId = typeof obj2.data?.open_id === "string" ? obj2.data.open_id : "";
19808
- if (!openId) return null;
19809
- const name = typeof obj2.data?.name === "string" ? obj2.data.name : "";
19810
- return name ? `${name} (${openId})` : openId;
19811
- }
19812
- async connect(options = {}) {
19813
- if (this.isSimulated()) {
19814
- return { credentials: { app_id: options["app_id"] ?? "cli_simulated" } };
19815
- }
19816
- const app_id = options["app_id"] ?? await this.initNewApp();
19817
- await this.runAuthLogin(app_id);
19818
- return { credentials: { app_id } };
19819
- }
19820
- /**
19821
- * Agent run 开始:把凭据作为 lark-cli「外部凭据」写进凭据文件(不进 agent env)。
19822
- * credentials 接受:app_id, app_secret, user_access_token?(及其 FEISHU_* 别名)。
19823
- * 企业 token 不随 job 下发,由这里用 app_id/app_secret 现换(换不到只是本轮没有 bot 身份,不抛)。
19824
- *
19825
- * 注入的 sessionEnv 键(全部非敏感):
19826
- * FEISHU_CREDS_FILE — 凭据文件路径,wrapper source 它
19827
- * FEISHU_RUN_TMPDIR — 本轮临时目录,cleanup 整个删掉
19828
- * LARK_BIN — 真实 lark-cli 绝对路径(防 wrapper 递归调用自身)
19829
- */
19830
- async inject(credentials2, sessionEnv) {
19831
- const appId = credentials2["app_id"] ?? credentials2["FEISHU_APP_ID"];
19832
- const appSecret = credentials2["app_secret"] ?? credentials2["FEISHU_APP_SECRET"];
19833
- if (!appId) throw new Error("feishu inject: missing app_id");
19834
- if (!appSecret) throw new Error("feishu inject: missing app_secret");
19835
- const userAccessToken = credentials2["user_access_token"] ?? credentials2["FEISHU_USER_ACCESS_TOKEN"];
19836
- const tenantAccessToken = this.isSimulated() ? null : await this.fetchTenantToken(appId, appSecret) ?? previousTenantToken(sessionEnv.get("FEISHU_CREDS_FILE"), appId);
19837
- const creds = await writeSessionCredsFile(this.config.slug, {
19838
- LARKSUITE_CLI_APP_ID: appId,
19839
- LARKSUITE_CLI_APP_SECRET: appSecret,
19840
- ...tenantAccessToken ? { LARKSUITE_CLI_TENANT_ACCESS_TOKEN: tenantAccessToken } : {},
19841
- ...userAccessToken ? { LARKSUITE_CLI_USER_ACCESS_TOKEN: userAccessToken } : {}
19842
- }, sessionEnv.get("FEISHU_RUN_TMPDIR"));
19843
- const configDir = (0, import_node_path12.join)(creds.dir, "lark-config");
19844
- await (0, import_promises5.mkdir)(configDir, { recursive: true, mode: 448 });
19845
- sessionEnv.set("LARK_BIN", await this.resolveAbsoluteLarkBin());
19846
- sessionEnv.set("FEISHU_CREDS_FILE", creds.file);
19847
- sessionEnv.set("FEISHU_RUN_TMPDIR", creds.dir);
19848
- sessionEnv.set("LARKSUITE_CLI_CONFIG_DIR", configDir);
19849
- }
19850
- /**
19851
- * 用 refresh_token 换一对新的 user token(飞书 v2 OAuth)。
19852
- *
19853
- * **纯换取,不落库**——持久化由调用方做(server 侧 `oauth.ts`)。这样平台知识
19854
- * (端点、字段名、~2h/~30d 的寿命)全部收在连接器里,与 GitHub App 的续期共用
19855
- * `_base/refresh.ts` 那套判定。
19856
- *
19857
- * 返回 null=没有可用的续期材料(未完成授权)。平台报错则抛,由上层收敛成 failed。
19858
- */
19859
- async refreshCredentials(credentials2) {
19860
- const appId = credentials2["app_id"];
19861
- const appSecret = credentials2["app_secret"];
19862
- const refreshToken = credentials2["refresh_token"];
19863
- if (!appId || !appSecret || !refreshToken) return null;
19864
- const resp = await fetch(FEISHU_TOKEN_URL, {
19865
- method: "POST",
19866
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
19867
- body: new URLSearchParams({
19868
- grant_type: "refresh_token",
19869
- refresh_token: refreshToken,
19870
- client_id: appId,
19871
- client_secret: appSecret
19872
- }).toString()
19873
- });
19874
- const data = await resp.json();
19875
- if (!resp.ok || data["error"]) {
19876
- const code2 = Number(data["code"]);
19877
- const msg = `feishu refresh failed: HTTP ${resp.status} code=${String(data["code"] ?? "")} ${String(data["error"] ?? "")}`.trim();
19878
- if (data["error"] === "invalid_grant" || FEISHU_REFRESH_REVOKED_CODES.has(code2)) throw new CredentialRevokedError(msg);
19879
- throw new Error(msg);
19880
- }
19881
- return parseFeishuTokenResponse(data, Date.now());
19882
- }
19883
- async initNewApp() {
19884
- await new Promise((resolve11, reject) => {
19885
- const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["config", "init", "--new"], { stdio: "inherit" });
19886
- proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli config init exited ${code2}`)));
19887
- proc.on("error", reject);
19888
- });
19889
- const { stdout } = await execFileAsync2(this.larkBin, ["config", "show", "--json"]);
19890
- const cfg = JSON.parse(stdout);
19891
- if (!cfg.appId) throw new Error("feishu connector: could not determine app_id after config init");
19892
- return cfg.appId;
19893
- }
19894
- async runAuthLogin(app_id) {
19895
- await new Promise((resolve11, reject) => {
19896
- const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["--profile", app_id, "auth", "login"], { stdio: "inherit" });
19897
- proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli auth login exited ${code2}`)));
19898
- proc.on("error", reject);
19899
- });
19900
- }
19901
- resolveLarkBin() {
19902
- return process.env["LARK_CLI_BIN"] ?? "lark-cli";
19903
- }
19904
- async resolveAbsoluteLarkBin() {
19905
- if (this.larkBin.includes("/")) return this.larkBin;
19906
- return this.resolveRealBin(this.larkBin);
19907
- }
19908
- };
19909
- }
19910
- });
19911
-
19912
- // ../connectors/src/feishu/user-auth.ts
19913
- async function startFeishuDeviceAuthorization(input) {
19914
- const basicAuth = Buffer.from(`${input.appId}:${input.appSecret}`).toString("base64");
19915
- const resp = await (input.fetchImpl ?? fetch)(FEISHU_DEVICE_AUTHORIZATION_URL, {
19916
- method: "POST",
19917
- headers: {
19918
- "Content-Type": "application/x-www-form-urlencoded",
19919
- "Authorization": `Basic ${basicAuth}`
19920
- },
19921
- body: new URLSearchParams({ client_id: input.appId, scope: input.scopes.join(" ") }).toString()
19922
- });
19923
- const data = await resp.json();
19924
- if (!resp.ok || data["error"]) {
19925
- throw new Error(String(data["error_description"] ?? data["error"] ?? "device authorization failed"));
19926
- }
19927
- const verificationUrl = String(data["verification_uri_complete"] ?? data["verification_uri"] ?? "");
19928
- if (!verificationUrl) throw new Error("no verification_uri in response");
19929
- const expiresIn = Number(data["expires_in"]);
19930
- const interval = Number(data["interval"]);
19931
- return {
19932
- verificationUrl,
19933
- deviceCode: String(data["device_code"] ?? ""),
19934
- ...Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresIn } : {},
19935
- ...Number.isFinite(interval) && interval > 0 ? { interval } : {}
19936
- };
19937
- }
19938
- async function pollFeishuDeviceToken(input) {
19939
- const resp = await (input.fetchImpl ?? fetch)(FEISHU_TOKEN_URL, {
19940
- method: "POST",
19941
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
19942
- body: new URLSearchParams({
19943
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
19944
- device_code: input.deviceCode,
19945
- client_id: input.appId,
19946
- client_secret: input.appSecret
19947
- }).toString()
19948
- });
19949
- const data = await resp.json();
19950
- const errStr = data["error"];
19951
- if (errStr === "authorization_pending" || errStr === "slow_down") return { status: "pending" };
19952
- if (!resp.ok || errStr) throw new Error(String(data["error_description"] ?? errStr ?? "login failed"));
19953
- if (!data["access_token"]) throw new Error("no access_token in response");
19954
- return { status: "ok", data };
19955
- }
19956
- async function fetchFeishuUserIdentity(accessToken, fetchImpl = fetch) {
19957
- try {
19958
- const resp = await fetchImpl(FEISHU_USER_INFO_URL, { headers: { authorization: `Bearer ${accessToken}` } });
19959
- const ui = await resp.json();
19960
- if (!resp.ok || ui.code !== 0) return {};
19961
- const name = ui.data?.name || ui.data?.en_name;
19962
- return {
19963
- ...ui.data?.open_id ? { openId: ui.data.open_id } : {},
19964
- ...name ? { name } : {}
19965
- };
19966
- } catch {
19967
- return {};
19968
- }
19969
- }
19970
- async function sendFeishuBotTextToUser(input) {
19971
- const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
19972
- if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0Cbot \u79C1\u4FE1\u672A\u53D1\u51FA");
19973
- const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_SEND_MESSAGE_URL}?receive_id_type=open_id`, {
19974
- method: "POST",
19975
- headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
19976
- body: JSON.stringify({ receive_id: input.openId, msg_type: "text", content: JSON.stringify({ text: input.text }) })
19977
- });
19978
- const body2 = await resp.json();
19979
- if (body2.code !== 0) throw new Error(`feishu bot \u79C1\u4FE1\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
19980
- }
19981
- async function lookupFeishuOpenIdByEmail(input) {
19982
- const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
19983
- if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0C\u672A\u80FD\u6309\u90AE\u7BB1\u67E5\u98DE\u4E66\u7528\u6237");
19984
- const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_BATCH_GET_ID_URL}?user_id_type=open_id`, {
19985
- method: "POST",
19986
- headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
19987
- body: JSON.stringify({ emails: [input.email], include_resigned: false })
19988
- });
19989
- const body2 = await resp.json();
19990
- if (body2.code !== 0) throw new Error(`feishu \u6309\u90AE\u7BB1\u67E5\u7528\u6237\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
19991
- const hit = body2.data?.user_list?.find((u) => u.email?.toLowerCase() === input.email.toLowerCase() && u.user_id);
19992
- return hit?.user_id ?? null;
19993
- }
19994
- function feishuUserAuthInvalidText(input) {
19995
- const head = `\u300C${input.agentName}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF08\u6388\u6743\u88AB\u64A4\u9500\u6216\u957F\u65F6\u95F4\u65E0\u6CD5\u7EED\u671F\uFF09\uFF0C\u4EE5\u4F60\u8EAB\u4EFD\u8C03\u7528\u98DE\u4E66\u7684\u529F\u80FD\u6682\u65F6\u7528\u4E0D\u4E86\uFF0Cbot \u8EAB\u4EFD\u4E0D\u53D7\u5F71\u54CD\u3002`;
19996
- return input.link ? `${head}
19997
- \u70B9\u8FD9\u4E2A\u94FE\u63A5\u91CD\u65B0\u6388\u6743\uFF08\u9700\u7528\u6536\u5230\u8FD9\u6761\u79C1\u4FE1\u7684\u98DE\u4E66\u8D26\u53F7\u767B\u5F55\uFF0C7 \u5929\u5185\u6709\u6548\uFF09\uFF1A
19998
- ${input.link}` : `${head}
19999
- \u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u91CD\u65B0\u6388\u6743\u98DE\u4E66\u3002`;
20000
- }
20001
- var FEISHU_DEVICE_AUTHORIZATION_URL, FEISHU_USER_INFO_URL, FEISHU_SEND_MESSAGE_URL, FEISHU_BATCH_GET_ID_URL;
20002
- var init_user_auth = __esm({
20003
- "../connectors/src/feishu/user-auth.ts"() {
20004
- "use strict";
20005
- init_feishu();
20006
- FEISHU_DEVICE_AUTHORIZATION_URL = "https://accounts.feishu.cn/oauth/v1/device_authorization";
20007
- FEISHU_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";
20008
- FEISHU_SEND_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
20009
- FEISHU_BATCH_GET_ID_URL = "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id";
20010
- }
20011
- });
20012
-
20013
19614
  // ../connectors/src/feishu/scopes.ts
20014
19615
  function scopesForDomains(domains) {
20015
19616
  const set = /* @__PURE__ */ new Set(["offline_access"]);
@@ -20042,6 +19643,11 @@ function scopesForSelection(sel, includeSendAsUser) {
20042
19643
  return scopes;
20043
19644
  }
20044
19645
  async function appHasScope(appId, appSecret, scope2, fetchImpl = fetch) {
19646
+ return (await fetchFeishuAppScopes(appId, appSecret, fetchImpl)).some(
19647
+ (s2) => s2.scope_name === scope2 && (!s2.scope_type || s2.scope_type === "user")
19648
+ );
19649
+ }
19650
+ async function fetchFeishuAppScopes(appId, appSecret, fetchImpl = fetch) {
20045
19651
  const tokenResp = await fetchImpl("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
20046
19652
  method: "POST",
20047
19653
  headers: { "Content-Type": "application/json; charset=utf-8" },
@@ -20056,9 +19662,7 @@ async function appHasScope(appId, appSecret, scope2, fetchImpl = fetch) {
20056
19662
  });
20057
19663
  const body2 = await resp.json();
20058
19664
  if (body2.code !== 0) throw new Error(`application/v6/scopes failed: code=${body2.code} msg=${body2.msg ?? ""}`);
20059
- return (body2.data?.scopes ?? []).some(
20060
- (s2) => s2.scope_name === scope2 && (!s2.scope_type || s2.scope_type === "user")
20061
- );
19665
+ return body2.data?.scopes ?? [];
20062
19666
  }
20063
19667
  function feishuAuthSelectionRecord(sel, sendAsUserRequested, tokenResponse) {
20064
19668
  const granted = typeof tokenResponse["scope"] === "string" ? tokenResponse["scope"].split(/\s+/).filter(Boolean) : void 0;
@@ -20069,7 +19673,84 @@ function feishuAuthSelectionRecord(sel, sendAsUserRequested, tokenResponse) {
20069
19673
  ...granted ? { grantedScopes: granted } : {}
20070
19674
  };
20071
19675
  }
20072
- var FEISHU_BASIC_SCOPES, FEISHU_DOMAIN_SCOPES, FEISHU_USER_SCOPES, FEISHU_USER_SCOPE_STRING, FEISHU_SEND_AS_USER_SCOPE, FEISHU_AUTH_SELECTION_VAR, AUTO_APPROVED;
19676
+ function feishuModuleRules() {
19677
+ const own3 = Object.entries(FEISHU_MODULE_ACCESS).flatMap(([m2, a]) => [
19678
+ ...a.commands.map((c) => ({ pattern: `cmd/${c}/**`, modules: [m2] })),
19679
+ ...a.apiServices.map((s2) => ({ pattern: `api/*/${s2}/**`, modules: [m2] }))
19680
+ ]);
19681
+ return [...own3, ...FEISHU_SHARED_RULES];
19682
+ }
19683
+ function ruleToExtglob(pattern) {
19684
+ const tail = pattern.endsWith("/**");
19685
+ const body2 = (tail ? pattern.slice(0, -3) : pattern).split("/").map((seg) => seg === "*" ? "+([!/])" : seg).join("/");
19686
+ return tail ? `${body2}?(/*)` : body2;
19687
+ }
19688
+ function feishuParseSelection(raw) {
19689
+ if (!raw) return { state: "none", modules: [] };
19690
+ let parsed;
19691
+ try {
19692
+ parsed = JSON.parse(raw);
19693
+ } catch {
19694
+ return { state: "invalid", modules: [] };
19695
+ }
19696
+ const modules = Array.isArray(parsed?.modules) ? [...new Set(parsed.modules.filter((m2) => typeof m2 === "string" && !!FEISHU_MODULE_ACCESS[m2]))] : [];
19697
+ return modules.length > 0 ? { state: "ok", modules } : { state: "invalid", modules: [] };
19698
+ }
19699
+ function feishuModulePolicyEnv(selection) {
19700
+ const picked = selection.state === "ok" ? selection.modules.filter((m2) => FEISHU_MODULE_ACCESS[m2]) : [];
19701
+ const label2 = (m2) => (FEISHU_MODULE_LABELS[m2] ?? m2).replace(/\s+/g, "");
19702
+ return {
19703
+ OASIS_FEISHU_POLICY: picked.length > 0 ? "ok" : selection.state === "ok" ? "invalid" : selection.state,
19704
+ OASIS_FEISHU_ALLOW: ` ${picked.join(" ")} `,
19705
+ OASIS_FEISHU_RULES: feishuModuleRules().map((r) => `${ruleToExtglob(r.pattern)}=${r.modules.join(",")}`).join(" "),
19706
+ OASIS_FEISHU_LABELS: Object.keys(FEISHU_MODULE_ACCESS).map((m2) => `${m2}=${label2(m2)}`).join(" "),
19707
+ OASIS_FEISHU_MODULES: picked.map((m2) => FEISHU_MODULE_LABELS[m2] ?? m2).join("\u3001")
19708
+ };
19709
+ }
19710
+ function feishuModuleAccessReport(input) {
19711
+ const granted = input.grantedScopes ? new Set(input.grantedScopes) : null;
19712
+ const tenant = /* @__PURE__ */ new Map();
19713
+ for (const s2 of input.appScopes ?? []) {
19714
+ if (s2.scope_type !== "tenant" || !s2.scope_name) continue;
19715
+ const ok3 = s2.grant_status === void 0 || s2.grant_status === 1;
19716
+ tenant.set(s2.scope_name, (tenant.get(s2.scope_name) ?? false) || ok3);
19717
+ }
19718
+ return input.modules.filter((m2) => FEISHU_MODULE_ACCESS[m2]).map((m2) => {
19719
+ const gaps = [];
19720
+ let anyGranted = false;
19721
+ let anyPending = false;
19722
+ for (const cap of FEISHU_MODULE_ACCESS[m2].botCapabilities) {
19723
+ if (cap.anyOf.some((s2) => tenant.get(s2) === true)) {
19724
+ anyGranted = true;
19725
+ continue;
19726
+ }
19727
+ const pending = cap.anyOf.some((s2) => tenant.has(s2));
19728
+ if (pending) anyPending = true;
19729
+ gaps.push({ label: cap.label, state: pending ? "pending" : "missing", scopes: [...cap.anyOf] });
19730
+ }
19731
+ const bot = input.appScopes === null ? "unknown" : gaps.length === 0 ? "granted" : anyGranted ? "partial" : anyPending ? "pending" : "missing";
19732
+ const missingUserScopes = granted ? (FEISHU_DOMAIN_SCOPES[m2]?.scopes ?? []).filter((s2) => !granted.has(s2)) : [];
19733
+ const user = input.userAuthInvalid ? "invalid" : !input.hasUserAuth ? "none" : !granted ? "unknown" : missingUserScopes.length > 0 ? "partial" : "granted";
19734
+ return {
19735
+ id: m2,
19736
+ label: FEISHU_MODULE_LABELS[m2] ?? m2,
19737
+ bot,
19738
+ missingBotCapabilities: bot === "unknown" ? [] : gaps,
19739
+ user,
19740
+ missingUserScopes: user === "partial" ? missingUserScopes : []
19741
+ };
19742
+ });
19743
+ }
19744
+ function feishuSendAsUserState(sel) {
19745
+ if (!sel.sendAsUser) return "off";
19746
+ if (!sel.sendAsUserRequested) return "app_not_enabled";
19747
+ if (sel.grantedScopes && !sel.grantedScopes.includes(FEISHU_SEND_AS_USER_SCOPE)) return "not_granted";
19748
+ return "on";
19749
+ }
19750
+ function feishuAppPermissionConsoleUrl(appId) {
19751
+ return `https://open.feishu.cn/app/${encodeURIComponent(appId)}/auth`;
19752
+ }
19753
+ var FEISHU_BASIC_SCOPES, FEISHU_DOMAIN_SCOPES, FEISHU_USER_SCOPES, FEISHU_USER_SCOPE_STRING, FEISHU_SEND_AS_USER_SCOPE, FEISHU_AUTH_SELECTION_VAR, AUTO_APPROVED, FEISHU_MODULE_ACCESS, DRIVE_DOCS_ONLY_SHORTCUTS, DRIVE_DOCS_ONLY_RESOURCES, DRIVE_SHARED_SHORTCUTS, FEISHU_SHARED_RULES, FEISHU_MODULE_LABELS;
20073
19754
  var init_scopes = __esm({
20074
19755
  "../connectors/src/feishu/scopes.ts"() {
20075
19756
  "use strict";
@@ -20472,6 +20153,582 @@ var init_scopes = __esm({
20472
20153
  FEISHU_SEND_AS_USER_SCOPE = "im:message.send_as_user";
20473
20154
  FEISHU_AUTH_SELECTION_VAR = "FEISHU_AUTH_SELECTION";
20474
20155
  AUTO_APPROVED = new Set(FEISHU_BASIC_SCOPES);
20156
+ FEISHU_MODULE_ACCESS = {
20157
+ im: { commands: ["im"], apiServices: ["im"], botCapabilities: [
20158
+ { label: "\u53D1\u6D88\u606F", anyOf: ["im:message:send_as_bot", "im:message", "im:message:send"] },
20159
+ { label: "\u8BFB\u6D88\u606F", anyOf: ["im:message:readonly", "im:message", "im:message.group_msg", "im:message.p2p_msg:readonly", "im:message.group_at_msg:readonly"] },
20160
+ { label: "\u7FA4\u4FE1\u606F", anyOf: ["im:chat:readonly", "im:chat:read", "im:chat"] }
20161
+ ] },
20162
+ calendar: { commands: ["calendar"], apiServices: ["calendar"], botCapabilities: [
20163
+ { label: "\u8BFB\u65E5\u7A0B", anyOf: ["calendar:calendar:readonly", "calendar:calendar", "calendar:calendar:read", "calendar:calendar.event:read"] },
20164
+ { label: "\u5199\u65E5\u7A0B", anyOf: ["calendar:calendar", "calendar:calendar.event:create", "calendar:calendar.event:update"] }
20165
+ ] },
20166
+ docs: { commands: ["docs", "mindnotes"], apiServices: ["docx", "doc", "docs", "mindnote", "mindnotes", "suite"], botCapabilities: [
20167
+ { label: "\u8BFB\u6587\u6863", anyOf: ["docx:document:readonly", "docx:document", "docs:document.content:read", "docs:doc:readonly", "docs:doc"] },
20168
+ { label: "\u5199\u6587\u6863", anyOf: ["docx:document", "docx:document:create", "docx:document:write_only", "docs:doc"] }
20169
+ ] },
20170
+ drive: { commands: ["markdown"], apiServices: [], botCapabilities: [
20171
+ { label: "\u4E0B\u8F7D\u6587\u4EF6", anyOf: ["drive:drive:readonly", "drive:drive", "drive:file:download", "drive:file:readonly", "drive:file"] },
20172
+ { label: "\u4E0A\u4F20\u6587\u4EF6", anyOf: ["drive:drive", "drive:file:upload", "drive:file"] }
20173
+ ] },
20174
+ sheets: { commands: ["sheets"], apiServices: ["sheets"], botCapabilities: [
20175
+ { label: "\u8BFB\u8868\u683C", anyOf: ["sheets:spreadsheet:readonly", "sheets:spreadsheet", "sheets:spreadsheet:read"] },
20176
+ { label: "\u5199\u8868\u683C", anyOf: ["sheets:spreadsheet", "sheets:spreadsheet:write_only", "sheets:spreadsheet:create"] }
20177
+ ] },
20178
+ base: { commands: ["base"], apiServices: ["bitable", "base"], botCapabilities: [
20179
+ { label: "\u8BFB\u8BB0\u5F55", anyOf: ["bitable:app:readonly", "bitable:app", "base:record:read", "base:app:read"] },
20180
+ { label: "\u5199\u8BB0\u5F55", anyOf: ["bitable:app", "base:record:create", "base:record:update"] }
20181
+ ] },
20182
+ wiki: { commands: ["wiki"], apiServices: ["wiki"], botCapabilities: [
20183
+ { label: "\u8BFB\u77E5\u8BC6\u5E93", anyOf: ["wiki:wiki:readonly", "wiki:wiki", "wiki:node:read", "wiki:space:read"] },
20184
+ { label: "\u5199\u77E5\u8BC6\u5E93", anyOf: ["wiki:wiki", "wiki:node:create", "wiki:space:write_only"] }
20185
+ ] },
20186
+ slides: { commands: ["slides"], apiServices: ["slides", "slides_ai"], botCapabilities: [
20187
+ { label: "\u8BFB\u5E7B\u706F\u7247", anyOf: ["slides:presentation:read", "slides:presentation:readonly", "slides:presentation"] },
20188
+ { label: "\u5199\u5E7B\u706F\u7247", anyOf: ["slides:presentation:update", "slides:presentation:write_only", "slides:presentation:create", "slides:presentation"] }
20189
+ ] },
20190
+ task: { commands: ["task"], apiServices: ["task"], botCapabilities: [
20191
+ { label: "\u8BFB\u4EFB\u52A1", anyOf: ["task:task:read", "task:task:readonly", "task:task"] },
20192
+ { label: "\u5199\u4EFB\u52A1", anyOf: ["task:task:write", "task:task"] }
20193
+ ] },
20194
+ approval: { commands: ["approval"], apiServices: ["approval"], botCapabilities: [
20195
+ { label: "\u8BFB\u5BA1\u6279", anyOf: ["approval:instance:read", "approval:approval:readonly", "approval:approval"] },
20196
+ { label: "\u5904\u7406\u5BA1\u6279", anyOf: ["approval:instance:write", "approval:task:write", "approval:approval"] }
20197
+ ] },
20198
+ mail: { commands: ["mail"], apiServices: ["mail"], botCapabilities: [
20199
+ { label: "\u8BFB\u90AE\u4EF6", anyOf: ["mail:user_mailbox.message:readonly", "mail:user_mailbox:readonly", "mail:user_mailbox"] }
20200
+ ] },
20201
+ minutes: { commands: ["minutes"], apiServices: ["minutes"], botCapabilities: [
20202
+ { label: "\u8BFB\u5999\u8BB0", anyOf: ["minutes:minutes:readonly", "minutes:minutes", "minutes:minutes.search:read"] }
20203
+ ] },
20204
+ vc: { commands: ["vc", "note"], apiServices: ["vc"], botCapabilities: [
20205
+ { label: "\u8BFB\u4F1A\u8BAE", anyOf: ["vc:meeting:readonly", "vc:meeting", "vc:meeting.meetingevent:read", "vc:record:readonly"] }
20206
+ ] },
20207
+ contact: { commands: ["contact"], apiServices: ["contact"], botCapabilities: [
20208
+ { label: "\u8BFB\u901A\u8BAF\u5F55", anyOf: ["contact:user.base:readonly", "contact:contact:readonly_as_app", "contact:contact:readonly", "contact:contact", "contact:user.basic_profile:readonly"] }
20209
+ ] },
20210
+ okr: { commands: ["okr"], apiServices: ["okr"], botCapabilities: [
20211
+ { label: "\u8BFB OKR", anyOf: ["okr:okr:readonly", "okr:okr", "okr:okr:read"] }
20212
+ ] },
20213
+ attendance: { commands: ["attendance"], apiServices: ["attendance"], botCapabilities: [
20214
+ { label: "\u8BFB\u8003\u52E4", anyOf: ["attendance:task:readonly", "attendance:record:read", "attendance:task"] }
20215
+ ] },
20216
+ board: { commands: ["whiteboard"], apiServices: ["board"], botCapabilities: [
20217
+ { label: "\u8BFB\u767D\u677F", anyOf: ["board:whiteboard:node:read"] },
20218
+ { label: "\u5199\u767D\u677F", anyOf: ["board:whiteboard:node:create"] }
20219
+ ] },
20220
+ spark: { commands: ["apps"], apiServices: ["spark"], botCapabilities: [
20221
+ { label: "\u8BFB\u5E94\u7528", anyOf: ["spark:app:read"] },
20222
+ { label: "\u5199\u5E94\u7528", anyOf: ["spark:app:write"] }
20223
+ ] }
20224
+ };
20225
+ DRIVE_DOCS_ONLY_SHORTCUTS = [
20226
+ // 评论
20227
+ "+add-comment",
20228
+ "+add-reply",
20229
+ "+batch-query-comments",
20230
+ "+delete-reply",
20231
+ "+list-comments",
20232
+ "+list-replies",
20233
+ "+react-reply",
20234
+ "+resolve-comment",
20235
+ "+restore-comment",
20236
+ "+update-reply",
20237
+ // 协作者与分享权限
20238
+ "+apply-permission",
20239
+ "+member-add",
20240
+ "+member-list",
20241
+ "+member-remove",
20242
+ "+permission-get-setting",
20243
+ // 导入导出、搜索、密级
20244
+ "+export",
20245
+ "+export-download",
20246
+ "+import",
20247
+ "+search",
20248
+ "+secure-label-list",
20249
+ "+secure-label-update"
20250
+ ];
20251
+ DRIVE_DOCS_ONLY_RESOURCES = [
20252
+ "file.comment.reply.reactions",
20253
+ "file.comment.replys",
20254
+ "file.comments",
20255
+ "permission.members",
20256
+ "permission.public",
20257
+ "user"
20258
+ ];
20259
+ DRIVE_SHARED_SHORTCUTS = ["+copy", "+create-folder", "+create-shortcut", "+delete", "+move", "+update-title", "+inspect", "+task_result"];
20260
+ FEISHU_SHARED_RULES = [
20261
+ ...DRIVE_DOCS_ONLY_SHORTCUTS.map((s2) => ({ pattern: `cmd/drive/${s2}/**`, modules: ["docs"] })),
20262
+ ...DRIVE_DOCS_ONLY_RESOURCES.map((r) => ({ pattern: `cmd/drive/${r}/*/**`, modules: ["docs"] })),
20263
+ ...DRIVE_SHARED_SHORTCUTS.map((s2) => ({ pattern: `cmd/drive/${s2}/**`, modules: ["docs", "drive"] })),
20264
+ ...["copy", "create_folder", "patch"].map((m2) => ({ pattern: `cmd/drive/files/${m2}/**`, modules: ["docs", "drive"] })),
20265
+ { pattern: "cmd/drive/metas/*/**", modules: ["docs", "drive"] },
20266
+ // drive 后面先写参数再写子命令的(`drive --as bot +add-comment`),认不出子命令归谁——谁都不放行。
20267
+ { pattern: "cmd/drive/-/**", modules: [] },
20268
+ { pattern: "cmd/drive/**", modules: ["drive"] },
20269
+ ...[
20270
+ "drive/v1/permissions/**",
20271
+ "drive/v2/permissions/**",
20272
+ "drive/permission/**",
20273
+ "drive/v1/files/*/comments/**",
20274
+ "drive/v1/files/*/new_comments/**",
20275
+ "drive/v2/files/*/comments/**",
20276
+ "drive/v2/files/*/likes/**",
20277
+ "drive/v1/files/*/subscribe/**",
20278
+ "drive/v1/files/*/get_subscribe/**",
20279
+ "drive/v1/files/*/delete_subscribe/**",
20280
+ "drive/v1/files/*/subscriptions/**",
20281
+ "drive/v1/user/**",
20282
+ "drive/v1/import_tasks/**",
20283
+ "drive/v1/export_tasks/**",
20284
+ "drive/v1/medias/**",
20285
+ "drive/v2/files/*/secure_label/**",
20286
+ "drive/v2/my_secure_labels/**"
20287
+ ].map((p2) => ({ pattern: `api/*/${p2}`, modules: ["docs"] })),
20288
+ ...[
20289
+ "*/drive/v1/files/create_folder/**",
20290
+ "*/drive/v1/files/create_shortcut/**",
20291
+ "*/drive/v1/files/task_check/**",
20292
+ "*/drive/v1/files/*/move/**",
20293
+ "*/drive/v1/files/*/copy/**",
20294
+ "DELETE/drive/v1/files/*",
20295
+ "PATCH/drive/v1/files/*",
20296
+ "*/drive/v1/metas/**",
20297
+ "*/drive/v2/files/query_by_token/**",
20298
+ "*/drive/explorer/**"
20299
+ ].map((p2) => ({ pattern: `api/${p2}`, modules: ["docs", "drive"] })),
20300
+ { pattern: "api/*/drive/**", modules: ["drive"] },
20301
+ { pattern: "api/*/search/v1/user/**", modules: ["contact"] },
20302
+ { pattern: "api/*/search/v2/message/**", modules: ["im"] },
20303
+ { pattern: "api/*/search/v2/doc_wiki/**", modules: ["docs", "wiki"] }
20304
+ ];
20305
+ FEISHU_MODULE_LABELS = {
20306
+ im: "\u5373\u65F6\u6D88\u606F",
20307
+ calendar: "\u65E5\u5386",
20308
+ docs: "\u4E91\u6587\u6863",
20309
+ drive: "\u4E91\u76D8",
20310
+ sheets: "\u7535\u5B50\u8868\u683C",
20311
+ base: "\u591A\u7EF4\u8868\u683C",
20312
+ wiki: "\u77E5\u8BC6\u5E93",
20313
+ slides: "\u5E7B\u706F\u7247",
20314
+ task: "\u4EFB\u52A1",
20315
+ approval: "\u5BA1\u6279",
20316
+ mail: "\u90AE\u4EF6",
20317
+ minutes: "\u5999\u8BB0",
20318
+ vc: "\u89C6\u9891\u4F1A\u8BAE",
20319
+ contact: "\u901A\u8BAF\u5F55",
20320
+ okr: "OKR",
20321
+ attendance: "\u8003\u52E4",
20322
+ board: "\u767D\u677F",
20323
+ spark: "Spark \u5E94\u7528"
20324
+ };
20325
+ }
20326
+ });
20327
+
20328
+ // ../connectors/src/feishu/index.ts
20329
+ async function fetchFeishuTenantAccessToken(appId, appSecret) {
20330
+ try {
20331
+ const resp = await fetch(FEISHU_TENANT_TOKEN_URL, {
20332
+ method: "POST",
20333
+ headers: { "Content-Type": "application/json; charset=utf-8" },
20334
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
20335
+ signal: AbortSignal.timeout(TENANT_TOKEN_TIMEOUT_MS)
20336
+ });
20337
+ const data = await resp.json();
20338
+ if (!resp.ok || data.code !== 0 || typeof data.tenant_access_token !== "string" || !data.tenant_access_token) {
20339
+ log("[feishu]", `\u6362\u4F01\u4E1A token \u5931\u8D25\uFF08\u672C\u8F6E bot \u8EAB\u4EFD\u4E0D\u53EF\u7528\uFF09: HTTP ${resp.status} code=${String(data.code)} ${String(data.msg ?? "")}`.trim());
20340
+ return null;
20341
+ }
20342
+ return data.tenant_access_token;
20343
+ } catch (err) {
20344
+ log("[feishu]", `\u6362\u4F01\u4E1A token \u5931\u8D25\uFF08\u672C\u8F6E bot \u8EAB\u4EFD\u4E0D\u53EF\u7528\uFF09: ${String(err?.message ?? err).slice(0, 200)}`);
20345
+ return null;
20346
+ }
20347
+ }
20348
+ function previousTenantToken(credsFile, appId) {
20349
+ if (!credsFile) return null;
20350
+ try {
20351
+ const prev = readSessionCredsFile(credsFile);
20352
+ if (prev["LARKSUITE_CLI_APP_ID"] !== appId) return null;
20353
+ return prev["LARKSUITE_CLI_TENANT_ACCESS_TOKEN"] || null;
20354
+ } catch {
20355
+ return null;
20356
+ }
20357
+ }
20358
+ function parseFeishuTokenResponse(data, nowMs) {
20359
+ const accessToken = String(data["access_token"] ?? "");
20360
+ const refreshToken = String(data["refresh_token"] ?? "");
20361
+ const expiresInSec = Number(data["expires_in"] ?? 7200);
20362
+ const refreshExpiresInSec = Number(
20363
+ data["refresh_token_expires_in"] ?? data["refresh_expires_in"] ?? 30 * 24 * 3600
20364
+ );
20365
+ const expiresAtMs = nowMs + expiresInSec * 1e3;
20366
+ return {
20367
+ credentials: {
20368
+ ...accessToken ? { user_access_token: accessToken } : {},
20369
+ ...refreshToken ? { refresh_token: refreshToken } : {},
20370
+ user_token_expires_at: String(expiresAtMs),
20371
+ refresh_token_expires_at: String(nowMs + refreshExpiresInSec * 1e3)
20372
+ },
20373
+ expiresAt: new Date(expiresAtMs)
20374
+ };
20375
+ }
20376
+ var import_node_child_process13, import_node_util4, import_node_path12, import_node_url, import_promises5, execFileAsync2, __dirname2, FEISHU_WELL_KNOWN_SKILLS_BASE, FEISHU_TOKEN_URL, FEISHU_TENANT_TOKEN_URL, FEISHU_USER_OPEN_ID_VAR, FEISHU_AUTHORIZED_BY_VAR, FEISHU_USER_AUTH_INVALID_VAR, FEISHU_REFRESH_REVOKED_CODES, TENANT_TOKEN_TIMEOUT_MS, FeishuConnector;
20377
+ var init_feishu = __esm({
20378
+ "../connectors/src/feishu/index.ts"() {
20379
+ "use strict";
20380
+ import_node_child_process13 = require("node:child_process");
20381
+ import_node_util4 = require("node:util");
20382
+ import_node_path12 = require("node:path");
20383
+ import_node_url = require("node:url");
20384
+ import_promises5 = require("node:fs/promises");
20385
+ init_base();
20386
+ init_logger();
20387
+ init_scopes();
20388
+ execFileAsync2 = (0, import_node_util4.promisify)(import_node_child_process13.execFile);
20389
+ __dirname2 = (0, import_node_path12.dirname)((0, import_node_url.fileURLToPath)(__esm_import_meta_url));
20390
+ FEISHU_WELL_KNOWN_SKILLS_BASE = "https://open.feishu.cn/.well-known/skills";
20391
+ FEISHU_TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v2/oauth/token";
20392
+ FEISHU_TENANT_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
20393
+ FEISHU_USER_OPEN_ID_VAR = "FEISHU_USER_OPEN_ID";
20394
+ FEISHU_AUTHORIZED_BY_VAR = "FEISHU_AUTHORIZED_BY";
20395
+ FEISHU_USER_AUTH_INVALID_VAR = "FEISHU_USER_AUTH_INVALID";
20396
+ FEISHU_REFRESH_REVOKED_CODES = /* @__PURE__ */ new Set([20024, 20026, 20037, 20064, 20073, 20074]);
20397
+ TENANT_TOKEN_TIMEOUT_MS = 5e3;
20398
+ FeishuConnector = class extends CliConnector {
20399
+ config = {
20400
+ slug: "feishu",
20401
+ type: "cli",
20402
+ // schema 要**声明本连接器实际消费的全部凭据**,不只是「让用户手填的那几个」。
20403
+ // 历史上这里只写了 app_id,而 inject() 其实还要 app_secret / user_access_token
20404
+ // (后两者由控制台 OAuth 流写进变量表,用户不手填)——于是 secret 标记缺失,
20405
+ // 密钥黑名单只能在 serve.ts 另手写一份。现在黑名单从 sensitiveVars 推导,schema 必须诚实。
20406
+ schema: [
20407
+ { key: "app_id", description: "\u98DE\u4E66 App ID\uFF08\u7559\u7A7A\u5219\u81EA\u52A8\u521B\u5EFA\u65B0\u5E94\u7528\uFF09", secret: false, required: false },
20408
+ { key: "app_secret", description: "\u98DE\u4E66 App Secret\uFF08\u63A7\u5236\u53F0\u6388\u6743\u6D41\u5199\u5165\uFF0C\u975E\u624B\u586B\uFF09", secret: true, required: true },
20409
+ { key: "user_access_token", description: "\u98DE\u4E66\u7528\u6237 token\uFF08OAuth \u6388\u6743\u540E\u5199\u5165\uFF0C~2h \u8F6E\u6362\uFF09", secret: true, required: false },
20410
+ { key: "auth_selection", description: "\u6388\u6743\u65F6\u52FE\u9009\u7684\u529F\u80FD\u6A21\u5757\uFF08\u63A7\u5236\u53F0\u6388\u6743\u6D41\u5199\u5165\u7684\u660E\u6587 JSON\uFF0C\u6309\u5B83\u9650\u5236 agent \u53EF\u7528\u6A21\u5757\uFF09", secret: false, required: false }
20411
+ ]
20412
+ };
20413
+ commandName = "lark-cli";
20414
+ requiredCommands = ["lark-cli"];
20415
+ envPrefix = "FEISHU_";
20416
+ /** server 侧从 actor 变量表组装凭据的声明(见 provision.ts)。这些会随 job 下发到节点。 */
20417
+ credentialSources = [
20418
+ { key: "app_id", from: "FEISHU_APP_ID", required: true },
20419
+ { key: "app_secret", from: "FEISHU_APP_SECRET", required: true },
20420
+ { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false },
20421
+ // 按 Agent 限制可用模块:inject 据此写放行表,wrapper 拦(个人选择优先,缺失回落组织连接的选择)。
20422
+ { key: "auth_selection", from: FEISHU_AUTH_SELECTION_VAR, required: false }
20423
+ ];
20424
+ /**
20425
+ * 续期材料——**只在 server 侧读,不下发节点**。refresh_token 能换出新的 user token,
20426
+ * 属于"长期材料",没有任何理由让它出现在 agent 所在的机器上。
20427
+ */
20428
+ refreshCredentialSources = [
20429
+ // App 凭据是**组织级**共享的(个人变量优先,缺失回落全局默认);用户 token 类只认个人变量。
20430
+ { key: "app_id", from: "FEISHU_APP_ID", orgDefault: true, required: true },
20431
+ { key: "app_secret", from: "FEISHU_APP_SECRET", orgDefault: true, required: true },
20432
+ { key: "refresh_token", from: "FEISHU_REFRESH_TOKEN", required: true },
20433
+ { key: "user_token_expires_at", from: "FEISHU_USER_TOKEN_EXPIRES_AT", required: false },
20434
+ { key: "refresh_token_expires_at", from: "FEISHU_REFRESH_TOKEN_EXPIRES_AT", required: false },
20435
+ { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false }
20436
+ ];
20437
+ /** 过期判定只认这个键——必须与注入时消费的 token 同源(见接口注释里的历史坑)。 */
20438
+ expiresAtCredentialKey = "user_token_expires_at";
20439
+ /**
20440
+ * 下发前 user token 至少还要剩 40 分钟,不够就换新(Bug 0201)。
20441
+ *
20442
+ * 必须 > `SESSION_CREDENTIAL_RENEW_INTERVAL_MS`(30 分钟):会话中途续期每 30 分钟问一次 server,
20443
+ * 问的就是这条门槛。门槛若只有通用的 5 分钟,一张还剩 20 分钟的 token 会被原样发回,
20444
+ * 20 分钟后失效、要再等 10 分钟才到下一拍——最坏断档近 25 分钟(`99991677 token expire`)。
20445
+ * 取 40 = 30 分钟间隔 + 10 分钟调度余量:任一拍之后剩余 > 40,到下一拍仍 > 10。
20446
+ *
20447
+ * 不像 GitHub 那样取全寿命(每次都换):飞书每次刷新都会轮换 refresh_token,
20448
+ * 换得越勤,「飞书已轮换、我们还没落库」的窗口就越常被碰上,所以只取刚好够用的值。
20449
+ */
20450
+ minRemainingLifetimeMs = 40 * 6e4;
20451
+ /**
20452
+ * 只由服务端定时任务续期(Bug 0318):refresh_token 一次性、每刷必轮换,派单与会话中途再各刷一次就是
20453
+ * 多个写者并发打断续期链;懒续期又让闲置超过 refresh_token 寿命的员工悄悄失效。派单只读库里最新的 token。
20454
+ */
20455
+ scheduledRefreshOnly = true;
20456
+ /** refresh_token 自身 ~30d 过期,过了只能重新授权。 */
20457
+ refreshMaterialExpiresAtCredentialKey = "refresh_token_expires_at";
20458
+ /**
20459
+ * refresh_token 过期后该清掉的键——**只清这个员工的授权产物**。
20460
+ * app_id / app_secret 刻意不在其中:它们代表组织级 App 凭据,与其他员工的连接无关。
20461
+ */
20462
+ clearOnExpiredCredentialKeys = [
20463
+ "user_access_token",
20464
+ "refresh_token",
20465
+ "user_token_expires_at",
20466
+ "refresh_token_expires_at"
20467
+ ];
20468
+ /**
20469
+ * 绝不允许进入 agent 进程环境的原始变量。
20470
+ * 含 APP_ID 与几个过期时间戳:它们本身不算密钥,但同属飞书凭据材料,一并挡在 agent 之外,
20471
+ * 避免 agent 绕开 wrapper 自行拼凭据(沿用既有行为)。
20472
+ */
20473
+ sensitiveVars = [
20474
+ "FEISHU_APP_ID",
20475
+ "FEISHU_APP_SECRET",
20476
+ "FEISHU_USER_ACCESS_TOKEN",
20477
+ "FEISHU_REFRESH_TOKEN",
20478
+ "FEISHU_USER_TOKEN_EXPIRES_AT",
20479
+ "FEISHU_REFRESH_TOKEN_EXPIRES_AT",
20480
+ FEISHU_USER_OPEN_ID_VAR,
20481
+ FEISHU_AUTHORIZED_BY_VAR,
20482
+ FEISHU_USER_AUTH_INVALID_VAR
20483
+ ];
20484
+ /** 宿主机登录态隔离,理由同 github(见接口注释):由通用装配层无条件设。 */
20485
+ hostCredentialIsolationEnvKeys = ["LARKSUITE_CLI_CONFIG_DIR"];
20486
+ /** inject() 写进 sessionEnv 的全部键——通用 cleanup() 按它逐个删。 */
20487
+ injectedEnvKeys = [
20488
+ "LARK_BIN",
20489
+ "FEISHU_CREDS_FILE",
20490
+ "FEISHU_RUN_TMPDIR",
20491
+ "LARKSUITE_CLI_CONFIG_DIR"
20492
+ ];
20493
+ /**
20494
+ * 自检:拿 user_access_token 问飞书「我是谁」,回 `{ok, identity, data:{name, open_id}}`。
20495
+ *
20496
+ * **不能用 `lark-cli auth status`**:注入外部凭据后整个 auth 命名空间会被拒
20497
+ * ("credentials are provided externally and do not support interactive management"),
20498
+ * 自检会恒为失败。实测 2026-08-03 踩到——`auth status` 只在**读宿主机 profile** 时才有输出,
20499
+ * 恰恰是我们要隔离掉的那条路径。
20500
+ */
20501
+ verifyArgs = ["api", "GET", "/open-apis/authen/v1/user_info"];
20502
+ /** 厂商技能包源——平台自动拉取飞书官方技能包时用。 */
20503
+ wellKnownSkillsBase = FEISHU_WELL_KNOWN_SKILLS_BASE;
20504
+ larkBin;
20505
+ fetchTenantToken;
20506
+ /** `fetchTenantToken` 只给测试替换(不打真飞书);生产一律用 {@link fetchFeishuTenantAccessToken}。 */
20507
+ constructor(larkBin, opts = {}) {
20508
+ super(__dirname2);
20509
+ this.larkBin = larkBin ?? this.resolveLarkBin();
20510
+ this.fetchTenantToken = opts.fetchTenantToken ?? fetchFeishuTenantAccessToken;
20511
+ }
20512
+ /**
20513
+ * 解析 user_info 的 JSON。lark-cli 的 `ok:false` 也是**退出码 0** 的正常输出,
20514
+ * 所以必须在这里判 ok——只看子进程退出码会把「凭据失效」当成自检通过。
20515
+ */
20516
+ parseIdentity(stdout) {
20517
+ let parsed;
20518
+ try {
20519
+ parsed = JSON.parse(stdout);
20520
+ } catch {
20521
+ return null;
20522
+ }
20523
+ const obj2 = parsed;
20524
+ if (obj2.ok !== true) return null;
20525
+ const openId = typeof obj2.data?.open_id === "string" ? obj2.data.open_id : "";
20526
+ if (!openId) return null;
20527
+ const name = typeof obj2.data?.name === "string" ? obj2.data.name : "";
20528
+ return name ? `${name} (${openId})` : openId;
20529
+ }
20530
+ async connect(options = {}) {
20531
+ if (this.isSimulated()) {
20532
+ return { credentials: { app_id: options["app_id"] ?? "cli_simulated" } };
20533
+ }
20534
+ const app_id = options["app_id"] ?? await this.initNewApp();
20535
+ await this.runAuthLogin(app_id);
20536
+ return { credentials: { app_id } };
20537
+ }
20538
+ /**
20539
+ * Agent run 开始:把凭据作为 lark-cli「外部凭据」写进凭据文件(不进 agent env)。
20540
+ * credentials 接受:app_id, app_secret, user_access_token?(及其 FEISHU_* 别名)。
20541
+ * 企业 token 不随 job 下发,由这里用 app_id/app_secret 现换(换不到只是本轮没有 bot 身份,不抛)。
20542
+ *
20543
+ * 注入的 sessionEnv 键(全部非敏感):
20544
+ * FEISHU_CREDS_FILE — 凭据文件路径,wrapper source 它
20545
+ * FEISHU_RUN_TMPDIR — 本轮临时目录,cleanup 整个删掉
20546
+ * LARK_BIN — 真实 lark-cli 绝对路径(防 wrapper 递归调用自身)
20547
+ */
20548
+ async inject(credentials2, sessionEnv) {
20549
+ const appId = credentials2["app_id"] ?? credentials2["FEISHU_APP_ID"];
20550
+ const appSecret = credentials2["app_secret"] ?? credentials2["FEISHU_APP_SECRET"];
20551
+ if (!appId) throw new Error("feishu inject: missing app_id");
20552
+ if (!appSecret) throw new Error("feishu inject: missing app_secret");
20553
+ const userAccessToken = credentials2["user_access_token"] ?? credentials2["FEISHU_USER_ACCESS_TOKEN"];
20554
+ const modulePolicy = feishuModulePolicyEnv(
20555
+ feishuParseSelection(credentials2["auth_selection"] ?? credentials2[FEISHU_AUTH_SELECTION_VAR])
20556
+ );
20557
+ const tenantAccessToken = this.isSimulated() ? null : await this.fetchTenantToken(appId, appSecret) ?? previousTenantToken(sessionEnv.get("FEISHU_CREDS_FILE"), appId);
20558
+ const creds = await writeSessionCredsFile(this.config.slug, {
20559
+ LARKSUITE_CLI_APP_ID: appId,
20560
+ LARKSUITE_CLI_APP_SECRET: appSecret,
20561
+ ...tenantAccessToken ? { LARKSUITE_CLI_TENANT_ACCESS_TOKEN: tenantAccessToken } : {},
20562
+ ...userAccessToken ? { LARKSUITE_CLI_USER_ACCESS_TOKEN: userAccessToken } : {},
20563
+ ...modulePolicy
20564
+ }, sessionEnv.get("FEISHU_RUN_TMPDIR"));
20565
+ const configDir = (0, import_node_path12.join)(creds.dir, "lark-config");
20566
+ await (0, import_promises5.mkdir)(configDir, { recursive: true, mode: 448 });
20567
+ sessionEnv.set("LARK_BIN", await this.resolveAbsoluteLarkBin());
20568
+ sessionEnv.set("FEISHU_CREDS_FILE", creds.file);
20569
+ sessionEnv.set("FEISHU_RUN_TMPDIR", creds.dir);
20570
+ sessionEnv.set("LARKSUITE_CLI_CONFIG_DIR", configDir);
20571
+ }
20572
+ /**
20573
+ * 用 refresh_token 换一对新的 user token(飞书 v2 OAuth)。
20574
+ *
20575
+ * **纯换取,不落库**——持久化由调用方做(server 侧 `oauth.ts`)。这样平台知识
20576
+ * (端点、字段名、~2h/~30d 的寿命)全部收在连接器里,与 GitHub App 的续期共用
20577
+ * `_base/refresh.ts` 那套判定。
20578
+ *
20579
+ * 返回 null=没有可用的续期材料(未完成授权)。平台报错则抛,由上层收敛成 failed。
20580
+ */
20581
+ async refreshCredentials(credentials2) {
20582
+ const appId = credentials2["app_id"];
20583
+ const appSecret = credentials2["app_secret"];
20584
+ const refreshToken = credentials2["refresh_token"];
20585
+ if (!appId || !appSecret || !refreshToken) return null;
20586
+ const resp = await fetch(FEISHU_TOKEN_URL, {
20587
+ method: "POST",
20588
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
20589
+ body: new URLSearchParams({
20590
+ grant_type: "refresh_token",
20591
+ refresh_token: refreshToken,
20592
+ client_id: appId,
20593
+ client_secret: appSecret
20594
+ }).toString()
20595
+ });
20596
+ const data = await resp.json();
20597
+ if (!resp.ok || data["error"]) {
20598
+ const code2 = Number(data["code"]);
20599
+ const msg = `feishu refresh failed: HTTP ${resp.status} code=${String(data["code"] ?? "")} ${String(data["error"] ?? "")}`.trim();
20600
+ if (data["error"] === "invalid_grant" || FEISHU_REFRESH_REVOKED_CODES.has(code2)) throw new CredentialRevokedError(msg);
20601
+ throw new Error(msg);
20602
+ }
20603
+ return parseFeishuTokenResponse(data, Date.now());
20604
+ }
20605
+ async initNewApp() {
20606
+ await new Promise((resolve11, reject) => {
20607
+ const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["config", "init", "--new"], { stdio: "inherit" });
20608
+ proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli config init exited ${code2}`)));
20609
+ proc.on("error", reject);
20610
+ });
20611
+ const { stdout } = await execFileAsync2(this.larkBin, ["config", "show", "--json"]);
20612
+ const cfg = JSON.parse(stdout);
20613
+ if (!cfg.appId) throw new Error("feishu connector: could not determine app_id after config init");
20614
+ return cfg.appId;
20615
+ }
20616
+ async runAuthLogin(app_id) {
20617
+ await new Promise((resolve11, reject) => {
20618
+ const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["--profile", app_id, "auth", "login"], { stdio: "inherit" });
20619
+ proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli auth login exited ${code2}`)));
20620
+ proc.on("error", reject);
20621
+ });
20622
+ }
20623
+ resolveLarkBin() {
20624
+ return process.env["LARK_CLI_BIN"] ?? "lark-cli";
20625
+ }
20626
+ async resolveAbsoluteLarkBin() {
20627
+ if (this.larkBin.includes("/")) return this.larkBin;
20628
+ return this.resolveRealBin(this.larkBin);
20629
+ }
20630
+ };
20631
+ }
20632
+ });
20633
+
20634
+ // ../connectors/src/feishu/user-auth.ts
20635
+ async function startFeishuDeviceAuthorization(input) {
20636
+ const basicAuth = Buffer.from(`${input.appId}:${input.appSecret}`).toString("base64");
20637
+ const resp = await (input.fetchImpl ?? fetch)(FEISHU_DEVICE_AUTHORIZATION_URL, {
20638
+ method: "POST",
20639
+ headers: {
20640
+ "Content-Type": "application/x-www-form-urlencoded",
20641
+ "Authorization": `Basic ${basicAuth}`
20642
+ },
20643
+ body: new URLSearchParams({ client_id: input.appId, scope: input.scopes.join(" ") }).toString()
20644
+ });
20645
+ const data = await resp.json();
20646
+ if (!resp.ok || data["error"]) {
20647
+ throw new Error(String(data["error_description"] ?? data["error"] ?? "device authorization failed"));
20648
+ }
20649
+ const verificationUrl = String(data["verification_uri_complete"] ?? data["verification_uri"] ?? "");
20650
+ if (!verificationUrl) throw new Error("no verification_uri in response");
20651
+ const expiresIn = Number(data["expires_in"]);
20652
+ const interval = Number(data["interval"]);
20653
+ return {
20654
+ verificationUrl,
20655
+ deviceCode: String(data["device_code"] ?? ""),
20656
+ ...Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresIn } : {},
20657
+ ...Number.isFinite(interval) && interval > 0 ? { interval } : {}
20658
+ };
20659
+ }
20660
+ async function pollFeishuDeviceToken(input) {
20661
+ const resp = await (input.fetchImpl ?? fetch)(FEISHU_TOKEN_URL, {
20662
+ method: "POST",
20663
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
20664
+ body: new URLSearchParams({
20665
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
20666
+ device_code: input.deviceCode,
20667
+ client_id: input.appId,
20668
+ client_secret: input.appSecret
20669
+ }).toString()
20670
+ });
20671
+ const data = await resp.json();
20672
+ const errStr = data["error"];
20673
+ if (errStr === "authorization_pending" || errStr === "slow_down") return { status: "pending" };
20674
+ if (!resp.ok || errStr) throw new Error(String(data["error_description"] ?? errStr ?? "login failed"));
20675
+ if (!data["access_token"]) throw new Error("no access_token in response");
20676
+ return { status: "ok", data };
20677
+ }
20678
+ async function fetchFeishuUserIdentity(accessToken, fetchImpl = fetch) {
20679
+ try {
20680
+ const resp = await fetchImpl(FEISHU_USER_INFO_URL, { headers: { authorization: `Bearer ${accessToken}` } });
20681
+ const ui = await resp.json();
20682
+ if (!resp.ok || ui.code !== 0) return {};
20683
+ const name = ui.data?.name || ui.data?.en_name;
20684
+ return {
20685
+ ...ui.data?.open_id ? { openId: ui.data.open_id } : {},
20686
+ ...name ? { name } : {}
20687
+ };
20688
+ } catch {
20689
+ return {};
20690
+ }
20691
+ }
20692
+ async function sendFeishuBotTextToUser(input) {
20693
+ const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
20694
+ if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0Cbot \u79C1\u4FE1\u672A\u53D1\u51FA");
20695
+ const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_SEND_MESSAGE_URL}?receive_id_type=open_id`, {
20696
+ method: "POST",
20697
+ headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
20698
+ body: JSON.stringify({ receive_id: input.openId, msg_type: "text", content: JSON.stringify({ text: input.text }) })
20699
+ });
20700
+ const body2 = await resp.json();
20701
+ if (body2.code !== 0) throw new Error(`feishu bot \u79C1\u4FE1\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
20702
+ }
20703
+ async function lookupFeishuOpenIdByEmail(input) {
20704
+ const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
20705
+ if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0C\u672A\u80FD\u6309\u90AE\u7BB1\u67E5\u98DE\u4E66\u7528\u6237");
20706
+ const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_BATCH_GET_ID_URL}?user_id_type=open_id`, {
20707
+ method: "POST",
20708
+ headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
20709
+ body: JSON.stringify({ emails: [input.email], include_resigned: false })
20710
+ });
20711
+ const body2 = await resp.json();
20712
+ if (body2.code !== 0) throw new Error(`feishu \u6309\u90AE\u7BB1\u67E5\u7528\u6237\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
20713
+ const hit = body2.data?.user_list?.find((u) => u.email?.toLowerCase() === input.email.toLowerCase() && u.user_id);
20714
+ return hit?.user_id ?? null;
20715
+ }
20716
+ function feishuUserAuthInvalidText(input) {
20717
+ const head = `\u300C${input.agentName}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF08\u6388\u6743\u88AB\u64A4\u9500\u6216\u957F\u65F6\u95F4\u65E0\u6CD5\u7EED\u671F\uFF09\uFF0C\u4EE5\u4F60\u8EAB\u4EFD\u8C03\u7528\u98DE\u4E66\u7684\u529F\u80FD\u6682\u65F6\u7528\u4E0D\u4E86\uFF0Cbot \u8EAB\u4EFD\u4E0D\u53D7\u5F71\u54CD\u3002`;
20718
+ return input.link ? `${head}
20719
+ \u70B9\u8FD9\u4E2A\u94FE\u63A5\u91CD\u65B0\u6388\u6743\uFF08\u9700\u7528\u6536\u5230\u8FD9\u6761\u79C1\u4FE1\u7684\u98DE\u4E66\u8D26\u53F7\u767B\u5F55\uFF0C7 \u5929\u5185\u6709\u6548\uFF09\uFF1A
20720
+ ${input.link}` : `${head}
20721
+ \u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u91CD\u65B0\u6388\u6743\u98DE\u4E66\u3002`;
20722
+ }
20723
+ var FEISHU_DEVICE_AUTHORIZATION_URL, FEISHU_USER_INFO_URL, FEISHU_SEND_MESSAGE_URL, FEISHU_BATCH_GET_ID_URL;
20724
+ var init_user_auth = __esm({
20725
+ "../connectors/src/feishu/user-auth.ts"() {
20726
+ "use strict";
20727
+ init_feishu();
20728
+ FEISHU_DEVICE_AUTHORIZATION_URL = "https://accounts.feishu.cn/oauth/v1/device_authorization";
20729
+ FEISHU_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";
20730
+ FEISHU_SEND_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
20731
+ FEISHU_BATCH_GET_ID_URL = "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id";
20475
20732
  }
20476
20733
  });
20477
20734
 
@@ -211892,6 +212149,54 @@ var init_registry_audit_sink = __esm({
211892
212149
  }
211893
212150
  });
211894
212151
 
212152
+ // ../server/src/feishu-module-access.ts
212153
+ async function feishuModuleAccess(store, actorId, fetchScopes = fetchFeishuAppScopes) {
212154
+ const read3 = (key2) => store.revealResolvedVariable(key2, actorId).catch(() => null);
212155
+ const appId = await read3("FEISHU_APP_ID");
212156
+ const appSecret = await read3("FEISHU_APP_SECRET");
212157
+ if (!appId || !appSecret) return { configured: false };
212158
+ const rawSelection = await read3(FEISHU_AUTH_SELECTION_VAR);
212159
+ const { state: selection, modules } = feishuParseSelection(rawSelection);
212160
+ let parsed = {};
212161
+ try {
212162
+ parsed = rawSelection ? JSON.parse(rawSelection) ?? {} : {};
212163
+ } catch {
212164
+ parsed = {};
212165
+ }
212166
+ const grantedScopes = Array.isArray(parsed.grantedScopes) ? parsed.grantedScopes.filter((s2) => typeof s2 === "string") : void 0;
212167
+ const hasUserAuth = Boolean(await read3("FEISHU_USER_ACCESS_TOKEN") || await read3("FEISHU_REFRESH_TOKEN"));
212168
+ const userAuthInvalid = Boolean(await read3(FEISHU_USER_AUTH_INVALID_VAR));
212169
+ let appScopes = null;
212170
+ let botCheckError;
212171
+ if (modules.length > 0) {
212172
+ try {
212173
+ appScopes = await fetchScopes(appId, appSecret);
212174
+ } catch (e) {
212175
+ botCheckError = String(e?.message ?? e).slice(0, 200);
212176
+ }
212177
+ }
212178
+ return {
212179
+ configured: true,
212180
+ selection,
212181
+ modules: feishuModuleAccessReport({ modules, grantedScopes, hasUserAuth, userAuthInvalid, appScopes }),
212182
+ hasUserAuth,
212183
+ userAuthInvalid,
212184
+ sendAsUser: feishuSendAsUserState({
212185
+ sendAsUser: parsed.sendAsUser === true,
212186
+ sendAsUserRequested: parsed.sendAsUserRequested === true,
212187
+ grantedScopes
212188
+ }),
212189
+ appConsoleUrl: feishuAppPermissionConsoleUrl(appId),
212190
+ ...botCheckError ? { botCheckError } : {}
212191
+ };
212192
+ }
212193
+ var init_feishu_module_access = __esm({
212194
+ "../server/src/feishu-module-access.ts"() {
212195
+ "use strict";
212196
+ init_src3();
212197
+ }
212198
+ });
212199
+
211895
212200
  // ../server/src/github-app-inventory.ts
211896
212201
  async function collectOasisGithubApps(svc) {
211897
212202
  const rows = (await svc.listVariables()).filter((r) => r.scope === "global" || r.scope === "personal");
@@ -218085,6 +218390,17 @@ ${composed}`;
218085
218390
  else html(false, result.error);
218086
218391
  return;
218087
218392
  }
218393
+ if (url.pathname === "/api/connectors/feishu/app/permissions" && req.method === "POST") {
218394
+ try {
218395
+ const { actorId } = JSON.parse(rawBody || "{}");
218396
+ const svc = await connectorActorsService();
218397
+ if (!svc) throw new Error("actors service unavailable");
218398
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(await feishuModuleAccess(svc, actorId)));
218399
+ } catch (e) {
218400
+ res.writeHead(500).end(JSON.stringify({ error: String(e) }));
218401
+ }
218402
+ return;
218403
+ }
218088
218404
  if (url.pathname === "/api/connectors/feishu/login/start" && req.method === "POST") {
218089
218405
  const startBody = JSON.parse(rawBody);
218090
218406
  const { appId, actorId } = startBody;
@@ -218578,6 +218894,7 @@ var init_server3 = __esm({
218578
218894
  init_assembly();
218579
218895
  init_child_live_turn();
218580
218896
  init_registry_audit_sink();
218897
+ init_feishu_module_access();
218581
218898
  init_src3();
218582
218899
  init_github_app_inventory();
218583
218900
  init_github_app_timing();
@@ -286720,53 +287037,43 @@ function createConnectorRenewalRuntime(options) {
286720
287037
  return "sent";
286721
287038
  };
286722
287039
  const recipientsFor = async (companyId, store, actorId) => {
286723
- const out = /* @__PURE__ */ new Set();
286724
287040
  const by = await store.revealVariable(FEISHU_AUTHORIZED_BY_VAR, actorId).catch(() => null);
286725
- if (by && isHumanActorId(by)) out.add(by);
286726
- if (out.size === 0 && actorId) {
286727
- if (isHumanActorId(actorId)) out.add(actorId);
286728
- else {
286729
- const human = await options.boundHuman(actorId, companyId).catch(() => null);
286730
- if (human) out.add(human);
286731
- }
286732
- }
286733
- return [...out];
287041
+ if (by && isHumanActorId(by)) return { recipients: [by], authorizer: by };
287042
+ if (!actorId) return { recipients: [], authorizer: null };
287043
+ if (isHumanActorId(actorId)) return { recipients: [actorId], authorizer: null };
287044
+ const human = await options.boundHuman(actorId, companyId).catch(() => null);
287045
+ return { recipients: human ? [human] : [], authorizer: null };
286734
287046
  };
286735
- const recoverOpenId = async (companyId, app, recipients) => {
286736
- for (const human of recipients) {
286737
- const email2 = await options.memberEmail?.(human, companyId).catch(() => null);
286738
- if (!email2) continue;
286739
- const openId = await lookupFeishuOpenIdByEmail({ ...app, email: email2 });
286740
- if (openId) return openId;
286741
- }
286742
- return null;
287047
+ const recoverOpenId = async (companyId, app, authorizer) => {
287048
+ const email2 = await options.memberEmail?.(authorizer, companyId).catch(() => null);
287049
+ return email2 ? lookupFeishuOpenIdByEmail({ ...app, email: email2 }) : null;
286743
287050
  };
286744
287051
  const deliverInvalidNotice = async (companyId, store, actorId, marker) => {
286745
287052
  const prev = marker.notice ?? { inApp: false, dm: "pending", attempts: 0 };
286746
287053
  const state = { ...prev, attempts: prev.attempts + 1 };
286747
287054
  const who = `actor=${actorId ?? "-"} \u7B2C ${state.attempts}/${MAX_NOTICE_ATTEMPTS} \u6B21`;
286748
287055
  const name = await agentName(companyId, actorId);
286749
- const recipients = await recipientsFor(companyId, store, actorId);
287056
+ const { recipients, authorizer } = await recipientsFor(companyId, store, actorId);
287057
+ const unverified = !authorizer && !await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId).catch(() => "?");
286750
287058
  if (!state.inApp) {
286751
287059
  try {
286752
287060
  const ledger = await options.notifications(companyId);
286753
- if (ledger) {
286754
- const rows = [];
286755
- for (const recipient of recipients) {
286756
- rows.push(await ledger.insert({
286757
- recipient,
286758
- category: "connector",
286759
- kind: "feishu-auth-invalid",
286760
- // 每次失效一条:键里带失效时刻,重新授权后再失效还会再提醒;同一次失效补发 / 重复扫描不重发(唯一约束)。
286761
- sourceKey: `connector:feishu:${actorId ?? "org"}@invalid:${marker.at}`,
286762
- title: `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u9700\u8981\u91CD\u65B0\u6388\u6743`,
286763
- ...actorId ? { actorId } : {},
286764
- enteredAt: marker.at
286765
- }));
286766
- }
286767
- if (rows.length > 0) options.onNotifications?.(companyId, rows);
287061
+ if (!ledger) throw new Error("\u8BE5\u516C\u53F8\u7684\u901A\u77E5\u53F0\u8D26\u4E0D\u53EF\u7528");
287062
+ if (recipients.length === 0) throw new Error("\u6CA1\u6709\u6536\u4EF6\u4EBA\uFF08\u6CA1\u8BB0\u6388\u6743\u4EBA\u3001\u5458\u5DE5\u4E5F\u6CA1\u6709\u7ED1\u5B9A\u771F\u4EBA\uFF09");
287063
+ const rows = [];
287064
+ for (const recipient of recipients) {
287065
+ rows.push(await ledger.insert({
287066
+ recipient,
287067
+ category: "connector",
287068
+ kind: "feishu-auth-invalid",
287069
+ // 每次失效一条:键里带失效时刻,重新授权后再失效还会再提醒;同一次失效补发 / 重复扫描不重发(唯一约束)。
287070
+ sourceKey: `connector:feishu:${actorId ?? "org"}@invalid:${marker.at}`,
287071
+ title: unverified ? `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u8BF7\u5230\u8FDE\u63A5\u5668\u9875\u91CD\u65B0\u8FDE\u63A5` : `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u9700\u8981\u91CD\u65B0\u6388\u6743`,
287072
+ ...actorId ? { actorId } : {},
287073
+ enteredAt: marker.at
287074
+ }));
286768
287075
  }
286769
- if (recipients.length === 0) console.warn(`[credential-refresh] \u98DE\u4E66\u6388\u6743\u5931\u6548\u7AD9\u5185\u901A\u77E5\u6CA1\u6709\u6536\u4EF6\u4EBA actor=${actorId ?? "-"}`);
287076
+ options.onNotifications?.(companyId, rows);
286770
287077
  state.inApp = true;
286771
287078
  } catch (err) {
286772
287079
  options.warn(new Error(`\u98DE\u4E66\u6388\u6743\u5931\u6548\u7AD9\u5185\u901A\u77E5\u672A\u5199\u5165\uFF08${who}\uFF0C\u4E0B\u4E00\u62CD\u91CD\u8BD5\uFF09: ${String(err)}`));
@@ -286775,12 +287082,13 @@ function createConnectorRenewalRuntime(options) {
286775
287082
  if (state.dm === "pending") {
286776
287083
  try {
286777
287084
  const app = await appCredentials(store, actorId);
286778
- const stored = await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId);
286779
- const recovered = !stored && app ? await recoverOpenId(companyId, app, recipients) : null;
286780
- const openId = stored || recovered;
287085
+ const storedOpenId = await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId);
287086
+ const recovered = !storedOpenId && app && authorizer ? await recoverOpenId(companyId, app, authorizer) : null;
287087
+ const openId = storedOpenId || recovered;
286781
287088
  if (!app || !openId) {
286782
287089
  state.dm = "unavailable";
286783
- console.warn(`[credential-refresh] \u98DE\u4E66\u6388\u6743\u5931\u6548\u79C1\u4FE1\u65E0\u4ECE\u53D1\u51FA\uFF08${app ? "\u6CA1\u8BB0\u6388\u6743\u4EBA\u3001\u901A\u8BAF\u5F55\u4E5F\u6CA1\u6309\u6210\u5458\u90AE\u7BB1\u627E\u5230\u672C\u4EBA" : "\u6CA1\u6709\u5E94\u7528\u51ED\u636E"}\uFF09actor=${actorId ?? "-"}\uFF1B\u53EA\u6709\u7AD9\u5185\u901A\u77E5`);
287090
+ const why = !app ? "\u6CA1\u6709\u5E94\u7528\u51ED\u636E" : unverified ? "\u6CA1\u8BB0\u539F\u6388\u6743\u4EBA\uFF0C\u6838\u5B9E\u4E0D\u4E86\u672C\u4EBA\uFF0C\u4E0D\u7B7E\u94FE\u63A5\uFF1B\u9700\u5230\u8FDE\u63A5\u5668\u9875\u91CD\u65B0\u8FDE\u63A5" : "\u901A\u8BAF\u5F55\u6CA1\u6309\u539F\u6388\u6743\u4EBA\u7684\u8D26\u53F7\u90AE\u7BB1\u627E\u5230\u4ED6";
287091
+ console.warn(`[credential-refresh] \u98DE\u4E66\u6388\u6743\u5931\u6548\u79C1\u4FE1\u65E0\u4ECE\u53D1\u51FA\uFF08${why}\uFF09actor=${actorId ?? "-"}\uFF1B\u53EA\u6709\u7AD9\u5185\u901A\u77E5`);
286784
287092
  } else {
286785
287093
  const token2 = await issueFeishuReauthToken(options.links, store, {
286786
287094
  companyId,
@@ -300776,7 +301084,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
300776
301084
  }
300777
301085
  }
300778
301086
  }
300779
- var CLI_VERSION = "2.2.111" ? "2.2.111" : "dev";
301087
+ var CLI_VERSION = "2.2.113" ? "2.2.113" : "dev";
300780
301088
  function apiHeaders(token2, company) {
300781
301089
  const headers = {
300782
301090
  authorization: `Bearer ${token2}`,
@@ -304947,7 +305255,7 @@ function shimScript() {
304947
305255
  }
304948
305256
 
304949
305257
  // src/index.ts
304950
- var PKG_VERSION = true ? "2.2.111" : "dev";
305258
+ var PKG_VERSION = true ? "2.2.113" : "dev";
304951
305259
  var LOCAL_BIN = localBin();
304952
305260
  var NPM_PREFIX = npmPrefix();
304953
305261
  var INSTANCE = DEFAULT_INSTANCE;