oasis_test_v2 2.2.110 → 2.2.112

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,391 +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
- function feishuUserAuthInvalidText(input) {
19982
- 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`;
19983
- return input.link ? `${head}
19984
- \u70B9\u8FD9\u4E2A\u94FE\u63A5\u91CD\u65B0\u6388\u6743\uFF08\u9700\u7528\u539F\u6388\u6743\u4EBA\u7684\u98DE\u4E66\u8D26\u53F7\u767B\u5F55\uFF0C7 \u5929\u5185\u6709\u6548\uFF09\uFF1A
19985
- ${input.link}` : `${head}
19986
- \u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u91CD\u65B0\u6388\u6743\u98DE\u4E66\u3002`;
19987
- }
19988
- var FEISHU_DEVICE_AUTHORIZATION_URL, FEISHU_USER_INFO_URL, FEISHU_SEND_MESSAGE_URL;
19989
- var init_user_auth = __esm({
19990
- "../connectors/src/feishu/user-auth.ts"() {
19991
- "use strict";
19992
- init_feishu();
19993
- FEISHU_DEVICE_AUTHORIZATION_URL = "https://accounts.feishu.cn/oauth/v1/device_authorization";
19994
- FEISHU_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";
19995
- FEISHU_SEND_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
19996
- }
19997
- });
19998
-
19999
19614
  // ../connectors/src/feishu/scopes.ts
20000
19615
  function scopesForDomains(domains) {
20001
19616
  const set = /* @__PURE__ */ new Set(["offline_access"]);
@@ -20028,6 +19643,11 @@ function scopesForSelection(sel, includeSendAsUser) {
20028
19643
  return scopes;
20029
19644
  }
20030
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) {
20031
19651
  const tokenResp = await fetchImpl("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
20032
19652
  method: "POST",
20033
19653
  headers: { "Content-Type": "application/json; charset=utf-8" },
@@ -20042,9 +19662,7 @@ async function appHasScope(appId, appSecret, scope2, fetchImpl = fetch) {
20042
19662
  });
20043
19663
  const body2 = await resp.json();
20044
19664
  if (body2.code !== 0) throw new Error(`application/v6/scopes failed: code=${body2.code} msg=${body2.msg ?? ""}`);
20045
- return (body2.data?.scopes ?? []).some(
20046
- (s2) => s2.scope_name === scope2 && (!s2.scope_type || s2.scope_type === "user")
20047
- );
19665
+ return body2.data?.scopes ?? [];
20048
19666
  }
20049
19667
  function feishuAuthSelectionRecord(sel, sendAsUserRequested, tokenResponse) {
20050
19668
  const granted = typeof tokenResponse["scope"] === "string" ? tokenResponse["scope"].split(/\s+/).filter(Boolean) : void 0;
@@ -20055,7 +19673,64 @@ function feishuAuthSelectionRecord(sel, sendAsUserRequested, tokenResponse) {
20055
19673
  ...granted ? { grantedScopes: granted } : {}
20056
19674
  };
20057
19675
  }
20058
- 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 feishuModulePolicyEnv(modules) {
19677
+ const picked = modules.filter((m2) => FEISHU_MODULE_ACCESS[m2]);
19678
+ if (picked.length === 0) return null;
19679
+ const allow = /* @__PURE__ */ new Set();
19680
+ for (const m2 of picked) {
19681
+ for (const c of FEISHU_MODULE_ACCESS[m2].commands) allow.add(`cmd:${c}`);
19682
+ for (const s2 of FEISHU_MODULE_ACCESS[m2].apiServices) allow.add(`api:${s2}`);
19683
+ }
19684
+ const need2 = /* @__PURE__ */ new Map();
19685
+ for (const [m2, a] of Object.entries(FEISHU_MODULE_ACCESS)) {
19686
+ for (const k2 of [...a.commands.map((c) => `cmd:${c}`), ...a.apiServices.map((s2) => `api:${s2}`)]) {
19687
+ if (allow.has(k2)) continue;
19688
+ need2.set(k2, [...need2.get(k2) ?? [], (FEISHU_MODULE_LABELS[m2] ?? m2).replace(/\s+/g, "")]);
19689
+ }
19690
+ }
19691
+ return {
19692
+ OASIS_FEISHU_ALLOW: ` ${[...allow].sort().join(" ")} `,
19693
+ OASIS_FEISHU_NEED: ` ${[...need2].sort(([a], [b2]) => a.localeCompare(b2)).map(([k2, v2]) => `${k2}=${v2.join("/")}`).join(" ")} `,
19694
+ OASIS_FEISHU_MODULES: picked.map((m2) => FEISHU_MODULE_LABELS[m2] ?? m2).join("\u3001")
19695
+ };
19696
+ }
19697
+ function feishuSelectedModules(raw) {
19698
+ if (!raw) return [];
19699
+ try {
19700
+ const parsed = JSON.parse(raw);
19701
+ return Array.isArray(parsed.modules) ? parsed.modules.filter((m2) => typeof m2 === "string" && !!FEISHU_MODULE_ACCESS[m2]) : [];
19702
+ } catch {
19703
+ return [];
19704
+ }
19705
+ }
19706
+ function feishuModuleAccessReport(input) {
19707
+ const granted = input.grantedScopes ? new Set(input.grantedScopes) : null;
19708
+ const tenant = (input.appScopes ?? []).filter((s2) => s2.scope_type === "tenant" && s2.scope_name);
19709
+ return input.modules.filter((m2) => FEISHU_MODULE_ACCESS[m2]).map((m2) => {
19710
+ const prefixes = FEISHU_MODULE_ACCESS[m2].botScopePrefixes;
19711
+ const mine = tenant.filter((s2) => prefixes.some((p2) => s2.scope_name.startsWith(p2)));
19712
+ const bot = input.appScopes === null ? "unknown" : mine.some((s2) => s2.grant_status === void 0 || s2.grant_status === 1) ? "granted" : mine.length > 0 ? "pending" : "missing";
19713
+ const missingUserScopes = granted ? (FEISHU_DOMAIN_SCOPES[m2]?.scopes ?? []).filter((s2) => !granted.has(s2)) : [];
19714
+ const user = input.userAuthInvalid ? "invalid" : !input.hasUserAuth ? "none" : missingUserScopes.length > 0 ? "partial" : "granted";
19715
+ return {
19716
+ id: m2,
19717
+ label: FEISHU_MODULE_LABELS[m2] ?? m2,
19718
+ bot,
19719
+ user,
19720
+ missingUserScopes: user === "partial" ? missingUserScopes : []
19721
+ };
19722
+ });
19723
+ }
19724
+ function feishuSendAsUserState(sel) {
19725
+ if (!sel.sendAsUser) return "off";
19726
+ if (!sel.sendAsUserRequested) return "app_not_enabled";
19727
+ if (sel.grantedScopes && !sel.grantedScopes.includes(FEISHU_SEND_AS_USER_SCOPE)) return "not_granted";
19728
+ return "on";
19729
+ }
19730
+ function feishuAppPermissionConsoleUrl(appId) {
19731
+ return `https://open.feishu.cn/app/${encodeURIComponent(appId)}/auth`;
19732
+ }
19733
+ 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, FEISHU_MODULE_LABELS;
20059
19734
  var init_scopes = __esm({
20060
19735
  "../connectors/src/feishu/scopes.ts"() {
20061
19736
  "use strict";
@@ -20458,6 +20133,453 @@ var init_scopes = __esm({
20458
20133
  FEISHU_SEND_AS_USER_SCOPE = "im:message.send_as_user";
20459
20134
  FEISHU_AUTH_SELECTION_VAR = "FEISHU_AUTH_SELECTION";
20460
20135
  AUTO_APPROVED = new Set(FEISHU_BASIC_SCOPES);
20136
+ FEISHU_MODULE_ACCESS = {
20137
+ im: { commands: ["im"], apiServices: ["im"], botScopePrefixes: ["im:"] },
20138
+ calendar: { commands: ["calendar"], apiServices: ["calendar"], botScopePrefixes: ["calendar:"] },
20139
+ docs: { commands: ["docs", "drive", "markdown", "mindnotes"], apiServices: ["docx", "doc", "docs", "drive", "mindnote", "mindnotes", "suite"], botScopePrefixes: ["docs:", "docx:", "space:"] },
20140
+ drive: { commands: ["drive", "markdown"], apiServices: ["drive"], botScopePrefixes: ["drive:"] },
20141
+ sheets: { commands: ["sheets"], apiServices: ["sheets"], botScopePrefixes: ["sheets:"] },
20142
+ base: { commands: ["base"], apiServices: ["bitable", "base"], botScopePrefixes: ["base:", "bitable:"] },
20143
+ wiki: { commands: ["wiki"], apiServices: ["wiki"], botScopePrefixes: ["wiki:"] },
20144
+ slides: { commands: ["slides"], apiServices: ["slides", "slides_ai"], botScopePrefixes: ["slides:"] },
20145
+ task: { commands: ["task"], apiServices: ["task"], botScopePrefixes: ["task:"] },
20146
+ approval: { commands: ["approval"], apiServices: ["approval"], botScopePrefixes: ["approval:"] },
20147
+ mail: { commands: ["mail"], apiServices: ["mail"], botScopePrefixes: ["mail:"] },
20148
+ minutes: { commands: ["minutes"], apiServices: ["minutes"], botScopePrefixes: ["minutes:"] },
20149
+ vc: { commands: ["vc", "note"], apiServices: ["vc"], botScopePrefixes: ["vc:"] },
20150
+ contact: { commands: ["contact"], apiServices: ["contact", "search"], botScopePrefixes: ["contact:"] },
20151
+ okr: { commands: ["okr"], apiServices: ["okr"], botScopePrefixes: ["okr:"] },
20152
+ attendance: { commands: ["attendance"], apiServices: ["attendance"], botScopePrefixes: ["attendance:"] },
20153
+ board: { commands: ["whiteboard"], apiServices: ["board"], botScopePrefixes: ["board:"] },
20154
+ spark: { commands: ["apps"], apiServices: ["spark"], botScopePrefixes: ["spark:"] }
20155
+ };
20156
+ FEISHU_MODULE_LABELS = {
20157
+ im: "\u5373\u65F6\u6D88\u606F",
20158
+ calendar: "\u65E5\u5386",
20159
+ docs: "\u4E91\u6587\u6863",
20160
+ drive: "\u4E91\u76D8",
20161
+ sheets: "\u7535\u5B50\u8868\u683C",
20162
+ base: "\u591A\u7EF4\u8868\u683C",
20163
+ wiki: "\u77E5\u8BC6\u5E93",
20164
+ slides: "\u5E7B\u706F\u7247",
20165
+ task: "\u4EFB\u52A1",
20166
+ approval: "\u5BA1\u6279",
20167
+ mail: "\u90AE\u4EF6",
20168
+ minutes: "\u5999\u8BB0",
20169
+ vc: "\u89C6\u9891\u4F1A\u8BAE",
20170
+ contact: "\u901A\u8BAF\u5F55",
20171
+ okr: "OKR",
20172
+ attendance: "\u8003\u52E4",
20173
+ board: "\u767D\u677F",
20174
+ spark: "Spark \u5E94\u7528"
20175
+ };
20176
+ }
20177
+ });
20178
+
20179
+ // ../connectors/src/feishu/index.ts
20180
+ async function fetchFeishuTenantAccessToken(appId, appSecret) {
20181
+ try {
20182
+ const resp = await fetch(FEISHU_TENANT_TOKEN_URL, {
20183
+ method: "POST",
20184
+ headers: { "Content-Type": "application/json; charset=utf-8" },
20185
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
20186
+ signal: AbortSignal.timeout(TENANT_TOKEN_TIMEOUT_MS)
20187
+ });
20188
+ const data = await resp.json();
20189
+ if (!resp.ok || data.code !== 0 || typeof data.tenant_access_token !== "string" || !data.tenant_access_token) {
20190
+ 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());
20191
+ return null;
20192
+ }
20193
+ return data.tenant_access_token;
20194
+ } catch (err) {
20195
+ 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)}`);
20196
+ return null;
20197
+ }
20198
+ }
20199
+ function previousTenantToken(credsFile, appId) {
20200
+ if (!credsFile) return null;
20201
+ try {
20202
+ const prev = readSessionCredsFile(credsFile);
20203
+ if (prev["LARKSUITE_CLI_APP_ID"] !== appId) return null;
20204
+ return prev["LARKSUITE_CLI_TENANT_ACCESS_TOKEN"] || null;
20205
+ } catch {
20206
+ return null;
20207
+ }
20208
+ }
20209
+ function parseFeishuTokenResponse(data, nowMs) {
20210
+ const accessToken = String(data["access_token"] ?? "");
20211
+ const refreshToken = String(data["refresh_token"] ?? "");
20212
+ const expiresInSec = Number(data["expires_in"] ?? 7200);
20213
+ const refreshExpiresInSec = Number(
20214
+ data["refresh_token_expires_in"] ?? data["refresh_expires_in"] ?? 30 * 24 * 3600
20215
+ );
20216
+ const expiresAtMs = nowMs + expiresInSec * 1e3;
20217
+ return {
20218
+ credentials: {
20219
+ ...accessToken ? { user_access_token: accessToken } : {},
20220
+ ...refreshToken ? { refresh_token: refreshToken } : {},
20221
+ user_token_expires_at: String(expiresAtMs),
20222
+ refresh_token_expires_at: String(nowMs + refreshExpiresInSec * 1e3)
20223
+ },
20224
+ expiresAt: new Date(expiresAtMs)
20225
+ };
20226
+ }
20227
+ 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;
20228
+ var init_feishu = __esm({
20229
+ "../connectors/src/feishu/index.ts"() {
20230
+ "use strict";
20231
+ import_node_child_process13 = require("node:child_process");
20232
+ import_node_util4 = require("node:util");
20233
+ import_node_path12 = require("node:path");
20234
+ import_node_url = require("node:url");
20235
+ import_promises5 = require("node:fs/promises");
20236
+ init_base();
20237
+ init_logger();
20238
+ init_scopes();
20239
+ execFileAsync2 = (0, import_node_util4.promisify)(import_node_child_process13.execFile);
20240
+ __dirname2 = (0, import_node_path12.dirname)((0, import_node_url.fileURLToPath)(__esm_import_meta_url));
20241
+ FEISHU_WELL_KNOWN_SKILLS_BASE = "https://open.feishu.cn/.well-known/skills";
20242
+ FEISHU_TOKEN_URL = "https://open.feishu.cn/open-apis/authen/v2/oauth/token";
20243
+ FEISHU_TENANT_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
20244
+ FEISHU_USER_OPEN_ID_VAR = "FEISHU_USER_OPEN_ID";
20245
+ FEISHU_AUTHORIZED_BY_VAR = "FEISHU_AUTHORIZED_BY";
20246
+ FEISHU_USER_AUTH_INVALID_VAR = "FEISHU_USER_AUTH_INVALID";
20247
+ FEISHU_REFRESH_REVOKED_CODES = /* @__PURE__ */ new Set([20024, 20026, 20037, 20064, 20073, 20074]);
20248
+ TENANT_TOKEN_TIMEOUT_MS = 5e3;
20249
+ FeishuConnector = class extends CliConnector {
20250
+ config = {
20251
+ slug: "feishu",
20252
+ type: "cli",
20253
+ // schema 要**声明本连接器实际消费的全部凭据**,不只是「让用户手填的那几个」。
20254
+ // 历史上这里只写了 app_id,而 inject() 其实还要 app_secret / user_access_token
20255
+ // (后两者由控制台 OAuth 流写进变量表,用户不手填)——于是 secret 标记缺失,
20256
+ // 密钥黑名单只能在 serve.ts 另手写一份。现在黑名单从 sensitiveVars 推导,schema 必须诚实。
20257
+ schema: [
20258
+ { key: "app_id", description: "\u98DE\u4E66 App ID\uFF08\u7559\u7A7A\u5219\u81EA\u52A8\u521B\u5EFA\u65B0\u5E94\u7528\uFF09", secret: false, required: false },
20259
+ { 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 },
20260
+ { 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 },
20261
+ { 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 }
20262
+ ]
20263
+ };
20264
+ commandName = "lark-cli";
20265
+ requiredCommands = ["lark-cli"];
20266
+ envPrefix = "FEISHU_";
20267
+ /** server 侧从 actor 变量表组装凭据的声明(见 provision.ts)。这些会随 job 下发到节点。 */
20268
+ credentialSources = [
20269
+ { key: "app_id", from: "FEISHU_APP_ID", required: true },
20270
+ { key: "app_secret", from: "FEISHU_APP_SECRET", required: true },
20271
+ { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false },
20272
+ // 按 Agent 限制可用模块:inject 据此写放行表,wrapper 拦(个人选择优先,缺失回落组织连接的选择)。
20273
+ { key: "auth_selection", from: FEISHU_AUTH_SELECTION_VAR, required: false }
20274
+ ];
20275
+ /**
20276
+ * 续期材料——**只在 server 侧读,不下发节点**。refresh_token 能换出新的 user token,
20277
+ * 属于"长期材料",没有任何理由让它出现在 agent 所在的机器上。
20278
+ */
20279
+ refreshCredentialSources = [
20280
+ // App 凭据是**组织级**共享的(个人变量优先,缺失回落全局默认);用户 token 类只认个人变量。
20281
+ { key: "app_id", from: "FEISHU_APP_ID", orgDefault: true, required: true },
20282
+ { key: "app_secret", from: "FEISHU_APP_SECRET", orgDefault: true, required: true },
20283
+ { key: "refresh_token", from: "FEISHU_REFRESH_TOKEN", required: true },
20284
+ { key: "user_token_expires_at", from: "FEISHU_USER_TOKEN_EXPIRES_AT", required: false },
20285
+ { key: "refresh_token_expires_at", from: "FEISHU_REFRESH_TOKEN_EXPIRES_AT", required: false },
20286
+ { key: "user_access_token", from: "FEISHU_USER_ACCESS_TOKEN", required: false }
20287
+ ];
20288
+ /** 过期判定只认这个键——必须与注入时消费的 token 同源(见接口注释里的历史坑)。 */
20289
+ expiresAtCredentialKey = "user_token_expires_at";
20290
+ /**
20291
+ * 下发前 user token 至少还要剩 40 分钟,不够就换新(Bug 0201)。
20292
+ *
20293
+ * 必须 > `SESSION_CREDENTIAL_RENEW_INTERVAL_MS`(30 分钟):会话中途续期每 30 分钟问一次 server,
20294
+ * 问的就是这条门槛。门槛若只有通用的 5 分钟,一张还剩 20 分钟的 token 会被原样发回,
20295
+ * 20 分钟后失效、要再等 10 分钟才到下一拍——最坏断档近 25 分钟(`99991677 token expire`)。
20296
+ * 取 40 = 30 分钟间隔 + 10 分钟调度余量:任一拍之后剩余 > 40,到下一拍仍 > 10。
20297
+ *
20298
+ * 不像 GitHub 那样取全寿命(每次都换):飞书每次刷新都会轮换 refresh_token,
20299
+ * 换得越勤,「飞书已轮换、我们还没落库」的窗口就越常被碰上,所以只取刚好够用的值。
20300
+ */
20301
+ minRemainingLifetimeMs = 40 * 6e4;
20302
+ /**
20303
+ * 只由服务端定时任务续期(Bug 0318):refresh_token 一次性、每刷必轮换,派单与会话中途再各刷一次就是
20304
+ * 多个写者并发打断续期链;懒续期又让闲置超过 refresh_token 寿命的员工悄悄失效。派单只读库里最新的 token。
20305
+ */
20306
+ scheduledRefreshOnly = true;
20307
+ /** refresh_token 自身 ~30d 过期,过了只能重新授权。 */
20308
+ refreshMaterialExpiresAtCredentialKey = "refresh_token_expires_at";
20309
+ /**
20310
+ * refresh_token 过期后该清掉的键——**只清这个员工的授权产物**。
20311
+ * app_id / app_secret 刻意不在其中:它们代表组织级 App 凭据,与其他员工的连接无关。
20312
+ */
20313
+ clearOnExpiredCredentialKeys = [
20314
+ "user_access_token",
20315
+ "refresh_token",
20316
+ "user_token_expires_at",
20317
+ "refresh_token_expires_at"
20318
+ ];
20319
+ /**
20320
+ * 绝不允许进入 agent 进程环境的原始变量。
20321
+ * 含 APP_ID 与几个过期时间戳:它们本身不算密钥,但同属飞书凭据材料,一并挡在 agent 之外,
20322
+ * 避免 agent 绕开 wrapper 自行拼凭据(沿用既有行为)。
20323
+ */
20324
+ sensitiveVars = [
20325
+ "FEISHU_APP_ID",
20326
+ "FEISHU_APP_SECRET",
20327
+ "FEISHU_USER_ACCESS_TOKEN",
20328
+ "FEISHU_REFRESH_TOKEN",
20329
+ "FEISHU_USER_TOKEN_EXPIRES_AT",
20330
+ "FEISHU_REFRESH_TOKEN_EXPIRES_AT",
20331
+ FEISHU_USER_OPEN_ID_VAR,
20332
+ FEISHU_AUTHORIZED_BY_VAR,
20333
+ FEISHU_USER_AUTH_INVALID_VAR
20334
+ ];
20335
+ /** 宿主机登录态隔离,理由同 github(见接口注释):由通用装配层无条件设。 */
20336
+ hostCredentialIsolationEnvKeys = ["LARKSUITE_CLI_CONFIG_DIR"];
20337
+ /** inject() 写进 sessionEnv 的全部键——通用 cleanup() 按它逐个删。 */
20338
+ injectedEnvKeys = [
20339
+ "LARK_BIN",
20340
+ "FEISHU_CREDS_FILE",
20341
+ "FEISHU_RUN_TMPDIR",
20342
+ "LARKSUITE_CLI_CONFIG_DIR"
20343
+ ];
20344
+ /**
20345
+ * 自检:拿 user_access_token 问飞书「我是谁」,回 `{ok, identity, data:{name, open_id}}`。
20346
+ *
20347
+ * **不能用 `lark-cli auth status`**:注入外部凭据后整个 auth 命名空间会被拒
20348
+ * ("credentials are provided externally and do not support interactive management"),
20349
+ * 自检会恒为失败。实测 2026-08-03 踩到——`auth status` 只在**读宿主机 profile** 时才有输出,
20350
+ * 恰恰是我们要隔离掉的那条路径。
20351
+ */
20352
+ verifyArgs = ["api", "GET", "/open-apis/authen/v1/user_info"];
20353
+ /** 厂商技能包源——平台自动拉取飞书官方技能包时用。 */
20354
+ wellKnownSkillsBase = FEISHU_WELL_KNOWN_SKILLS_BASE;
20355
+ larkBin;
20356
+ fetchTenantToken;
20357
+ /** `fetchTenantToken` 只给测试替换(不打真飞书);生产一律用 {@link fetchFeishuTenantAccessToken}。 */
20358
+ constructor(larkBin, opts = {}) {
20359
+ super(__dirname2);
20360
+ this.larkBin = larkBin ?? this.resolveLarkBin();
20361
+ this.fetchTenantToken = opts.fetchTenantToken ?? fetchFeishuTenantAccessToken;
20362
+ }
20363
+ /**
20364
+ * 解析 user_info 的 JSON。lark-cli 的 `ok:false` 也是**退出码 0** 的正常输出,
20365
+ * 所以必须在这里判 ok——只看子进程退出码会把「凭据失效」当成自检通过。
20366
+ */
20367
+ parseIdentity(stdout) {
20368
+ let parsed;
20369
+ try {
20370
+ parsed = JSON.parse(stdout);
20371
+ } catch {
20372
+ return null;
20373
+ }
20374
+ const obj2 = parsed;
20375
+ if (obj2.ok !== true) return null;
20376
+ const openId = typeof obj2.data?.open_id === "string" ? obj2.data.open_id : "";
20377
+ if (!openId) return null;
20378
+ const name = typeof obj2.data?.name === "string" ? obj2.data.name : "";
20379
+ return name ? `${name} (${openId})` : openId;
20380
+ }
20381
+ async connect(options = {}) {
20382
+ if (this.isSimulated()) {
20383
+ return { credentials: { app_id: options["app_id"] ?? "cli_simulated" } };
20384
+ }
20385
+ const app_id = options["app_id"] ?? await this.initNewApp();
20386
+ await this.runAuthLogin(app_id);
20387
+ return { credentials: { app_id } };
20388
+ }
20389
+ /**
20390
+ * Agent run 开始:把凭据作为 lark-cli「外部凭据」写进凭据文件(不进 agent env)。
20391
+ * credentials 接受:app_id, app_secret, user_access_token?(及其 FEISHU_* 别名)。
20392
+ * 企业 token 不随 job 下发,由这里用 app_id/app_secret 现换(换不到只是本轮没有 bot 身份,不抛)。
20393
+ *
20394
+ * 注入的 sessionEnv 键(全部非敏感):
20395
+ * FEISHU_CREDS_FILE — 凭据文件路径,wrapper source 它
20396
+ * FEISHU_RUN_TMPDIR — 本轮临时目录,cleanup 整个删掉
20397
+ * LARK_BIN — 真实 lark-cli 绝对路径(防 wrapper 递归调用自身)
20398
+ */
20399
+ async inject(credentials2, sessionEnv) {
20400
+ const appId = credentials2["app_id"] ?? credentials2["FEISHU_APP_ID"];
20401
+ const appSecret = credentials2["app_secret"] ?? credentials2["FEISHU_APP_SECRET"];
20402
+ if (!appId) throw new Error("feishu inject: missing app_id");
20403
+ if (!appSecret) throw new Error("feishu inject: missing app_secret");
20404
+ const userAccessToken = credentials2["user_access_token"] ?? credentials2["FEISHU_USER_ACCESS_TOKEN"];
20405
+ const modulePolicy = feishuModulePolicyEnv(
20406
+ feishuSelectedModules(credentials2["auth_selection"] ?? credentials2[FEISHU_AUTH_SELECTION_VAR])
20407
+ );
20408
+ const tenantAccessToken = this.isSimulated() ? null : await this.fetchTenantToken(appId, appSecret) ?? previousTenantToken(sessionEnv.get("FEISHU_CREDS_FILE"), appId);
20409
+ const creds = await writeSessionCredsFile(this.config.slug, {
20410
+ LARKSUITE_CLI_APP_ID: appId,
20411
+ LARKSUITE_CLI_APP_SECRET: appSecret,
20412
+ ...tenantAccessToken ? { LARKSUITE_CLI_TENANT_ACCESS_TOKEN: tenantAccessToken } : {},
20413
+ ...userAccessToken ? { LARKSUITE_CLI_USER_ACCESS_TOKEN: userAccessToken } : {},
20414
+ ...modulePolicy ?? {}
20415
+ }, sessionEnv.get("FEISHU_RUN_TMPDIR"));
20416
+ const configDir = (0, import_node_path12.join)(creds.dir, "lark-config");
20417
+ await (0, import_promises5.mkdir)(configDir, { recursive: true, mode: 448 });
20418
+ sessionEnv.set("LARK_BIN", await this.resolveAbsoluteLarkBin());
20419
+ sessionEnv.set("FEISHU_CREDS_FILE", creds.file);
20420
+ sessionEnv.set("FEISHU_RUN_TMPDIR", creds.dir);
20421
+ sessionEnv.set("LARKSUITE_CLI_CONFIG_DIR", configDir);
20422
+ }
20423
+ /**
20424
+ * 用 refresh_token 换一对新的 user token(飞书 v2 OAuth)。
20425
+ *
20426
+ * **纯换取,不落库**——持久化由调用方做(server 侧 `oauth.ts`)。这样平台知识
20427
+ * (端点、字段名、~2h/~30d 的寿命)全部收在连接器里,与 GitHub App 的续期共用
20428
+ * `_base/refresh.ts` 那套判定。
20429
+ *
20430
+ * 返回 null=没有可用的续期材料(未完成授权)。平台报错则抛,由上层收敛成 failed。
20431
+ */
20432
+ async refreshCredentials(credentials2) {
20433
+ const appId = credentials2["app_id"];
20434
+ const appSecret = credentials2["app_secret"];
20435
+ const refreshToken = credentials2["refresh_token"];
20436
+ if (!appId || !appSecret || !refreshToken) return null;
20437
+ const resp = await fetch(FEISHU_TOKEN_URL, {
20438
+ method: "POST",
20439
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
20440
+ body: new URLSearchParams({
20441
+ grant_type: "refresh_token",
20442
+ refresh_token: refreshToken,
20443
+ client_id: appId,
20444
+ client_secret: appSecret
20445
+ }).toString()
20446
+ });
20447
+ const data = await resp.json();
20448
+ if (!resp.ok || data["error"]) {
20449
+ const code2 = Number(data["code"]);
20450
+ const msg = `feishu refresh failed: HTTP ${resp.status} code=${String(data["code"] ?? "")} ${String(data["error"] ?? "")}`.trim();
20451
+ if (data["error"] === "invalid_grant" || FEISHU_REFRESH_REVOKED_CODES.has(code2)) throw new CredentialRevokedError(msg);
20452
+ throw new Error(msg);
20453
+ }
20454
+ return parseFeishuTokenResponse(data, Date.now());
20455
+ }
20456
+ async initNewApp() {
20457
+ await new Promise((resolve11, reject) => {
20458
+ const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["config", "init", "--new"], { stdio: "inherit" });
20459
+ proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli config init exited ${code2}`)));
20460
+ proc.on("error", reject);
20461
+ });
20462
+ const { stdout } = await execFileAsync2(this.larkBin, ["config", "show", "--json"]);
20463
+ const cfg = JSON.parse(stdout);
20464
+ if (!cfg.appId) throw new Error("feishu connector: could not determine app_id after config init");
20465
+ return cfg.appId;
20466
+ }
20467
+ async runAuthLogin(app_id) {
20468
+ await new Promise((resolve11, reject) => {
20469
+ const proc = (0, import_node_child_process13.spawn)(this.larkBin, ["--profile", app_id, "auth", "login"], { stdio: "inherit" });
20470
+ proc.on("close", (code2) => code2 === 0 ? resolve11() : reject(new Error(`lark-cli auth login exited ${code2}`)));
20471
+ proc.on("error", reject);
20472
+ });
20473
+ }
20474
+ resolveLarkBin() {
20475
+ return process.env["LARK_CLI_BIN"] ?? "lark-cli";
20476
+ }
20477
+ async resolveAbsoluteLarkBin() {
20478
+ if (this.larkBin.includes("/")) return this.larkBin;
20479
+ return this.resolveRealBin(this.larkBin);
20480
+ }
20481
+ };
20482
+ }
20483
+ });
20484
+
20485
+ // ../connectors/src/feishu/user-auth.ts
20486
+ async function startFeishuDeviceAuthorization(input) {
20487
+ const basicAuth = Buffer.from(`${input.appId}:${input.appSecret}`).toString("base64");
20488
+ const resp = await (input.fetchImpl ?? fetch)(FEISHU_DEVICE_AUTHORIZATION_URL, {
20489
+ method: "POST",
20490
+ headers: {
20491
+ "Content-Type": "application/x-www-form-urlencoded",
20492
+ "Authorization": `Basic ${basicAuth}`
20493
+ },
20494
+ body: new URLSearchParams({ client_id: input.appId, scope: input.scopes.join(" ") }).toString()
20495
+ });
20496
+ const data = await resp.json();
20497
+ if (!resp.ok || data["error"]) {
20498
+ throw new Error(String(data["error_description"] ?? data["error"] ?? "device authorization failed"));
20499
+ }
20500
+ const verificationUrl = String(data["verification_uri_complete"] ?? data["verification_uri"] ?? "");
20501
+ if (!verificationUrl) throw new Error("no verification_uri in response");
20502
+ const expiresIn = Number(data["expires_in"]);
20503
+ const interval = Number(data["interval"]);
20504
+ return {
20505
+ verificationUrl,
20506
+ deviceCode: String(data["device_code"] ?? ""),
20507
+ ...Number.isFinite(expiresIn) && expiresIn > 0 ? { expiresIn } : {},
20508
+ ...Number.isFinite(interval) && interval > 0 ? { interval } : {}
20509
+ };
20510
+ }
20511
+ async function pollFeishuDeviceToken(input) {
20512
+ const resp = await (input.fetchImpl ?? fetch)(FEISHU_TOKEN_URL, {
20513
+ method: "POST",
20514
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
20515
+ body: new URLSearchParams({
20516
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
20517
+ device_code: input.deviceCode,
20518
+ client_id: input.appId,
20519
+ client_secret: input.appSecret
20520
+ }).toString()
20521
+ });
20522
+ const data = await resp.json();
20523
+ const errStr = data["error"];
20524
+ if (errStr === "authorization_pending" || errStr === "slow_down") return { status: "pending" };
20525
+ if (!resp.ok || errStr) throw new Error(String(data["error_description"] ?? errStr ?? "login failed"));
20526
+ if (!data["access_token"]) throw new Error("no access_token in response");
20527
+ return { status: "ok", data };
20528
+ }
20529
+ async function fetchFeishuUserIdentity(accessToken, fetchImpl = fetch) {
20530
+ try {
20531
+ const resp = await fetchImpl(FEISHU_USER_INFO_URL, { headers: { authorization: `Bearer ${accessToken}` } });
20532
+ const ui = await resp.json();
20533
+ if (!resp.ok || ui.code !== 0) return {};
20534
+ const name = ui.data?.name || ui.data?.en_name;
20535
+ return {
20536
+ ...ui.data?.open_id ? { openId: ui.data.open_id } : {},
20537
+ ...name ? { name } : {}
20538
+ };
20539
+ } catch {
20540
+ return {};
20541
+ }
20542
+ }
20543
+ async function sendFeishuBotTextToUser(input) {
20544
+ const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
20545
+ if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0Cbot \u79C1\u4FE1\u672A\u53D1\u51FA");
20546
+ const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_SEND_MESSAGE_URL}?receive_id_type=open_id`, {
20547
+ method: "POST",
20548
+ headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
20549
+ body: JSON.stringify({ receive_id: input.openId, msg_type: "text", content: JSON.stringify({ text: input.text }) })
20550
+ });
20551
+ const body2 = await resp.json();
20552
+ if (body2.code !== 0) throw new Error(`feishu bot \u79C1\u4FE1\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
20553
+ }
20554
+ async function lookupFeishuOpenIdByEmail(input) {
20555
+ const token2 = await (input.fetchTenantToken ?? fetchFeishuTenantAccessToken)(input.appId, input.appSecret);
20556
+ if (!token2) throw new Error("\u6362\u4F01\u4E1A token \u5931\u8D25\uFF0C\u672A\u80FD\u6309\u90AE\u7BB1\u67E5\u98DE\u4E66\u7528\u6237");
20557
+ const resp = await (input.fetchImpl ?? fetch)(`${FEISHU_BATCH_GET_ID_URL}?user_id_type=open_id`, {
20558
+ method: "POST",
20559
+ headers: { "content-type": "application/json; charset=utf-8", authorization: `Bearer ${token2}` },
20560
+ body: JSON.stringify({ emails: [input.email], include_resigned: false })
20561
+ });
20562
+ const body2 = await resp.json();
20563
+ if (body2.code !== 0) throw new Error(`feishu \u6309\u90AE\u7BB1\u67E5\u7528\u6237\u5931\u8D25: code=${String(body2.code)} ${body2.msg ?? ""}`.trim());
20564
+ const hit = body2.data?.user_list?.find((u) => u.email?.toLowerCase() === input.email.toLowerCase() && u.user_id);
20565
+ return hit?.user_id ?? null;
20566
+ }
20567
+ function feishuUserAuthInvalidText(input) {
20568
+ 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`;
20569
+ return input.link ? `${head}
20570
+ \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
20571
+ ${input.link}` : `${head}
20572
+ \u8BF7\u5230 Oasis \u8BE5 Agent \u7684\u300C\u8FDE\u63A5\u5668\u300D\u9875\u91CD\u65B0\u6388\u6743\u98DE\u4E66\u3002`;
20573
+ }
20574
+ var FEISHU_DEVICE_AUTHORIZATION_URL, FEISHU_USER_INFO_URL, FEISHU_SEND_MESSAGE_URL, FEISHU_BATCH_GET_ID_URL;
20575
+ var init_user_auth = __esm({
20576
+ "../connectors/src/feishu/user-auth.ts"() {
20577
+ "use strict";
20578
+ init_feishu();
20579
+ FEISHU_DEVICE_AUTHORIZATION_URL = "https://accounts.feishu.cn/oauth/v1/device_authorization";
20580
+ FEISHU_USER_INFO_URL = "https://open.feishu.cn/open-apis/authen/v1/user_info";
20581
+ FEISHU_SEND_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages";
20582
+ FEISHU_BATCH_GET_ID_URL = "https://open.feishu.cn/open-apis/contact/v3/users/batch_get_id";
20461
20583
  }
20462
20584
  });
20463
20585
 
@@ -20569,6 +20691,16 @@ async function persistFeishuUserAuthorization(store, actorId, data, nowMs, who =
20569
20691
  await put3(FEISHU_AUTHORIZED_BY_VAR, who.authorizedBy);
20570
20692
  await store.deleteVariable(FEISHU_USER_AUTH_INVALID_VAR, actorId);
20571
20693
  }
20694
+ async function readFeishuUserAuthInvalid(store, actorId) {
20695
+ const raw = await store.revealVariable(FEISHU_USER_AUTH_INVALID_VAR, actorId).catch(() => null);
20696
+ if (!raw) return null;
20697
+ try {
20698
+ const m2 = JSON.parse(raw);
20699
+ return typeof m2?.at === "string" ? m2 : null;
20700
+ } catch {
20701
+ return null;
20702
+ }
20703
+ }
20572
20704
  async function markFeishuUserAuthInvalid(store, actorId, marker) {
20573
20705
  await store.putVariable({
20574
20706
  key: FEISHU_USER_AUTH_INVALID_VAR,
@@ -23421,6 +23553,7 @@ var init_scheduled_refresh = __esm({
23421
23553
  counts2[outcome.status] = (counts2[outcome.status] ?? 0) + 1;
23422
23554
  }
23423
23555
  }
23556
+ await this.deps.afterCompany?.(companyId, store).catch((error2) => this.deps.warn(new Error(`\u5B9A\u65F6\u7EED\u671F\u6536\u5C3E company=${companyId}: ${String(error2)}`)));
23424
23557
  } catch (error2) {
23425
23558
  this.deps.warn(new Error(`\u5B9A\u65F6\u7EED\u671F company=${companyId}: ${String(error2)}`));
23426
23559
  }
@@ -24524,7 +24657,7 @@ function feishuReauthUrl(apiBase, token2) {
24524
24657
  return `${apiBase.replace(/\/+$/, "")}/api/connectors/feishu/reauth?t=${encodeURIComponent(token2)}`;
24525
24658
  }
24526
24659
  async function issueFeishuReauthToken(links, store, claims) {
24527
- const openId = await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, claims.actorId).catch(() => null);
24660
+ const openId = await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, claims.actorId).catch(() => null) || claims.fallbackSubject;
24528
24661
  if (!openId) return null;
24529
24662
  return links.issue({
24530
24663
  companyId: claims.companyId,
@@ -211867,6 +212000,54 @@ var init_registry_audit_sink = __esm({
211867
212000
  }
211868
212001
  });
211869
212002
 
212003
+ // ../server/src/feishu-module-access.ts
212004
+ async function feishuModuleAccess(store, actorId, fetchScopes = fetchFeishuAppScopes) {
212005
+ const read3 = (key2) => store.revealResolvedVariable(key2, actorId).catch(() => null);
212006
+ const appId = await read3("FEISHU_APP_ID");
212007
+ const appSecret = await read3("FEISHU_APP_SECRET");
212008
+ if (!appId || !appSecret) return { configured: false };
212009
+ const rawSelection = await read3(FEISHU_AUTH_SELECTION_VAR);
212010
+ const modules = feishuSelectedModules(rawSelection);
212011
+ let parsed = {};
212012
+ try {
212013
+ parsed = rawSelection ? JSON.parse(rawSelection) : {};
212014
+ } catch {
212015
+ parsed = {};
212016
+ }
212017
+ const grantedScopes = Array.isArray(parsed.grantedScopes) ? parsed.grantedScopes.filter((s2) => typeof s2 === "string") : void 0;
212018
+ const hasUserAuth = Boolean(await read3("FEISHU_USER_ACCESS_TOKEN") || await read3("FEISHU_REFRESH_TOKEN"));
212019
+ const userAuthInvalid = Boolean(await read3(FEISHU_USER_AUTH_INVALID_VAR));
212020
+ let appScopes = null;
212021
+ let botCheckError;
212022
+ if (modules.length > 0) {
212023
+ try {
212024
+ appScopes = await fetchScopes(appId, appSecret);
212025
+ } catch (e) {
212026
+ botCheckError = String(e?.message ?? e).slice(0, 200);
212027
+ }
212028
+ }
212029
+ return {
212030
+ configured: true,
212031
+ restricted: modules.length > 0,
212032
+ modules: feishuModuleAccessReport({ modules, grantedScopes, hasUserAuth, userAuthInvalid, appScopes }),
212033
+ hasUserAuth,
212034
+ userAuthInvalid,
212035
+ sendAsUser: feishuSendAsUserState({
212036
+ sendAsUser: parsed.sendAsUser === true,
212037
+ sendAsUserRequested: parsed.sendAsUserRequested === true,
212038
+ grantedScopes
212039
+ }),
212040
+ appConsoleUrl: feishuAppPermissionConsoleUrl(appId),
212041
+ ...botCheckError ? { botCheckError } : {}
212042
+ };
212043
+ }
212044
+ var init_feishu_module_access = __esm({
212045
+ "../server/src/feishu-module-access.ts"() {
212046
+ "use strict";
212047
+ init_src3();
212048
+ }
212049
+ });
212050
+
211870
212051
  // ../server/src/github-app-inventory.ts
211871
212052
  async function collectOasisGithubApps(svc) {
211872
212053
  const rows = (await svc.listVariables()).filter((r) => r.scope === "global" || r.scope === "personal");
@@ -218060,6 +218241,17 @@ ${composed}`;
218060
218241
  else html(false, result.error);
218061
218242
  return;
218062
218243
  }
