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/feishu/wrapper.sh +90 -0
- package/dist/index.js +748 -468
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
|
218099
|
-
|
|
218100
|
-
|
|
218101
|
-
|
|
218102
|
-
|
|
218103
|
-
|
|
218104
|
-
|
|
218105
|
-
|
|
218106
|
-
|
|
218107
|
-
|
|
218108
|
-
|
|
218109
|
-
|
|
218110
|
-
|
|
218111
|
-
|
|
218112
|
-
|
|
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
|
-
|
|
218141
|
-
|
|
218142
|
-
|
|
218143
|
-
|
|
218144
|
-
|
|
218145
|
-
|
|
218146
|
-
|
|
218147
|
-
|
|
218148
|
-
|
|
218149
|
-
|
|
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
|
-
|
|
218178
|
-
|
|
218179
|
-
|
|
218180
|
-
|
|
218181
|
-
|
|
218182
|
-
|
|
218183
|
-
|
|
218184
|
-
|
|
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
|
|
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
|
-
|
|
286688
|
-
|
|
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
|
|
286705
|
-
const
|
|
286706
|
-
|
|
286707
|
-
|
|
286708
|
-
|
|
286709
|
-
|
|
286710
|
-
|
|
286711
|
-
|
|
286712
|
-
|
|
286713
|
-
|
|
286714
|
-
|
|
286715
|
-
|
|
286716
|
-
|
|
286717
|
-
|
|
286718
|
-
|
|
286719
|
-
|
|
286720
|
-
|
|
286721
|
-
|
|
286722
|
-
|
|
286723
|
-
|
|
286724
|
-
|
|
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
|
-
|
|
286729
|
-
|
|
286730
|
-
|
|
286731
|
-
|
|
286732
|
-
|
|
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.
|
|
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.
|
|
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;
|