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