218244
+ if (url.pathname === "/api/connectors/feishu/app/permissions" && req.method === "POST") {
218245
+ try {
218246
+ const { actorId } = JSON.parse(rawBody || "{}");
218247
+ const svc = await connectorActorsService();
218248
+ if (!svc) throw new Error("actors service unavailable");
218249
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(await feishuModuleAccess(svc, actorId)));
218250
+ } catch (e) {
218251
+ res.writeHead(500).end(JSON.stringify({ error: String(e) }));
218252
+ }
218253
+ return;
218254
+ }
218063
218255
  if (url.pathname === "/api/connectors/feishu/login/start" && req.method === "POST") {
218064
218256
  const startBody = JSON.parse(rawBody);
218065
218257
  const { appId, actorId } = startBody;
@@ -218072,6 +218264,12 @@ ${composed}`;
218072
218264
  res.writeHead(400).end(JSON.stringify({ error: "\u8BF7\u81F3\u5C11\u9009\u62E9\u4E00\u4E2A\u529F\u80FD\u6A21\u5757" }));
218073
218265
  return;
218074
218266
  }
218267
+ const devicePending = opts.connectorAuthPending?.githubApp;
218268
+ if (!devicePending) {
218269
+ console.error("[feishu-oauth] connectorAuthPending store unavailable \u2014 refusing to start device auth");
218270
+ res.writeHead(503).end(JSON.stringify({ error: "\u6388\u6743\u767B\u8BB0\u670D\u52A1\u6682\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5" }));
218271
+ return;
218272
+ }
218075
218273
  try {
218076
218274
  const startSvc = await connectorActorsService();
218077
218275
  const appSecret = startSvc ? await startSvc.revealResolvedVariable("FEISHU_APP_SECRET", actorId) : null;
@@ -218095,26 +218293,21 @@ ${composed}`;
218095
218293
  scopes: scopesForSelection(selection, sendAsUserRequested)
218096
218294
  });
218097
218295
  if (!deviceCode) throw new Error("no device_code in response");
218098
- const devicePending = opts.connectorAuthPending?.githubApp;
218099
- if (devicePending) {
218100
- const now = Date.now();
218101
- await devicePending.put({
218102
- state: feishuDevicePendingKey(deviceCode),
218103
- ...currentCompanyId ? { companyId: currentCompanyId } : {},
218104
- value: {
218105
- connector: "feishu",
218106
- appId,
218107
- actorId: actorId ?? "",
218108
- modules: selection.modules,
218109
- sendAsUser: selection.sendAsUser,
218110
- sendAsUserRequested
218111
- },
218112
- createdAtMs: now,
218113
- expiresAtMs: now + FEISHU_DEVICE_PENDING_TTL_MS
218114
- });
218115
- } else {
218116
- console.error("[feishu-oauth] connectorAuthPending store unavailable \u2014 auth selection will not be saved");
218117
- }
218296
+ const now = Date.now();
218297
+ await devicePending.put({
218298
+ state: feishuDevicePendingKey(deviceCode),
218299
+ ...currentCompanyId ? { companyId: currentCompanyId } : {},
218300
+ value: {
218301
+ connector: "feishu",
218302
+ appId,
218303
+ actorId: actorId ?? "",
218304
+ modules: selection.modules,
218305
+ sendAsUser: selection.sendAsUser,
218306
+ sendAsUserRequested
218307
+ },
218308
+ createdAtMs: now,
218309
+ expiresAtMs: now + FEISHU_DEVICE_PENDING_TTL_MS
218310
+ });
218118
218311
  res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify({
218119
218312
  verificationUrl,
218120
218313
  deviceCode,
@@ -218137,18 +218330,22 @@ ${composed}`;
218137
218330
  try {
218138
218331
  const devicePending = opts.connectorAuthPending?.githubApp;
218139
218332
  const started = devicePending ? await devicePending.get(feishuDevicePendingKey(deviceCode), Date.now()) : { status: "missing" };
218140
- let startedSelection = null;
218141
- if (started.status === "found") {
218142
- const v2 = started.value.value;
218143
- if (v2["appId"] !== appId || v2["actorId"] !== (actorId ?? "") || (started.value.companyId ?? "") !== (currentCompanyId ?? "")) {
218144
- res.writeHead(400).end(JSON.stringify({ error: "\u8FD9\u6B21\u6388\u6743\u4E0D\u662F\u7531\u5F53\u524D\u5E94\u7528\u6216\u5458\u5DE5\u53D1\u8D77\u7684\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743" }));
218145
- return;
218146
- }
218147
- const selection = normalizeFeishuAuthSelection({ modules: v2["modules"], sendAsUser: v2["sendAsUser"] });
218148
- if (selection) {
218149
- startedSelection = { selection, sendAsUserRequested: selection.sendAsUser && v2["sendAsUserRequested"] === true };
218150
- }
218333
+ if (started.status !== "found") {
218334
+ console.error(`[feishu-oauth] no login/start registration for this device code (${started.status}) \u2014 refusing token exchange`);
218335
+ res.writeHead(409).end(JSON.stringify({ ok: false, restart: true, error: "\u8FD9\u6B21\u6388\u6743\u7684\u767B\u8BB0\u5DF2\u5931\u6548\uFF08\u8D85\u65F6\u6216\u670D\u52A1\u5DF2\u66F4\u65B0\uFF09\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743" }));
218336
+ return;
218337
+ }
218338
+ const v2 = started.value.value;
218339
+ if (v2["appId"] !== appId || v2["actorId"] !== (actorId ?? "") || (started.value.companyId ?? "") !== (currentCompanyId ?? "")) {
218340
+ res.writeHead(400).end(JSON.stringify({ ok: false, restart: true, error: "\u8FD9\u6B21\u6388\u6743\u4E0D\u662F\u7531\u5F53\u524D\u5E94\u7528\u6216\u5458\u5DE5\u53D1\u8D77\u7684\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743" }));
218341
+ return;
218342
+ }
218343
+ const selection = normalizeFeishuAuthSelection({ modules: v2["modules"], sendAsUser: v2["sendAsUser"] });
218344
+ if (!selection) {
218345
+ res.writeHead(409).end(JSON.stringify({ ok: false, restart: true, error: "\u8FD9\u6B21\u6388\u6743\u7684\u767B\u8BB0\u5DF2\u635F\u574F\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u6388\u6743" }));
218346
+ return;
218151
218347
  }
218348
+ const startedSelection = { selection, sendAsUserRequested: selection.sendAsUser && v2["sendAsUserRequested"] === true };
218152
218349
  const pollSvc = await connectorActorsService();
218153
218350
  const appSecret = pollSvc ? await pollSvc.revealResolvedVariable("FEISHU_APP_SECRET", actorId) : null;
218154
218351
  if (!appSecret) throw new Error("FEISHU_APP_SECRET not found \u2014 please complete app setup first");
@@ -218174,18 +218371,14 @@ ${composed}`;
218174
218371
  ...isHumanActorId(actor) ? { authorizedBy: actor } : {}
218175
218372
  }));
218176
218373
  const nameScope = actorId ? { scope: "personal", actorId } : { scope: "global" };
218177
- if (startedSelection) {
218178
- const record10 = feishuAuthSelectionRecord(startedSelection.selection, startedSelection.sendAsUserRequested, data);
218179
- await svc.putVariable({
218180
- key: FEISHU_AUTH_SELECTION_VAR,
218181
- value: JSON.stringify(record10),
218182
- ...nameScope,
218183
- connectorId: "feishu",
218184
- encrypted: false
218185
- });
218186
- } else {
218187
- console.error(`[feishu-oauth] no login/start registration for this device code (${started.status}) \u2014 auth selection not saved`);
218188
- }
218374
+ const record10 = feishuAuthSelectionRecord(startedSelection.selection, startedSelection.sendAsUserRequested, data);
218375
+ await svc.putVariable({
218376
+ key: FEISHU_AUTH_SELECTION_VAR,
218377
+ value: JSON.stringify(record10),
218378
+ ...nameScope,
218379
+ connectorId: "feishu",
218380
+ encrypted: false
218381
+ });
218189
218382
  if (!who.openId) console.error("[feishu-oauth] user_info \u672A\u53D6\u5230\u6388\u6743\u4EBA open_id\uFF1A\u6388\u6743\u5931\u6548\u65F6\u65E0\u6CD5\u79C1\u4FE1\u672C\u4EBA\u3001\u4E5F\u7B7E\u4E0D\u51FA\u4E00\u952E\u91CD\u65B0\u6388\u6743\u94FE\u63A5");
218190
218383
  try {
218191
218384
  const secret = await svc.revealResolvedVariable("FEISHU_APP_SECRET", actorId);
@@ -218552,6 +218745,7 @@ var init_server3 = __esm({
218552
218745
  init_assembly();
218553
218746
  init_child_live_turn();
218554
218747
  init_registry_audit_sink();
218748
+ init_feishu_module_access();
218555
218749
  init_src3();
218556
218750
  init_github_app_inventory();
218557
218751
  init_github_app_timing();
@@ -286671,6 +286865,7 @@ function createTodoRuntime(options) {
286671
286865
  init_src();
286672
286866
  init_src3();
286673
286867
  init_src11();
286868
+ var MAX_NOTICE_ATTEMPTS = 36;
286674
286869
  function createConnectorRenewalRuntime(options) {
286675
286870
  const now = () => options.now?.() ?? Date.now();
286676
286871
  const agentName = async (companyId, actorId) => {
@@ -286679,13 +286874,17 @@ function createConnectorRenewalRuntime(options) {
286679
286874
  const hit = (await svc?.listActors().catch(() => []))?.find((a) => a.id === actorId);
286680
286875
  return hit?.name || actorId;
286681
286876
  };
286682
- const notifyFeishuUser = async (store, actorId, text7) => {
286683
- const openId = await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId);
286684
- if (!openId) return "no-open-id";
286877
+ const appCredentials = async (store, actorId) => {
286685
286878
  const appId = await store.revealResolvedVariable("FEISHU_APP_ID", actorId);
286686
286879
  const appSecret = await store.revealResolvedVariable("FEISHU_APP_SECRET", actorId);
286687
- if (!appId || !appSecret) return "no-app";
286688
- await sendFeishuBotTextToUser({ appId, appSecret, openId, text: text7 });
286880
+ return appId && appSecret ? { appId, appSecret } : null;
286881
+ };
286882
+ const notifyFeishuUser = async (store, actorId, text7, openId) => {
286883
+ const to = openId || await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId);
286884
+ if (!to) return "no-open-id";
286885
+ const app = await appCredentials(store, actorId);
286886
+ if (!app) return "no-app";
286887
+ await sendFeishuBotTextToUser({ ...app, openId: to, text: text7 });
286689
286888
  return "sent";
286690
286889
  };
286691
286890
  const recipientsFor = async (companyId, store, actorId) => {
@@ -286701,36 +286900,110 @@ function createConnectorRenewalRuntime(options) {
286701
286900
  }
286702
286901
  return [...out];
286703
286902
  };
286704
- const onFeishuInvalidated = async (e, store) => {
286705
- const at2 = new Date(now()).toISOString();
286706
- await markFeishuUserAuthInvalid(store, e.actorId, { at: at2, reason: e.outcome.detail ?? "\u7EED\u671F\u6750\u6599\u5DF2\u5931\u6548" });
286707
- const name = await agentName(e.companyId, e.actorId);
286708
- const token2 = await issueFeishuReauthToken(options.links, store, { companyId: e.companyId, actorId: e.actorId, purpose: "reauth" });
286709
- const link = token2 && options.apiBase ? feishuReauthUrl(options.apiBase, token2) : void 0;
286710
- const ledger = await options.notifications(e.companyId);
286711
- if (ledger) {
286712
- const fresh = [];
286713
- for (const recipient of await recipientsFor(e.companyId, store, e.actorId)) {
286714
- const row = await ledger.insert({
286715
- recipient,
286716
- category: "connector",
286717
- kind: "feishu-auth-invalid",
286718
- // 每次失效一条:键里带失效时刻,重新授权后再失效还会再提醒;同一次失效重复扫描不重发(唯一约束)。
286719
- sourceKey: `connector:feishu:${e.actorId ?? "org"}@invalid:${at2}`,
286720
- title: `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u9700\u8981\u91CD\u65B0\u6388\u6743`,
286721
- ...e.actorId ? { actorId: e.actorId } : {},
286722
- enteredAt: at2
286723
- });
286724
- if (row.enteredAt === at2) fresh.push(row);
286903
+ const recoverOpenId = async (companyId, app, recipients) => {
286904
+ for (const human of recipients) {
286905
+ const email2 = await options.memberEmail?.(human, companyId).catch(() => null);
286906
+ if (!email2) continue;
286907
+ const openId = await lookupFeishuOpenIdByEmail({ ...app, email: email2 });
286908
+ if (openId) return openId;
286909
+ }
286910
+ return null;
286911
+ };
286912
+ const deliverInvalidNotice = async (companyId, store, actorId, marker) => {
286913
+ const prev = marker.notice ?? { inApp: false, dm: "pending", attempts: 0 };
286914
+ const state = { ...prev, attempts: prev.attempts + 1 };
286915
+ const who = `actor=${actorId ?? "-"} \u7B2C ${state.attempts}/${MAX_NOTICE_ATTEMPTS} \u6B21`;
286916
+ const name = await agentName(companyId, actorId);
286917
+ const recipients = await recipientsFor(companyId, store, actorId);
286918
+ if (!state.inApp) {
286919
+ try {
286920
+ const ledger = await options.notifications(companyId);
286921
+ if (ledger) {
286922
+ const rows = [];
286923
+ for (const recipient of recipients) {
286924
+ rows.push(await ledger.insert({
286925
+ recipient,
286926
+ category: "connector",
286927
+ kind: "feishu-auth-invalid",
286928
+ // 每次失效一条:键里带失效时刻,重新授权后再失效还会再提醒;同一次失效补发 / 重复扫描不重发(唯一约束)。
286929
+ sourceKey: `connector:feishu:${actorId ?? "org"}@invalid:${marker.at}`,
286930
+ title: `\u300C${name}\u300D\u7684\u98DE\u4E66\u7528\u6237\u6388\u6743\u5DF2\u5931\u6548\uFF0C\u9700\u8981\u91CD\u65B0\u6388\u6743`,
286931
+ ...actorId ? { actorId } : {},
286932
+ enteredAt: marker.at
286933
+ }));
286934
+ }
286935
+ if (rows.length > 0) options.onNotifications?.(companyId, rows);
286936
+ }
286937
+ 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 ?? "-"}`);
286938
+ state.inApp = true;
286939
+ } catch (err) {
286940
+ 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)}`));
286725
286941
  }
286726
- if (fresh.length > 0) options.onNotifications?.(e.companyId, fresh);
286727
286942
  }
286728
- try {
286729
- const sent = await notifyFeishuUser(store, e.actorId, feishuUserAuthInvalidText({ agentName: name, ...link ? { link } : {} }));
286730
- if (sent !== "sent") console.warn(`[credential-refresh] \u98DE\u4E66\u6388\u6743\u5931\u6548\u79C1\u4FE1\u672A\u53D1\uFF08${sent}\uFF09actor=${e.actorId ?? "-"}`);
286731
- } catch (err) {
286732
- options.warn(new Error(`\u98DE\u4E66\u6388\u6743\u5931\u6548\u79C1\u4FE1\u53D1\u9001\u5931\u8D25 actor=${e.actorId ?? "-"}: ${String(err)}`));
286943
+ if (state.dm === "pending") {
286944
+ try {
286945
+ const app = await appCredentials(store, actorId);
286946
+ const stored = await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, actorId);
286947
+ const recovered = !stored && app ? await recoverOpenId(companyId, app, recipients) : null;
286948
+ const openId = stored || recovered;
286949
+ if (!app || !openId) {
286950
+ state.dm = "unavailable";
286951
+ 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`);
286952
+ } else {
286953
+ const token2 = await issueFeishuReauthToken(options.links, store, {
286954
+ companyId,
286955
+ actorId,
286956
+ purpose: "reauth",
286957
+ ...recovered ? { fallbackSubject: recovered } : {}
286958
+ });
286959
+ const link = token2 && options.apiBase ? feishuReauthUrl(options.apiBase, token2) : void 0;
286960
+ await notifyFeishuUser(store, actorId, feishuUserAuthInvalidText({ agentName: name, ...link ? { link } : {} }), openId);
286961
+ state.dm = "sent";
286962
+ }
286963
+ } catch (err) {
286964
+ options.warn(new Error(`\u98DE\u4E66\u6388\u6743\u5931\u6548\u79C1\u4FE1\u53D1\u9001\u5931\u8D25\uFF08${who}\uFF0C\u4E0B\u4E00\u62CD\u91CD\u8BD5\uFF09: ${String(err)}`));
286965
+ }
286966
+ }
286967
+ if ((!state.inApp || state.dm === "pending") && state.attempts >= MAX_NOTICE_ATTEMPTS) {
286968
+ options.warn(new Error(`\u98DE\u4E66\u6388\u6743\u5931\u6548\u63D0\u9192\u8865\u53D1 ${state.attempts} \u6B21\u4ECD\u672A\u9001\u5230\uFF0C\u653E\u5F03 actor=${actorId ?? "-"} \u7AD9\u5185=${String(state.inApp)} \u79C1\u4FE1=${state.dm}`));
286733
286969
  }
286970
+ return state;
286971
+ };
286972
+ const saveNoticeState = async (store, actorId, at2, notice) => {
286973
+ await withConnectorCredentialLock("feishu", actorId, async () => {
286974
+ const current = await readFeishuUserAuthInvalid(store, actorId);
286975
+ if (current?.at !== at2) return;
286976
+ await markFeishuUserAuthInvalid(store, actorId, { ...current, notice });
286977
+ });
286978
+ };
286979
+ const handledThisSweep = /* @__PURE__ */ new Set();
286980
+ const sweepKey = (companyId, actorId) => JSON.stringify([companyId, actorId ?? ""]);
286981
+ const onFeishuInvalidated = async (e, store) => {
286982
+ const marker = {
286983
+ at: new Date(now()).toISOString(),
286984
+ reason: e.outcome.detail ?? "\u7EED\u671F\u6750\u6599\u5DF2\u5931\u6548",
286985
+ notice: { inApp: false, dm: "pending", attempts: 0 }
286986
+ };
286987
+ await markFeishuUserAuthInvalid(store, e.actorId, marker);
286988
+ handledThisSweep.add(sweepKey(e.companyId, e.actorId));
286989
+ await saveNoticeState(store, e.actorId, marker.at, await deliverInvalidNotice(e.companyId, store, e.actorId, marker));
286990
+ };
286991
+ const retryPendingNotices = async (companyId, store) => {
286992
+ const svc = await options.actors(companyId);
286993
+ if (!svc) return;
286994
+ const ids2 = [void 0, ...(await svc.listActors({ status: "active" })).map((a) => a.id)];
286995
+ for (const actorId of ids2) {
286996
+ if (handledThisSweep.delete(sweepKey(companyId, actorId))) continue;
286997
+ const marker = await readFeishuUserAuthInvalid(store, actorId);
286998
+ const n = marker?.notice;
286999
+ if (!marker || !n || n.inApp && n.dm !== "pending" || n.attempts >= MAX_NOTICE_ATTEMPTS) continue;
287000
+ try {
287001
+ await saveNoticeState(store, actorId, marker.at, await deliverInvalidNotice(companyId, store, actorId, marker));
287002
+ } catch (err) {
287003
+ options.warn(new Error(`\u98DE\u4E66\u6388\u6743\u5931\u6548\u63D0\u9192\u8865\u53D1\u5931\u8D25 actor=${actorId ?? "-"}: ${String(err)}`));
287004
+ }
287005
+ }
287006
+ for (const k2 of handledThisSweep) if (JSON.parse(k2)[0] === companyId) handledThisSweep.delete(k2);
286734
287007
  };
286735
287008
  const onFeishuRenewed = async (e, store) => {
286736
287009
  if (await store.revealVariable(FEISHU_USER_OPEN_ID_VAR, e.actorId)) return;
@@ -286770,6 +287043,7 @@ function createConnectorRenewalRuntime(options) {
286770
287043
  onRenewed: async (e, store) => {
286771
287044
  if (e.connector.config.slug === "feishu") await onFeishuRenewed(e, store);
286772
287045
  },
287046
+ afterCompany: retryPendingNotices,
286773
287047
  warn: options.warn,
286774
287048
  ...options.now ? { now: options.now } : {}
286775
287049
  });
@@ -286790,7 +287064,8 @@ function createConnectorRenewalRuntime(options) {
286790
287064
  await notifyFeishuUser(
286791
287065
  store,
286792
287066
  claims.actorId,
286793
- `\u6709\u4EBA\u7528\u53E6\u4E00\u4E2A\u98DE\u4E66\u8D26\u53F7${who.userName ? `\uFF08${who.userName}\uFF09` : ""}\u6253\u5F00\u4E86\u300C${name}\u300D\u7684\u91CD\u65B0\u6388\u6743\u94FE\u63A5\uFF0C\u5DF2\u88AB\u62D2\u7EDD\uFF0C\u6388\u6743\u672A\u6539\u53D8\u3002`
287067
+ `\u6709\u4EBA\u7528\u53E6\u4E00\u4E2A\u98DE\u4E66\u8D26\u53F7${who.userName ? `\uFF08${who.userName}\uFF09` : ""}\u6253\u5F00\u4E86\u300C${name}\u300D\u7684\u91CD\u65B0\u6388\u6743\u94FE\u63A5\uFF0C\u5DF2\u88AB\u62D2\u7EDD\uFF0C\u6388\u6743\u672A\u6539\u53D8\u3002`,
287068
+ claims.subject
286794
287069
  ).catch(() => void 0);
286795
287070
  }
286796
287071
  });
@@ -290573,6 +290848,11 @@ async function startServe(opts) {
290573
290848
  auditSink.notifyNotificationsChanged(recipients);
290574
290849
  },
290575
290850
  boundHuman: async (agentId, companyId) => (await (await resolveAssistantsCtx(companyId)).service.getByAgent(agentId))?.humanActorId ?? null,
290851
+ // 成员表行上带 actorId;邮箱在控制面账号上(登录时验证过)。
290852
+ memberEmail: async (humanId, companyId) => {
290853
+ const row = (await controlPlaneStore.listMembers(companyId)).find((m2) => m2.actorId === humanId);
290854
+ return row ? (await controlPlaneStore.getAccount(row.accountId))?.email ?? null : null;
290855
+ },
290576
290856
  links: createConnectorReauthLinkIssuer({ secretFile: path30.join(opts.dir, "connector-reauth-link-secret") }),
290577
290857
  apiBase: remoteApiUrl,
290578
290858
  warn: (error2) => console.warn(`[credential-refresh] ${String(error2)}`)
@@ -300664,7 +300944,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
300664
300944
  }
300665
300945
  }
300666
300946
  }
300667
- var CLI_VERSION = "2.2.110" ? "2.2.110" : "dev";
300947
+ var CLI_VERSION = "2.2.112" ? "2.2.112" : "dev";
300668
300948
  function apiHeaders(token2, company) {
300669
300949
  const headers = {
300670
300950
  authorization: `Bearer ${token2}`,
@@ -304835,7 +305115,7 @@ function shimScript() {
304835
305115
  }
304836
305116
 
304837
305117
  // src/index.ts
304838
- var PKG_VERSION = true ? "2.2.110" : "dev";
305118
+ var PKG_VERSION = true ? "2.2.112" : "dev";
304839
305119
  var LOCAL_BIN = localBin();
304840
305120
  var NPM_PREFIX = npmPrefix();
304841
305121
  var INSTANCE = DEFAULT_INSTANCE;