自动同步: 2026-07-01 02:10
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
# OpenClaw Feishu/Lark
|
||||
|
||||
Official OpenClaw channel plugin for Feishu and Lark workplace chats. Community maintained by @m1heng.
|
||||
|
||||
Install from OpenClaw:
|
||||
|
||||
```bash
|
||||
openclaw plugin add @openclaw/feishu
|
||||
```
|
||||
|
||||
Configure the Feishu/Lark app credentials in OpenClaw, then connect the plugin to the chats where agents should receive and send messages.
|
||||
Generated
Vendored
+515
@@ -0,0 +1,515 @@
|
||||
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
||||
import { isRecord, normalizeOptionalString, normalizeStringEntries, readStringValue } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { DEFAULT_ACCOUNT_ID, createAccountListHelpers, hasConfiguredAccountValue, normalizeAccountId, normalizeOptionalAccountId, resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
|
||||
import { coerceSecretRef } from "openclaw/plugin-sdk/provider-auth";
|
||||
//#region extensions/feishu/src/comment-target.ts
|
||||
const FEISHU_COMMENT_FILE_TYPES = [
|
||||
"doc",
|
||||
"docx",
|
||||
"file",
|
||||
"sheet",
|
||||
"slides"
|
||||
];
|
||||
function normalizeCommentFileType(value) {
|
||||
return typeof value === "string" && FEISHU_COMMENT_FILE_TYPES.includes(value) ? value : void 0;
|
||||
}
|
||||
function buildFeishuCommentTarget(params) {
|
||||
return `comment:${params.fileType}:${params.fileToken}:${params.commentId}`;
|
||||
}
|
||||
function parseFeishuCommentTarget(raw) {
|
||||
const trimmed = raw?.trim();
|
||||
if (!trimmed?.startsWith("comment:")) return null;
|
||||
const parts = trimmed.split(":");
|
||||
if (parts.length !== 4) return null;
|
||||
const fileType = normalizeCommentFileType(parts[1]);
|
||||
const fileToken = parts[2]?.trim();
|
||||
const commentId = parts[3]?.trim();
|
||||
if (!fileType || !fileToken || !commentId) return null;
|
||||
return {
|
||||
fileType,
|
||||
fileToken,
|
||||
commentId
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/comment-shared.ts
|
||||
function encodeQuery(params) {
|
||||
const query = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
const trimmed = value?.trim();
|
||||
if (trimmed) query.set(key, trimmed);
|
||||
}
|
||||
const queryString = query.toString();
|
||||
return queryString ? `?${queryString}` : "";
|
||||
}
|
||||
const readString = readStringValue;
|
||||
const normalizeString = normalizeOptionalString;
|
||||
const isRecord$1 = isRecord;
|
||||
function formatFeishuApiError(error, options = {}) {
|
||||
if (!isRecord$1(error)) return typeof error === "string" ? error : JSON.stringify(error);
|
||||
const config = isRecord$1(error.config) ? error.config : void 0;
|
||||
const response = isRecord$1(error.response) ? error.response : void 0;
|
||||
const responseData = isRecord$1(response?.data) ? response?.data : void 0;
|
||||
const feishuLogId = readString(responseData?.log_id) || (options.includeNestedErrorLogId ? readString(isRecord$1(responseData?.error) ? responseData.error.log_id : void 0) : void 0);
|
||||
const nestedError = isRecord$1(responseData?.error) ? responseData.error : void 0;
|
||||
return JSON.stringify({
|
||||
message: typeof error.message === "string" ? error.message : typeof error === "string" ? error : JSON.stringify(error),
|
||||
code: readString(error.code),
|
||||
method: readString(config?.method),
|
||||
url: readString(config?.url),
|
||||
...options.includeConfigParams ? { params: config?.params } : {},
|
||||
http_status: typeof response?.status === "number" ? response.status : void 0,
|
||||
feishu_code: typeof responseData?.code === "number" ? responseData.code : readString(responseData?.code),
|
||||
feishu_msg: readString(responseData?.msg),
|
||||
feishu_log_id: feishuLogId,
|
||||
feishu_troubleshooter: readString(responseData?.troubleshooter) || readString(nestedError?.troubleshooter)
|
||||
});
|
||||
}
|
||||
function formatFeishuApiFailure(error, errorPrefix, options = {}) {
|
||||
return `${errorPrefix}: ${formatFeishuApiError(error, options) || "unknown error"}`;
|
||||
}
|
||||
function createFeishuApiError(error, errorPrefix, options = {}) {
|
||||
return new Error(formatFeishuApiFailure(error, errorPrefix, options), { cause: error });
|
||||
}
|
||||
const FEISHU_SEND_RATE_LIMIT_CODES = new Set([230020, 11232]);
|
||||
const FEISHU_SEND_MAX_RETRIES = 2;
|
||||
const FEISHU_SEND_RETRY_BASE_MS = 500;
|
||||
/**
|
||||
* Returns a numeric rate-limit signal when an AxiosError indicates a retryable
|
||||
* Feishu message-API rate limit. Sources, in priority order:
|
||||
* 1. Gateway-level HTTP 429 (app-wide quota; `x-ogw-ratelimit-reset` header)
|
||||
* 2. Business-level `code` in `error.response.data.code` matching
|
||||
* FEISHU_SEND_RATE_LIMIT_CODES (e.g. 230020 per-chat, 11232 tenant-level).
|
||||
* Returns `undefined` for all other errors so they propagate without retry.
|
||||
*/
|
||||
function getFeishuSendRateLimitCode(error) {
|
||||
if (!isRecord$1(error)) return;
|
||||
const response = isRecord$1(error.response) ? error.response : void 0;
|
||||
if (typeof response?.status === "number" && response.status === 429) return 429;
|
||||
const code = (isRecord$1(response?.data) ? response.data : void 0)?.code;
|
||||
return typeof code === "number" && FEISHU_SEND_RATE_LIMIT_CODES.has(code) ? code : void 0;
|
||||
}
|
||||
/**
|
||||
* Returns a retryable rate-limit code when a fulfilled (non-throwing) Feishu
|
||||
* SDK response embeds it in the response body. The Feishu node SDK can resolve
|
||||
* with `{ code: 11232, msg: "..." }` instead of throwing — see typing.ts
|
||||
* (getBackoffCodeFromResponse) and issue #28157 for the same behavior on
|
||||
* messageReaction.create. Without this classification, requestFeishuApi would
|
||||
* `return` the rate-limited body and downstream `assertFeishuMessageApiSuccess`
|
||||
* would fail once with no retry.
|
||||
*/
|
||||
function getFeishuSendRateLimitCodeFromResponse(response) {
|
||||
if (!isRecord$1(response)) return;
|
||||
const code = response.code;
|
||||
return typeof code === "number" && FEISHU_SEND_RATE_LIMIT_CODES.has(code) ? code : void 0;
|
||||
}
|
||||
async function requestFeishuApi(request, errorPrefix, options = {}) {
|
||||
const retryDelayMs = options.retryDelayMs ?? FEISHU_SEND_RETRY_BASE_MS;
|
||||
let lastFulfilledRateLimit;
|
||||
for (let attempt = 0; attempt <= FEISHU_SEND_MAX_RETRIES; attempt++) {
|
||||
if (attempt > 0) await new Promise((resolve) => {
|
||||
setTimeout(resolve, attempt * retryDelayMs);
|
||||
});
|
||||
try {
|
||||
const result = await request();
|
||||
const fulfilledRateLimit = getFeishuSendRateLimitCodeFromResponse(result);
|
||||
if (fulfilledRateLimit !== void 0) {
|
||||
lastFulfilledRateLimit = {
|
||||
response: result,
|
||||
code: fulfilledRateLimit
|
||||
};
|
||||
if (attempt < FEISHU_SEND_MAX_RETRIES) continue;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (!(attempt < FEISHU_SEND_MAX_RETRIES && getFeishuSendRateLimitCode(error) !== void 0)) throw createFeishuApiError(error, errorPrefix, options);
|
||||
}
|
||||
}
|
||||
if (lastFulfilledRateLimit) throw createFeishuApiError(Object.assign(/* @__PURE__ */ new Error(`Request fulfilled with rate-limit code ${lastFulfilledRateLimit.code}`), { response: {
|
||||
status: 200,
|
||||
data: lastFulfilledRateLimit.response
|
||||
} }), errorPrefix, options);
|
||||
throw createFeishuApiError(/* @__PURE__ */ new Error("unreachable"), errorPrefix, options);
|
||||
}
|
||||
function readDocsLinkUrl(element) {
|
||||
const docsLink = isRecord$1(element.docs_link) ? element.docs_link : void 0;
|
||||
return normalizeString(docsLink?.url) || normalizeString(docsLink?.link) || normalizeString(element.url) || normalizeString(element.link) || void 0;
|
||||
}
|
||||
function readMentionUserId(element) {
|
||||
const mention = isRecord$1(element.mention) ? element.mention : void 0;
|
||||
return normalizeString((isRecord$1(element.person) ? element.person : void 0)?.user_id) || normalizeString(mention?.user_id) || normalizeString(mention?.open_id) || normalizeString(element.mention_user) || normalizeString(element.user_id) || void 0;
|
||||
}
|
||||
function readMentionDisplayText(element, userId) {
|
||||
const mention = isRecord$1(element.mention) ? element.mention : void 0;
|
||||
const mentionName = normalizeString(mention?.name) || normalizeString(mention?.display_name) || normalizeString(element.name);
|
||||
return mentionName ? `@${mentionName}` : `@${userId}`;
|
||||
}
|
||||
function normalizeCommentText(parts) {
|
||||
return parts.join("").trim() || void 0;
|
||||
}
|
||||
function normalizeCommentSemanticText(parts) {
|
||||
return parts.join("").replace(/\s+/g, " ").trim() || void 0;
|
||||
}
|
||||
function readElementTextPreservingWhitespace(element) {
|
||||
return (isRecord$1(element.text_run) ? readString(element.text_run.content) || readString(element.text_run.text) : void 0) || readString(element.text) || readString(element.content) || readString(element.name) || void 0;
|
||||
}
|
||||
const FEISHU_LINK_TOKEN_MIN_LENGTH = 22;
|
||||
const FEISHU_LINK_TOKEN_MAX_LENGTH = 28;
|
||||
const COMMENT_LINK_KIND_ALIASES = new Map([
|
||||
["doc", "doc"],
|
||||
["docs", "doc"],
|
||||
["docx", "docx"],
|
||||
["sheet", "sheet"],
|
||||
["sheets", "sheet"],
|
||||
["slide", "slides"],
|
||||
["slides", "slides"],
|
||||
["file", "file"],
|
||||
["files", "file"],
|
||||
["wiki", "wiki"],
|
||||
["mindnote", "mindnote"],
|
||||
["mindnotes", "mindnote"],
|
||||
["bitable", "bitable"],
|
||||
["base", "base"]
|
||||
]);
|
||||
function isCommentFileType(value) {
|
||||
return typeof value === "string" && FEISHU_COMMENT_FILE_TYPES.includes(value);
|
||||
}
|
||||
function isReasonableFeishuLinkToken(token) {
|
||||
return typeof token === "string" && token.length >= FEISHU_LINK_TOKEN_MIN_LENGTH && token.length <= FEISHU_LINK_TOKEN_MAX_LENGTH;
|
||||
}
|
||||
function parseCommentLinkedDocumentPath(pathname) {
|
||||
const segments = normalizeStringEntries(pathname.split("/"));
|
||||
const offset = segments[0]?.toLowerCase() === "space" ? 1 : 0;
|
||||
const kind = COMMENT_LINK_KIND_ALIASES.get(segments[offset]?.toLowerCase() ?? "");
|
||||
const token = normalizeString(segments[offset + 1]);
|
||||
if (!kind || !isReasonableFeishuLinkToken(token)) return null;
|
||||
return {
|
||||
urlKind: kind,
|
||||
token
|
||||
};
|
||||
}
|
||||
function hasResolvedLinkedDocumentReference(link) {
|
||||
return link.urlKind !== "unknown" && (Boolean(link.resolvedObjToken) || Boolean(link.wikiNodeToken));
|
||||
}
|
||||
function resolveCommentLinkedDocumentFromUrl(params) {
|
||||
const link = {
|
||||
rawUrl: params.rawUrl,
|
||||
urlKind: "unknown"
|
||||
};
|
||||
try {
|
||||
const parsedPath = parseCommentLinkedDocumentPath(new URL(params.rawUrl).pathname);
|
||||
if (!parsedPath) return link;
|
||||
const { urlKind, token } = parsedPath;
|
||||
link.urlKind = urlKind;
|
||||
if (urlKind === "wiki") {
|
||||
link.urlKind = "wiki";
|
||||
link.wikiNodeToken = token;
|
||||
} else {
|
||||
link.resolvedObjType = urlKind;
|
||||
link.resolvedObjToken = token;
|
||||
}
|
||||
if (link.resolvedObjType && link.resolvedObjToken && isCommentFileType(link.resolvedObjType) && params.currentDocument?.fileType === link.resolvedObjType && params.currentDocument.fileToken === link.resolvedObjToken) link.isCurrentDocument = true;
|
||||
else if (link.resolvedObjType && link.resolvedObjToken && isCommentFileType(link.resolvedObjType)) link.isCurrentDocument = false;
|
||||
} catch {
|
||||
return link;
|
||||
}
|
||||
return link;
|
||||
}
|
||||
function parseCommentContentElements(params) {
|
||||
const elements = Array.isArray(params.elements) ? params.elements : [];
|
||||
const plainTextParts = [];
|
||||
const semanticTextParts = [];
|
||||
const mentions = [];
|
||||
const linkedDocuments = [];
|
||||
const botIds = new Set(Array.from(params.botOpenIds ?? []).map((value) => normalizeString(value)).filter((value) => Boolean(value)));
|
||||
const linkedDocumentKeys = /* @__PURE__ */ new Set();
|
||||
let botMentioned = false;
|
||||
for (const rawElement of elements) {
|
||||
if (!isRecord$1(rawElement)) continue;
|
||||
const element = rawElement;
|
||||
const type = normalizeString(element.type);
|
||||
const text = (type === "text_run" ? readElementTextPreservingWhitespace(element) : void 0) || (type === "text" ? readElementTextPreservingWhitespace(element) : void 0) || (type === "docs_link" || type === "link" ? readDocsLinkUrl(element) : void 0) || (type === "mention" || type === "mention_user" || type === "person" ? (() => {
|
||||
const userId = readMentionUserId(element);
|
||||
return userId ? readMentionDisplayText(element, userId) : void 0;
|
||||
})() : void 0) || readElementTextPreservingWhitespace(element) || void 0;
|
||||
if (type === "mention" || type === "mention_user" || type === "person") {
|
||||
const userId = readMentionUserId(element);
|
||||
if (userId) {
|
||||
const displayText = readMentionDisplayText(element, userId);
|
||||
const isBotMention = botIds.has(userId);
|
||||
mentions.push({
|
||||
userId,
|
||||
displayText,
|
||||
isBotMention
|
||||
});
|
||||
plainTextParts.push(displayText);
|
||||
if (!isBotMention) semanticTextParts.push(displayText);
|
||||
else botMentioned = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (type === "docs_link" || type === "link") {
|
||||
const rawUrl = readDocsLinkUrl(element);
|
||||
if (rawUrl) {
|
||||
plainTextParts.push(rawUrl);
|
||||
semanticTextParts.push(rawUrl);
|
||||
const linkedDocument = resolveCommentLinkedDocumentFromUrl({
|
||||
rawUrl,
|
||||
currentDocument: params.currentDocument
|
||||
});
|
||||
if (hasResolvedLinkedDocumentReference(linkedDocument)) {
|
||||
const key = [
|
||||
linkedDocument.rawUrl,
|
||||
linkedDocument.urlKind,
|
||||
linkedDocument.resolvedObjType,
|
||||
linkedDocument.resolvedObjToken,
|
||||
linkedDocument.wikiNodeToken
|
||||
].join(":");
|
||||
if (!linkedDocumentKeys.has(key)) {
|
||||
linkedDocumentKeys.add(key);
|
||||
linkedDocuments.push(linkedDocument);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
plainTextParts.push(text);
|
||||
semanticTextParts.push(text);
|
||||
}
|
||||
}
|
||||
return {
|
||||
plainText: normalizeCommentText(plainTextParts),
|
||||
semanticText: normalizeCommentSemanticText(semanticTextParts),
|
||||
mentions,
|
||||
linkedDocuments,
|
||||
botMentioned
|
||||
};
|
||||
}
|
||||
function extractReplyText(reply) {
|
||||
if (!reply || !isRecord$1(reply.content)) return;
|
||||
return parseCommentContentElements({ elements: Array.isArray(reply.content.elements) ? reply.content.elements : [] }).plainText;
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/accounts.ts
|
||||
var accounts_exports = /* @__PURE__ */ __exportAll({
|
||||
FeishuSecretRefUnavailableError: () => FeishuSecretRefUnavailableError,
|
||||
inspectFeishuCredentials: () => inspectFeishuCredentials,
|
||||
listEnabledFeishuAccounts: () => listEnabledFeishuAccounts,
|
||||
listFeishuAccountIds: () => listFeishuAccountIds,
|
||||
resolveDefaultFeishuAccountId: () => resolveDefaultFeishuAccountId,
|
||||
resolveDefaultFeishuAccountSelection: () => resolveDefaultFeishuAccountSelection,
|
||||
resolveFeishuAccount: () => resolveFeishuAccount,
|
||||
resolveFeishuCredentials: () => resolveFeishuCredentials,
|
||||
resolveFeishuRuntimeAccount: () => resolveFeishuRuntimeAccount
|
||||
});
|
||||
const { listAccountIds: listFeishuAccountIds, resolveDefaultAccountId } = createAccountListHelpers("feishu", {
|
||||
allowUnlistedDefaultAccount: true,
|
||||
hasImplicitDefaultAccount: (cfg) => {
|
||||
const feishu = cfg.channels?.feishu;
|
||||
return hasConfiguredAccountValue(feishu?.appId) && hasConfiguredAccountValue(feishu?.appSecret);
|
||||
}
|
||||
});
|
||||
function formatSecretRefLabel(ref) {
|
||||
return `${ref.source}:${ref.provider}:${ref.id}`;
|
||||
}
|
||||
var FeishuSecretRefUnavailableError = class extends Error {
|
||||
constructor(path, ref) {
|
||||
super(`${path}: unresolved SecretRef "${formatSecretRefLabel(ref)}". Resolve this command against an active gateway runtime snapshot before reading it.`);
|
||||
this.name = "FeishuSecretRefUnavailableError";
|
||||
this.path = path;
|
||||
}
|
||||
};
|
||||
function resolveFeishuSecretLike(params) {
|
||||
const asString = normalizeString(params.value);
|
||||
if (asString) return asString;
|
||||
const ref = coerceSecretRef(params.value);
|
||||
if (!ref) return;
|
||||
if (params.mode === "inspect") {
|
||||
if (params.allowEnvSecretRefRead && ref.source === "env") {
|
||||
const envValue = normalizeString(process.env[ref.id]);
|
||||
if (envValue) return envValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new FeishuSecretRefUnavailableError(params.path, ref);
|
||||
}
|
||||
function resolveFeishuBaseCredentials(cfg, mode) {
|
||||
const appId = resolveFeishuSecretLike({
|
||||
value: cfg?.appId,
|
||||
path: "channels.feishu.appId",
|
||||
mode,
|
||||
allowEnvSecretRefRead: true
|
||||
});
|
||||
const appSecret = resolveFeishuSecretLike({
|
||||
value: cfg?.appSecret,
|
||||
path: "channels.feishu.appSecret",
|
||||
mode,
|
||||
allowEnvSecretRefRead: true
|
||||
});
|
||||
if (!appId || !appSecret) return null;
|
||||
return {
|
||||
appId,
|
||||
appSecret,
|
||||
domain: cfg?.domain ?? "feishu"
|
||||
};
|
||||
}
|
||||
function resolveFeishuEventSecrets(cfg, mode) {
|
||||
return {
|
||||
encryptKey: (cfg?.connectionMode ?? "websocket") === "webhook" ? resolveFeishuSecretLike({
|
||||
value: cfg?.encryptKey,
|
||||
path: "channels.feishu.encryptKey",
|
||||
mode,
|
||||
allowEnvSecretRefRead: true
|
||||
}) : normalizeString(cfg?.encryptKey),
|
||||
verificationToken: resolveFeishuSecretLike({
|
||||
value: cfg?.verificationToken,
|
||||
path: "channels.feishu.verificationToken",
|
||||
mode,
|
||||
allowEnvSecretRefRead: true
|
||||
})
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolve the default account selection and its source.
|
||||
*/
|
||||
function resolveDefaultFeishuAccountSelection(cfg) {
|
||||
const preferred = normalizeOptionalAccountId((cfg.channels?.feishu)?.defaultAccount);
|
||||
if (preferred) return {
|
||||
accountId: preferred,
|
||||
source: "explicit-default"
|
||||
};
|
||||
const ids = listFeishuAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) return {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
source: "mapped-default"
|
||||
};
|
||||
return {
|
||||
accountId: ids[0] ?? DEFAULT_ACCOUNT_ID,
|
||||
source: "fallback"
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolve the default account ID.
|
||||
*/
|
||||
function resolveDefaultFeishuAccountId(cfg) {
|
||||
return resolveDefaultAccountId(cfg);
|
||||
}
|
||||
function resolveRawFeishuAccountConfig(accounts, accountId) {
|
||||
if (!accounts || typeof accounts !== "object") return;
|
||||
if (Object.hasOwn(accounts, accountId)) return accounts[accountId];
|
||||
const normalized = accountId.toLowerCase();
|
||||
const matchKey = Object.keys(accounts).find((key) => key.toLowerCase() === normalized);
|
||||
return matchKey ? accounts[matchKey] : void 0;
|
||||
}
|
||||
/**
|
||||
* Merge top-level config with account-specific config.
|
||||
* Account-specific fields override top-level fields.
|
||||
*/
|
||||
function mergeFeishuAccountConfig(cfg, accountId) {
|
||||
const feishuCfg = cfg.channels?.feishu;
|
||||
const accounts = feishuCfg?.accounts;
|
||||
const accountTools = resolveRawFeishuAccountConfig(accounts, accountId)?.tools;
|
||||
const merged = resolveMergedAccountConfig({
|
||||
channelConfig: feishuCfg,
|
||||
accounts,
|
||||
accountId,
|
||||
omitKeys: ["defaultAccount"],
|
||||
nestedObjectKeys: ["tools"]
|
||||
});
|
||||
const topTools = feishuCfg?.tools;
|
||||
if (merged.tools === void 0 && topTools !== void 0) return {
|
||||
...merged,
|
||||
tools: topTools
|
||||
};
|
||||
if (topTools?.bitable === false || topTools?.bitable === void 0 && topTools?.base === false) return {
|
||||
...merged,
|
||||
tools: {
|
||||
...merged.tools,
|
||||
bitable: false,
|
||||
base: false
|
||||
}
|
||||
};
|
||||
if (accountTools?.bitable === void 0 && accountTools?.base !== void 0) return {
|
||||
...merged,
|
||||
tools: {
|
||||
...merged.tools,
|
||||
bitable: accountTools.base,
|
||||
base: accountTools.base
|
||||
}
|
||||
};
|
||||
return merged;
|
||||
}
|
||||
function resolveFeishuCredentials(cfg, options) {
|
||||
const mode = options?.mode ?? (options?.allowUnresolvedSecretRef ? "inspect" : "strict");
|
||||
const base = resolveFeishuBaseCredentials(cfg, mode);
|
||||
if (!base) return null;
|
||||
const eventSecrets = resolveFeishuEventSecrets(cfg, mode);
|
||||
return {
|
||||
...base,
|
||||
...eventSecrets
|
||||
};
|
||||
}
|
||||
function inspectFeishuCredentials(cfg) {
|
||||
return resolveFeishuCredentials(cfg, { mode: "inspect" });
|
||||
}
|
||||
function buildResolvedFeishuAccount(params) {
|
||||
const hasExplicitAccountId = typeof params.accountId === "string" && params.accountId.trim() !== "";
|
||||
const defaultSelection = hasExplicitAccountId ? null : resolveDefaultFeishuAccountSelection(params.cfg);
|
||||
const accountId = hasExplicitAccountId ? normalizeAccountId(params.accountId) : defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID;
|
||||
const selectionSource = hasExplicitAccountId ? "explicit" : defaultSelection?.source ?? "fallback";
|
||||
const baseEnabled = (params.cfg.channels?.feishu)?.enabled !== false;
|
||||
const merged = mergeFeishuAccountConfig(params.cfg, accountId);
|
||||
const accountEnabled = merged.enabled !== false;
|
||||
const enabled = baseEnabled && accountEnabled;
|
||||
const baseCreds = resolveFeishuBaseCredentials(merged, params.baseMode);
|
||||
const eventSecrets = resolveFeishuEventSecrets(merged, params.eventSecretMode);
|
||||
const accountName = merged.name;
|
||||
return {
|
||||
accountId,
|
||||
selectionSource,
|
||||
enabled,
|
||||
configured: Boolean(baseCreds),
|
||||
name: typeof accountName === "string" ? accountName.trim() || void 0 : void 0,
|
||||
appId: baseCreds?.appId,
|
||||
appSecret: baseCreds?.appSecret,
|
||||
encryptKey: eventSecrets.encryptKey,
|
||||
verificationToken: eventSecrets.verificationToken,
|
||||
domain: baseCreds?.domain ?? "feishu",
|
||||
config: merged
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Resolve a read-only Feishu account snapshot for CLI/config surfaces.
|
||||
* Unresolved SecretRefs are treated as unavailable instead of throwing.
|
||||
*/
|
||||
function resolveFeishuAccount(params) {
|
||||
return buildResolvedFeishuAccount({
|
||||
...params,
|
||||
baseMode: "inspect",
|
||||
eventSecretMode: "inspect"
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Resolve a runtime Feishu account.
|
||||
* Required app credentials stay strict; event-only secrets can be required by callers.
|
||||
*/
|
||||
function resolveFeishuRuntimeAccount(params, options) {
|
||||
return buildResolvedFeishuAccount({
|
||||
...params,
|
||||
baseMode: "strict",
|
||||
eventSecretMode: options?.requireEventSecrets ? "strict" : "inspect"
|
||||
});
|
||||
}
|
||||
/**
|
||||
* List all enabled and configured accounts.
|
||||
*/
|
||||
function listEnabledFeishuAccounts(cfg) {
|
||||
return listFeishuAccountIds(cfg).map((accountId) => resolveFeishuAccount({
|
||||
cfg,
|
||||
accountId
|
||||
})).filter((account) => account.enabled && account.configured);
|
||||
}
|
||||
//#endregion
|
||||
export { normalizeCommentFileType as _, resolveDefaultFeishuAccountId as a, encodeQuery as c, isRecord$1 as d, normalizeString as f, buildFeishuCommentTarget as g, requestFeishuApi as h, listFeishuAccountIds as i, extractReplyText as l, readString as m, inspectFeishuCredentials as n, resolveFeishuAccount as o, parseCommentContentElements as p, listEnabledFeishuAccounts as r, resolveFeishuRuntimeAccount as s, accounts_exports as t, formatFeishuApiError as u, parseFeishuCommentTarget as v };
|
||||
Generated
Vendored
+2377
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+193
@@ -0,0 +1,193 @@
|
||||
import { finiteSecondsToTimerSafeMilliseconds } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { renderQrTerminal } from "openclaw/plugin-sdk/media-runtime";
|
||||
//#region extensions/feishu/src/app-registration.ts
|
||||
/**
|
||||
* Feishu app registration via OAuth device-code flow.
|
||||
*
|
||||
* Migrated from feishu-plugin-cli's `feishu-auth.ts` and `install-prompts.ts`.
|
||||
* Replaces axios with native fetch, removes inquirer/ora/chalk in favor of
|
||||
* the openclaw WizardPrompter surface.
|
||||
*/
|
||||
const FEISHU_ACCOUNTS_URL = "https://accounts.feishu.cn";
|
||||
const LARK_ACCOUNTS_URL = "https://accounts.larksuite.com";
|
||||
const REGISTRATION_PATH = "/oauth/v1/app/registration";
|
||||
const REQUEST_TIMEOUT_MS = 1e4;
|
||||
const DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS = 5;
|
||||
const DEFAULT_REGISTRATION_EXPIRE_SECONDS = 600;
|
||||
function accountsBaseUrl(domain) {
|
||||
return domain === "lark" ? LARK_ACCOUNTS_URL : FEISHU_ACCOUNTS_URL;
|
||||
}
|
||||
async function postRegistration(baseUrl, body) {
|
||||
return await fetchFeishuJson({
|
||||
url: `${baseUrl}${REGISTRATION_PATH}`,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(body).toString(),
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
||||
},
|
||||
auditContext: "feishu.app-registration.post"
|
||||
});
|
||||
}
|
||||
async function fetchFeishuJson(params) {
|
||||
const { response, release } = await fetchWithSsrFGuard({
|
||||
url: params.url,
|
||||
init: params.init,
|
||||
policy: { allowedHostnames: [new URL(params.url).hostname] },
|
||||
auditContext: params.auditContext
|
||||
});
|
||||
try {
|
||||
return await response.json();
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Step 1: Initialize registration and verify the environment supports
|
||||
* `client_secret` auth.
|
||||
*
|
||||
* @throws If the environment does not support `client_secret`.
|
||||
*/
|
||||
async function initAppRegistration(domain = "feishu") {
|
||||
if (!(await postRegistration(accountsBaseUrl(domain), { action: "init" })).supported_auth_methods?.includes("client_secret")) throw new Error("Current environment does not support client_secret auth method");
|
||||
}
|
||||
/**
|
||||
* Step 2: Begin the device-code flow. Returns a device code and a QR URL
|
||||
* that the user should scan with Feishu/Lark mobile app.
|
||||
*/
|
||||
async function beginAppRegistration(domain = "feishu") {
|
||||
const res = await postRegistration(accountsBaseUrl(domain), {
|
||||
action: "begin",
|
||||
archetype: "PersonalAgent",
|
||||
auth_method: "client_secret",
|
||||
request_user_info: "open_id"
|
||||
});
|
||||
const qrUrl = new URL(res.verification_uri_complete);
|
||||
qrUrl.searchParams.set("from", "oc_onboard");
|
||||
qrUrl.searchParams.set("tp", "ob_cli_app");
|
||||
return {
|
||||
deviceCode: res.device_code,
|
||||
qrUrl: qrUrl.toString(),
|
||||
userCode: res.user_code,
|
||||
interval: finiteSecondsToTimerSafeMilliseconds(res.interval) === void 0 ? DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS : res.interval,
|
||||
expireIn: finiteSecondsToTimerSafeMilliseconds(res.expire_in) === void 0 ? DEFAULT_REGISTRATION_EXPIRE_SECONDS : res.expire_in
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Step 3: Poll for authorization result until success, denial, expiry, or
|
||||
* timeout. Automatically handles domain switching when `tenant_brand` is
|
||||
* detected as "lark".
|
||||
*/
|
||||
async function pollAppRegistration(params) {
|
||||
const { deviceCode, expireIn, initialDomain = "feishu", abortSignal, tp } = params;
|
||||
let currentInterval = params.interval;
|
||||
let domain = initialDomain;
|
||||
let domainSwitched = false;
|
||||
const expireInMs = finiteSecondsToTimerSafeMilliseconds(expireIn) ?? finiteSecondsToTimerSafeMilliseconds(DEFAULT_REGISTRATION_EXPIRE_SECONDS) ?? REQUEST_TIMEOUT_MS;
|
||||
const deadline = Date.now() + expireInMs;
|
||||
while (Date.now() < deadline) {
|
||||
if (abortSignal?.aborted) return { status: "timeout" };
|
||||
const baseUrl = accountsBaseUrl(domain);
|
||||
let pollRes;
|
||||
try {
|
||||
pollRes = await postRegistration(baseUrl, {
|
||||
action: "poll",
|
||||
device_code: deviceCode,
|
||||
...tp ? { tp } : {}
|
||||
});
|
||||
} catch {
|
||||
await sleepRegistrationPollInterval(currentInterval);
|
||||
continue;
|
||||
}
|
||||
if (pollRes.user_info?.tenant_brand) {
|
||||
const isLark = pollRes.user_info.tenant_brand === "lark";
|
||||
if (!domainSwitched && isLark) {
|
||||
domain = "lark";
|
||||
domainSwitched = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (pollRes.client_id && pollRes.client_secret) return {
|
||||
status: "success",
|
||||
result: {
|
||||
appId: pollRes.client_id,
|
||||
appSecret: pollRes.client_secret,
|
||||
domain,
|
||||
openId: pollRes.user_info?.open_id
|
||||
}
|
||||
};
|
||||
if (pollRes.error) if (pollRes.error === "authorization_pending") {} else if (pollRes.error === "slow_down") currentInterval += 5;
|
||||
else if (pollRes.error === "access_denied") return { status: "access_denied" };
|
||||
else if (pollRes.error === "expired_token") return { status: "expired" };
|
||||
else return {
|
||||
status: "error",
|
||||
message: `${pollRes.error}: ${pollRes.error_description ?? "unknown"}`
|
||||
};
|
||||
await sleepRegistrationPollInterval(currentInterval);
|
||||
}
|
||||
return { status: "timeout" };
|
||||
}
|
||||
/**
|
||||
* Print QR code directly to stdout.
|
||||
*
|
||||
* QR codes must be printed without any surrounding box/border decoration,
|
||||
* otherwise the pattern is corrupted and cannot be scanned.
|
||||
*/
|
||||
async function printQrCode(url) {
|
||||
const output = await renderQrTerminal(url);
|
||||
process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
|
||||
}
|
||||
/**
|
||||
* Fetch the app owner's open_id using the application.v6.application.get API.
|
||||
*
|
||||
* Used during setup to auto-populate security policy allowlists.
|
||||
* Returns undefined on any failure (fail-open).
|
||||
*/
|
||||
async function getAppOwnerOpenId(params) {
|
||||
const baseUrl = params.domain === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn";
|
||||
try {
|
||||
const tokenData = await fetchFeishuJson({
|
||||
url: `${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
app_id: params.appId,
|
||||
app_secret: params.appSecret
|
||||
}),
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
||||
},
|
||||
auditContext: "feishu.app-registration.owner-token"
|
||||
});
|
||||
if (!tokenData.tenant_access_token) return;
|
||||
const appData = await fetchFeishuJson({
|
||||
url: `${baseUrl}/open-apis/application/v6/applications/${params.appId}?user_id_type=open_id`,
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.tenant_access_token}`,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
||||
},
|
||||
auditContext: "feishu.app-registration.owner-app"
|
||||
});
|
||||
if (appData.code !== 0) return;
|
||||
const app = appData.data?.app;
|
||||
const owner = app?.owner;
|
||||
return (owner?.owner_type ?? owner?.type) === 2 && owner?.owner_id ? owner.owner_id : app?.creator_id ?? owner?.owner_id;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
function sleepRegistrationPollInterval(intervalSeconds) {
|
||||
return sleep(finiteSecondsToTimerSafeMilliseconds(intervalSeconds) ?? finiteSecondsToTimerSafeMilliseconds(DEFAULT_REGISTRATION_POLL_INTERVAL_SECONDS) ?? REQUEST_TIMEOUT_MS);
|
||||
}
|
||||
//#endregion
|
||||
export { beginAppRegistration, getAppOwnerOpenId, initAppRegistration, pollAppRegistration, printQrCode };
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { transcribeFirstAudio as transcribeFirstAudio$1 } from "openclaw/plugin-sdk/media-runtime";
|
||||
//#region extensions/feishu/src/audio-preflight.runtime.ts
|
||||
async function transcribeFirstAudio(...args) {
|
||||
return await transcribeFirstAudio$1(...args);
|
||||
}
|
||||
//#endregion
|
||||
export { transcribeFirstAudio };
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
||||
//#region extensions/feishu/channel-entry.ts
|
||||
var channel_entry_default = defineBundledChannelEntry({
|
||||
id: "feishu",
|
||||
name: "Feishu",
|
||||
description: "Feishu/Lark channel plugin",
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
exportName: "feishuPlugin"
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./secret-contract-api.js",
|
||||
exportName: "channelSecrets"
|
||||
},
|
||||
runtime: {
|
||||
specifier: "./runtime-api.js",
|
||||
exportName: "setFeishuRuntime"
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { channel_entry_default as default };
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { t as feishuPlugin } from "./channel-uEJPBCR9.js";
|
||||
export { feishuPlugin };
|
||||
Generated
Vendored
+2635
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+692
@@ -0,0 +1,692 @@
|
||||
import { o as resolveFeishuAccount, s as resolveFeishuRuntimeAccount, v as parseFeishuCommentTarget } from "./accounts-Cfzht2Hc.js";
|
||||
import { h as listFeishuDirectoryPeers, m as listFeishuDirectoryGroups, o as buildFeishuPresentationCardElements } from "./channel-uEJPBCR9.js";
|
||||
import { r as createFeishuClient } from "./client-BwgAJuvo.js";
|
||||
import { c as getChatInfo, l as getChatMembers, r as cleanupAmbientCommentTypingReaction, t as deliverCommentThreadText, u as getFeishuMemberInfo } from "./drive-djUl8IRC.js";
|
||||
import { chunkTextForOutbound } from "./runtime-api.js";
|
||||
import { a as sendCardFeishu, c as sendStructuredCardFeishu, g as shouldSuppressFeishuTextForVoiceMedia, h as sendMediaFeishu, i as resolveFeishuCardTemplate, n as getMessageFeishu, o as sendMarkdownCardFeishu, s as sendMessageFeishu, t as editMessageFeishu } from "./send-B02jdUkY.js";
|
||||
import { t as probeFeishu } from "./probe-EyeJQb3C.js";
|
||||
import { interactiveReplyToPresentation, normalizeInteractiveReply, normalizeMessagePresentation, renderMessagePresentationFallbackText, resolveInteractiveTextFallback } from "openclaw/plugin-sdk/interactive-runtime";
|
||||
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import path from "node:path";
|
||||
import { attachChannelToResult, createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
|
||||
import { resolvePayloadMediaUrls, sendPayloadMediaSequenceAndFinalize, sendTextMediaPayload } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { statRegularFileSync } from "openclaw/plugin-sdk/security-runtime";
|
||||
//#region extensions/feishu/src/directory.ts
|
||||
async function listFeishuDirectoryPeersLive(params) {
|
||||
const account = resolveFeishuAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId
|
||||
});
|
||||
if (!account.configured) return listFeishuDirectoryPeers(params);
|
||||
try {
|
||||
const client = createFeishuClient(account);
|
||||
const peers = [];
|
||||
const limit = params.limit ?? 50;
|
||||
const response = await client.contact.user.list({ params: { page_size: Math.min(limit, 50) } });
|
||||
if (response.code !== 0) throw new Error(response.msg || `code ${response.code}`);
|
||||
const q = normalizeLowercaseStringOrEmpty(params.query);
|
||||
for (const user of response.data?.items ?? []) {
|
||||
if (user.open_id) {
|
||||
const name = user.name || "";
|
||||
if (!q || normalizeLowercaseStringOrEmpty(user.open_id).includes(q) || normalizeLowercaseStringOrEmpty(name).includes(q)) peers.push({
|
||||
kind: "user",
|
||||
id: user.open_id,
|
||||
name: name || void 0
|
||||
});
|
||||
}
|
||||
if (peers.length >= limit) break;
|
||||
}
|
||||
return peers;
|
||||
} catch (err) {
|
||||
if (params.fallbackToStatic === false) throw err instanceof Error ? err : /* @__PURE__ */ new Error("Feishu live peer lookup failed");
|
||||
return listFeishuDirectoryPeers(params);
|
||||
}
|
||||
}
|
||||
async function listFeishuDirectoryGroupsLive(params) {
|
||||
const account = resolveFeishuAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId
|
||||
});
|
||||
if (!account.configured) return listFeishuDirectoryGroups(params);
|
||||
try {
|
||||
const client = createFeishuClient(account);
|
||||
const groups = [];
|
||||
const limit = params.limit ?? 50;
|
||||
const response = await client.im.chat.list({ params: { page_size: Math.min(limit, 100) } });
|
||||
if (response.code !== 0) throw new Error(response.msg || `code ${response.code}`);
|
||||
const q = normalizeLowercaseStringOrEmpty(params.query);
|
||||
for (const chat of response.data?.items ?? []) {
|
||||
if (chat.chat_id) {
|
||||
const name = chat.name || "";
|
||||
if (!q || normalizeLowercaseStringOrEmpty(chat.chat_id).includes(q) || normalizeLowercaseStringOrEmpty(name).includes(q)) groups.push({
|
||||
kind: "group",
|
||||
id: chat.chat_id,
|
||||
name: name || void 0
|
||||
});
|
||||
}
|
||||
if (groups.length >= limit) break;
|
||||
}
|
||||
return groups;
|
||||
} catch (err) {
|
||||
if (params.fallbackToStatic === false) throw err instanceof Error ? err : /* @__PURE__ */ new Error("Feishu live group lookup failed");
|
||||
return listFeishuDirectoryGroups(params);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/outbound.ts
|
||||
const RENDERED_FEISHU_CARD = Symbol("openclaw.renderedFeishuCard");
|
||||
function normalizePossibleLocalImagePath(text) {
|
||||
const raw = text?.trim();
|
||||
if (!raw) return null;
|
||||
if (/\s/.test(raw)) return null;
|
||||
if (/^(https?:\/\/|data:|file:\/\/)/i.test(raw)) return null;
|
||||
const ext = normalizeLowercaseStringOrEmpty(path.extname(raw));
|
||||
if (![
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".gif",
|
||||
".webp",
|
||||
".bmp",
|
||||
".ico",
|
||||
".tiff"
|
||||
].includes(ext)) return null;
|
||||
if (!path.isAbsolute(raw)) return null;
|
||||
try {
|
||||
if (statRegularFileSync(raw).missing) return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
function shouldUseCard(text) {
|
||||
return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
|
||||
}
|
||||
function markRenderedFeishuCard(card) {
|
||||
Object.defineProperty(card, RENDERED_FEISHU_CARD, {
|
||||
value: true,
|
||||
enumerable: false
|
||||
});
|
||||
return card;
|
||||
}
|
||||
function escapeFeishuCardMarkdownText(text) {
|
||||
return text.replace(/[&<>]/g, (char) => {
|
||||
switch (char) {
|
||||
case "&": return "&";
|
||||
case "<": return "<";
|
||||
case ">": return ">";
|
||||
default: return char;
|
||||
}
|
||||
});
|
||||
}
|
||||
function resolveSafeFeishuButtonUrl(url) {
|
||||
const trimmed = typeof url === "string" ? url.trim() : "";
|
||||
if (!trimmed) return;
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return parsed.protocol === "https:" || parsed.protocol === "http:" ? trimmed : void 0;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
function sanitizeNativeFeishuButtonBehavior(behavior) {
|
||||
if (!isRecord(behavior)) return;
|
||||
if (behavior.type === "open_url") {
|
||||
const safeUrl = resolveSafeFeishuButtonUrl(behavior.default_url) ?? resolveSafeFeishuButtonUrl(behavior.url);
|
||||
return safeUrl ? {
|
||||
type: "open_url",
|
||||
default_url: safeUrl
|
||||
} : void 0;
|
||||
}
|
||||
if (behavior.type === "callback" && isRecord(behavior.value) && behavior.value.oc === "ocf1") return {
|
||||
type: "callback",
|
||||
value: behavior.value
|
||||
};
|
||||
}
|
||||
function sanitizeNativeFeishuCardButton(button) {
|
||||
if (!isRecord(button)) return;
|
||||
const text = isRecord(button.text) && typeof button.text.content === "string" ? button.text.content : void 0;
|
||||
if (!text?.trim()) return;
|
||||
const style = button.type === "danger" ? "danger" : button.type === "primary" || button.type === "success" ? "primary" : void 0;
|
||||
const behaviors = Array.isArray(button.behaviors) ? button.behaviors.map((behavior) => sanitizeNativeFeishuButtonBehavior(behavior)).filter((behavior) => Boolean(behavior)) : [];
|
||||
const rootSafeUrl = resolveSafeFeishuButtonUrl(button.url);
|
||||
if (rootSafeUrl) behaviors.push({
|
||||
type: "open_url",
|
||||
default_url: rootSafeUrl
|
||||
});
|
||||
if (isRecord(button.value) && button.value.oc === "ocf1") behaviors.push({
|
||||
type: "callback",
|
||||
value: button.value
|
||||
});
|
||||
if (behaviors.length === 0) return;
|
||||
return {
|
||||
tag: "button",
|
||||
text: {
|
||||
tag: "plain_text",
|
||||
content: text
|
||||
},
|
||||
type: style === "danger" ? "danger" : style === "primary" || style === "success" ? "primary" : "default",
|
||||
behaviors
|
||||
};
|
||||
}
|
||||
function sanitizeNativeFeishuCardElements(element) {
|
||||
if (!isRecord(element) || typeof element.tag !== "string") return [];
|
||||
if (element.tag === "hr") return [{ tag: "hr" }];
|
||||
if (element.tag === "markdown" && typeof element.content === "string") return [{
|
||||
tag: "markdown",
|
||||
content: escapeFeishuCardMarkdownText(element.content)
|
||||
}];
|
||||
if (element.tag === "button") {
|
||||
const button = sanitizeNativeFeishuCardButton(element);
|
||||
return button ? [button] : [];
|
||||
}
|
||||
if (element.tag === "action" && Array.isArray(element.actions)) return element.actions.map((action) => sanitizeNativeFeishuCardButton(action)).filter((action) => Boolean(action));
|
||||
return [];
|
||||
}
|
||||
function sanitizeNativeFeishuCard(card) {
|
||||
const body = isRecord(card.body) ? card.body : void 0;
|
||||
const elements = (Array.isArray(body?.elements) ? body.elements : []).flatMap((element) => sanitizeNativeFeishuCardElements(element)).filter((element) => Boolean(element));
|
||||
if (elements.length === 0) return;
|
||||
const header = isRecord(card.header) ? card.header : void 0;
|
||||
const title = isRecord(header?.title) && typeof header.title.content === "string" ? header.title.content : void 0;
|
||||
return markRenderedFeishuCard({
|
||||
schema: "2.0",
|
||||
config: { width_mode: "fill" },
|
||||
...title?.trim() ? { header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: title
|
||||
},
|
||||
template: resolveFeishuCardTemplate(typeof header?.template === "string" ? header.template : void 0) ?? "blue"
|
||||
} } : {},
|
||||
body: { elements }
|
||||
});
|
||||
}
|
||||
function readNativeFeishuCard(payload) {
|
||||
const feishuData = payload.channelData?.feishu;
|
||||
if (!isRecord(feishuData)) return;
|
||||
const card = feishuData.card ?? feishuData.interactiveCard;
|
||||
if (!isRecord(card)) return;
|
||||
if (card[RENDERED_FEISHU_CARD] === true) return card;
|
||||
return sanitizeNativeFeishuCard(card);
|
||||
}
|
||||
function buildFeishuPayloadCard(params) {
|
||||
const nativeCard = readNativeFeishuCard(params.payload);
|
||||
if (nativeCard) return nativeCard;
|
||||
const interactive = normalizeInteractiveReply(params.payload.interactive);
|
||||
const presentation = normalizeMessagePresentation(params.payload.presentation) ?? (interactive ? interactiveReplyToPresentation(interactive) : void 0);
|
||||
if (!presentation && !interactive) return;
|
||||
const text = resolveInteractiveTextFallback({
|
||||
text: params.text ?? params.payload.text,
|
||||
interactive
|
||||
});
|
||||
const elements = presentation ? buildFeishuPresentationCardElements({
|
||||
presentation,
|
||||
fallbackText: text
|
||||
}) : [{
|
||||
tag: "markdown",
|
||||
content: renderMessagePresentationFallbackText({
|
||||
text,
|
||||
presentation
|
||||
})
|
||||
}];
|
||||
const identityTitle = params.identity ? params.identity.emoji ? `${params.identity.emoji} ${params.identity.name ?? ""}`.trim() : params.identity.name ?? "" : "";
|
||||
const title = presentation?.title ?? identityTitle;
|
||||
const template = resolveFeishuCardTemplate(presentation?.tone === "danger" ? "red" : presentation?.tone === "warning" ? "orange" : presentation?.tone === "success" ? "green" : "blue");
|
||||
return markRenderedFeishuCard({
|
||||
schema: "2.0",
|
||||
config: { width_mode: "fill" },
|
||||
...title ? { header: {
|
||||
title: {
|
||||
tag: "plain_text",
|
||||
content: title
|
||||
},
|
||||
template: template ?? "blue"
|
||||
} } : {},
|
||||
body: { elements }
|
||||
});
|
||||
}
|
||||
function renderFeishuPresentationPayload({ payload, presentation, ctx }) {
|
||||
const card = buildFeishuPayloadCard({
|
||||
payload,
|
||||
text: payload.text,
|
||||
identity: ctx.identity
|
||||
});
|
||||
if (!card) return null;
|
||||
const existingFeishuData = isRecord(payload.channelData?.feishu) ? payload.channelData.feishu : void 0;
|
||||
return {
|
||||
...payload,
|
||||
text: renderMessagePresentationFallbackText({
|
||||
text: payload.text,
|
||||
presentation
|
||||
}),
|
||||
channelData: {
|
||||
...payload.channelData,
|
||||
feishu: {
|
||||
...existingFeishuData,
|
||||
card
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function resolveReplyToMessageId(params) {
|
||||
const replyToId = params.replyToId?.trim();
|
||||
if (replyToId) return replyToId;
|
||||
if (params.threadId == null) return;
|
||||
return String(params.threadId).trim() || void 0;
|
||||
}
|
||||
function resolveFeishuMediaReplyMode(params) {
|
||||
const trimmedReplyToId = params.replyToId?.trim() || void 0;
|
||||
return {
|
||||
replyToMessageId: resolveReplyToMessageId(params),
|
||||
replyInThread: params.threadId != null && !trimmedReplyToId
|
||||
};
|
||||
}
|
||||
async function sendCommentThreadReply(params) {
|
||||
const target = parseFeishuCommentTarget(params.to);
|
||||
if (!target) return null;
|
||||
const client = createFeishuClient(resolveFeishuAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId
|
||||
}));
|
||||
const replyId = params.replyId?.trim();
|
||||
try {
|
||||
const result = await deliverCommentThreadText(client, {
|
||||
file_token: target.fileToken,
|
||||
file_type: target.fileType,
|
||||
comment_id: target.commentId,
|
||||
content: params.text
|
||||
});
|
||||
return {
|
||||
messageId: typeof result.reply_id === "string" && result.reply_id || typeof result.comment_id === "string" && result.comment_id || "",
|
||||
chatId: target.commentId,
|
||||
result
|
||||
};
|
||||
} finally {
|
||||
if (replyId) cleanupAmbientCommentTypingReaction({
|
||||
client,
|
||||
deliveryContext: {
|
||||
channel: "feishu",
|
||||
to: params.to,
|
||||
threadId: replyId
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
async function sendOutboundText(params) {
|
||||
const { cfg, to, text, accountId, replyToMessageId, replyInThread } = params;
|
||||
const commentResult = await sendCommentThreadReply({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
replyId: replyToMessageId,
|
||||
accountId
|
||||
});
|
||||
if (commentResult) return commentResult;
|
||||
const renderMode = resolveFeishuAccount({
|
||||
cfg,
|
||||
accountId
|
||||
}).config?.renderMode ?? "auto";
|
||||
if (renderMode === "card" || renderMode === "auto" && shouldUseCard(text)) return sendMarkdownCardFeishu({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
return sendMessageFeishu({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
}
|
||||
const feishuOutbound = {
|
||||
deliveryMode: "direct",
|
||||
chunker: chunkTextForOutbound,
|
||||
chunkerMode: "markdown",
|
||||
textChunkLimit: 4e3,
|
||||
presentationCapabilities: {
|
||||
supported: true,
|
||||
buttons: true,
|
||||
selects: false,
|
||||
context: true,
|
||||
divider: true,
|
||||
limits: {
|
||||
actions: {
|
||||
maxActions: 20,
|
||||
maxActionsPerRow: 5,
|
||||
maxLabelLength: 40,
|
||||
maxValueBytes: 1024
|
||||
},
|
||||
text: {
|
||||
maxLength: 4e3,
|
||||
encoding: "characters",
|
||||
markdownDialect: "markdown"
|
||||
}
|
||||
}
|
||||
},
|
||||
renderPresentation: renderFeishuPresentationPayload,
|
||||
sendPayload: async (ctx) => {
|
||||
const card = buildFeishuPayloadCard({
|
||||
payload: ctx.payload,
|
||||
text: ctx.text,
|
||||
identity: ctx.identity
|
||||
});
|
||||
if (!card) return await sendTextMediaPayload({
|
||||
channel: "feishu",
|
||||
ctx,
|
||||
adapter: feishuOutbound
|
||||
});
|
||||
const replyToMessageId = resolveReplyToMessageId({
|
||||
replyToId: ctx.replyToId,
|
||||
threadId: ctx.threadId
|
||||
});
|
||||
if (parseFeishuCommentTarget(ctx.to)) return await sendTextMediaPayload({
|
||||
channel: "feishu",
|
||||
ctx: {
|
||||
...ctx,
|
||||
payload: {
|
||||
...ctx.payload,
|
||||
text: renderMessagePresentationFallbackText({
|
||||
text: ctx.payload.text,
|
||||
presentation: normalizeMessagePresentation(ctx.payload.presentation) ?? (() => {
|
||||
const interactive = normalizeInteractiveReply(ctx.payload.interactive);
|
||||
return interactive ? interactiveReplyToPresentation(interactive) : void 0;
|
||||
})()
|
||||
}),
|
||||
interactive: void 0,
|
||||
presentation: void 0,
|
||||
channelData: void 0
|
||||
}
|
||||
},
|
||||
adapter: feishuOutbound
|
||||
});
|
||||
const mediaUrls = normalizeStringEntries(resolvePayloadMediaUrls(ctx.payload));
|
||||
return attachChannelToResult("feishu", await sendPayloadMediaSequenceAndFinalize({
|
||||
text: ctx.payload.text ?? "",
|
||||
mediaUrls,
|
||||
send: async ({ mediaUrl }) => await sendMediaFeishu({
|
||||
cfg: ctx.cfg,
|
||||
to: ctx.to,
|
||||
mediaUrl,
|
||||
accountId: ctx.accountId ?? void 0,
|
||||
mediaLocalRoots: ctx.mediaLocalRoots,
|
||||
replyToMessageId,
|
||||
...ctx.payload.audioAsVoice === true || ctx.audioAsVoice === true ? { audioAsVoice: true } : {}
|
||||
}),
|
||||
finalize: async () => await sendCardFeishu({
|
||||
cfg: ctx.cfg,
|
||||
to: ctx.to,
|
||||
card,
|
||||
replyToMessageId,
|
||||
replyInThread: ctx.threadId != null && !ctx.replyToId,
|
||||
accountId: ctx.accountId ?? void 0
|
||||
})
|
||||
}));
|
||||
},
|
||||
...createAttachedChannelResultAdapter({
|
||||
channel: "feishu",
|
||||
sendText: async ({ cfg, to, text, accountId, replyToId, threadId, mediaLocalRoots, identity }) => {
|
||||
const { replyToMessageId, replyInThread } = resolveFeishuMediaReplyMode({
|
||||
replyToId,
|
||||
threadId
|
||||
});
|
||||
const localImagePath = normalizePossibleLocalImagePath(text);
|
||||
if (localImagePath) try {
|
||||
return await sendMediaFeishu({
|
||||
cfg,
|
||||
to,
|
||||
mediaUrl: localImagePath,
|
||||
accountId: accountId ?? void 0,
|
||||
replyToMessageId,
|
||||
replyInThread,
|
||||
mediaLocalRoots
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[feishu] local image path auto-send failed:`, err);
|
||||
}
|
||||
if (parseFeishuCommentTarget(to)) return await sendOutboundText({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId: accountId ?? void 0,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
const renderMode = resolveFeishuAccount({
|
||||
cfg,
|
||||
accountId: accountId ?? void 0
|
||||
}).config?.renderMode ?? "auto";
|
||||
if (renderMode === "card" || renderMode === "auto" && shouldUseCard(text)) {
|
||||
const header = identity ? {
|
||||
title: identity.emoji ? `${identity.emoji} ${identity.name ?? ""}`.trim() : identity.name ?? "",
|
||||
template: "blue"
|
||||
} : void 0;
|
||||
return await sendStructuredCardFeishu({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
replyToMessageId,
|
||||
replyInThread,
|
||||
accountId: accountId ?? void 0,
|
||||
header: header?.title ? header : void 0
|
||||
});
|
||||
}
|
||||
return await sendOutboundText({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId: accountId ?? void 0,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
},
|
||||
sendMedia: async ({ cfg, to, text, mediaUrl, audioAsVoice, accountId, mediaLocalRoots, replyToId, threadId }) => {
|
||||
const { replyToMessageId, replyInThread } = resolveFeishuMediaReplyMode({
|
||||
replyToId,
|
||||
threadId
|
||||
});
|
||||
if (parseFeishuCommentTarget(to)) return await sendOutboundText({
|
||||
cfg,
|
||||
to,
|
||||
text: [text?.trim(), mediaUrl?.trim()].filter(Boolean).join("\n\n") || mediaUrl || text || "",
|
||||
accountId: accountId ?? void 0,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
const suppressTextForVoiceMedia = mediaUrl !== void 0 && shouldSuppressFeishuTextForVoiceMedia({
|
||||
mediaUrl,
|
||||
audioAsVoice
|
||||
});
|
||||
if (text?.trim() && !suppressTextForVoiceMedia) await sendOutboundText({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId: accountId ?? void 0,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
if (mediaUrl) try {
|
||||
const result = await sendMediaFeishu({
|
||||
cfg,
|
||||
to,
|
||||
mediaUrl,
|
||||
accountId: accountId ?? void 0,
|
||||
mediaLocalRoots,
|
||||
replyToMessageId,
|
||||
replyInThread,
|
||||
...audioAsVoice === true ? { audioAsVoice: true } : {}
|
||||
});
|
||||
if (result.voiceIntentDegradedToFile && text?.trim()) await sendOutboundText({
|
||||
cfg,
|
||||
to,
|
||||
text,
|
||||
accountId: accountId ?? void 0,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
||||
return await sendOutboundText({
|
||||
cfg,
|
||||
to,
|
||||
text: [text?.trim(), `📎 ${mediaUrl}`].filter(Boolean).join("\n\n"),
|
||||
accountId: accountId ?? void 0,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
}
|
||||
return await sendOutboundText({
|
||||
cfg,
|
||||
to,
|
||||
text: text ?? "",
|
||||
accountId: accountId ?? void 0,
|
||||
replyToMessageId,
|
||||
replyInThread
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/pins.ts
|
||||
function assertFeishuPinApiSuccess(response, action) {
|
||||
if (response.code !== 0) throw new Error(`Feishu ${action} failed: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
function normalizePin(pin) {
|
||||
return {
|
||||
messageId: pin.message_id,
|
||||
chatId: pin.chat_id,
|
||||
operatorId: pin.operator_id,
|
||||
operatorIdType: pin.operator_id_type,
|
||||
createTime: pin.create_time
|
||||
};
|
||||
}
|
||||
async function createPinFeishu(params) {
|
||||
const account = resolveFeishuRuntimeAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId
|
||||
});
|
||||
if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
const response = await createFeishuClient(account).im.pin.create({ data: { message_id: params.messageId } });
|
||||
assertFeishuPinApiSuccess(response, "pin create");
|
||||
return response.data?.pin ? normalizePin(response.data.pin) : null;
|
||||
}
|
||||
async function removePinFeishu(params) {
|
||||
const account = resolveFeishuRuntimeAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId
|
||||
});
|
||||
if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
assertFeishuPinApiSuccess(await createFeishuClient(account).im.pin.delete({ path: { message_id: params.messageId } }), "pin delete");
|
||||
}
|
||||
async function listPinsFeishu(params) {
|
||||
const account = resolveFeishuRuntimeAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId
|
||||
});
|
||||
if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
const response = await createFeishuClient(account).im.pin.list({ params: {
|
||||
chat_id: params.chatId,
|
||||
...params.startTime ? { start_time: params.startTime } : {},
|
||||
...params.endTime ? { end_time: params.endTime } : {},
|
||||
...typeof params.pageSize === "number" ? { page_size: Math.max(1, Math.min(100, Math.floor(params.pageSize))) } : {},
|
||||
...params.pageToken ? { page_token: params.pageToken } : {}
|
||||
} });
|
||||
assertFeishuPinApiSuccess(response, "pin list");
|
||||
return {
|
||||
chatId: params.chatId,
|
||||
pins: (response.data?.items ?? []).map(normalizePin),
|
||||
hasMore: response.data?.has_more === true,
|
||||
pageToken: response.data?.page_token
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/reactions.ts
|
||||
function resolveConfiguredFeishuClient(params) {
|
||||
const account = resolveFeishuRuntimeAccount(params);
|
||||
if (!account.configured) throw new Error(`Feishu account "${account.accountId}" not configured`);
|
||||
return createFeishuClient(account);
|
||||
}
|
||||
function assertFeishuReactionApiSuccess(response, action) {
|
||||
if (response.code !== 0) throw new Error(`Feishu ${action} failed: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
/**
|
||||
* Add a reaction (emoji) to a message.
|
||||
* @param emojiType - Feishu emoji type, e.g., "SMILE", "THUMBSUP", "HEART"
|
||||
* @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
|
||||
*/
|
||||
async function addReactionFeishu(params) {
|
||||
const { cfg, messageId, emojiType, accountId } = params;
|
||||
const response = await resolveConfiguredFeishuClient({
|
||||
cfg,
|
||||
accountId
|
||||
}).im.messageReaction.create({
|
||||
path: { message_id: messageId },
|
||||
data: { reaction_type: { emoji_type: emojiType } }
|
||||
});
|
||||
assertFeishuReactionApiSuccess(response, "add reaction");
|
||||
const reactionId = response.data?.reaction_id;
|
||||
if (!reactionId) throw new Error("Feishu add reaction failed: no reaction_id returned");
|
||||
return { reactionId };
|
||||
}
|
||||
/**
|
||||
* Remove a reaction from a message.
|
||||
*/
|
||||
async function removeReactionFeishu(params) {
|
||||
const { cfg, messageId, reactionId, accountId } = params;
|
||||
assertFeishuReactionApiSuccess(await resolveConfiguredFeishuClient({
|
||||
cfg,
|
||||
accountId
|
||||
}).im.messageReaction.delete({ path: {
|
||||
message_id: messageId,
|
||||
reaction_id: reactionId
|
||||
} }), "remove reaction");
|
||||
}
|
||||
/**
|
||||
* List all reactions for a message.
|
||||
*/
|
||||
async function listReactionsFeishu(params) {
|
||||
const { cfg, messageId, emojiType, accountId } = params;
|
||||
const response = await resolveConfiguredFeishuClient({
|
||||
cfg,
|
||||
accountId
|
||||
}).im.messageReaction.list({
|
||||
path: { message_id: messageId },
|
||||
params: emojiType ? { reaction_type: emojiType } : void 0
|
||||
});
|
||||
assertFeishuReactionApiSuccess(response, "list reactions");
|
||||
return (response.data?.items ?? []).map((item) => ({
|
||||
reactionId: item.reaction_id ?? "",
|
||||
emojiType: item.reaction_type?.emoji_type ?? "",
|
||||
operatorType: item.operator_type === "app" ? "app" : "user",
|
||||
operatorId: item.operator_id?.open_id ?? item.operator_id?.user_id ?? item.operator_id?.union_id ?? ""
|
||||
}));
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/channel.runtime.ts
|
||||
const feishuChannelRuntime = {
|
||||
listFeishuDirectoryGroupsLive,
|
||||
listFeishuDirectoryPeersLive,
|
||||
feishuOutbound: { ...feishuOutbound },
|
||||
createPinFeishu,
|
||||
listPinsFeishu,
|
||||
removePinFeishu,
|
||||
probeFeishu,
|
||||
addReactionFeishu,
|
||||
listReactionsFeishu,
|
||||
removeReactionFeishu,
|
||||
getChatInfo,
|
||||
getChatMembers,
|
||||
getFeishuMemberInfo,
|
||||
editMessageFeishu,
|
||||
getMessageFeishu,
|
||||
sendCardFeishu,
|
||||
sendMessageFeishu
|
||||
};
|
||||
//#endregion
|
||||
export { feishuChannelRuntime };
|
||||
Generated
Vendored
+163
@@ -0,0 +1,163 @@
|
||||
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
||||
import { createRequire } from "node:module";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import * as Lark from "@larksuiteoapi/node-sdk";
|
||||
import { readPluginPackageVersion, resolveAmbientNodeProxyAgent } from "openclaw/plugin-sdk/extension-shared";
|
||||
//#region extensions/feishu/src/client-timeout.ts
|
||||
/** Default HTTP timeout for Feishu API requests (30 seconds). */
|
||||
const FEISHU_HTTP_TIMEOUT_MS = 3e4;
|
||||
const FEISHU_HTTP_TIMEOUT_MAX_MS = 3e5;
|
||||
const FEISHU_HTTP_TIMEOUT_ENV_VAR = "OPENCLAW_FEISHU_HTTP_TIMEOUT_MS";
|
||||
function resolveConfiguredHttpTimeoutMs(creds) {
|
||||
const clampTimeout = (value) => {
|
||||
return Math.min(Math.max(Math.floor(value), 1), FEISHU_HTTP_TIMEOUT_MAX_MS);
|
||||
};
|
||||
const fromDirectField = creds.httpTimeoutMs;
|
||||
if (typeof fromDirectField === "number" && Number.isFinite(fromDirectField) && fromDirectField > 0) return clampTimeout(fromDirectField);
|
||||
const envRaw = process.env[FEISHU_HTTP_TIMEOUT_ENV_VAR];
|
||||
if (envRaw) {
|
||||
const envValue = parseStrictPositiveInteger(envRaw);
|
||||
if (envValue !== void 0) return clampTimeout(envValue);
|
||||
}
|
||||
const timeout = creds.config?.httpTimeoutMs;
|
||||
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) return FEISHU_HTTP_TIMEOUT_MS;
|
||||
return clampTimeout(timeout);
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/client.ts
|
||||
var client_exports = /* @__PURE__ */ __exportAll({
|
||||
FEISHU_HTTP_TIMEOUT_ENV_VAR: () => FEISHU_HTTP_TIMEOUT_ENV_VAR,
|
||||
FEISHU_HTTP_TIMEOUT_MAX_MS: () => FEISHU_HTTP_TIMEOUT_MAX_MS,
|
||||
FEISHU_HTTP_TIMEOUT_MS: () => FEISHU_HTTP_TIMEOUT_MS,
|
||||
FEISHU_USER_AGENT: () => FEISHU_USER_AGENT,
|
||||
createEventDispatcher: () => createEventDispatcher,
|
||||
createFeishuClient: () => createFeishuClient,
|
||||
createFeishuWSClient: () => createFeishuWSClient,
|
||||
getFeishuUserAgent: () => getFeishuUserAgent,
|
||||
pluginVersion: () => pluginVersion
|
||||
});
|
||||
const pluginVersion = readPluginPackageVersion({ require: createRequire(import.meta.url) });
|
||||
const FEISHU_USER_AGENT = `openclaw-feishu-builtin/${pluginVersion}/${process.platform}`;
|
||||
const FEISHU_WS_CONFIG = {
|
||||
PingInterval: 30,
|
||||
PingTimeout: 3
|
||||
};
|
||||
/** User-Agent header value for all Feishu API requests. */
|
||||
function getFeishuUserAgent() {
|
||||
return FEISHU_USER_AGENT;
|
||||
}
|
||||
let feishuClientSdk = {
|
||||
AppType: Lark.AppType,
|
||||
Client: Lark.Client,
|
||||
defaultHttpInstance: Lark.defaultHttpInstance,
|
||||
Domain: Lark.Domain,
|
||||
EventDispatcher: Lark.EventDispatcher,
|
||||
LoggerLevel: Lark.LoggerLevel,
|
||||
WSClient: Lark.WSClient
|
||||
};
|
||||
function setRequestUserAgent(req) {
|
||||
const request = req;
|
||||
const headers = request.headers;
|
||||
if (!headers) {
|
||||
request.headers = { "User-Agent": getFeishuUserAgent() };
|
||||
return req;
|
||||
}
|
||||
const maybeAxiosHeaders = headers;
|
||||
if (typeof maybeAxiosHeaders.set === "function") {
|
||||
maybeAxiosHeaders.set("User-Agent", getFeishuUserAgent());
|
||||
return req;
|
||||
}
|
||||
headers["User-Agent"] = getFeishuUserAgent();
|
||||
return req;
|
||||
}
|
||||
Lark.defaultHttpInstance.interceptors?.request?.use(setRequestUserAgent);
|
||||
async function getWsProxyAgent() {
|
||||
return resolveAmbientNodeProxyAgent();
|
||||
}
|
||||
const clientCache = /* @__PURE__ */ new Map();
|
||||
function resolveDomain(domain) {
|
||||
if (domain === "lark") return feishuClientSdk.Domain.Lark;
|
||||
if (domain === "feishu" || !domain) return feishuClientSdk.Domain.Feishu;
|
||||
return domain.replace(/\/+$/, "");
|
||||
}
|
||||
/**
|
||||
* Create an HTTP instance that delegates to the Lark SDK's default instance
|
||||
* but injects a default request timeout and User-Agent header to prevent
|
||||
* indefinite hangs and set a standardized User-Agent per OAPI best practices.
|
||||
*/
|
||||
function createTimeoutHttpInstance(defaultTimeoutMs) {
|
||||
const base = feishuClientSdk.defaultHttpInstance;
|
||||
function injectTimeout(opts) {
|
||||
return {
|
||||
timeout: defaultTimeoutMs,
|
||||
...opts
|
||||
};
|
||||
}
|
||||
return {
|
||||
request: (opts) => base.request(injectTimeout(opts)),
|
||||
get: (url, opts) => base.get(url, injectTimeout(opts)),
|
||||
post: (url, data, opts) => base.post(url, data, injectTimeout(opts)),
|
||||
put: (url, data, opts) => base.put(url, data, injectTimeout(opts)),
|
||||
patch: (url, data, opts) => base.patch(url, data, injectTimeout(opts)),
|
||||
delete: (url, opts) => base.delete(url, injectTimeout(opts)),
|
||||
head: (url, opts) => base.head(url, injectTimeout(opts)),
|
||||
options: (url, opts) => base.options(url, injectTimeout(opts))
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Create or get a cached Feishu client for an account.
|
||||
* Accepts any object with appId, appSecret, and optional domain/accountId.
|
||||
*/
|
||||
function createFeishuClient(creds) {
|
||||
const { accountId = "default", appId, appSecret, domain } = creds;
|
||||
const defaultHttpTimeoutMs = resolveConfiguredHttpTimeoutMs(creds);
|
||||
if (!appId || !appSecret) throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
||||
const cached = clientCache.get(accountId);
|
||||
if (cached && cached.config.appId === appId && cached.config.appSecret === appSecret && cached.config.domain === domain && cached.config.httpTimeoutMs === defaultHttpTimeoutMs) return cached.client;
|
||||
const client = new feishuClientSdk.Client({
|
||||
appId,
|
||||
appSecret,
|
||||
appType: feishuClientSdk.AppType.SelfBuild,
|
||||
domain: resolveDomain(domain),
|
||||
httpInstance: createTimeoutHttpInstance(defaultHttpTimeoutMs)
|
||||
});
|
||||
clientCache.set(accountId, {
|
||||
client,
|
||||
config: {
|
||||
appId,
|
||||
appSecret,
|
||||
domain,
|
||||
httpTimeoutMs: defaultHttpTimeoutMs
|
||||
}
|
||||
});
|
||||
return client;
|
||||
}
|
||||
/**
|
||||
* Create a Feishu WebSocket client for an account.
|
||||
* Note: WSClient is not cached since each call creates a new connection.
|
||||
*/
|
||||
async function createFeishuWSClient(account, callbacks = {}) {
|
||||
const { accountId, appId, appSecret, domain } = account;
|
||||
if (!appId || !appSecret) throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
||||
const agent = await getWsProxyAgent();
|
||||
return new feishuClientSdk.WSClient({
|
||||
appId,
|
||||
appSecret,
|
||||
domain: resolveDomain(domain),
|
||||
...callbacks,
|
||||
loggerLevel: feishuClientSdk.LoggerLevel.info,
|
||||
wsConfig: FEISHU_WS_CONFIG,
|
||||
...agent ? { agent } : {}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Create an event dispatcher for an account.
|
||||
*/
|
||||
function createEventDispatcher(account) {
|
||||
return new feishuClientSdk.EventDispatcher({
|
||||
encryptKey: account.encryptKey,
|
||||
verificationToken: account.verificationToken
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
export { getFeishuUserAgent as a, createFeishuWSClient as i, createEventDispatcher as n, createFeishuClient as r, client_exports as t };
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { a as parseFeishuTargetId, i as parseFeishuDirectConversationId, r as parseFeishuConversationId, t as buildFeishuConversationId } from "./conversation-id-DuL575sn.js";
|
||||
import { t as messageActionTargetAliases } from "./security-audit-BIeA3W3Q.js";
|
||||
import { t as collectFeishuSecurityAuditFindings } from "./security-audit-shared-BIHeF-S_.js";
|
||||
export { buildFeishuConversationId, collectFeishuSecurityAuditFindings, messageActionTargetAliases, parseFeishuConversationId, parseFeishuDirectConversationId, parseFeishuTargetId };
|
||||
Generated
Vendored
+139
@@ -0,0 +1,139 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
//#region extensions/feishu/src/conversation-id.ts
|
||||
function normalizeText(value) {
|
||||
if (typeof value !== "string") return;
|
||||
return value.trim() || void 0;
|
||||
}
|
||||
function buildFeishuConversationId(params) {
|
||||
const chatId = normalizeText(params.chatId) ?? "unknown";
|
||||
const senderOpenId = normalizeText(params.senderOpenId);
|
||||
const topicId = normalizeText(params.topicId);
|
||||
switch (params.scope) {
|
||||
case "group_sender": return senderOpenId ? `${chatId}:sender:${senderOpenId}` : chatId;
|
||||
case "group_topic": return topicId ? `${chatId}:topic:${topicId}` : chatId;
|
||||
case "group_topic_sender":
|
||||
if (topicId && senderOpenId) return `${chatId}:topic:${topicId}:sender:${senderOpenId}`;
|
||||
if (topicId) return `${chatId}:topic:${topicId}`;
|
||||
return senderOpenId ? `${chatId}:sender:${senderOpenId}` : chatId;
|
||||
default: return chatId;
|
||||
}
|
||||
}
|
||||
function parseFeishuTargetId(raw) {
|
||||
const target = normalizeText(raw);
|
||||
if (!target) return;
|
||||
const withoutProvider = target.replace(/^(feishu|lark):/i, "").trim();
|
||||
if (!withoutProvider) return;
|
||||
const lowered = normalizeLowercaseStringOrEmpty(withoutProvider);
|
||||
for (const prefix of [
|
||||
"chat:",
|
||||
"group:",
|
||||
"channel:",
|
||||
"user:",
|
||||
"dm:",
|
||||
"open_id:"
|
||||
]) if (lowered.startsWith(prefix)) return normalizeText(withoutProvider.slice(prefix.length));
|
||||
return withoutProvider;
|
||||
}
|
||||
function parseFeishuDirectConversationId(raw) {
|
||||
const target = normalizeText(raw);
|
||||
if (!target) return;
|
||||
const withoutProvider = target.replace(/^(feishu|lark):/i, "").trim();
|
||||
if (!withoutProvider) return;
|
||||
const lowered = normalizeLowercaseStringOrEmpty(withoutProvider);
|
||||
for (const prefix of [
|
||||
"user:",
|
||||
"dm:",
|
||||
"open_id:"
|
||||
]) if (lowered.startsWith(prefix)) return normalizeText(withoutProvider.slice(prefix.length));
|
||||
const id = parseFeishuTargetId(target);
|
||||
if (!id) return;
|
||||
if (id.startsWith("ou_") || id.startsWith("on_")) return id;
|
||||
}
|
||||
function parseFeishuConversationId(params) {
|
||||
const conversationId = normalizeText(params.conversationId);
|
||||
const parentConversationId = normalizeText(params.parentConversationId);
|
||||
if (!conversationId) return null;
|
||||
const topicSenderMatch = conversationId.match(/^(.+):topic:([^:]+):sender:([^:]+)$/i);
|
||||
if (topicSenderMatch) {
|
||||
const [, chatId, topicId, senderOpenId] = topicSenderMatch;
|
||||
return {
|
||||
canonicalConversationId: buildFeishuConversationId({
|
||||
chatId,
|
||||
scope: "group_topic_sender",
|
||||
topicId,
|
||||
senderOpenId
|
||||
}),
|
||||
chatId,
|
||||
topicId,
|
||||
senderOpenId,
|
||||
scope: "group_topic_sender"
|
||||
};
|
||||
}
|
||||
const topicMatch = conversationId.match(/^(.+):topic:([^:]+)$/i);
|
||||
if (topicMatch) {
|
||||
const [, chatId, topicId] = topicMatch;
|
||||
return {
|
||||
canonicalConversationId: buildFeishuConversationId({
|
||||
chatId,
|
||||
scope: "group_topic",
|
||||
topicId
|
||||
}),
|
||||
chatId,
|
||||
topicId,
|
||||
scope: "group_topic"
|
||||
};
|
||||
}
|
||||
const senderMatch = conversationId.match(/^(.+):sender:([^:]+)$/i);
|
||||
if (senderMatch) {
|
||||
const [, chatId, senderOpenId] = senderMatch;
|
||||
return {
|
||||
canonicalConversationId: buildFeishuConversationId({
|
||||
chatId,
|
||||
scope: "group_sender",
|
||||
senderOpenId
|
||||
}),
|
||||
chatId,
|
||||
senderOpenId,
|
||||
scope: "group_sender"
|
||||
};
|
||||
}
|
||||
if (parentConversationId) return {
|
||||
canonicalConversationId: buildFeishuConversationId({
|
||||
chatId: parentConversationId,
|
||||
scope: "group_topic",
|
||||
topicId: conversationId
|
||||
}),
|
||||
chatId: parentConversationId,
|
||||
topicId: conversationId,
|
||||
scope: "group_topic"
|
||||
};
|
||||
return {
|
||||
canonicalConversationId: conversationId,
|
||||
chatId: conversationId,
|
||||
scope: "group"
|
||||
};
|
||||
}
|
||||
function buildFeishuModelOverrideParentCandidates(parentConversationId) {
|
||||
const rawId = normalizeText(parentConversationId);
|
||||
if (!rawId) return [];
|
||||
const topicSenderMatch = rawId.match(/^(.+):topic:([^:]+):sender:([^:]+)$/i);
|
||||
if (topicSenderMatch) {
|
||||
const chatId = normalizeLowercaseStringOrEmpty(topicSenderMatch[1]);
|
||||
const topicId = normalizeLowercaseStringOrEmpty(topicSenderMatch[2]);
|
||||
if (chatId && topicId) return [`${chatId}:topic:${topicId}`, chatId];
|
||||
return [];
|
||||
}
|
||||
const topicMatch = rawId.match(/^(.+):topic:([^:]+)$/i);
|
||||
if (topicMatch) {
|
||||
const chatId = normalizeLowercaseStringOrEmpty(topicMatch[1]);
|
||||
return chatId ? [chatId] : [];
|
||||
}
|
||||
const senderMatch = rawId.match(/^(.+):sender:([^:]+)$/i);
|
||||
if (senderMatch) {
|
||||
const chatId = normalizeLowercaseStringOrEmpty(senderMatch[1]);
|
||||
return chatId ? [chatId] : [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
//#endregion
|
||||
export { parseFeishuTargetId as a, parseFeishuDirectConversationId as i, buildFeishuModelOverrideParentCandidates as n, parseFeishuConversationId as r, buildFeishuConversationId as t };
|
||||
Generated
Vendored
+920
@@ -0,0 +1,920 @@
|
||||
import { c as encodeQuery, d as isRecord$1, i as listFeishuAccountIds, l as extractReplyText, m as readString, o as resolveFeishuAccount, r as listEnabledFeishuAccounts, s as resolveFeishuRuntimeAccount, u as formatFeishuApiError, v as parseFeishuCommentTarget } from "./accounts-Cfzht2Hc.js";
|
||||
import { r as createFeishuClient } from "./client-BwgAJuvo.js";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { optionalPositiveIntegerSchema } from "openclaw/plugin-sdk/channel-actions";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { Type } from "typebox";
|
||||
import { readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers";
|
||||
//#region extensions/feishu/src/tools-config.ts
|
||||
/**
|
||||
* Default tool configuration.
|
||||
* - doc, chat, wiki, drive, scopes, bitable/base: enabled by default
|
||||
* - perm: disabled by default (sensitive operation)
|
||||
*/
|
||||
const DEFAULT_TOOLS_CONFIG = {
|
||||
doc: true,
|
||||
chat: true,
|
||||
wiki: true,
|
||||
drive: true,
|
||||
perm: false,
|
||||
scopes: true,
|
||||
bitable: true,
|
||||
base: true
|
||||
};
|
||||
/**
|
||||
* Resolve tools config with defaults.
|
||||
*
|
||||
* `base` is a backward-compatible alias for the Bitable tool family. When both
|
||||
* keys are present, the explicit `bitable` value wins and `base` mirrors it.
|
||||
*/
|
||||
function resolveToolsConfig(cfg) {
|
||||
const bitable = cfg?.bitable ?? cfg?.base ?? DEFAULT_TOOLS_CONFIG.bitable;
|
||||
return {
|
||||
...DEFAULT_TOOLS_CONFIG,
|
||||
...cfg,
|
||||
bitable,
|
||||
base: bitable
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/tool-account.ts
|
||||
function resolveImplicitToolAccountId(params) {
|
||||
const explicitAccountId = normalizeOptionalString(params.executeParams?.accountId);
|
||||
if (explicitAccountId) return explicitAccountId;
|
||||
const contextualAccountId = normalizeOptionalString(params.defaultAccountId);
|
||||
if (contextualAccountId && listFeishuAccountIds(params.api.config).includes(contextualAccountId)) {
|
||||
if (resolveFeishuAccount({
|
||||
cfg: params.api.config,
|
||||
accountId: contextualAccountId
|
||||
}).enabled) return contextualAccountId;
|
||||
}
|
||||
const configuredDefaultAccountId = normalizeOptionalString((params.api.config?.channels?.feishu)?.defaultAccount);
|
||||
if (configuredDefaultAccountId) return configuredDefaultAccountId;
|
||||
if (params.requiredTool && params.api.config) for (const accountId of listFeishuAccountIds(params.api.config)) {
|
||||
const account = resolveFeishuAccount({
|
||||
cfg: params.api.config,
|
||||
accountId
|
||||
});
|
||||
if (account.enabled && account.configured && resolveToolsConfig(account.config.tools)[params.requiredTool.family]) return accountId;
|
||||
}
|
||||
}
|
||||
function resolveFeishuToolAccount(params) {
|
||||
if (!params.api.config) throw new Error("Feishu config unavailable");
|
||||
const account = resolveFeishuRuntimeAccount({
|
||||
cfg: params.api.config,
|
||||
accountId: resolveImplicitToolAccountId(params)
|
||||
});
|
||||
if (params.requiredTool && !resolveToolsConfig(account.config.tools)[params.requiredTool.family]) throw new Error(`Feishu ${params.requiredTool.label} tools are disabled for account "${account.accountId}"`);
|
||||
return account;
|
||||
}
|
||||
function createFeishuToolClient(params) {
|
||||
return createFeishuClient(resolveFeishuToolAccount(params));
|
||||
}
|
||||
function resolveAnyEnabledFeishuToolsConfig(accounts) {
|
||||
const merged = {
|
||||
doc: false,
|
||||
chat: false,
|
||||
wiki: false,
|
||||
drive: false,
|
||||
perm: false,
|
||||
scopes: false,
|
||||
bitable: false,
|
||||
base: false
|
||||
};
|
||||
for (const account of accounts) {
|
||||
const cfg = resolveToolsConfig(account.config.tools);
|
||||
merged.doc = merged.doc || cfg.doc;
|
||||
merged.chat = merged.chat || cfg.chat;
|
||||
merged.wiki = merged.wiki || cfg.wiki;
|
||||
merged.drive = merged.drive || cfg.drive;
|
||||
merged.perm = merged.perm || cfg.perm;
|
||||
merged.scopes = merged.scopes || cfg.scopes;
|
||||
merged.bitable = merged.bitable || cfg.bitable;
|
||||
merged.base = merged.base || cfg.base;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/chat-schema.ts
|
||||
const CHAT_ACTION_VALUES = [
|
||||
"members",
|
||||
"info",
|
||||
"member_info"
|
||||
];
|
||||
const MEMBER_ID_TYPE_VALUES = [
|
||||
"open_id",
|
||||
"user_id",
|
||||
"union_id"
|
||||
];
|
||||
const FeishuChatSchema = Type.Object({
|
||||
action: Type.Unsafe({
|
||||
type: "string",
|
||||
enum: [...CHAT_ACTION_VALUES],
|
||||
description: "Action to run: members | info | member_info"
|
||||
}),
|
||||
chat_id: Type.Optional(Type.String({ description: "Chat ID (from URL or event payload)" })),
|
||||
member_id: Type.Optional(Type.String({ description: "Member ID for member_info lookups" })),
|
||||
page_size: optionalPositiveIntegerSchema({
|
||||
maximum: 100,
|
||||
description: "Page size (1-100, default 50)"
|
||||
}),
|
||||
page_token: Type.Optional(Type.String({ description: "Pagination token" })),
|
||||
member_id_type: Type.Optional(Type.Unsafe({
|
||||
type: "string",
|
||||
enum: [...MEMBER_ID_TYPE_VALUES],
|
||||
description: "Member ID type (default: open_id)"
|
||||
}))
|
||||
});
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/chat.ts
|
||||
function json(data) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2)
|
||||
}],
|
||||
details: data
|
||||
};
|
||||
}
|
||||
function readChatPageSize(params) {
|
||||
return readPositiveIntegerParam(params, "page_size", {
|
||||
max: 100,
|
||||
message: "page_size must be a positive integer between 1 and 100"
|
||||
});
|
||||
}
|
||||
async function getChatInfo(client, chatId) {
|
||||
const res = await client.im.chat.get({ path: { chat_id: chatId } });
|
||||
if (res.code !== 0) throw new Error(res.msg);
|
||||
const chat = res.data;
|
||||
return {
|
||||
chat_id: chatId,
|
||||
name: chat?.name,
|
||||
description: chat?.description,
|
||||
owner_id: chat?.owner_id,
|
||||
tenant_key: chat?.tenant_key,
|
||||
user_count: chat?.user_count,
|
||||
chat_mode: chat?.chat_mode,
|
||||
chat_type: chat?.chat_type,
|
||||
join_message_visibility: chat?.join_message_visibility,
|
||||
leave_message_visibility: chat?.leave_message_visibility,
|
||||
membership_approval: chat?.membership_approval,
|
||||
moderation_permission: chat?.moderation_permission,
|
||||
avatar: chat?.avatar
|
||||
};
|
||||
}
|
||||
async function getChatMembers(client, chatId, pageSize, pageToken, memberIdType) {
|
||||
const page_size = pageSize ? Math.max(1, Math.min(100, pageSize)) : 50;
|
||||
const res = await client.im.chatMembers.get({
|
||||
path: { chat_id: chatId },
|
||||
params: {
|
||||
page_size,
|
||||
page_token: pageToken,
|
||||
member_id_type: memberIdType ?? "open_id"
|
||||
}
|
||||
});
|
||||
if (res.code !== 0) throw new Error(res.msg);
|
||||
return {
|
||||
chat_id: chatId,
|
||||
has_more: res.data?.has_more,
|
||||
page_token: res.data?.page_token,
|
||||
members: res.data?.items?.map((item) => ({
|
||||
member_id: item.member_id,
|
||||
name: item.name,
|
||||
tenant_key: item.tenant_key,
|
||||
member_id_type: item.member_id_type
|
||||
})) ?? []
|
||||
};
|
||||
}
|
||||
async function getFeishuMemberInfo(client, memberId, memberIdType = "open_id") {
|
||||
const res = await client.contact.user.get({
|
||||
path: { user_id: memberId },
|
||||
params: {
|
||||
user_id_type: memberIdType,
|
||||
department_id_type: "open_department_id"
|
||||
}
|
||||
});
|
||||
if (res.code !== 0) throw new Error(res.msg);
|
||||
const user = res.data?.user;
|
||||
return {
|
||||
member_id: memberId,
|
||||
member_id_type: memberIdType,
|
||||
open_id: user?.open_id,
|
||||
user_id: user?.user_id,
|
||||
union_id: user?.union_id,
|
||||
name: user?.name,
|
||||
en_name: user?.en_name,
|
||||
nickname: user?.nickname,
|
||||
email: user?.email,
|
||||
enterprise_email: user?.enterprise_email,
|
||||
mobile: user?.mobile,
|
||||
mobile_visible: user?.mobile_visible,
|
||||
status: user?.status,
|
||||
avatar: user?.avatar,
|
||||
department_ids: user?.department_ids,
|
||||
department_path: user?.department_path,
|
||||
leader_user_id: user?.leader_user_id,
|
||||
city: user?.city,
|
||||
country: user?.country,
|
||||
work_station: user?.work_station,
|
||||
join_time: user?.join_time,
|
||||
is_tenant_manager: user?.is_tenant_manager,
|
||||
employee_no: user?.employee_no,
|
||||
employee_type: user?.employee_type,
|
||||
description: user?.description,
|
||||
job_title: user?.job_title,
|
||||
geo: user?.geo
|
||||
};
|
||||
}
|
||||
function registerFeishuChatTools(api) {
|
||||
if (!api.config) return;
|
||||
const accounts = listEnabledFeishuAccounts(api.config);
|
||||
if (accounts.length === 0) return;
|
||||
const firstAccount = accounts[0];
|
||||
if (!resolveToolsConfig(firstAccount.config.tools).chat) return;
|
||||
const getClient = () => createFeishuClient(firstAccount);
|
||||
api.registerTool({
|
||||
name: "feishu_chat",
|
||||
label: "Feishu Chat",
|
||||
description: "Feishu chat operations. Actions: members, info, member_info",
|
||||
parameters: FeishuChatSchema,
|
||||
async execute(_toolCallId, params) {
|
||||
const rawParams = params;
|
||||
const p = params;
|
||||
try {
|
||||
const client = getClient();
|
||||
switch (p.action) {
|
||||
case "members":
|
||||
if (!p.chat_id) return json({ error: "chat_id is required for action members" });
|
||||
return json(await getChatMembers(client, p.chat_id, readChatPageSize(rawParams), p.page_token, p.member_id_type));
|
||||
case "info":
|
||||
if (!p.chat_id) return json({ error: "chat_id is required for action info" });
|
||||
return json(await getChatInfo(client, p.chat_id));
|
||||
case "member_info":
|
||||
if (!p.member_id) return json({ error: "member_id is required for action member_info" });
|
||||
return json(await getFeishuMemberInfo(client, p.member_id, p.member_id_type ?? "open_id"));
|
||||
default: return json({ error: `Unknown action: ${String(p.action)}` });
|
||||
}
|
||||
} catch (err) {
|
||||
return json({ error: formatFeishuApiError(err, { includeNestedErrorLogId: true }) });
|
||||
}
|
||||
}
|
||||
}, { name: "feishu_chat" });
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/tool-result.ts
|
||||
function jsonToolResult(data) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(data, null, 2)
|
||||
}],
|
||||
details: data
|
||||
};
|
||||
}
|
||||
function unknownToolActionResult(action) {
|
||||
return jsonToolResult({ error: `Unknown action: ${String(action)}` });
|
||||
}
|
||||
function toolExecutionErrorResult(error) {
|
||||
return jsonToolResult({ error: formatErrorMessage(error) });
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/comment-reaction.ts
|
||||
const COMMENT_TYPING_REACTION_TYPE = "Typing";
|
||||
const COMMENT_REACTION_TIMEOUT_MS = 3e4;
|
||||
const commentTypingReactionState = /* @__PURE__ */ new Map();
|
||||
function buildCommentTypingReactionKey(params) {
|
||||
return `${params.fileType}:${params.fileToken}:${params.replyId}`;
|
||||
}
|
||||
function ensureCommentTypingReactionState(key) {
|
||||
const existing = commentTypingReactionState.get(key);
|
||||
if (existing) return existing;
|
||||
const created = {
|
||||
active: false,
|
||||
cleaned: false,
|
||||
cleanupPromise: void 0
|
||||
};
|
||||
commentTypingReactionState.set(key, created);
|
||||
return created;
|
||||
}
|
||||
async function requestCommentTypingReactionWithClient(params) {
|
||||
try {
|
||||
const response = await params.client.request({
|
||||
method: "POST",
|
||||
url: `/open-apis/drive/v2/files/${encodeURIComponent(params.fileToken)}/comments/reaction` + encodeQuery({ file_type: params.fileType }),
|
||||
data: {
|
||||
action: params.action,
|
||||
reply_id: params.replyId,
|
||||
reaction_type: COMMENT_TYPING_REACTION_TYPE
|
||||
},
|
||||
timeout: COMMENT_REACTION_TIMEOUT_MS
|
||||
});
|
||||
if (response.code === 0) return true;
|
||||
params.runtime?.log?.(`${params.logPrefix ?? "[feishu]"}: comment typing reaction ${params.action} failed reply=${params.replyId} file=${params.fileType}:${params.fileToken} code=${response.code ?? "unknown"} msg=${response.msg ?? "unknown"} log_id=${response.log_id ?? response.error?.log_id ?? "unknown"}`);
|
||||
} catch (error) {
|
||||
params.runtime?.log?.(`${params.logPrefix ?? "[feishu]"}: comment typing reaction ${params.action} threw reply=${params.replyId} file=${params.fileType}:${params.fileToken} error=${formatCommentReactionFailure(error)}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function formatCommentReactionFailure(error) {
|
||||
return formatFeishuApiError(error, { includeNestedErrorLogId: true });
|
||||
}
|
||||
async function requestCommentTypingReaction(params) {
|
||||
const account = resolveFeishuRuntimeAccount({
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId
|
||||
});
|
||||
if (!account.configured || !(account.config.typingIndicator ?? true)) return false;
|
||||
return requestCommentTypingReactionWithClient({
|
||||
client: createFeishuClient(account),
|
||||
fileToken: params.fileToken,
|
||||
fileType: params.fileType,
|
||||
replyId: params.replyId,
|
||||
action: params.action,
|
||||
runtime: params.runtime,
|
||||
logPrefix: `feishu[${account.accountId}]`
|
||||
});
|
||||
}
|
||||
async function cleanupCommentTypingReactionByKey(params) {
|
||||
const state = ensureCommentTypingReactionState(params.key);
|
||||
if (state.cleaned) return false;
|
||||
if (state.cleanupPromise) return await state.cleanupPromise;
|
||||
const cleanupPromise = (async () => {
|
||||
if (!state.active) {
|
||||
state.cleaned = true;
|
||||
return false;
|
||||
}
|
||||
const deleted = await params.performDelete();
|
||||
if (deleted) {
|
||||
state.cleaned = true;
|
||||
state.active = false;
|
||||
}
|
||||
return deleted;
|
||||
})();
|
||||
state.cleanupPromise = cleanupPromise;
|
||||
try {
|
||||
return await cleanupPromise;
|
||||
} finally {
|
||||
state.cleanupPromise = void 0;
|
||||
if (state.cleaned) {
|
||||
state.active = false;
|
||||
commentTypingReactionState.delete(params.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function cleanupAmbientCommentTypingReaction(params) {
|
||||
const deliveryContext = params.deliveryContext;
|
||||
if (deliveryContext?.channel && deliveryContext.channel !== "feishu" && deliveryContext.channel !== "feishu-comment") return false;
|
||||
const target = parseFeishuCommentTarget(deliveryContext?.to);
|
||||
const replyId = typeof deliveryContext?.threadId === "string" || typeof deliveryContext?.threadId === "number" ? String(deliveryContext.threadId).trim() : "";
|
||||
if (!target || !replyId) return false;
|
||||
return cleanupCommentTypingReactionByKey({
|
||||
key: buildCommentTypingReactionKey({
|
||||
fileToken: target.fileToken,
|
||||
fileType: target.fileType,
|
||||
replyId
|
||||
}),
|
||||
performDelete: () => requestCommentTypingReactionWithClient({
|
||||
client: params.client,
|
||||
fileToken: target.fileToken,
|
||||
fileType: target.fileType,
|
||||
replyId,
|
||||
action: "delete",
|
||||
runtime: params.runtime,
|
||||
logPrefix: "[feishu]"
|
||||
})
|
||||
});
|
||||
}
|
||||
function createCommentTypingReactionLifecycle(params) {
|
||||
const key = params.replyId?.trim() ? buildCommentTypingReactionKey({
|
||||
fileToken: params.fileToken,
|
||||
fileType: params.fileType,
|
||||
replyId: params.replyId.trim()
|
||||
}) : void 0;
|
||||
const state = key ? ensureCommentTypingReactionState(key) : void 0;
|
||||
return {
|
||||
start: async () => {
|
||||
const replyId = params.replyId?.trim();
|
||||
if (!state || state.cleaned || state.active || !replyId) return;
|
||||
state.active = await requestCommentTypingReaction({
|
||||
cfg: params.cfg,
|
||||
fileToken: params.fileToken,
|
||||
fileType: params.fileType,
|
||||
replyId,
|
||||
action: "add",
|
||||
accountId: params.accountId,
|
||||
runtime: params.runtime
|
||||
});
|
||||
},
|
||||
cleanup: async () => {
|
||||
const replyId = params.replyId?.trim();
|
||||
if (!key || !replyId) return;
|
||||
await cleanupCommentTypingReactionByKey({
|
||||
key,
|
||||
performDelete: () => requestCommentTypingReaction({
|
||||
cfg: params.cfg,
|
||||
fileToken: params.fileToken,
|
||||
fileType: params.fileType,
|
||||
replyId,
|
||||
action: "delete",
|
||||
accountId: params.accountId,
|
||||
runtime: params.runtime
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/drive-schema.ts
|
||||
const FileType = Type.Union([
|
||||
Type.Literal("doc"),
|
||||
Type.Literal("docx"),
|
||||
Type.Literal("sheet"),
|
||||
Type.Literal("bitable"),
|
||||
Type.Literal("folder"),
|
||||
Type.Literal("file"),
|
||||
Type.Literal("mindnote"),
|
||||
Type.Literal("shortcut")
|
||||
]);
|
||||
const CommentFileType = Type.Union([
|
||||
Type.Literal("doc"),
|
||||
Type.Literal("docx"),
|
||||
Type.Literal("sheet"),
|
||||
Type.Literal("file"),
|
||||
Type.Literal("slides")
|
||||
]);
|
||||
const FeishuDriveSchema = Type.Union([
|
||||
Type.Object({
|
||||
action: Type.Literal("list"),
|
||||
folder_token: Type.Optional(Type.String({ description: "Folder token (optional, omit for root directory)" }))
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("info"),
|
||||
file_token: Type.String({ description: "File or folder token" }),
|
||||
type: FileType
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("create_folder"),
|
||||
name: Type.String({ description: "Folder name" }),
|
||||
folder_token: Type.Optional(Type.String({ description: "Parent folder token (optional, omit for root)" }))
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("move"),
|
||||
file_token: Type.String({ description: "File token to move" }),
|
||||
type: FileType,
|
||||
folder_token: Type.String({ description: "Target folder token" })
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("delete"),
|
||||
file_token: Type.String({ description: "File token to delete" }),
|
||||
type: FileType
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("list_comments"),
|
||||
file_token: Type.String({ description: "Document token" }),
|
||||
file_type: Type.Optional(CommentFileType),
|
||||
page_size: Type.Optional(Type.Integer({
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
description: "Page size"
|
||||
})),
|
||||
page_token: Type.Optional(Type.String({ description: "Comment page token" }))
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("list_comment_replies"),
|
||||
file_token: Type.String({ description: "Document token" }),
|
||||
file_type: Type.Optional(CommentFileType),
|
||||
comment_id: Type.String({ description: "Comment id" }),
|
||||
page_size: Type.Optional(Type.Integer({
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
description: "Page size"
|
||||
})),
|
||||
page_token: Type.Optional(Type.String({ description: "Reply page token" }))
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("add_comment"),
|
||||
file_token: Type.String({ description: "Document token" }),
|
||||
file_type: Type.Optional(Type.Union([Type.Literal("doc"), Type.Literal("docx")], { description: "Document type. Defaults to docx when omitted." })),
|
||||
content: Type.String({ description: "Comment text content" }),
|
||||
block_id: Type.Optional(Type.String({ description: "Optional docx block id for a local comment. Omit to create a full-document comment." }))
|
||||
}),
|
||||
Type.Object({
|
||||
action: Type.Literal("reply_comment"),
|
||||
file_token: Type.String({ description: "Document token" }),
|
||||
file_type: Type.Optional(CommentFileType),
|
||||
comment_id: Type.String({ description: "Comment id" }),
|
||||
content: Type.String({ description: "Reply text content" })
|
||||
})
|
||||
]);
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/drive.ts
|
||||
var FeishuReplyCommentError = class extends Error {
|
||||
constructor(params) {
|
||||
super(params.message);
|
||||
this.name = "FeishuReplyCommentError";
|
||||
this.httpStatus = params.httpStatus;
|
||||
this.feishuCode = params.feishuCode;
|
||||
this.feishuMsg = params.feishuMsg;
|
||||
this.feishuLogId = params.feishuLogId;
|
||||
}
|
||||
};
|
||||
const FEISHU_DRIVE_REQUEST_TIMEOUT_MS = 3e4;
|
||||
function getDriveInternalClient(client) {
|
||||
return client;
|
||||
}
|
||||
function buildReplyElements(content) {
|
||||
return [{
|
||||
type: "text",
|
||||
text: content
|
||||
}];
|
||||
}
|
||||
async function requestDriveApi(params) {
|
||||
return await getDriveInternalClient(params.client).request({
|
||||
method: params.method,
|
||||
url: params.url,
|
||||
params: params.query ?? {},
|
||||
data: params.data ?? {},
|
||||
timeout: FEISHU_DRIVE_REQUEST_TIMEOUT_MS
|
||||
});
|
||||
}
|
||||
function assertDriveApiSuccess(response) {
|
||||
if (response.code !== 0) throw new Error(response.msg ?? "Feishu Drive API request failed");
|
||||
return response;
|
||||
}
|
||||
function normalizeCommentReply(reply) {
|
||||
return {
|
||||
reply_id: reply.reply_id,
|
||||
user_id: reply.user_id,
|
||||
create_time: reply.create_time,
|
||||
update_time: reply.update_time,
|
||||
text: extractReplyText(reply)
|
||||
};
|
||||
}
|
||||
function normalizeCommentCard(comment) {
|
||||
const replies = comment.reply_list?.replies ?? [];
|
||||
const rootReply = replies[0];
|
||||
return {
|
||||
comment_id: comment.comment_id,
|
||||
user_id: comment.user_id,
|
||||
create_time: comment.create_time,
|
||||
update_time: comment.update_time,
|
||||
is_solved: comment.is_solved,
|
||||
is_whole: comment.is_whole,
|
||||
quote: comment.quote,
|
||||
text: extractReplyText(rootReply),
|
||||
has_more_replies: comment.has_more,
|
||||
replies_page_token: comment.page_token,
|
||||
replies: replies.slice(1).map(normalizeCommentReply)
|
||||
};
|
||||
}
|
||||
function normalizeCommentPageSize(pageSize) {
|
||||
if (typeof pageSize !== "number" || !Number.isFinite(pageSize)) return;
|
||||
return String(Math.min(Math.max(Math.floor(pageSize), 1), 100));
|
||||
}
|
||||
function resolveAmbientCommentTarget(context) {
|
||||
const deliveryContext = context?.deliveryContext;
|
||||
if (deliveryContext?.channel && deliveryContext.channel !== "feishu") return null;
|
||||
return parseFeishuCommentTarget(deliveryContext?.to);
|
||||
}
|
||||
function applyAmbientCommentDefaults(params, context) {
|
||||
const ambient = resolveAmbientCommentTarget(context);
|
||||
if (!ambient) return params;
|
||||
return {
|
||||
...params,
|
||||
file_token: params.file_token?.trim() || ambient.fileToken,
|
||||
file_type: params.file_type ?? ambient.fileType,
|
||||
comment_id: params.comment_id?.trim() || ambient.commentId
|
||||
};
|
||||
}
|
||||
function applyAddCommentAmbientDefaults(params, context) {
|
||||
const ambient = resolveAmbientCommentTarget(context);
|
||||
if (!ambient || ambient.fileType !== "doc" && ambient.fileType !== "docx") return params;
|
||||
return {
|
||||
...params,
|
||||
file_token: params.file_token?.trim() || ambient.fileToken,
|
||||
file_type: params.file_type ?? ambient.fileType
|
||||
};
|
||||
}
|
||||
function applyAddCommentDefaults(params) {
|
||||
const fileType = params.file_type ?? "docx";
|
||||
if (!params.file_type) console.info(`[feishu_drive] add_comment missing file_type; defaulting to docx file_token=${params.file_token ?? "unknown"}`);
|
||||
return {
|
||||
...params,
|
||||
file_type: fileType
|
||||
};
|
||||
}
|
||||
function applyCommentFileTypeDefault(params, action) {
|
||||
const fileType = params.file_type ?? "docx";
|
||||
if (!params.file_type) console.info(`[feishu_drive] ${action} missing file_type; defaulting to docx file_token=${params.file_token ?? "unknown"}`);
|
||||
return {
|
||||
...params,
|
||||
file_type: fileType
|
||||
};
|
||||
}
|
||||
function formatDriveApiError(error) {
|
||||
return formatFeishuApiError(error, { includeConfigParams: true });
|
||||
}
|
||||
function extractDriveApiErrorMeta(error) {
|
||||
if (!isRecord$1(error)) return { message: typeof error === "string" ? error : JSON.stringify(error) };
|
||||
const response = isRecord$1(error.response) ? error.response : void 0;
|
||||
const responseData = isRecord$1(response?.data) ? response?.data : void 0;
|
||||
return {
|
||||
message: typeof error.message === "string" ? error.message : typeof error === "string" ? error : JSON.stringify(error),
|
||||
httpStatus: typeof response?.status === "number" ? response.status : void 0,
|
||||
feishuCode: typeof responseData?.code === "number" ? responseData.code : readString(responseData?.code),
|
||||
feishuMsg: readString(responseData?.msg),
|
||||
feishuLogId: readString(responseData?.log_id)
|
||||
};
|
||||
}
|
||||
function isReplyNotAllowedError(error) {
|
||||
if (!(error instanceof FeishuReplyCommentError)) return false;
|
||||
return error.feishuCode === 1069302;
|
||||
}
|
||||
async function getRootFolderToken(client) {
|
||||
const internalClient = getDriveInternalClient(client);
|
||||
const domain = internalClient.domain ?? "https://open.feishu.cn";
|
||||
const res = await internalClient.httpInstance.get(`${domain}/open-apis/drive/explorer/v2/root_folder/meta`);
|
||||
if (res.code !== 0) throw new Error(res.msg ?? "Failed to get root folder");
|
||||
const token = res.data?.token;
|
||||
if (!token) throw new Error("Root folder token not found");
|
||||
return token;
|
||||
}
|
||||
async function listFolder(client, folderToken) {
|
||||
const validFolderToken = folderToken && folderToken !== "0" ? folderToken : void 0;
|
||||
const res = await client.drive.file.list({ params: validFolderToken ? { folder_token: validFolderToken } : {} });
|
||||
if (res.code !== 0) throw new Error(res.msg);
|
||||
return {
|
||||
files: res.data?.files?.map((f) => ({
|
||||
token: f.token,
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
url: f.url,
|
||||
created_time: f.created_time,
|
||||
modified_time: f.modified_time,
|
||||
owner_id: f.owner_id
|
||||
})) ?? [],
|
||||
next_page_token: res.data?.next_page_token
|
||||
};
|
||||
}
|
||||
async function getFileInfo(client, fileToken, folderToken) {
|
||||
const res = await client.drive.file.list({ params: folderToken ? { folder_token: folderToken } : {} });
|
||||
if (res.code !== 0) throw new Error(res.msg);
|
||||
const file = res.data?.files?.find((f) => f.token === fileToken);
|
||||
if (!file) throw new Error(`File not found: ${fileToken}`);
|
||||
return {
|
||||
token: file.token,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
url: file.url,
|
||||
created_time: file.created_time,
|
||||
modified_time: file.modified_time,
|
||||
owner_id: file.owner_id
|
||||
};
|
||||
}
|
||||
async function createFolder(client, name, folderToken) {
|
||||
let effectiveToken = folderToken && folderToken !== "0" ? folderToken : "0";
|
||||
if (effectiveToken === "0") try {
|
||||
effectiveToken = await getRootFolderToken(client);
|
||||
} catch {}
|
||||
const res = await client.drive.file.createFolder({ data: {
|
||||
name,
|
||||
folder_token: effectiveToken
|
||||
} });
|
||||
if (res.code !== 0) throw new Error(res.msg);
|
||||
return {
|
||||
token: res.data?.token,
|
||||
url: res.data?.url
|
||||
};
|
||||
}
|
||||
async function moveFile(client, fileToken, type, folderToken) {
|
||||
const res = await client.drive.file.move({
|
||||
path: { file_token: fileToken },
|
||||
data: {
|
||||
type,
|
||||
folder_token: folderToken
|
||||
}
|
||||
});
|
||||
if (res.code !== 0) throw new Error(res.msg);
|
||||
return {
|
||||
success: true,
|
||||
task_id: res.data?.task_id
|
||||
};
|
||||
}
|
||||
async function deleteFile(client, fileToken, type) {
|
||||
const res = await client.drive.file.delete({
|
||||
path: { file_token: fileToken },
|
||||
params: { type }
|
||||
});
|
||||
if (res.code !== 0) throw new Error(res.msg);
|
||||
return {
|
||||
success: true,
|
||||
task_id: res.data?.task_id
|
||||
};
|
||||
}
|
||||
async function listComments(client, params) {
|
||||
const response = assertDriveApiSuccess(await requestDriveApi({
|
||||
client,
|
||||
method: "GET",
|
||||
url: `/open-apis/drive/v1/files/${encodeURIComponent(params.file_token)}/comments` + encodeQuery({
|
||||
file_type: params.file_type,
|
||||
page_size: normalizeCommentPageSize(params.page_size),
|
||||
page_token: params.page_token,
|
||||
user_id_type: "open_id"
|
||||
})
|
||||
}));
|
||||
return {
|
||||
has_more: response.data?.has_more ?? false,
|
||||
page_token: response.data?.page_token,
|
||||
comments: (response.data?.items ?? []).map(normalizeCommentCard)
|
||||
};
|
||||
}
|
||||
async function listCommentReplies(client, params) {
|
||||
const response = assertDriveApiSuccess(await requestDriveApi({
|
||||
client,
|
||||
method: "GET",
|
||||
url: `/open-apis/drive/v1/files/${encodeURIComponent(params.file_token)}/comments/${encodeURIComponent(params.comment_id)}/replies` + encodeQuery({
|
||||
file_type: params.file_type,
|
||||
page_size: normalizeCommentPageSize(params.page_size),
|
||||
page_token: params.page_token,
|
||||
user_id_type: "open_id"
|
||||
})
|
||||
}));
|
||||
return {
|
||||
has_more: response.data?.has_more ?? false,
|
||||
page_token: response.data?.page_token,
|
||||
replies: (response.data?.items ?? []).map(normalizeCommentReply)
|
||||
};
|
||||
}
|
||||
async function addComment(client, params) {
|
||||
if (params.block_id?.trim() && params.file_type !== "docx") throw new Error("block_id is only supported for docx comments");
|
||||
return {
|
||||
success: true,
|
||||
...assertDriveApiSuccess(await requestDriveApi({
|
||||
client,
|
||||
method: "POST",
|
||||
url: `/open-apis/drive/v1/files/${encodeURIComponent(params.file_token)}/new_comments`,
|
||||
data: {
|
||||
file_type: params.file_type,
|
||||
reply_elements: buildReplyElements(params.content),
|
||||
...params.block_id?.trim() ? { anchor: { block_id: params.block_id.trim() } } : {}
|
||||
}
|
||||
})).data
|
||||
};
|
||||
}
|
||||
async function queryCommentById(client, params) {
|
||||
return assertDriveApiSuccess(await requestDriveApi({
|
||||
client,
|
||||
method: "POST",
|
||||
url: `/open-apis/drive/v1/files/${encodeURIComponent(params.file_token)}/comments/batch_query` + encodeQuery({
|
||||
file_type: params.file_type,
|
||||
user_id_type: "open_id"
|
||||
}),
|
||||
data: { comment_ids: [params.comment_id] }
|
||||
})).data?.items?.find((comment) => comment.comment_id?.trim() === params.comment_id);
|
||||
}
|
||||
async function replyComment(client, params) {
|
||||
const url = `/open-apis/drive/v1/files/${encodeURIComponent(params.file_token)}/comments/${encodeURIComponent(params.comment_id)}/replies`;
|
||||
const query = { file_type: params.file_type };
|
||||
try {
|
||||
const response = await requestDriveApi({
|
||||
client,
|
||||
method: "POST",
|
||||
url,
|
||||
query,
|
||||
data: { content: { elements: [{
|
||||
type: "text_run",
|
||||
text_run: { text: params.content }
|
||||
}] } }
|
||||
});
|
||||
if (response.code === 0) return {
|
||||
success: true,
|
||||
...response.data
|
||||
};
|
||||
console.warn(`[feishu_drive] replyComment failed comment=${params.comment_id} file_type=${params.file_type} code=${response.code ?? "unknown"} msg=${response.msg ?? "unknown"} log_id=${response.log_id ?? "unknown"}`);
|
||||
throw new FeishuReplyCommentError({
|
||||
message: response.msg ?? "Feishu Drive reply comment failed",
|
||||
feishuCode: response.code,
|
||||
feishuMsg: response.msg,
|
||||
feishuLogId: response.log_id
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof FeishuReplyCommentError) throw error;
|
||||
const meta = extractDriveApiErrorMeta(error);
|
||||
console.warn(`[feishu_drive] replyComment threw comment=${params.comment_id} file_type=${params.file_type} error=${formatDriveApiError(error)}`);
|
||||
throw new FeishuReplyCommentError({
|
||||
message: meta.message,
|
||||
httpStatus: meta.httpStatus,
|
||||
feishuCode: meta.feishuCode,
|
||||
feishuMsg: meta.feishuMsg,
|
||||
feishuLogId: meta.feishuLogId
|
||||
});
|
||||
}
|
||||
}
|
||||
async function deliverCommentThreadText(client, params) {
|
||||
let isWholeComment = params.is_whole_comment;
|
||||
if (isWholeComment === void 0) try {
|
||||
isWholeComment = (await queryCommentById(client, params))?.is_whole === true;
|
||||
} catch (error) {
|
||||
console.warn(`[feishu_drive] comment metadata preflight failed comment=${params.comment_id} file_type=${params.file_type} error=${formatErrorMessage(error)}`);
|
||||
isWholeComment = false;
|
||||
}
|
||||
if (isWholeComment) {
|
||||
if (params.file_type !== "doc" && params.file_type !== "docx") throw new Error(`Whole-document comment follow-ups are only supported for doc/docx (got ${params.file_type})`);
|
||||
const wholeCommentFileType = params.file_type;
|
||||
console.info(`[feishu_drive] whole-comment compatibility path comment=${params.comment_id} file_type=${params.file_type} mode=add_comment`);
|
||||
return {
|
||||
delivery_mode: "add_comment",
|
||||
...await addComment(client, {
|
||||
file_token: params.file_token,
|
||||
file_type: wholeCommentFileType,
|
||||
content: params.content
|
||||
})
|
||||
};
|
||||
}
|
||||
try {
|
||||
return {
|
||||
delivery_mode: "reply_comment",
|
||||
...await replyComment(client, params)
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof FeishuReplyCommentError && isReplyNotAllowedError(error)) {
|
||||
if (params.file_type !== "doc" && params.file_type !== "docx") throw error;
|
||||
const fallbackFileType = params.file_type;
|
||||
console.info(`[feishu_drive] reply-not-allowed compatibility path comment=${params.comment_id} file_type=${params.file_type} mode=add_comment log_id=${error.feishuLogId ?? "unknown"}`);
|
||||
return {
|
||||
delivery_mode: "add_comment",
|
||||
...await addComment(client, {
|
||||
file_token: params.file_token,
|
||||
file_type: fallbackFileType,
|
||||
content: params.content
|
||||
})
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function registerFeishuDriveTools(api) {
|
||||
if (!api.config) return;
|
||||
const accounts = listEnabledFeishuAccounts(api.config);
|
||||
if (accounts.length === 0) return;
|
||||
if (!resolveAnyEnabledFeishuToolsConfig(accounts).drive) return;
|
||||
api.registerTool((ctx) => {
|
||||
const defaultAccountId = ctx.agentAccountId;
|
||||
return {
|
||||
name: "feishu_drive",
|
||||
label: "Feishu Drive",
|
||||
description: "Feishu cloud storage operations. Actions: list, info, create_folder, move, delete, list_comments, list_comment_replies, add_comment, reply_comment",
|
||||
parameters: FeishuDriveSchema,
|
||||
async execute(_toolCallId, params) {
|
||||
const p = params;
|
||||
try {
|
||||
const client = createFeishuToolClient({
|
||||
api,
|
||||
executeParams: p,
|
||||
defaultAccountId,
|
||||
requiredTool: {
|
||||
family: "drive",
|
||||
label: "Drive"
|
||||
}
|
||||
});
|
||||
switch (p.action) {
|
||||
case "list": return jsonToolResult(await listFolder(client, p.folder_token));
|
||||
case "info": return jsonToolResult(await getFileInfo(client, p.file_token));
|
||||
case "create_folder": return jsonToolResult(await createFolder(client, p.name, p.folder_token));
|
||||
case "move": return jsonToolResult(await moveFile(client, p.file_token, p.type, p.folder_token));
|
||||
case "delete": return jsonToolResult(await deleteFile(client, p.file_token, p.type));
|
||||
case "list_comments": return jsonToolResult(await listComments(client, applyCommentFileTypeDefault(applyAmbientCommentDefaults(p, ctx), "list_comments")));
|
||||
case "list_comment_replies": return jsonToolResult(await listCommentReplies(client, applyCommentFileTypeDefault(applyAmbientCommentDefaults(p, ctx), "list_comment_replies")));
|
||||
case "add_comment": {
|
||||
const resolved = applyAddCommentDefaults(applyAddCommentAmbientDefaults(p, ctx));
|
||||
try {
|
||||
return jsonToolResult(await addComment(client, resolved));
|
||||
} finally {
|
||||
cleanupAmbientCommentTypingReaction({
|
||||
client: getDriveInternalClient(client),
|
||||
deliveryContext: ctx.deliveryContext
|
||||
});
|
||||
}
|
||||
}
|
||||
case "reply_comment": {
|
||||
const resolved = applyCommentFileTypeDefault(applyAmbientCommentDefaults(p, ctx), "reply_comment");
|
||||
try {
|
||||
return jsonToolResult(await deliverCommentThreadText(client, resolved));
|
||||
} finally {
|
||||
cleanupAmbientCommentTypingReaction({
|
||||
client: getDriveInternalClient(client),
|
||||
deliveryContext: ctx.deliveryContext
|
||||
});
|
||||
}
|
||||
}
|
||||
default: return unknownToolActionResult(p.action);
|
||||
}
|
||||
} catch (err) {
|
||||
return toolExecutionErrorResult(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, { name: "feishu_drive" });
|
||||
}
|
||||
//#endregion
|
||||
export { jsonToolResult as a, getChatInfo as c, registerFeishuChatTools as d, createFeishuToolClient as f, resolveToolsConfig as h, createCommentTypingReactionLifecycle as i, getChatMembers as l, resolveFeishuToolAccount as m, registerFeishuDriveTools as n, toolExecutionErrorResult as o, resolveAnyEnabledFeishuToolsConfig as p, cleanupAmbientCommentTypingReaction as r, unknownToolActionResult as s, deliverCommentThreadText as t, getFeishuMemberInfo as u };
|
||||
Generated
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
import { registerFeishuSubagentHooks } from "./subagent-hooks-api.js";
|
||||
import { defineBundledChannelEntry, loadBundledEntryExportSync } from "openclaw/plugin-sdk/channel-entry-contract";
|
||||
//#region extensions/feishu/index.ts
|
||||
function registerFeishuDocTools(api) {
|
||||
loadBundledEntryExportSync(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "registerFeishuDocTools"
|
||||
})(api);
|
||||
}
|
||||
function registerFeishuChatTools(api) {
|
||||
loadBundledEntryExportSync(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "registerFeishuChatTools"
|
||||
})(api);
|
||||
}
|
||||
function registerFeishuWikiTools(api) {
|
||||
loadBundledEntryExportSync(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "registerFeishuWikiTools"
|
||||
})(api);
|
||||
}
|
||||
function registerFeishuDriveTools(api) {
|
||||
loadBundledEntryExportSync(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "registerFeishuDriveTools"
|
||||
})(api);
|
||||
}
|
||||
function registerFeishuPermTools(api) {
|
||||
loadBundledEntryExportSync(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "registerFeishuPermTools"
|
||||
})(api);
|
||||
}
|
||||
function registerFeishuBitableTools(api) {
|
||||
loadBundledEntryExportSync(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "registerFeishuBitableTools"
|
||||
})(api);
|
||||
}
|
||||
var feishu_default = defineBundledChannelEntry({
|
||||
id: "feishu",
|
||||
name: "Feishu",
|
||||
description: "Feishu/Lark channel plugin",
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./channel-plugin-api.js",
|
||||
exportName: "feishuPlugin"
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./secret-contract-api.js",
|
||||
exportName: "channelSecrets"
|
||||
},
|
||||
runtime: {
|
||||
specifier: "./runtime-api.js",
|
||||
exportName: "setFeishuRuntime"
|
||||
},
|
||||
registerFull(api) {
|
||||
registerFeishuSubagentHooks(api);
|
||||
registerFeishuDocTools(api);
|
||||
registerFeishuChatTools(api);
|
||||
registerFeishuWikiTools(api);
|
||||
registerFeishuDriveTools(api);
|
||||
registerFeishuPermTools(api);
|
||||
registerFeishuBitableTools(api);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { feishu_default as default };
|
||||
Generated
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
//#region extensions/feishu/src/dedup-migrations.ts
|
||||
const DEDUP_TTL_MS = 1440 * 60 * 1e3;
|
||||
const STORE_MAX_ENTRIES = 1e4;
|
||||
function safeNamespaceFromFileName(fileName) {
|
||||
if (!fileName.endsWith(".json")) return null;
|
||||
const namespace = fileName.slice(0, -5).trim();
|
||||
return namespace ? namespace : null;
|
||||
}
|
||||
function readLegacyDedupeData(filePath) {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
||||
const out = {};
|
||||
for (const [messageId, seenAt] of Object.entries(parsed)) if (typeof seenAt === "number" && Number.isFinite(seenAt) && seenAt > 0) out[messageId] = seenAt;
|
||||
return out;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
function dedupeStoreKey(namespace, messageId) {
|
||||
return createHash("sha256").update(`${namespace}\0${messageId}`, "utf8").digest("hex").slice(0, 32);
|
||||
}
|
||||
function remainingTtlMs(seenAt, now) {
|
||||
return Math.max(1, DEDUP_TTL_MS - (now - seenAt));
|
||||
}
|
||||
function buildMigrationEntries(namespace, sourcePath, now) {
|
||||
return Object.entries(readLegacyDedupeData(sourcePath)).flatMap(([messageId, seenAt]) => {
|
||||
if (now - seenAt >= DEDUP_TTL_MS) return [];
|
||||
return [{
|
||||
key: dedupeStoreKey(namespace, messageId),
|
||||
value: {
|
||||
namespace,
|
||||
messageId,
|
||||
seenAt
|
||||
},
|
||||
ttlMs: remainingTtlMs(seenAt, now)
|
||||
}];
|
||||
});
|
||||
}
|
||||
const detectFeishuLegacyStateMigrations = ({ stateDir }) => {
|
||||
const dedupDir = path.join(stateDir, "feishu", "dedup");
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(dedupDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const now = Date.now();
|
||||
return entries.flatMap((entry) => {
|
||||
if (!entry.isFile()) return [];
|
||||
const namespace = safeNamespaceFromFileName(entry.name);
|
||||
if (!namespace) return [];
|
||||
const sourcePath = path.join(dedupDir, entry.name);
|
||||
if (buildMigrationEntries(namespace, sourcePath, now).length === 0) return [];
|
||||
return [{
|
||||
kind: "plugin-state-import",
|
||||
label: `Feishu ${namespace} dedupe`,
|
||||
sourcePath,
|
||||
targetPath: `plugin state:dedup.${namespace}`,
|
||||
pluginId: "feishu",
|
||||
namespace: `dedup.${namespace}`,
|
||||
maxEntries: STORE_MAX_ENTRIES,
|
||||
scopeKey: "",
|
||||
cleanupSource: "rename",
|
||||
readEntries: () => buildMigrationEntries(namespace, sourcePath, now)
|
||||
}];
|
||||
});
|
||||
};
|
||||
//#endregion
|
||||
export { detectFeishuLegacyStateMigrations };
|
||||
Generated
Vendored
+62
@@ -0,0 +1,62 @@
|
||||
import { r as listEnabledFeishuAccounts, s as resolveFeishuRuntimeAccount } from "./accounts-Cfzht2Hc.js";
|
||||
import { f as fetchBotIdentityForMonitor } from "./monitor.state-CGL1occz.js";
|
||||
//#region extensions/feishu/src/monitor.ts
|
||||
let monitorAccountRuntimePromise;
|
||||
async function loadMonitorAccountRuntime() {
|
||||
monitorAccountRuntimePromise ??= import("./monitor.account-C2bq3aSK.js");
|
||||
return await monitorAccountRuntimePromise;
|
||||
}
|
||||
async function monitorFeishuProvider(opts = {}) {
|
||||
const cfg = opts.config;
|
||||
if (!cfg) throw new Error("Config is required for Feishu monitor");
|
||||
const log = opts.runtime?.log ?? console.log;
|
||||
if (opts.accountId) {
|
||||
const account = resolveFeishuRuntimeAccount({
|
||||
cfg,
|
||||
accountId: opts.accountId
|
||||
}, { requireEventSecrets: true });
|
||||
if (!account.enabled || !account.configured) throw new Error(`Feishu account "${opts.accountId}" not configured or disabled`);
|
||||
const { monitorSingleAccount } = await loadMonitorAccountRuntime();
|
||||
return monitorSingleAccount({
|
||||
cfg,
|
||||
account,
|
||||
channelRuntime: opts.channelRuntime,
|
||||
runtime: opts.runtime,
|
||||
abortSignal: opts.abortSignal
|
||||
});
|
||||
}
|
||||
const accounts = listEnabledFeishuAccounts(cfg);
|
||||
if (accounts.length === 0) throw new Error("No enabled Feishu accounts configured");
|
||||
log(`feishu: starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`);
|
||||
const { monitorSingleAccount } = await loadMonitorAccountRuntime();
|
||||
const monitorPromises = [];
|
||||
for (const account of accounts) {
|
||||
if (opts.abortSignal?.aborted) {
|
||||
log("feishu: abort signal received during startup preflight; stopping startup");
|
||||
break;
|
||||
}
|
||||
const { botOpenId, botName } = await fetchBotIdentityForMonitor(account, {
|
||||
runtime: opts.runtime,
|
||||
abortSignal: opts.abortSignal
|
||||
});
|
||||
if (opts.abortSignal?.aborted) {
|
||||
log("feishu: abort signal received during startup preflight; stopping startup");
|
||||
break;
|
||||
}
|
||||
monitorPromises.push(monitorSingleAccount({
|
||||
cfg,
|
||||
account,
|
||||
channelRuntime: opts.channelRuntime,
|
||||
runtime: opts.runtime,
|
||||
abortSignal: opts.abortSignal,
|
||||
botOpenIdSource: {
|
||||
kind: "prefetched",
|
||||
botOpenId,
|
||||
botName
|
||||
}
|
||||
}));
|
||||
}
|
||||
await Promise.all(monitorPromises);
|
||||
}
|
||||
//#endregion
|
||||
export { monitorFeishuProvider };
|
||||
Generated
Vendored
+5683
File diff suppressed because it is too large
Load Diff
npm/projects/openclaw-feishu-dc69f44688/node_modules/@openclaw/feishu/dist/monitor.state-CGL1occz.js
Generated
Vendored
+173
@@ -0,0 +1,173 @@
|
||||
import { t as probeFeishu } from "./probe-EyeJQb3C.js";
|
||||
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, createFixedWindowRateLimiter, createWebhookAnomalyTracker } from "openclaw/plugin-sdk/webhook-ingress";
|
||||
//#region extensions/feishu/src/monitor.startup.ts
|
||||
const FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS = 3e4;
|
||||
const FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV = "OPENCLAW_FEISHU_STARTUP_PROBE_TIMEOUT_MS";
|
||||
function resolveStartupProbeTimeoutMs(env = process.env) {
|
||||
const raw = env[FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV];
|
||||
if (raw) {
|
||||
const parsed = parseStrictPositiveInteger(raw);
|
||||
if (parsed !== void 0) return parsed;
|
||||
console.warn(`[feishu] ${FEISHU_STARTUP_BOT_INFO_TIMEOUT_ENV}="${raw}" is invalid; using default ${FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS}ms`);
|
||||
}
|
||||
return FEISHU_STARTUP_BOT_INFO_TIMEOUT_DEFAULT_MS;
|
||||
}
|
||||
const FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS = resolveStartupProbeTimeoutMs();
|
||||
function isTimeoutErrorMessage(message) {
|
||||
const lower = normalizeLowercaseStringOrEmpty(message);
|
||||
return lower.includes("timeout") || lower.includes("timed out");
|
||||
}
|
||||
function isAbortErrorMessage(message) {
|
||||
return normalizeLowercaseStringOrEmpty(message).includes("aborted");
|
||||
}
|
||||
async function fetchBotIdentityForMonitor(account, options = {}) {
|
||||
if (options.abortSignal?.aborted) return {};
|
||||
const timeoutMs = options.timeoutMs ?? FEISHU_STARTUP_BOT_INFO_TIMEOUT_MS;
|
||||
const result = await probeFeishu(account, {
|
||||
timeoutMs,
|
||||
abortSignal: options.abortSignal
|
||||
});
|
||||
if (result.ok) return {
|
||||
botOpenId: result.botOpenId,
|
||||
botName: result.botName
|
||||
};
|
||||
const probeError = result.error ?? void 0;
|
||||
if (options.abortSignal?.aborted || isAbortErrorMessage(probeError)) return {};
|
||||
if (isTimeoutErrorMessage(probeError)) (options.runtime?.error ?? console.error)(`feishu[${account.accountId}]: bot info probe timed out after ${timeoutMs}ms; continuing startup`);
|
||||
return {};
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/monitor.state.ts
|
||||
const wsClients = /* @__PURE__ */ new Map();
|
||||
const httpServers = /* @__PURE__ */ new Map();
|
||||
const botOpenIds = /* @__PURE__ */ new Map();
|
||||
const botNames = /* @__PURE__ */ new Map();
|
||||
const botIdentityRevisions = /* @__PURE__ */ new Map();
|
||||
const FEISHU_WEBHOOK_MAX_BODY_BYTES = 64 * 1024;
|
||||
const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 5e3;
|
||||
const FEISHU_HTTP_SERVER_CLOSE_TIMEOUT_MS = 5e3;
|
||||
const FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS = {
|
||||
windowMs: 6e4,
|
||||
maxRequests: 120,
|
||||
maxTrackedKeys: 4096
|
||||
};
|
||||
const FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS = {
|
||||
maxTrackedKeys: 4096,
|
||||
ttlMs: 360 * 6e4,
|
||||
logEvery: 25
|
||||
};
|
||||
function coercePositiveInt(value, fallback) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
||||
const normalized = Math.floor(value);
|
||||
return normalized > 0 ? normalized : fallback;
|
||||
}
|
||||
function resolveFeishuWebhookRateLimitDefaultsForTest(defaults) {
|
||||
const resolved = defaults;
|
||||
return {
|
||||
windowMs: coercePositiveInt(resolved?.windowMs, FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.windowMs),
|
||||
maxRequests: coercePositiveInt(resolved?.maxRequests, FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.maxRequests),
|
||||
maxTrackedKeys: coercePositiveInt(resolved?.maxTrackedKeys, FEISHU_WEBHOOK_RATE_LIMIT_FALLBACK_DEFAULTS.maxTrackedKeys)
|
||||
};
|
||||
}
|
||||
function resolveFeishuWebhookAnomalyDefaultsForTest(defaults) {
|
||||
const resolved = defaults;
|
||||
return {
|
||||
maxTrackedKeys: coercePositiveInt(resolved?.maxTrackedKeys, FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.maxTrackedKeys),
|
||||
ttlMs: coercePositiveInt(resolved?.ttlMs, FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.ttlMs),
|
||||
logEvery: coercePositiveInt(resolved?.logEvery, FEISHU_WEBHOOK_ANOMALY_FALLBACK_DEFAULTS.logEvery)
|
||||
};
|
||||
}
|
||||
const feishuWebhookRateLimitDefaults = resolveFeishuWebhookRateLimitDefaultsForTest(WEBHOOK_RATE_LIMIT_DEFAULTS);
|
||||
const feishuWebhookAnomalyDefaults = resolveFeishuWebhookAnomalyDefaultsForTest(WEBHOOK_ANOMALY_COUNTER_DEFAULTS);
|
||||
const feishuWebhookRateLimiter = createFixedWindowRateLimiter({
|
||||
windowMs: feishuWebhookRateLimitDefaults.windowMs,
|
||||
maxRequests: feishuWebhookRateLimitDefaults.maxRequests,
|
||||
maxTrackedKeys: feishuWebhookRateLimitDefaults.maxTrackedKeys
|
||||
});
|
||||
const feishuWebhookAnomalyTracker = createWebhookAnomalyTracker({
|
||||
maxTrackedKeys: feishuWebhookAnomalyDefaults.maxTrackedKeys,
|
||||
ttlMs: feishuWebhookAnomalyDefaults.ttlMs,
|
||||
logEvery: feishuWebhookAnomalyDefaults.logEvery
|
||||
});
|
||||
function readBotIdentityRevision(accountId) {
|
||||
return botIdentityRevisions.get(accountId) ?? 0;
|
||||
}
|
||||
function bumpBotIdentityRevision(accountId) {
|
||||
botIdentityRevisions.set(accountId, readBotIdentityRevision(accountId) + 1);
|
||||
}
|
||||
function captureBotIdentitySnapshot(accountId) {
|
||||
return { revision: readBotIdentityRevision(accountId) };
|
||||
}
|
||||
function clearFeishuBotIdentityStateIfUnchanged(accountId, snapshot) {
|
||||
if (readBotIdentityRevision(accountId) !== snapshot.revision) return;
|
||||
botOpenIds.delete(accountId);
|
||||
botNames.delete(accountId);
|
||||
bumpBotIdentityRevision(accountId);
|
||||
}
|
||||
function setFeishuBotIdentityState(accountId, identity) {
|
||||
botOpenIds.set(accountId, identity.botOpenId);
|
||||
if (identity.botName) botNames.set(accountId, identity.botName);
|
||||
else botNames.delete(accountId);
|
||||
bumpBotIdentityRevision(accountId);
|
||||
}
|
||||
function clearFeishuBotIdentityState(accountId) {
|
||||
botOpenIds.delete(accountId);
|
||||
botNames.delete(accountId);
|
||||
bumpBotIdentityRevision(accountId);
|
||||
}
|
||||
function isServerNotRunningError(error) {
|
||||
return error.code === "ERR_SERVER_NOT_RUNNING";
|
||||
}
|
||||
async function closeFeishuHttpServer(server) {
|
||||
await new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
const settle = (err) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(fallbackTimer);
|
||||
if (!err || isServerNotRunningError(err)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
reject(err);
|
||||
};
|
||||
const fallbackTimer = setTimeout(() => {
|
||||
try {
|
||||
server.closeAllConnections();
|
||||
settle();
|
||||
} catch (err) {
|
||||
settle(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
}, FEISHU_HTTP_SERVER_CLOSE_TIMEOUT_MS);
|
||||
try {
|
||||
server.close((err) => {
|
||||
settle(err);
|
||||
});
|
||||
} catch (err) {
|
||||
settle(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
});
|
||||
}
|
||||
async function closeTrackedFeishuHttpServer(accountId, server) {
|
||||
const identitySnapshot = captureBotIdentitySnapshot(accountId);
|
||||
try {
|
||||
await closeFeishuHttpServer(server);
|
||||
} finally {
|
||||
if (httpServers.get(accountId) === server) {
|
||||
httpServers.delete(accountId);
|
||||
clearFeishuBotIdentityStateIfUnchanged(accountId, identitySnapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
function recordWebhookStatus(runtime, accountId, path, statusCode) {
|
||||
feishuWebhookAnomalyTracker.record({
|
||||
key: `${accountId}:${path}:${statusCode}`,
|
||||
statusCode,
|
||||
log: runtime?.log ?? console.log,
|
||||
message: (count) => `feishu[${accountId}]: webhook anomaly path=${path} status=${statusCode} count=${count}`
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
export { clearFeishuBotIdentityState as a, httpServers as c, wsClients as d, fetchBotIdentityForMonitor as f, botOpenIds as i, recordWebhookStatus as l, FEISHU_WEBHOOK_MAX_BODY_BYTES as n, closeTrackedFeishuHttpServer as o, botNames as r, feishuWebhookRateLimiter as s, FEISHU_WEBHOOK_BODY_TIMEOUT_MS as t, setFeishuBotIdentityState as u };
|
||||
Generated
Vendored
+161
@@ -0,0 +1,161 @@
|
||||
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
||||
import { r as createFeishuClient } from "./client-BwgAJuvo.js";
|
||||
import { asDateTimestampMs, resolveExpiresAtMsFromDurationMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
//#region extensions/feishu/src/async.ts
|
||||
const RACE_TIMEOUT = Symbol("race-timeout");
|
||||
const RACE_ABORT = Symbol("race-abort");
|
||||
async function raceWithTimeoutAndAbort(promise, options = {}) {
|
||||
if (options.abortSignal?.aborted) return { status: "aborted" };
|
||||
if (options.timeoutMs === void 0 && !options.abortSignal) return {
|
||||
status: "resolved",
|
||||
value: await promise
|
||||
};
|
||||
let timeoutHandle;
|
||||
let abortHandler;
|
||||
const contenders = [promise];
|
||||
if (options.timeoutMs !== void 0) {
|
||||
const timeoutMs = resolveTimerTimeoutMs(options.timeoutMs, 1);
|
||||
contenders.push(new Promise((resolve) => {
|
||||
timeoutHandle = setTimeout(() => resolve(RACE_TIMEOUT), timeoutMs);
|
||||
}));
|
||||
}
|
||||
if (options.abortSignal) contenders.push(new Promise((resolve) => {
|
||||
abortHandler = () => resolve(RACE_ABORT);
|
||||
options.abortSignal?.addEventListener("abort", abortHandler, { once: true });
|
||||
}));
|
||||
try {
|
||||
const result = await Promise.race(contenders);
|
||||
if (result === RACE_TIMEOUT) return { status: "timeout" };
|
||||
if (result === RACE_ABORT) return { status: "aborted" };
|
||||
return {
|
||||
status: "resolved",
|
||||
value: result
|
||||
};
|
||||
} finally {
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle);
|
||||
if (abortHandler) options.abortSignal?.removeEventListener("abort", abortHandler);
|
||||
}
|
||||
}
|
||||
function waitForAbortableDelay(delayMs, abortSignal) {
|
||||
if (abortSignal?.aborted) return Promise.resolve(false);
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = (value) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
if (handleAbort) abortSignal?.removeEventListener("abort", handleAbort);
|
||||
resolve(value);
|
||||
};
|
||||
const handleAbort = () => {
|
||||
finish(false);
|
||||
};
|
||||
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
||||
if (abortSignal?.aborted) {
|
||||
finish(false);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => finish(true), resolveTimerTimeoutMs(delayMs, 1));
|
||||
timer.unref?.();
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/probe.ts
|
||||
var probe_exports = /* @__PURE__ */ __exportAll({
|
||||
FEISHU_PROBE_REQUEST_TIMEOUT_MS: () => FEISHU_PROBE_REQUEST_TIMEOUT_MS,
|
||||
probeFeishu: () => probeFeishu
|
||||
});
|
||||
/** Cache probe results to reduce repeated health-check calls.
|
||||
* Gateway health checks call probeFeishu() every minute; without caching this
|
||||
* burns ~43,200 calls/month, easily exceeding Feishu's free-tier quota.
|
||||
* Successful bot info is effectively static, while failures are cached briefly
|
||||
* to avoid hammering the API during transient outages. */
|
||||
const probeCache = /* @__PURE__ */ new Map();
|
||||
const PROBE_SUCCESS_TTL_MS = 600 * 1e3;
|
||||
const PROBE_ERROR_TTL_MS = 60 * 1e3;
|
||||
const MAX_PROBE_CACHE_SIZE = 64;
|
||||
const FEISHU_PROBE_REQUEST_TIMEOUT_MS = 1e4;
|
||||
function setCachedProbeResult(cacheKey, result, ttlMs) {
|
||||
const expiresAt = resolveExpiresAtMsFromDurationMs(ttlMs);
|
||||
if (expiresAt === void 0) {
|
||||
probeCache.delete(cacheKey);
|
||||
return result;
|
||||
}
|
||||
probeCache.set(cacheKey, {
|
||||
result,
|
||||
expiresAt
|
||||
});
|
||||
if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
|
||||
const oldest = probeCache.keys().next().value;
|
||||
if (oldest !== void 0) probeCache.delete(oldest);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async function probeFeishu(creds, options = {}) {
|
||||
if (!creds?.appId || !creds?.appSecret) return {
|
||||
ok: false,
|
||||
error: "missing credentials (appId, appSecret)"
|
||||
};
|
||||
if (options.abortSignal?.aborted) return {
|
||||
ok: false,
|
||||
appId: creds.appId,
|
||||
error: "probe aborted"
|
||||
};
|
||||
const timeoutMs = options.timeoutMs ?? 1e4;
|
||||
const cacheKey = creds.accountId ?? `${creds.appId}:${creds.appSecret.slice(0, 8)}`;
|
||||
const cached = probeCache.get(cacheKey);
|
||||
if (cached) {
|
||||
const now = asDateTimestampMs(Date.now());
|
||||
const expiresAt = asDateTimestampMs(cached.expiresAt);
|
||||
if (now !== void 0 && expiresAt !== void 0 && expiresAt > now) return cached.result;
|
||||
probeCache.delete(cacheKey);
|
||||
}
|
||||
try {
|
||||
const responseResult = await raceWithTimeoutAndAbort(createFeishuClient(creds).request({
|
||||
method: "POST",
|
||||
url: "/open-apis/bot/v1/openclaw_bot/ping",
|
||||
data: { needBotInfo: true },
|
||||
timeout: timeoutMs
|
||||
}), {
|
||||
timeoutMs,
|
||||
abortSignal: options.abortSignal
|
||||
});
|
||||
if (responseResult.status === "aborted") return {
|
||||
ok: false,
|
||||
appId: creds.appId,
|
||||
error: "probe aborted"
|
||||
};
|
||||
if (responseResult.status === "timeout") return setCachedProbeResult(cacheKey, {
|
||||
ok: false,
|
||||
appId: creds.appId,
|
||||
error: `probe timed out after ${timeoutMs}ms`
|
||||
}, PROBE_ERROR_TTL_MS);
|
||||
const response = responseResult.value;
|
||||
if (options.abortSignal?.aborted) return {
|
||||
ok: false,
|
||||
appId: creds.appId,
|
||||
error: "probe aborted"
|
||||
};
|
||||
if (response.code !== 0) return setCachedProbeResult(cacheKey, {
|
||||
ok: false,
|
||||
appId: creds.appId,
|
||||
error: `API error: ${response.msg || `code ${response.code}`}`
|
||||
}, PROBE_ERROR_TTL_MS);
|
||||
const botInfo = response.data?.pingBotInfo;
|
||||
return setCachedProbeResult(cacheKey, {
|
||||
ok: true,
|
||||
appId: creds.appId,
|
||||
botName: botInfo?.botName,
|
||||
botOpenId: botInfo?.botID
|
||||
}, PROBE_SUCCESS_TTL_MS);
|
||||
} catch (err) {
|
||||
return setCachedProbeResult(cacheKey, {
|
||||
ok: false,
|
||||
appId: creds.appId,
|
||||
error: formatErrorMessage(err)
|
||||
}, PROBE_ERROR_TTL_MS);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
export { waitForAbortableDelay as i, probe_exports as n, raceWithTimeoutAndAbort as r, probeFeishu as t };
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import "node:module";
|
||||
//#region \0rolldown/runtime.js
|
||||
var __defProp = Object.defineProperty;
|
||||
var __exportAll = (all, no_symbols) => {
|
||||
let target = {};
|
||||
for (var name in all) __defProp(target, name, {
|
||||
get: all[name],
|
||||
enumerable: true
|
||||
});
|
||||
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
||||
return target;
|
||||
};
|
||||
//#endregion
|
||||
export { __exportAll as t };
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
||||
//#region extensions/feishu/src/runtime.ts
|
||||
const { setRuntime: setFeishuRuntime, getRuntime: getFeishuRuntime } = createPluginRuntimeStore({
|
||||
pluginId: "feishu",
|
||||
errorMessage: "Feishu runtime not initialized"
|
||||
});
|
||||
//#endregion
|
||||
export { setFeishuRuntime as n, getFeishuRuntime as t };
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { n as setFeishuRuntime } from "./runtime-C5JxBWZp.js";
|
||||
import { createReplyPrefixContext } from "openclaw/plugin-sdk/channel-outbound";
|
||||
import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
|
||||
import { PAIRING_APPROVED_MESSAGE, buildProbeChannelStatusSummary, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/channel-status";
|
||||
import { chunkTextForOutbound } from "openclaw/plugin-sdk/text-chunking";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import { loadSessionStore, resolveSessionStoreEntry } from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import { DEFAULT_ACCOUNT_ID, buildChannelConfigSchema, createActionGate, createDedupeCache } from "openclaw/plugin-sdk/core";
|
||||
import { buildAgentMediaPayload } from "openclaw/plugin-sdk/agent-media-payload";
|
||||
import { evaluateSupplementalContextVisibility, filterSupplementalContextItems, resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
|
||||
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
||||
import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-ingress";
|
||||
export { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE, buildAgentMediaPayload, buildChannelConfigSchema, buildProbeChannelStatusSummary, chunkTextForOutbound, createActionGate, createChannelPairingController, createDedupeCache, createDefaultChannelRuntimeState, createReplyPrefixContext, evaluateSupplementalContextVisibility, filterSupplementalContextItems, isRequestBodyLimitError, loadSessionStore, normalizeAgentId, readJsonFileWithFallback, readRequestBodyWithLimit, requestBodyErrorToText, resolveChannelContextVisibilityMode, resolveSessionStoreEntry, setFeishuRuntime };
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { n as setFeishuRuntime } from "./runtime-C5JxBWZp.js";
|
||||
export { setFeishuRuntime };
|
||||
Generated
Vendored
+119
@@ -0,0 +1,119 @@
|
||||
import { collectConditionalChannelFieldAssignments, collectSimpleChannelFieldAssignments, getChannelSurface, hasOwnProperty, normalizeSecretStringValue } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
||||
//#region extensions/feishu/src/secret-contract.ts
|
||||
const secretTargetRegistryEntries = [
|
||||
{
|
||||
id: "channels.feishu.accounts.*.appSecret",
|
||||
targetType: "channels.feishu.accounts.*.appSecret",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "channels.feishu.accounts.*.appSecret",
|
||||
secretShape: "secret_input",
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true
|
||||
},
|
||||
{
|
||||
id: "channels.feishu.accounts.*.encryptKey",
|
||||
targetType: "channels.feishu.accounts.*.encryptKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "channels.feishu.accounts.*.encryptKey",
|
||||
secretShape: "secret_input",
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true
|
||||
},
|
||||
{
|
||||
id: "channels.feishu.accounts.*.verificationToken",
|
||||
targetType: "channels.feishu.accounts.*.verificationToken",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "channels.feishu.accounts.*.verificationToken",
|
||||
secretShape: "secret_input",
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true
|
||||
},
|
||||
{
|
||||
id: "channels.feishu.appSecret",
|
||||
targetType: "channels.feishu.appSecret",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "channels.feishu.appSecret",
|
||||
secretShape: "secret_input",
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true
|
||||
},
|
||||
{
|
||||
id: "channels.feishu.encryptKey",
|
||||
targetType: "channels.feishu.encryptKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "channels.feishu.encryptKey",
|
||||
secretShape: "secret_input",
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true
|
||||
},
|
||||
{
|
||||
id: "channels.feishu.verificationToken",
|
||||
targetType: "channels.feishu.verificationToken",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "channels.feishu.verificationToken",
|
||||
secretShape: "secret_input",
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true
|
||||
}
|
||||
];
|
||||
function collectRuntimeConfigAssignments(params) {
|
||||
const resolved = getChannelSurface(params.config, "feishu");
|
||||
if (!resolved) return;
|
||||
const { channel: feishu, surface } = resolved;
|
||||
collectSimpleChannelFieldAssignments({
|
||||
channelKey: "feishu",
|
||||
field: "appSecret",
|
||||
channel: feishu,
|
||||
surface,
|
||||
defaults: params.defaults,
|
||||
context: params.context,
|
||||
topInactiveReason: "no enabled account inherits this top-level Feishu appSecret.",
|
||||
accountInactiveReason: "Feishu account is disabled."
|
||||
});
|
||||
const baseConnectionMode = normalizeSecretStringValue(feishu.connectionMode) === "webhook" ? "webhook" : "websocket";
|
||||
const resolveAccountMode = (account) => hasOwnProperty(account, "connectionMode") ? normalizeSecretStringValue(account.connectionMode) : baseConnectionMode;
|
||||
collectConditionalChannelFieldAssignments({
|
||||
channelKey: "feishu",
|
||||
field: "encryptKey",
|
||||
channel: feishu,
|
||||
surface,
|
||||
defaults: params.defaults,
|
||||
context: params.context,
|
||||
topLevelActiveWithoutAccounts: baseConnectionMode === "webhook",
|
||||
topLevelInheritedAccountActive: ({ account, enabled }) => enabled && !hasOwnProperty(account, "encryptKey") && resolveAccountMode(account) === "webhook",
|
||||
accountActive: ({ account, enabled }) => enabled && resolveAccountMode(account) === "webhook",
|
||||
topInactiveReason: "no enabled Feishu webhook-mode surface inherits this top-level encryptKey.",
|
||||
accountInactiveReason: "Feishu account is disabled or not running in webhook mode."
|
||||
});
|
||||
collectConditionalChannelFieldAssignments({
|
||||
channelKey: "feishu",
|
||||
field: "verificationToken",
|
||||
channel: feishu,
|
||||
surface,
|
||||
defaults: params.defaults,
|
||||
context: params.context,
|
||||
topLevelActiveWithoutAccounts: baseConnectionMode === "webhook",
|
||||
topLevelInheritedAccountActive: ({ account, enabled }) => enabled && !hasOwnProperty(account, "verificationToken") && resolveAccountMode(account) === "webhook",
|
||||
accountActive: ({ account, enabled }) => enabled && resolveAccountMode(account) === "webhook",
|
||||
topInactiveReason: "no enabled Feishu webhook-mode surface inherits this top-level verificationToken.",
|
||||
accountInactiveReason: "Feishu account is disabled or not running in webhook mode."
|
||||
});
|
||||
}
|
||||
const channelSecrets = {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments
|
||||
};
|
||||
//#endregion
|
||||
export { collectRuntimeConfigAssignments as n, secretTargetRegistryEntries as r, channelSecrets as t };
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-ChjJKAJ9.js";
|
||||
export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import "./security-audit-shared-BIHeF-S_.js";
|
||||
//#region extensions/feishu/src/message-action-contract.ts
|
||||
const messageActionTargetAliases = {
|
||||
read: { aliases: ["messageId"] },
|
||||
pin: { aliases: ["messageId"] },
|
||||
unpin: { aliases: ["messageId"] },
|
||||
"list-pins": { aliases: ["chatId"] },
|
||||
"channel-info": { aliases: ["chatId"] }
|
||||
};
|
||||
//#endregion
|
||||
export { messageActionTargetAliases as t };
|
||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
//#region extensions/feishu/src/security-audit-shared.ts
|
||||
function asRecord(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
||||
}
|
||||
function hasNonEmptyString(value) {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
function isFeishuDocToolEnabled(cfg) {
|
||||
const feishu = asRecord(asRecord(cfg.channels)?.feishu);
|
||||
if (!feishu || feishu.enabled === false) return false;
|
||||
const baseTools = asRecord(feishu.tools);
|
||||
const baseDocEnabled = baseTools?.doc !== false;
|
||||
const baseAppId = hasNonEmptyString(feishu.appId);
|
||||
const baseAppSecret = hasConfiguredSecretInput(feishu.appSecret, cfg.secrets?.defaults);
|
||||
const baseConfigured = baseAppId && baseAppSecret;
|
||||
const accounts = asRecord(feishu.accounts);
|
||||
if (!accounts || Object.keys(accounts).length === 0) return baseDocEnabled && baseConfigured;
|
||||
for (const accountValue of Object.values(accounts)) {
|
||||
const account = asRecord(accountValue) ?? {};
|
||||
if (account.enabled === false) continue;
|
||||
if (!((asRecord(account.tools) ?? baseTools)?.doc !== false)) continue;
|
||||
if ((hasNonEmptyString(account.appId) || baseAppId) && (hasConfiguredSecretInput(account.appSecret, cfg.secrets?.defaults) || baseAppSecret)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function collectFeishuSecurityAuditFindings(params) {
|
||||
if (!isFeishuDocToolEnabled(params.cfg)) return [];
|
||||
return [{
|
||||
checkId: "channels.feishu.doc_owner_open_id",
|
||||
severity: "warn",
|
||||
title: "Feishu doc create can grant requester permissions",
|
||||
detail: "channels.feishu tools include \"doc\"; feishu_doc action \"create\" can grant document access to the trusted requesting Feishu user.",
|
||||
remediation: "Disable channels.feishu.tools.doc when not needed, and restrict tool access for untrusted prompts."
|
||||
}];
|
||||
}
|
||||
//#endregion
|
||||
export { collectFeishuSecurityAuditFindings as t };
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { t as collectFeishuSecurityAuditFindings } from "./security-audit-shared-BIHeF-S_.js";
|
||||
export { collectFeishuSecurityAuditFindings };
|
||||
Generated
Vendored
+1219
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+140
@@ -0,0 +1,140 @@
|
||||
import { d as isRecord } from "./accounts-Cfzht2Hc.js";
|
||||
import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
|
||||
//#region extensions/feishu/src/card-interaction.ts
|
||||
const FEISHU_CARD_INTERACTION_VERSION = "ocf1";
|
||||
function isInteractionKind(value) {
|
||||
return value === "button" || value === "quick" || value === "meta";
|
||||
}
|
||||
function isMetadataValue(value) {
|
||||
return value === null || value === void 0 || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
||||
}
|
||||
function createFeishuCardInteractionEnvelope(envelope) {
|
||||
return {
|
||||
oc: FEISHU_CARD_INTERACTION_VERSION,
|
||||
...envelope
|
||||
};
|
||||
}
|
||||
function buildFeishuCardActionTextFallback(event) {
|
||||
const actionValue = event.action.value;
|
||||
if (isRecord(actionValue)) {
|
||||
if (typeof actionValue.text === "string") return actionValue.text;
|
||||
if (typeof actionValue.command === "string") return actionValue.command;
|
||||
return JSON.stringify(actionValue);
|
||||
}
|
||||
return String(actionValue);
|
||||
}
|
||||
function decodeFeishuCardAction(params) {
|
||||
const { event, now = Date.now() } = params;
|
||||
const actionValue = event.action.value;
|
||||
if (!isRecord(actionValue) || actionValue.oc !== "ocf1") return {
|
||||
kind: "legacy",
|
||||
text: buildFeishuCardActionTextFallback(event)
|
||||
};
|
||||
if (!isInteractionKind(actionValue.k) || typeof actionValue.a !== "string" || !actionValue.a) return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
if (actionValue.q !== void 0 && typeof actionValue.q !== "string") return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
if (actionValue.m !== void 0) {
|
||||
if (!isRecord(actionValue.m)) return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
for (const value of Object.values(actionValue.m)) if (!isMetadataValue(value)) return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
}
|
||||
if (actionValue.c !== void 0) {
|
||||
if (!isRecord(actionValue.c)) return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
if (actionValue.c.u !== void 0 && typeof actionValue.c.u !== "string") return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
if (actionValue.c.h !== void 0 && typeof actionValue.c.h !== "string") return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
if (actionValue.c.s !== void 0 && typeof actionValue.c.s !== "string") return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
if (actionValue.c.e !== void 0 && !Number.isFinite(actionValue.c.e)) return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
if (actionValue.c.t !== void 0 && actionValue.c.t !== "p2p" && actionValue.c.t !== "group") return {
|
||||
kind: "invalid",
|
||||
reason: "malformed"
|
||||
};
|
||||
if (typeof actionValue.c.e === "number" && actionValue.c.e < now) return {
|
||||
kind: "invalid",
|
||||
reason: "stale"
|
||||
};
|
||||
const expectedUser = actionValue.c.u?.trim();
|
||||
if (expectedUser && expectedUser !== (event.operator.open_id ?? "").trim()) return {
|
||||
kind: "invalid",
|
||||
reason: "wrong_user"
|
||||
};
|
||||
const expectedChat = actionValue.c.h?.trim();
|
||||
if (expectedChat && expectedChat !== (event.context.chat_id ?? "").trim()) return {
|
||||
kind: "invalid",
|
||||
reason: "wrong_conversation"
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "structured",
|
||||
envelope: actionValue
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region extensions/feishu/src/send-result.ts
|
||||
function resolveFeishuReceiptKind(msgType) {
|
||||
switch (msgType) {
|
||||
case "audio": return "voice";
|
||||
case "image":
|
||||
case "media":
|
||||
case "file": return "media";
|
||||
case "interactive": return "card";
|
||||
case "post":
|
||||
case "text": return "text";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
function createFeishuSendReceipt(params) {
|
||||
const messageId = params.messageId?.trim();
|
||||
const chatId = params.chatId.trim();
|
||||
return createMessageReceiptFromOutboundResults({
|
||||
results: messageId ? [{
|
||||
channel: "feishu",
|
||||
messageId,
|
||||
chatId,
|
||||
conversationId: chatId
|
||||
}] : [],
|
||||
...chatId ? { threadId: chatId } : {},
|
||||
kind: params.kind ?? "unknown"
|
||||
});
|
||||
}
|
||||
function assertFeishuMessageApiSuccess(response, errorPrefix) {
|
||||
if (response.code !== 0) throw new Error(`${errorPrefix}: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
function toFeishuSendResult(response, chatId, kind) {
|
||||
const messageId = response.data?.message_id ?? "unknown";
|
||||
return {
|
||||
messageId,
|
||||
chatId,
|
||||
receipt: createFeishuSendReceipt({
|
||||
messageId,
|
||||
chatId,
|
||||
kind
|
||||
})
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { FEISHU_CARD_INTERACTION_VERSION as a, decodeFeishuCardAction as c, toFeishuSendResult as i, createFeishuSendReceipt as n, buildFeishuCardActionTextFallback as o, resolveFeishuReceiptKind as r, createFeishuCardInteractionEnvelope as s, assertFeishuMessageApiSuccess as t };
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { r as testing, t as createFeishuThreadBindingManager } from "./thread-bindings-V0bwk0A1.js";
|
||||
export { createFeishuThreadBindingManager, testing as feishuThreadBindingTesting };
|
||||
Generated
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
import { r as parseFeishuConversationId, t as buildFeishuConversationId } from "./conversation-id-DuL575sn.js";
|
||||
//#region extensions/feishu/src/session-conversation.ts
|
||||
function resolveFeishuParentConversationCandidates(rawId) {
|
||||
const parsed = parseFeishuConversationId({ conversationId: rawId });
|
||||
if (!parsed) return [];
|
||||
switch (parsed.scope) {
|
||||
case "group_topic_sender": return [buildFeishuConversationId({
|
||||
chatId: parsed.chatId,
|
||||
scope: "group_topic",
|
||||
topicId: parsed.topicId
|
||||
}), parsed.chatId];
|
||||
case "group_topic":
|
||||
case "group_sender": return [parsed.chatId];
|
||||
default: return [];
|
||||
}
|
||||
}
|
||||
function resolveFeishuSessionConversation(params) {
|
||||
const parsed = parseFeishuConversationId({ conversationId: params.rawId });
|
||||
if (!parsed) return null;
|
||||
return {
|
||||
id: parsed.canonicalConversationId,
|
||||
baseConversationId: parsed.chatId,
|
||||
parentConversationCandidates: resolveFeishuParentConversationCandidates(parsed.canonicalConversationId)
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
export { resolveFeishuSessionConversation as t };
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { t as resolveFeishuSessionConversation } from "./session-conversation-CZSMgac-.js";
|
||||
export { resolveFeishuSessionConversation as resolveSessionConversation };
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { i as feishuSetupAdapter, n as feishuSetupWizard, t as feishuPlugin } from "./channel-uEJPBCR9.js";
|
||||
export { feishuPlugin, feishuSetupAdapter, feishuSetupWizard };
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
|
||||
//#region extensions/feishu/setup-entry.ts
|
||||
var setup_entry_default = defineBundledChannelSetupEntry({
|
||||
importMetaUrl: import.meta.url,
|
||||
features: { legacyStateMigrations: true },
|
||||
plugin: {
|
||||
specifier: "./setup-api.js",
|
||||
exportName: "feishuPlugin"
|
||||
},
|
||||
legacyStateMigrations: {
|
||||
specifier: "./legacy-state-migrations-api.js",
|
||||
exportName: "detectFeishuLegacyStateMigrations"
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./secret-contract-api.js",
|
||||
exportName: "channelSecrets"
|
||||
},
|
||||
runtime: {
|
||||
specifier: "./runtime-setter-api.js",
|
||||
exportName: "setFeishuRuntime"
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
export { setup_entry_default as default };
|
||||
Generated
Vendored
+235
@@ -0,0 +1,235 @@
|
||||
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
||||
import { r as normalizeFeishuTarget } from "./targets-BUjQ1TcA.js";
|
||||
import { r as parseFeishuConversationId, t as buildFeishuConversationId } from "./conversation-id-DuL575sn.js";
|
||||
import { n as getFeishuThreadBindingManager } from "./thread-bindings-V0bwk0A1.js";
|
||||
import { normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
//#region extensions/feishu/src/subagent-hooks.ts
|
||||
var subagent_hooks_exports = /* @__PURE__ */ __exportAll({
|
||||
handleFeishuSubagentDeliveryTarget: () => handleFeishuSubagentDeliveryTarget,
|
||||
handleFeishuSubagentEnded: () => handleFeishuSubagentEnded,
|
||||
handleFeishuSubagentSpawning: () => handleFeishuSubagentSpawning
|
||||
});
|
||||
function summarizeError(err) {
|
||||
if (err instanceof Error) return err.message;
|
||||
if (typeof err === "string") return err;
|
||||
return "error";
|
||||
}
|
||||
function stripProviderPrefix(raw) {
|
||||
return raw.replace(/^(feishu|lark):/i, "").trim();
|
||||
}
|
||||
function resolveFeishuRequesterConversation(params) {
|
||||
const manager = getFeishuThreadBindingManager(params.accountId);
|
||||
if (!manager) return null;
|
||||
const rawTo = params.to?.trim();
|
||||
const withoutProviderPrefix = rawTo ? stripProviderPrefix(rawTo) : "";
|
||||
const normalizedTarget = rawTo ? normalizeFeishuTarget(rawTo) : null;
|
||||
const threadId = params.threadId != null && params.threadId !== "" ? String(params.threadId).trim() : "";
|
||||
const isChatTarget = /^(chat|group|channel):/i.test(withoutProviderPrefix);
|
||||
const parsedRequesterTopic = normalizedTarget && threadId && isChatTarget ? parseFeishuConversationId({
|
||||
conversationId: buildFeishuConversationId({
|
||||
chatId: normalizedTarget,
|
||||
scope: "group_topic",
|
||||
topicId: threadId
|
||||
}),
|
||||
parentConversationId: normalizedTarget
|
||||
}) : null;
|
||||
const requesterSessionKey = params.requesterSessionKey?.trim();
|
||||
if (requesterSessionKey) {
|
||||
const existingBindings = manager.listBySessionKey(requesterSessionKey);
|
||||
if (existingBindings.length === 1) {
|
||||
const existing = existingBindings[0];
|
||||
return {
|
||||
accountId: existing.accountId,
|
||||
conversationId: existing.conversationId,
|
||||
parentConversationId: existing.parentConversationId
|
||||
};
|
||||
}
|
||||
if (existingBindings.length > 1) {
|
||||
if (rawTo && normalizedTarget && !threadId && !isChatTarget) {
|
||||
const directMatches = existingBindings.filter((entry) => entry.accountId === manager.accountId && entry.conversationId === normalizedTarget && !entry.parentConversationId);
|
||||
if (directMatches.length === 1) {
|
||||
const existing = directMatches[0];
|
||||
return {
|
||||
accountId: existing.accountId,
|
||||
conversationId: existing.conversationId,
|
||||
parentConversationId: existing.parentConversationId
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (parsedRequesterTopic) {
|
||||
const matchingTopicBindings = existingBindings.filter((entry) => {
|
||||
const parsed = parseFeishuConversationId({
|
||||
conversationId: entry.conversationId,
|
||||
parentConversationId: entry.parentConversationId
|
||||
});
|
||||
return parsed?.chatId === parsedRequesterTopic.chatId && parsed?.topicId === parsedRequesterTopic.topicId;
|
||||
});
|
||||
if (matchingTopicBindings.length === 1) {
|
||||
const existing = matchingTopicBindings[0];
|
||||
return {
|
||||
accountId: existing.accountId,
|
||||
conversationId: existing.conversationId,
|
||||
parentConversationId: existing.parentConversationId
|
||||
};
|
||||
}
|
||||
const senderScopedTopicBindings = matchingTopicBindings.filter((entry) => {
|
||||
return parseFeishuConversationId({
|
||||
conversationId: entry.conversationId,
|
||||
parentConversationId: entry.parentConversationId
|
||||
})?.scope === "group_topic_sender";
|
||||
});
|
||||
if (senderScopedTopicBindings.length === 1 && matchingTopicBindings.length === senderScopedTopicBindings.length) {
|
||||
const existing = senderScopedTopicBindings[0];
|
||||
return {
|
||||
accountId: existing.accountId,
|
||||
conversationId: existing.conversationId,
|
||||
parentConversationId: existing.parentConversationId
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!rawTo) return null;
|
||||
if (!normalizedTarget) return null;
|
||||
if (threadId) {
|
||||
if (!isChatTarget) return null;
|
||||
return {
|
||||
accountId: manager.accountId,
|
||||
conversationId: buildFeishuConversationId({
|
||||
chatId: normalizedTarget,
|
||||
scope: "group_topic",
|
||||
topicId: threadId
|
||||
}),
|
||||
parentConversationId: normalizedTarget
|
||||
};
|
||||
}
|
||||
if (isChatTarget) return null;
|
||||
return {
|
||||
accountId: manager.accountId,
|
||||
conversationId: normalizedTarget
|
||||
};
|
||||
}
|
||||
function resolveFeishuDeliveryOrigin(params) {
|
||||
const deliveryTo = params.deliveryTo?.trim();
|
||||
const deliveryThreadId = params.deliveryThreadId?.trim();
|
||||
if (deliveryTo) return {
|
||||
channel: "feishu",
|
||||
accountId: params.accountId,
|
||||
to: deliveryTo,
|
||||
...deliveryThreadId ? { threadId: deliveryThreadId } : {}
|
||||
};
|
||||
const parsed = parseFeishuConversationId({
|
||||
conversationId: params.conversationId,
|
||||
parentConversationId: params.parentConversationId
|
||||
});
|
||||
if (parsed?.topicId) return {
|
||||
channel: "feishu",
|
||||
accountId: params.accountId,
|
||||
to: `chat:${params.parentConversationId?.trim() || parsed.chatId}`,
|
||||
threadId: parsed.topicId
|
||||
};
|
||||
return {
|
||||
channel: "feishu",
|
||||
accountId: params.accountId,
|
||||
to: `user:${params.conversationId}`
|
||||
};
|
||||
}
|
||||
function resolveMatchingChildBinding(params) {
|
||||
const manager = getFeishuThreadBindingManager(params.accountId);
|
||||
if (!manager) return null;
|
||||
const childBindings = manager.listBySessionKey(params.childSessionKey.trim());
|
||||
if (childBindings.length === 0) return null;
|
||||
const requesterConversation = resolveFeishuRequesterConversation({
|
||||
accountId: manager.accountId,
|
||||
to: params.requesterOrigin?.to,
|
||||
threadId: params.requesterOrigin?.threadId,
|
||||
requesterSessionKey: params.requesterSessionKey
|
||||
});
|
||||
if (requesterConversation) {
|
||||
const matched = childBindings.find((entry) => entry.accountId === requesterConversation.accountId && entry.conversationId === requesterConversation.conversationId && normalizeOptionalString(entry.parentConversationId) === normalizeOptionalString(requesterConversation.parentConversationId));
|
||||
if (matched) return matched;
|
||||
}
|
||||
return childBindings.length === 1 ? childBindings[0] : null;
|
||||
}
|
||||
async function handleFeishuSubagentSpawning(event, ctx) {
|
||||
if (!event.threadRequested) return;
|
||||
if (normalizeOptionalLowercaseString(event.requester?.channel) !== "feishu") return;
|
||||
const manager = getFeishuThreadBindingManager(event.requester?.accountId);
|
||||
if (!manager) return {
|
||||
status: "error",
|
||||
error: "Feishu current-conversation binding is unavailable because the Feishu account monitor is not active."
|
||||
};
|
||||
const conversation = resolveFeishuRequesterConversation({
|
||||
accountId: event.requester?.accountId,
|
||||
to: event.requester?.to,
|
||||
threadId: event.requester?.threadId,
|
||||
requesterSessionKey: ctx.requesterSessionKey
|
||||
});
|
||||
if (!conversation) return {
|
||||
status: "error",
|
||||
error: "Feishu current-conversation binding is only available in direct messages or topic conversations."
|
||||
};
|
||||
try {
|
||||
const binding = manager.bindConversation({
|
||||
conversationId: conversation.conversationId,
|
||||
parentConversationId: conversation.parentConversationId,
|
||||
targetKind: "subagent",
|
||||
targetSessionKey: event.childSessionKey,
|
||||
metadata: {
|
||||
agentId: event.agentId,
|
||||
label: event.label,
|
||||
boundBy: "system",
|
||||
deliveryTo: event.requester?.to,
|
||||
deliveryThreadId: event.requester?.threadId != null && event.requester.threadId !== "" ? String(event.requester.threadId) : void 0
|
||||
}
|
||||
});
|
||||
if (!binding) return {
|
||||
status: "error",
|
||||
error: "Unable to bind this Feishu conversation to the spawned subagent session. Session mode is unavailable for this target."
|
||||
};
|
||||
return {
|
||||
status: "ok",
|
||||
threadBindingReady: true,
|
||||
deliveryOrigin: resolveFeishuDeliveryOrigin({
|
||||
conversationId: binding.conversationId,
|
||||
parentConversationId: binding.parentConversationId,
|
||||
accountId: binding.accountId,
|
||||
deliveryTo: binding.deliveryTo,
|
||||
deliveryThreadId: binding.deliveryThreadId
|
||||
})
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: "error",
|
||||
error: `Feishu conversation bind failed: ${summarizeError(err)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
function handleFeishuSubagentDeliveryTarget(event) {
|
||||
if (!event.expectsCompletionMessage) return;
|
||||
if (normalizeOptionalLowercaseString(event.requesterOrigin?.channel) !== "feishu") return;
|
||||
const binding = resolveMatchingChildBinding({
|
||||
accountId: event.requesterOrigin?.accountId,
|
||||
childSessionKey: event.childSessionKey,
|
||||
requesterSessionKey: event.requesterSessionKey,
|
||||
requesterOrigin: {
|
||||
to: event.requesterOrigin?.to,
|
||||
threadId: event.requesterOrigin?.threadId
|
||||
}
|
||||
});
|
||||
if (!binding) return;
|
||||
return { origin: resolveFeishuDeliveryOrigin({
|
||||
conversationId: binding.conversationId,
|
||||
parentConversationId: binding.parentConversationId,
|
||||
accountId: binding.accountId,
|
||||
deliveryTo: binding.deliveryTo,
|
||||
deliveryThreadId: binding.deliveryThreadId
|
||||
}) };
|
||||
}
|
||||
function handleFeishuSubagentEnded(event) {
|
||||
getFeishuThreadBindingManager(event.accountId)?.unbindBySessionKey(event.targetSessionKey);
|
||||
}
|
||||
//#endregion
|
||||
export { subagent_hooks_exports as i, handleFeishuSubagentEnded as n, handleFeishuSubagentSpawning as r, handleFeishuSubagentDeliveryTarget as t };
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
//#region extensions/feishu/subagent-hooks-api.ts
|
||||
let feishuSubagentHooksPromise = null;
|
||||
function loadFeishuSubagentHooksModule() {
|
||||
feishuSubagentHooksPromise ??= import("./subagent-hooks-1pqt5tAA.js").then((n) => n.i);
|
||||
return feishuSubagentHooksPromise;
|
||||
}
|
||||
function registerFeishuSubagentHooks(api) {
|
||||
api.on("subagent_delivery_target", async (event) => {
|
||||
const { handleFeishuSubagentDeliveryTarget } = await loadFeishuSubagentHooksModule();
|
||||
return handleFeishuSubagentDeliveryTarget(event);
|
||||
});
|
||||
api.on("subagent_ended", async (event) => {
|
||||
const { handleFeishuSubagentEnded } = await loadFeishuSubagentHooksModule();
|
||||
handleFeishuSubagentEnded(event);
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
export { registerFeishuSubagentHooks };
|
||||
Generated
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
//#region extensions/feishu/src/targets.ts
|
||||
const CHAT_ID_PREFIX = "oc_";
|
||||
const OPEN_ID_PREFIX = "ou_";
|
||||
const USER_ID_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
function stripProviderPrefix(raw) {
|
||||
return raw.replace(/^(feishu|lark):/i, "").trim();
|
||||
}
|
||||
function detectIdType(id) {
|
||||
const trimmed = id.trim();
|
||||
if (trimmed.startsWith(CHAT_ID_PREFIX)) return "chat_id";
|
||||
if (trimmed.startsWith(OPEN_ID_PREFIX)) return "open_id";
|
||||
if (USER_ID_REGEX.test(trimmed)) return "user_id";
|
||||
return null;
|
||||
}
|
||||
function normalizeFeishuTarget(raw) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return null;
|
||||
const withoutProvider = stripProviderPrefix(trimmed);
|
||||
const lowered = normalizeLowercaseStringOrEmpty(withoutProvider);
|
||||
if (lowered.startsWith("chat:")) return withoutProvider.slice(5).trim() || null;
|
||||
if (lowered.startsWith("group:")) return withoutProvider.slice(6).trim() || null;
|
||||
if (lowered.startsWith("channel:")) return withoutProvider.slice(8).trim() || null;
|
||||
if (lowered.startsWith("user:")) return withoutProvider.slice(5).trim() || null;
|
||||
if (lowered.startsWith("dm:")) return withoutProvider.slice(3).trim() || null;
|
||||
if (lowered.startsWith("open_id:")) return withoutProvider.slice(8).trim() || null;
|
||||
return withoutProvider;
|
||||
}
|
||||
function resolveReceiveIdType(id) {
|
||||
const trimmed = id.trim();
|
||||
const lowered = normalizeLowercaseStringOrEmpty(trimmed);
|
||||
if (lowered.startsWith("chat:") || lowered.startsWith("group:") || lowered.startsWith("channel:")) return "chat_id";
|
||||
if (lowered.startsWith("open_id:")) return "open_id";
|
||||
if (lowered.startsWith("user:") || lowered.startsWith("dm:")) return trimmed.replace(/^(user|dm):/i, "").trim().startsWith(OPEN_ID_PREFIX) ? "open_id" : "user_id";
|
||||
if (trimmed.startsWith(CHAT_ID_PREFIX)) return "chat_id";
|
||||
if (trimmed.startsWith(OPEN_ID_PREFIX)) return "open_id";
|
||||
return "user_id";
|
||||
}
|
||||
function looksLikeFeishuId(raw) {
|
||||
const trimmed = stripProviderPrefix(raw.trim());
|
||||
if (!trimmed) return false;
|
||||
if (/^(chat|group|channel|user|dm|open_id):/i.test(trimmed)) return true;
|
||||
if (trimmed.startsWith(CHAT_ID_PREFIX)) return true;
|
||||
if (trimmed.startsWith(OPEN_ID_PREFIX)) return true;
|
||||
return false;
|
||||
}
|
||||
//#endregion
|
||||
export { resolveReceiveIdType as i, looksLikeFeishuId as n, normalizeFeishuTarget as r, detectIdType as t };
|
||||
Generated
Vendored
+222
@@ -0,0 +1,222 @@
|
||||
import { registerSessionBindingAdapter, resolveThreadBindingConversationIdFromBindingId, resolveThreadBindingIdleTimeoutMsForChannel, resolveThreadBindingMaxAgeMsForChannel, unregisterSessionBindingAdapter } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { normalizeAccountId, resolveAgentIdFromSessionKey } from "openclaw/plugin-sdk/routing";
|
||||
//#region extensions/feishu/src/thread-bindings.ts
|
||||
const FEISHU_THREAD_BINDINGS_STATE_KEY = Symbol.for("openclaw.feishuThreadBindingsState");
|
||||
let state;
|
||||
function getState() {
|
||||
if (!state) {
|
||||
const globalStore = globalThis;
|
||||
state = globalStore[FEISHU_THREAD_BINDINGS_STATE_KEY] ?? {
|
||||
managersByAccountId: /* @__PURE__ */ new Map(),
|
||||
bindingsByAccountConversation: /* @__PURE__ */ new Map()
|
||||
};
|
||||
globalStore[FEISHU_THREAD_BINDINGS_STATE_KEY] = state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
function resolveBindingKey(params) {
|
||||
return `${params.accountId}:${params.conversationId}`;
|
||||
}
|
||||
function toSessionBindingTargetKind(raw) {
|
||||
return raw === "subagent" ? "subagent" : "session";
|
||||
}
|
||||
function toFeishuTargetKind(raw) {
|
||||
return raw === "subagent" ? "subagent" : "acp";
|
||||
}
|
||||
function toSessionBindingRecord(record, defaults) {
|
||||
const idleExpiresAt = defaults.idleTimeoutMs > 0 ? record.lastActivityAt + defaults.idleTimeoutMs : void 0;
|
||||
const maxAgeExpiresAt = defaults.maxAgeMs > 0 ? record.boundAt + defaults.maxAgeMs : void 0;
|
||||
const expiresAt = idleExpiresAt != null && maxAgeExpiresAt != null ? Math.min(idleExpiresAt, maxAgeExpiresAt) : idleExpiresAt ?? maxAgeExpiresAt;
|
||||
return {
|
||||
bindingId: resolveBindingKey({
|
||||
accountId: record.accountId,
|
||||
conversationId: record.conversationId
|
||||
}),
|
||||
targetSessionKey: record.targetSessionKey,
|
||||
targetKind: toSessionBindingTargetKind(record.targetKind),
|
||||
conversation: {
|
||||
channel: "feishu",
|
||||
accountId: record.accountId,
|
||||
conversationId: record.conversationId,
|
||||
parentConversationId: record.parentConversationId
|
||||
},
|
||||
status: "active",
|
||||
boundAt: record.boundAt,
|
||||
expiresAt,
|
||||
metadata: {
|
||||
agentId: record.agentId,
|
||||
label: record.label,
|
||||
boundBy: record.boundBy,
|
||||
deliveryTo: record.deliveryTo,
|
||||
deliveryThreadId: record.deliveryThreadId,
|
||||
lastActivityAt: record.lastActivityAt,
|
||||
idleTimeoutMs: defaults.idleTimeoutMs,
|
||||
maxAgeMs: defaults.maxAgeMs
|
||||
}
|
||||
};
|
||||
}
|
||||
function createFeishuThreadBindingManager(params) {
|
||||
const accountId = normalizeAccountId(params.accountId);
|
||||
const existing = getState().managersByAccountId.get(accountId);
|
||||
if (existing) return existing;
|
||||
const idleTimeoutMs = resolveThreadBindingIdleTimeoutMsForChannel({
|
||||
cfg: params.cfg,
|
||||
channel: "feishu",
|
||||
accountId
|
||||
});
|
||||
const maxAgeMs = resolveThreadBindingMaxAgeMsForChannel({
|
||||
cfg: params.cfg,
|
||||
channel: "feishu",
|
||||
accountId
|
||||
});
|
||||
const manager = {
|
||||
accountId,
|
||||
getByConversationId: (conversationId) => getState().bindingsByAccountConversation.get(resolveBindingKey({
|
||||
accountId,
|
||||
conversationId
|
||||
})),
|
||||
listBySessionKey: (targetSessionKey) => [...getState().bindingsByAccountConversation.values()].filter((record) => record.accountId === accountId && record.targetSessionKey === targetSessionKey),
|
||||
bindConversation: ({ conversationId, parentConversationId, targetKind, targetSessionKey, metadata }) => {
|
||||
const normalizedConversationId = conversationId.trim();
|
||||
const normalizedTargetSessionKey = targetSessionKey.trim();
|
||||
if (!normalizedConversationId || !normalizedTargetSessionKey) return null;
|
||||
const existingLocal = getState().bindingsByAccountConversation.get(resolveBindingKey({
|
||||
accountId,
|
||||
conversationId: normalizedConversationId
|
||||
}));
|
||||
const now = Date.now();
|
||||
const record = {
|
||||
accountId,
|
||||
conversationId: normalizedConversationId,
|
||||
parentConversationId: normalizeOptionalString(parentConversationId) ?? existingLocal?.parentConversationId,
|
||||
deliveryTo: typeof metadata?.deliveryTo === "string" && metadata.deliveryTo.trim() ? metadata.deliveryTo.trim() : existingLocal?.deliveryTo,
|
||||
deliveryThreadId: typeof metadata?.deliveryThreadId === "string" && metadata.deliveryThreadId.trim() ? metadata.deliveryThreadId.trim() : existingLocal?.deliveryThreadId,
|
||||
targetKind: toFeishuTargetKind(targetKind),
|
||||
targetSessionKey: normalizedTargetSessionKey,
|
||||
agentId: typeof metadata?.agentId === "string" && metadata.agentId.trim() ? metadata.agentId.trim() : existingLocal?.agentId ?? resolveAgentIdFromSessionKey(normalizedTargetSessionKey),
|
||||
label: typeof metadata?.label === "string" && metadata.label.trim() ? metadata.label.trim() : existingLocal?.label,
|
||||
boundBy: typeof metadata?.boundBy === "string" && metadata.boundBy.trim() ? metadata.boundBy.trim() : existingLocal?.boundBy,
|
||||
boundAt: now,
|
||||
lastActivityAt: now
|
||||
};
|
||||
getState().bindingsByAccountConversation.set(resolveBindingKey({
|
||||
accountId,
|
||||
conversationId: normalizedConversationId
|
||||
}), record);
|
||||
return record;
|
||||
},
|
||||
touchConversation: (conversationId, at = Date.now()) => {
|
||||
const key = resolveBindingKey({
|
||||
accountId,
|
||||
conversationId
|
||||
});
|
||||
const existingRecord = getState().bindingsByAccountConversation.get(key);
|
||||
if (!existingRecord) return null;
|
||||
const updated = {
|
||||
...existingRecord,
|
||||
lastActivityAt: at
|
||||
};
|
||||
getState().bindingsByAccountConversation.set(key, updated);
|
||||
return updated;
|
||||
},
|
||||
unbindConversation: (conversationId) => {
|
||||
const key = resolveBindingKey({
|
||||
accountId,
|
||||
conversationId
|
||||
});
|
||||
const existingRecord = getState().bindingsByAccountConversation.get(key);
|
||||
if (!existingRecord) return null;
|
||||
getState().bindingsByAccountConversation.delete(key);
|
||||
return existingRecord;
|
||||
},
|
||||
unbindBySessionKey: (targetSessionKey) => {
|
||||
const removed = [];
|
||||
for (const record of getState().bindingsByAccountConversation.values()) {
|
||||
if (record.accountId !== accountId || record.targetSessionKey !== targetSessionKey) continue;
|
||||
getState().bindingsByAccountConversation.delete(resolveBindingKey({
|
||||
accountId,
|
||||
conversationId: record.conversationId
|
||||
}));
|
||||
removed.push(record);
|
||||
}
|
||||
return removed;
|
||||
},
|
||||
stop: () => {
|
||||
for (const key of getState().bindingsByAccountConversation.keys()) if (key.startsWith(`${accountId}:`)) getState().bindingsByAccountConversation.delete(key);
|
||||
getState().managersByAccountId.delete(accountId);
|
||||
unregisterSessionBindingAdapter({
|
||||
channel: "feishu",
|
||||
accountId,
|
||||
adapter: sessionBindingAdapter
|
||||
});
|
||||
}
|
||||
};
|
||||
const sessionBindingAdapter = {
|
||||
channel: "feishu",
|
||||
accountId,
|
||||
capabilities: { placements: ["current"] },
|
||||
bind: async (input) => {
|
||||
if (input.conversation.channel !== "feishu" || input.placement === "child") return null;
|
||||
const bound = manager.bindConversation({
|
||||
conversationId: input.conversation.conversationId,
|
||||
parentConversationId: input.conversation.parentConversationId,
|
||||
targetKind: input.targetKind,
|
||||
targetSessionKey: input.targetSessionKey,
|
||||
metadata: input.metadata
|
||||
});
|
||||
return bound ? toSessionBindingRecord(bound, {
|
||||
idleTimeoutMs,
|
||||
maxAgeMs
|
||||
}) : null;
|
||||
},
|
||||
listBySession: (targetSessionKey) => manager.listBySessionKey(targetSessionKey).map((entry) => toSessionBindingRecord(entry, {
|
||||
idleTimeoutMs,
|
||||
maxAgeMs
|
||||
})),
|
||||
resolveByConversation: (ref) => {
|
||||
if (ref.channel !== "feishu") return null;
|
||||
const found = manager.getByConversationId(ref.conversationId);
|
||||
return found ? toSessionBindingRecord(found, {
|
||||
idleTimeoutMs,
|
||||
maxAgeMs
|
||||
}) : null;
|
||||
},
|
||||
touch: (bindingId, at) => {
|
||||
const conversationId = resolveThreadBindingConversationIdFromBindingId({
|
||||
accountId,
|
||||
bindingId
|
||||
});
|
||||
if (conversationId) manager.touchConversation(conversationId, at);
|
||||
},
|
||||
unbind: async (input) => {
|
||||
if (input.targetSessionKey?.trim()) return manager.unbindBySessionKey(input.targetSessionKey.trim()).map((entry) => toSessionBindingRecord(entry, {
|
||||
idleTimeoutMs,
|
||||
maxAgeMs
|
||||
}));
|
||||
const conversationId = resolveThreadBindingConversationIdFromBindingId({
|
||||
accountId,
|
||||
bindingId: input.bindingId
|
||||
});
|
||||
if (!conversationId) return [];
|
||||
const removed = manager.unbindConversation(conversationId);
|
||||
return removed ? [toSessionBindingRecord(removed, {
|
||||
idleTimeoutMs,
|
||||
maxAgeMs
|
||||
})] : [];
|
||||
}
|
||||
};
|
||||
registerSessionBindingAdapter(sessionBindingAdapter);
|
||||
getState().managersByAccountId.set(accountId, manager);
|
||||
return manager;
|
||||
}
|
||||
function getFeishuThreadBindingManager(accountId) {
|
||||
return getState().managersByAccountId.get(normalizeAccountId(accountId)) ?? null;
|
||||
}
|
||||
const testing = { resetFeishuThreadBindingsForTests() {
|
||||
for (const manager of getState().managersByAccountId.values()) manager.stop();
|
||||
getState().managersByAccountId.clear();
|
||||
getState().bindingsByAccountConversation.clear();
|
||||
} };
|
||||
//#endregion
|
||||
export { getFeishuThreadBindingManager as n, testing as r, createFeishuThreadBindingManager as t };
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Lark Technologies Pte. Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice, shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Generated
Vendored
+737
@@ -0,0 +1,737 @@
|
||||
# Feishu open interface SDK
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
[中文](https://github.com/larksuite/node-sdk/blob/main/README.zh.md)
|
||||
## Overview
|
||||
[Feishu Open Platform](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM) provides a series of atomic APIs on the server side to realize diversified functions, but the actual coding process is not very smooth: when using these APIs to complete operation, a lot of extra work needs to be considered, such as token acquisition and maintenance, data encryption and decryption, request signature verification, etc.; in the actual coding process, the semantics of function calls are missing, cause mental burden.
|
||||
|
||||
All of these make the overall development experience poor. In order to make the open capability easy to use, we have written this SDK, which integrates all the verbose logic into built-in processing, provides a complete type hints, and provides external semantics. Programming interface to improve coding experience.
|
||||
|
||||
Here are some official tutorials based on the sdk:
|
||||
* [Quickly develop interactive cards](https://open.feishu.cn/document/home/quickly-develop-interactive-cards/introduction)
|
||||
* [Quick Start of Personnel and Attendance Management System](https://open.feishu.cn/document/home/quick-start-of-personnel-and-attendance-management-system/overview)
|
||||
* [Quick access to Base](https://open.feishu.cn/document/home/quick-access-to-base/preparation)
|
||||
* [Department personnel management based on web app](https://open.feishu.cn/document/home/quick-access-to-base/department-personnel-management-based-on-web-app/overview)
|
||||
* [Quick access to Contact API](https://open.feishu.cn/document/home/quick-access-to-contact-api/introduction)
|
||||
* [Automatic attendance management based on approval](https://open.feishu.cn/document/home/automatic-attendance-management-based-on-approval/introduction)
|
||||
|
||||
## Concept
|
||||
- Development documentation: A reference to the open interface of the open platform, **a must-see for developers, you can use the search function to query documents efficiently**. [More introduction instructions](https://open.feishu.cn/document/) .
|
||||
|
||||
- Developer background: The management background of the developer's development application, [more introduction](https://open.feishu.cn/app/).
|
||||
|
||||
- Enterprise self-built Application: The application can only be installed and used within the enterprise, [more introduction](https://open.feishu.cn/document/uQjL04CN/ukzM04SOzQjL5MDN).
|
||||
|
||||
- ISV Application: The app will be listed in the [ISV Application](https://app.feishu.cn/?lang=zh-CN)
|
||||
Display, each enterprise can choose to install, [more introduction instructions](https://open.feishu.cn/document/uQjL04CN/ugTO5UjL4kTO14CO5kTN).
|
||||
|
||||
## Installation
|
||||
npm
|
||||
```shell script
|
||||
npm install @larksuiteoapi/node-sdk
|
||||
````
|
||||
yarn
|
||||
````
|
||||
yarn add @larksuiteoapi/node-sdk
|
||||
````
|
||||
|
||||
## How to use
|
||||
Provides two versions of ECMAScript and CommonJS, and supports the use of native Javascript and Typescript. The examples are all taking Typescript as an example.
|
||||
|
||||
Typescript
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
````
|
||||
CommonJS
|
||||
````javascript
|
||||
const lark = require('@larksuiteoapi/node-sdk');
|
||||
````
|
||||
ECMAScript
|
||||
````javascript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
````
|
||||
### API Call
|
||||
The list of all APIs on Feishu Open Platform: [click here](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/server-api-list).
|
||||
|
||||
The SDK provides a semantic calling method. You only need to construct a client instance according to the relevant parameters, and then use the semantic method (*client.business domain.resource.method*) on it to complete the API call, the calling process and the calling result. There are complete types for prompting, such as sending a message to a group chat:
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const client = new lark.Client({
|
||||
appId: 'app id',
|
||||
appSecret: 'app secret',
|
||||
appType: lark.AppType.SelfBuild,
|
||||
domain: lark.Domain.Feishu,
|
||||
});
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'receive_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text',
|
||||
},
|
||||
});
|
||||
````
|
||||
> tips:
|
||||
> * If you want to debug an API, you can click the link in the comment to enter the API debugging platform for debugging:
|
||||
> 
|
||||
> * How to obtain the semantic call interface:[Click here](https://github.com/larksuite/node-sdk/issues/42)
|
||||
|
||||
#### Create Client
|
||||
For self-built applications, you can use the following code to create a client:
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const client = new lark. Client({
|
||||
appId: 'app id',
|
||||
appSecret: 'app secret'
|
||||
});
|
||||
````
|
||||
|
||||
For store apps, the specified appType that needs to be displayed is lark.AppType.ISV:
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const client = new lark.Client({
|
||||
appId: 'app id',
|
||||
appSecret: 'app secret',
|
||||
appType: lark.AppType.ISV,
|
||||
});
|
||||
````
|
||||
**When using the client of the created store application to initiate an API call, you need to manually pass the [tenant_key](https://open.feishu.cn/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/g#d15ab5d)**, you can Use `lark.withTenantKey` to do it:
|
||||
```typescript
|
||||
client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'chat_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
}, lark.withTenantKey('tenant key'));
|
||||
````
|
||||
|
||||
#### `Client` construction parameters:
|
||||
| Parameter | Description | Type | Required | Default |
|
||||
| ---- | ---- | ---- | ---- | ---- |
|
||||
| appId | app id | string | yes | - |
|
||||
| appSecret | app secret | string | yes | - |
|
||||
| domain | The domain of the application, divided into Feishu (https://open.feishu.cn), lark (https://open.larksuite.com), others (the complete domain name needs to be passed) | Domain | string | no | Domain.Feishu |
|
||||
| httpInstance | The http instance of the sdk sending request。*By default, the sdk uses axios.create() to construct a defaultHttpInstance to make http calls.* | HttpInstance | No | defaultHttpInstance。*It can be imported from sdk and add interceptors on it.* |
|
||||
| loggerLevel | Log Level | LoggerLevel | No | info |
|
||||
| logger | - | Logger | No | - |
|
||||
| cache | Cache | Cache | No | - |
|
||||
| disableTokenCache | Whether to disable the cache, if disabled, the token will not be cached, and it will be re-pulled every time it needs to be used | boolean | No | false |
|
||||
| appType | The type of application, divided into store application or self-built application | AppType | No | AppType.SelfBuild |
|
||||
| helpDeskId | helpdesk id | string | no | - |
|
||||
| helpDeskToken | helpdesk token | string | no | - |
|
||||
|
||||
#### Pagination
|
||||
For the interface whose return value is presented in the form of pagination, it provides iterator encapsulation (the method name suffix is WithIterator), which improves the usability and eliminates the tedious operation of repeatedly obtaining data according to page_token, such as obtaining the user list:
|
||||
``` typescript
|
||||
// Process 20 pieces of data each time
|
||||
for await (const items of await client.contact.user.listWithIterator({
|
||||
params: {
|
||||
department_id: '0',
|
||||
page_size: 20,
|
||||
},
|
||||
})) {
|
||||
console.log(items);
|
||||
}
|
||||
|
||||
// You can also use next to manually control the iteration, fetching 20 pieces of data each time
|
||||
const listIterator = await SDKClient.contact.user.listWithIterator({
|
||||
params: {
|
||||
department_id: '0',
|
||||
page_size: 20,
|
||||
},
|
||||
});
|
||||
const { value } = await listIterator[Symbol.asyncIterator]().next();
|
||||
console.log(value);
|
||||
````
|
||||
* Of course, you can also use the version without iterator encapsulation. In this case, you need to manually perform paging calls each time according to the returned page_token. *
|
||||
#### File upload
|
||||
In the same way as calling ordinary API, you can pass the parameters according to the type prompt, and the processing of file upload is encapsulated inside, such as:
|
||||
```typescript
|
||||
const res = await client.im.file.create({
|
||||
data: {
|
||||
file_type: 'mp4',
|
||||
file_name: 'test.mp4',
|
||||
file: fs.readFileSync('file path'),
|
||||
},
|
||||
});
|
||||
````
|
||||
#### File download
|
||||
The returned binary stream is encapsulated, eliminating the processing of the stream itself, just call the writeFile method to write the data to the file, such as:
|
||||
```typescript
|
||||
const resp = await client.im.file.get({
|
||||
path: {
|
||||
file_key: 'file key',
|
||||
},
|
||||
});
|
||||
await resp.writeFile(`filepath.suffix`);
|
||||
````
|
||||
If you want to customize the processing of the stream, you can call the getReadableStream method to obtain the stream, such as writing the stream to a file:
|
||||
```typescript
|
||||
import * as fs from 'fs';
|
||||
|
||||
const resp = await client.im.file.get({
|
||||
path: {
|
||||
file_key: 'file key',
|
||||
},
|
||||
});
|
||||
const readableStream = resp.getReadableStream();
|
||||
const writableStream = fs.createWriteStream('file url');
|
||||
readableStream.pipe(writableStream);
|
||||
```
|
||||
> Note: The stream can only be consumed once, if writeFile is used to consume the stream, getReadableStream will report an error when obtaining the stream/the obtained stream is empty; If you need to consume multiple streams, you can use getReadableStream to obtain the stream, then read the data in the stream for caching, and use the cached data to the consumer.
|
||||
|
||||
#### Normal call
|
||||
Some old versions of the open interface cannot generate corresponding semantic calling methods, and you need to use the request method on the client to make manual calls:
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const client = new lark. Client({
|
||||
appId: 'app id',
|
||||
appSecret: 'app secret',
|
||||
appType: lark.AppType.SelfBuild,
|
||||
domain: lark.Domain.Feishu,
|
||||
});
|
||||
|
||||
const res = await client. request({
|
||||
method: 'POST',
|
||||
url: 'xxx',
|
||||
data: {},
|
||||
params: {},
|
||||
});
|
||||
````
|
||||
#### Message Card
|
||||
When sending [message card](https://open.feishu.cn/document/ukTMukTMukTM/uczM3QjL3MzN04yNzcDN), it will first be in the [message card builder](https://open.feishu.cn/document/ukTMukTMukTM/uYzM3QjL2MzN04iNzcDN/message-card-builder) to build a message card template, get the generated template json, replace the content-related parts with data, and use the result as a parameter to support the message card api. Such as sending a simple message card with `title` and `content`:
|
||||
```typescript
|
||||
client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'your receive_id',
|
||||
content: JSON.stringify({
|
||||
"config": {
|
||||
"wide_screen_mode": true
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": "Card Content"
|
||||
}
|
||||
],
|
||||
"header": {
|
||||
"template": "blue",
|
||||
"title": {
|
||||
"content": "Card Title",
|
||||
"tag": "plain_text"
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
msg_type: 'interactive'
|
||||
}
|
||||
})
|
||||
```
|
||||

|
||||
There will be a problem: **If the content of the message card is relatively rich, the generated template json is relatively large, and there will be more content that needs to be filled with data, and manual maintenance is more cumbersome**. To solve this problem, The Open-Platform provides the ability of [Template Message](https://open.feishu.cn/document/tools-and-resources/message-card-builder#3e1f2c7c).When sending a message card, you only need to provide the template id and the data content of the template. The sdk encapsulates this ability in terms of calling, and the interface that supports message cards will synchronously add a ByCard calling method, only need to pass `template_id` and `template_variable`. The above call can be rewritten as:
|
||||
```typescript
|
||||
client.im.message.createByCard({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'your receive_id',
|
||||
template_id: 'your template_id',
|
||||
template_variable: {
|
||||
content: "Card Content",
|
||||
title: "Card Title"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
If you want to quickly experience Message Card, you can use a basic card built into the sdk:
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'your receive_id',
|
||||
content: lark.messageCard.defaultCard({
|
||||
title: 'Card Title',
|
||||
content: 'Card Content'
|
||||
}),
|
||||
msg_type: 'interactive'
|
||||
}
|
||||
})
|
||||
```
|
||||

|
||||
|
||||
#### Configure request options
|
||||
If you want to modify the parameters of the request during the API call, such as carrying some headers, custom tenantToken, etc., you can use the second parameter of the request method to modify:
|
||||
```typescript
|
||||
await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'receive_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text',
|
||||
},
|
||||
}, {
|
||||
headers: {
|
||||
customizedHeaderKey: 'customizedHeaderValue'
|
||||
}
|
||||
});
|
||||
````
|
||||
The SDK also encapsulates commonly used modification operations into methods, which can be used:
|
||||
|
||||
| Method | Description |
|
||||
| ---- | ---- |
|
||||
| withTenantKey | Set tenant key |
|
||||
| withTenantToken | Set tenant token |
|
||||
| withHelpDeskCredential | Whether to bring in the [Service Desk token](https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN) |
|
||||
| withUserAccessToken | Set access token |
|
||||
| withAll | Combines the results of the above methods |
|
||||
|
||||
```typescript
|
||||
await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'receive_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text',
|
||||
},
|
||||
}, lark.withTenantToken('tenant token'));
|
||||
|
||||
await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'receive_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text',
|
||||
},
|
||||
}, lark.withAll([
|
||||
lark.withTenantToken('tenant token'),
|
||||
lark.withTenantKey('tenant key')
|
||||
]));
|
||||
````
|
||||
|
||||
### Events handling
|
||||
For a list of all events opened on Feishu Open Platform, please click [here](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-list).
|
||||
|
||||
For the event processing scenario, we care about is only what kind of events to listen for, and what we do after the event occurs. other work such as data decryption we don't want to care about. The SDK provides an intuitive way to describe this part of the logic:
|
||||
1. Construct an instance of the event handler `EventDispatcher`;
|
||||
2. Register the events to be monitored and their handler functions on the instance;
|
||||
3. Bind the instance to the service;
|
||||
|
||||
`EventDispatcher` will perform operations such as data decryption internally. If no relevant parameters are passed, it will be automatically ignored.
|
||||
```typescript
|
||||
import http from 'http';
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const eventDispatcher = new lark.EventDispatcher({
|
||||
encryptKey: 'encrypt key'
|
||||
}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const chatId = data.message.chat_id;
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: chatId,
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
});
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
const server = http.createServer();
|
||||
server.on('request', lark.adaptDefault('/webhook/event', eventDispatcher));
|
||||
server.listen(3000);
|
||||
````
|
||||
|
||||
#### `EventDispatcher` constructor parameters
|
||||
|
||||
| Parameter | Description | Type | Required | Default |
|
||||
| ---- | ---- | ---- | ---- | ---- |
|
||||
| [encryptKey](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case) | Push data encryption key, required when enabling encrypted push use for data decryption | string | no | - |
|
||||
| loggerLevel | log level | LoggerLevel | no | lark.LoggerLevel.info |
|
||||
| logger | - | Logger | No | - |
|
||||
| cache | Cache | Cache | No | - |
|
||||
|
||||
> Note: Some events are v1.0 version and are no longer maintained. The SDK retains support for them. It is strongly recommended to use new versions of events that are consistent with their functions. Move the mouse to the corresponding event subscription function to see the relevant documents:
|
||||

|
||||
|
||||
#### Combined with express
|
||||
The SDK provides an adapter for experss to convert eventDispatcher into express middleware, which can be seamlessly combined with services written using express (*The use of bodyParser in the example is not necessary, but the community mostly uses it to format body data*):
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
|
||||
const server = express();
|
||||
server.use(bodyParser.json());
|
||||
|
||||
const eventDispatcher = new lark.EventDispatcher({
|
||||
encryptKey: 'encryptKey',
|
||||
}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const chatId = data.message.chat_id;
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: chatId,
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
});
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
server.use('/webhook/event', lark.adaptExpress(eventDispatcher));
|
||||
server.listen(3000);
|
||||
````
|
||||
#### Combined with Koa
|
||||
The SDK provides an adapter for Koa to convert eventDispatcher into Koa middleware, which can be seamlessly combined with services written using Koa (*The use of koa-body in the example is not necessary, but the community mostly uses it to format body data*):
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
import Koa from 'koa';
|
||||
import koaBody from 'koa-body';
|
||||
|
||||
const server = new Koa();
|
||||
server.use(koaBody());
|
||||
|
||||
const eventDispatcher = new lark.EventDispatcher({
|
||||
encryptKey: 'encryptKey',
|
||||
}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const open_chat_id = data.message.chat_id;
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: open_chat_id,
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
server.use(lark.adaptKoa('/webhook/event', eventDispatcher));
|
||||
server.listen(3000);
|
||||
````
|
||||
#### Combined with koa-router
|
||||
When using Koa to write services, in most cases, koa-router is used to process routing, so the SDK also provides adaptations for this situation:
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import koaBody from 'koa-body';
|
||||
|
||||
const server = new Koa();
|
||||
const router = new Router();
|
||||
server.use(koaBody());
|
||||
|
||||
const eventDispatcher = new lark.EventDispatcher({
|
||||
encryptKey: 'encryptKey',
|
||||
}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const open_chat_id = data.message.chat_id;
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: open_chat_id,
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
router.post('/webhook/event', lark.adaptKoaRouter(eventDispatcher));
|
||||
server.use(router.routes());
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
#### Custom adapter
|
||||
|
||||
If you want to adapt to services written by other libraries, you currently need to encapsulate the corresponding adapter yourself. Pass the received event data and request headers to the invoke method of the instantiated `eventDispatcher` for event processing:
|
||||
|
||||
```typescript
|
||||
const data = server.getData();
|
||||
const headers = server.getHeaders();
|
||||
const assigned = Object.assign(
|
||||
Object.create({ headers }),
|
||||
data,
|
||||
);
|
||||
const result = await dispatcher.invoke(assigned);
|
||||
server.sendResult(result);
|
||||
```
|
||||
|
||||
#### Challenge check
|
||||
When configuring the event request address, The Open Platform will push a POST request in `application/json` format to the request address. The POST request is used to verify the validity of the configured request address, and the request body will carry a `challenge` field , **The application needs to return the received challenge value to the Open Platform within 1 second**. See: [ Document](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case)
|
||||
|
||||
The adapter provided by the sdk above encapsulates the verification logic. Set the `autoChallenge` field in the options parameter to true to enable:
|
||||
```typescript
|
||||
// adaptDefault
|
||||
lark.adaptDefault('/webhook/event', eventDispatcher, {
|
||||
autoChallenge: true,
|
||||
});
|
||||
// express
|
||||
lark.adaptExpress(eventDispatcher, {
|
||||
autoChallenge: true,
|
||||
});
|
||||
// koa
|
||||
lark.adaptKoa('/webhook/event', eventDispatcher, {
|
||||
autoChallenge: true,
|
||||
});
|
||||
// koa-router
|
||||
router.post(
|
||||
'/webhook/event',
|
||||
lark.adaptKoaRouter(eventDispatcher, {
|
||||
autoChallenge: true,
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### Subscribing to Events Using Long Connection Mode
|
||||
Official Documentation:[Documentation:](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/event-subscription-guide/long-connection-mode#62c8b8c8)
|
||||
|
||||
Developers establish a WebSocket full-duplex channel with the open platform by integrating the Lark SDK. When an event callback occurs, the open platform will send messages to the developer through this channel.Compared with the traditional Webhook mode, the long connection mode significantly reduces the access cost, reducing the original development cycle of about 1 week to 5 minutes. The specific advantages are as follows:
|
||||
|
||||
* There is no need to use intranet penetration tools during the testing phase. Event callbacks can be received in the local development environment through the long connection mode.
|
||||
* Authentication is only performed when the connection is established. Subsequent event pushes are all plaintext data, and developers do not need to handle decryption and signature verification logic.
|
||||
* Only need to ensure that the running environment has the ability to access the public network, no need to provide public IP or domain name.
|
||||
* There is no need to deploy a firewall and configure a whitelist.
|
||||
|
||||
> Points to Note
|
||||
* Similar to Webhook, under the long connection mode, developers need to complete processing within 3 seconds after receiving a message, otherwise, a timeout re-push will be triggered.
|
||||
* Message pushing is in cluster mode, it does not support broadcasting, that is, if multiple clients are deployed for the same application, only one random client will receive the message.
|
||||
* Currently, the long connection mode only supports event subscriptions and does not support callback subscriptions
|
||||
|
||||
SDK supports this function integration,`1.24.0` and later versions are available, sample code:
|
||||
```typescript
|
||||
import * as Lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const baseConfig = {
|
||||
appId: 'xxx',
|
||||
appSecret: 'xxx'
|
||||
}
|
||||
|
||||
const client = new Lark.Client(baseConfig);
|
||||
|
||||
const wsClient = new Lark.WSClient({...baseConfig, loggerLevel: Lark.LoggerLevel.info});
|
||||
|
||||
wsClient.start({
|
||||
eventDispatcher: new Lark.EventDispatcher({}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const {
|
||||
message: { chat_id, content}
|
||||
} = data;
|
||||
await client.im.v1.message.create({
|
||||
params: {
|
||||
receive_id_type: "chat_id"
|
||||
},
|
||||
data: {
|
||||
receive_id: chat_id,
|
||||
content: Lark.messageCard.defaultCard({
|
||||
title: `reply: ${JSON.parse(content).text}`,
|
||||
content: 'hello'
|
||||
}),
|
||||
msg_type: 'interactive'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
### Channel module
|
||||
|
||||
`Channel` is a high-level module built on top of `WSClient` / `Client` that bundles transport, message normalization, safety policy, outbound sending, streaming replies, media upload, and card interactions into a single abstraction. It is the recommended entry point for conversational bots (AI chat, streaming replies, interactive card buttons, etc.).
|
||||
|
||||
```typescript
|
||||
import { createLarkChannel } from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const channel = createLarkChannel({ appId, appSecret });
|
||||
|
||||
channel.on('message', async (msg) => {
|
||||
await channel.send(
|
||||
msg.chatId,
|
||||
{ markdown: `received: ${msg.content}` },
|
||||
{ replyTo: msg.messageId },
|
||||
);
|
||||
});
|
||||
|
||||
await channel.connect();
|
||||
```
|
||||
|
||||
Full reference (event subscription, sending, streaming, low-level helpers, error handling, configuration, etc.) — see [docs/channel.md](./docs/channel.md).
|
||||
|
||||
### [Message Card](https://open.feishu.cn/document/ukTMukTMukTM/uczM3QjL3MzN04yNzcDN)
|
||||
|
||||
The processing of the Message Card is also a kind of Event processing. The only difference between the two is that the processor of the Message Card is used to respond to the events generated by the interaction between the user and the Message Card. If the processor has a return value (*the value structure should be in line with the structure defined by [Message Card Structure](https://open.feishu.cn/document/ukTMukTMukTM/uEjNwUjLxYDM14SM2ATN)*), then the return value is used to update the responded message card:
|
||||
|
||||
```typescript
|
||||
import http from 'http';
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
import type { InteractiveCardActionEvent, InteractiveCard } from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const cardDispatcher = new lark.CardActionHandler(
|
||||
{
|
||||
encryptKey: 'encrypt key',
|
||||
verificationToken: 'verification token'
|
||||
},
|
||||
async (data: InteractiveCardActionEvent) => {
|
||||
console.log(data);
|
||||
const newCard: InteractiveCard = {
|
||||
// your new interactive card content
|
||||
header: {},
|
||||
elements: []
|
||||
};
|
||||
return newCard;
|
||||
}
|
||||
);
|
||||
|
||||
const server = http.createServer();
|
||||
server.on('request', lark.adaptDefault('/webhook/card', cardDispatcher));
|
||||
server.listen(3000);
|
||||
````
|
||||
|
||||
#### `CardActionHandler` construction parameters
|
||||
|
||||
| Parameter | Description | Type | Required | Default |
|
||||
| ---- | ---- | ---- | ---- | ---- |
|
||||
| [encryptKey](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case) | Push data encryption key, required when enabling encrypted push use for data decryption | string | no | - |
|
||||
| [verificationToken](https://open.feishu.cn/document/ukTMukTMukTM/uYzMxEjL2MTMx4iNzETM) | Security verification, it needs to be used when enabling message security verification | string | No | - |
|
||||
| loggerLevel | Log Level | LoggerLevel | No | LoggerLevel.info |
|
||||
| logger | - | Logger | No | - |
|
||||
| cache | Cache | Cache | No | - |
|
||||
|
||||
### App Registration
|
||||
The SDK provides a `registerApp` method for one-click app creation based on the OAuth 2.0 Device Authorization Grant (RFC 8628) protocol. It returns a verification URL that users can open in Feishu/Lark to authorize and automatically register an app, obtaining credentials without manually creating one on the developer console.
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
try {
|
||||
const result = await lark.registerApp({
|
||||
onQRCodeReady(info) {
|
||||
console.log(`Scan the QR code: ${info.url}`);
|
||||
console.log(`Link expires in ${info.expireIn} seconds`);
|
||||
},
|
||||
onStatusChange(info) {
|
||||
// Handle status changes: 'polling' | 'slow_down' | 'domain_switched'
|
||||
},
|
||||
});
|
||||
|
||||
console.log('App ID:', result.client_id);
|
||||
console.log('App Secret:', result.client_secret);
|
||||
|
||||
// Use the credentials to initialize a Client
|
||||
const client = new lark.Client({
|
||||
appId: result.client_id,
|
||||
appSecret: result.client_secret,
|
||||
});
|
||||
} catch (e) {
|
||||
// e.code: 'access_denied' | 'expired_token' | 'abort' | ...
|
||||
// e.description: error description
|
||||
console.error(e.code, e.description);
|
||||
}
|
||||
```
|
||||
|
||||
#### `registerApp` parameters
|
||||
|
||||
| Parameter | Description | Type | Required | Default |
|
||||
| ---- | ---- | ---- | ---- | ---- |
|
||||
| domain | Custom accounts domain (host only) | string | No | `accounts.feishu.cn` |
|
||||
| larkDomain | Custom Lark accounts domain (host only), used when tenant brand is detected as Lark | string | No | `accounts.larksuite.com` |
|
||||
| source | Source identifier, appended to the QR code URL `from` parameter as `node-sdk/{source}` | string | No | - |
|
||||
| signal | `AbortSignal` to cancel the polling | AbortSignal | No | - |
|
||||
| onQRCodeReady | Callback when the verification URL is ready. Receives `{ url, expireIn }`. You can render the URL as a QR code for users to scan, or display it as a link | function | Yes | - |
|
||||
| onStatusChange | Callback on polling status changes. Receives `{ status, interval? }`. Status values: `polling`, `slow_down`, `domain_switched` | function | No | - |
|
||||
| appPreset | Pre-fill values for the app-creation page. All fields are optional; users can still edit them on the page | AppPreset | No | - |
|
||||
| appPreset.avatar | App avatar URL(s). 1-6 URLs supported; the first one is selected by default. Allowed formats: png / jpg / jpeg / webp / gif (gif is sampled to a single frame, not animated) | string \| string[] | No | - |
|
||||
| appPreset.name | App name. Supports the `{user}` placeholder (replaced with the scanning user's name) | string | No | - |
|
||||
| appPreset.desc | App description. Supports the `{user}` placeholder | string | No | - |
|
||||
|
||||
#### Return value
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---- | ---- | ---- |
|
||||
| client_id | string | App ID |
|
||||
| client_secret | string | App Secret |
|
||||
| user_info | object (optional) | Scanning user info |
|
||||
| user_info.open_id | string (optional) | User's open_id |
|
||||
| user_info.tenant_brand | string (optional) | `"feishu"` or `"lark"` |
|
||||
|
||||
#### Error handling
|
||||
The thrown error object contains `code` and `description` fields:
|
||||
|
||||
| code | Description |
|
||||
| ---- | ---- |
|
||||
| `access_denied` | User denied the authorization |
|
||||
| `expired_token` | QR code expired or polling timed out |
|
||||
| `abort` | Cancelled via AbortSignal |
|
||||
|
||||
### Tool method
|
||||
#### AESCipher
|
||||
Decrypt. If [Encrypted Push](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case) is configured, the open platform will push encrypted data, At this time, the data needs to be decrypted, and this method can be called for convenient decryption. (In general, the decryption logic is built into the SDK, and no manual processing is required).
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
new lark.AESCipher('encrypt key').decrypt('content');
|
||||
````
|
||||
|
||||
## Examples
|
||||
[Rapidly develop auto-responder bot](https://github.com/larksuite/lark-samples/blob/main/react_and_nodejs/robot/README.zh.md)
|
||||
|
||||
## Blog
|
||||
[ISV Application Development Guide](https://bytedance.feishu.cn/docx/RUZKdGwdyoH4KexMJgDcITQnn0b)
|
||||
|
||||
## LICENSE
|
||||
MIT
|
||||
|
||||
## Contact Us
|
||||
Click [Server SDK](https://open.feishu.cn/document/ukTMukTMukTM/uETO1YjLxkTN24SM5UjN) in the upper right corner of the page 【Is this document helpful to you? 】Submit feedback
|
||||
Generated
Vendored
+807
@@ -0,0 +1,807 @@
|
||||
# 飞书开放接口SDK
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
[English](https://github.com/larksuite/node-sdk/blob/main/README.md)
|
||||
|
||||
## 概述
|
||||
|
||||
[飞书开放平台](https://open.feishu.cn/document/ukTMukTMukTM/uITNz4iM1MjLyUzM)提供了一系列服务端的原子api来实现多元化的功能,但在实际编码过程中感受不是很顺畅,原因在于使用这些api完成功能时,需要考虑很多额外的工作,如token的获取及其维护、数据加解密、请求的验签等等;再者,在实际编码过程中,少了函数调用的语义化描述,类型系统的支持,使得心智负担过重。
|
||||
|
||||
凡此种种,都使得整体的开发体验不佳,基于此,为了让开放能力变得易用,我们编写了该SDK,将所有冗长的逻辑内置处理,提供完备的类型系统,对外提供语义化的编程接口,提高编码体验。😙
|
||||
|
||||
以下是一些基于该sdk的官方教程:
|
||||
|
||||
- [快速开发互动卡片](https://open.feishu.cn/document/home/quickly-develop-interactive-cards/introduction)
|
||||
- [快速开发人员及考勤管理系统](https://open.feishu.cn/document/home/quick-start-of-personnel-and-attendance-management-system/overview)
|
||||
- [快速接入多维表格](https://open.feishu.cn/document/home/quick-access-to-base/preparation)
|
||||
- [基于网页应用实现部门人员管理](https://open.feishu.cn/document/home/quick-access-to-base/department-personnel-management-based-on-web-app/overview)
|
||||
- [快速接入通讯录](https://open.feishu.cn/document/home/quick-access-to-contact-api/introduction)
|
||||
- [基于审批实现自动考勤管理](https://open.feishu.cn/document/home/automatic-attendance-management-based-on-approval/introduction)
|
||||
|
||||
## 概念
|
||||
|
||||
- 开发文档:开放平台的开放接口的参考,**开发者必看,可以使用搜索功能,高效的查询文档**。[更多介绍说明](https://open.feishu.cn/document/) 。
|
||||
- 开发者后台:开发者开发应用的管理后台,[更多介绍说明](https://open.feishu.cn/app/) 。
|
||||
- 企业自建应用:应用仅仅可在本企业内安装使用,[更多介绍说明](https://open.feishu.cn/document/uQjL04CN/ukzM04SOzQjL5MDN) 。
|
||||
- 应用商店应用:应用会在 [应用目录](https://app.feishu.cn/?lang=zh-CN)
|
||||
展示,各个企业可以选择安装,[更多介绍说明](https://open.feishu.cn/document/uQjL04CN/ugTO5UjL4kTO14CO5kTN) 。
|
||||
|
||||
## 安装
|
||||
|
||||
npm
|
||||
|
||||
```shell
|
||||
npm install @larksuiteoapi/node-sdk
|
||||
```
|
||||
|
||||
yarn
|
||||
|
||||
```
|
||||
yarn add @larksuiteoapi/node-sdk
|
||||
```
|
||||
|
||||
## 如何使用
|
||||
|
||||
提供ECMAScript,CommonJS2个版本,支持原生Javascript和Typescript的使用,示例均以Typescript为例。
|
||||
|
||||
Typescript
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
```
|
||||
|
||||
CommonJS
|
||||
|
||||
```javascript
|
||||
const lark = require('@larksuiteoapi/node-sdk');
|
||||
```
|
||||
|
||||
ECMAScript
|
||||
|
||||
```javascript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
```
|
||||
|
||||
### api调用
|
||||
|
||||
飞书开放平台开放的所有 API 列表,可点击[这里查看](https://open.feishu.cn/document/ukTMukTMukTM/uYTM5UjL2ETO14iNxkTN/server-api-list)。
|
||||
|
||||
SDK提供了语义化的调用方式,只需要依据相关参数构造出client实例,接着使用其上的语义化方法(*client.业务域.资源.方法*)即可完成api调用,调用过程及调用结果均有完备的类型进行提示,如向群聊中发送消息:
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const client = new lark.Client({
|
||||
appId: 'app id',
|
||||
appSecret: 'app secret',
|
||||
appType: lark.AppType.SelfBuild,
|
||||
domain: lark.Domain.Feishu,
|
||||
});
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'receive_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> tips:
|
||||
>
|
||||
> - 如果想调试某个api,可以点击注释中的链接进入api调试台进行调试:
|
||||
> 
|
||||
> - 如何获取语义化调用接口:[点击这里](https://github.com/larksuite/node-sdk/issues/42)
|
||||
|
||||
#### 创建client
|
||||
|
||||
对于自建应用,可以使用下面的代码创建一个client:
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const client = new lark.Client({
|
||||
appId: 'app id',
|
||||
appSecret: 'app secret'
|
||||
});
|
||||
```
|
||||
|
||||
对于商店应用,需要显示的指定appType为lark.AppType.ISV:
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const client = new lark.Client({
|
||||
appId: 'app id',
|
||||
appSecret: 'app secret',
|
||||
appType: lark.AppType.ISV,
|
||||
});
|
||||
```
|
||||
|
||||
**使用创建好的商店应用的client发起api调用时,还需在请求时手动传递[tenant\_key](https://open.feishu.cn/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/g#d15ab5d)**,可以使用lark.withTenantKey来完成:
|
||||
|
||||
```typescript
|
||||
client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'chat_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
}, lark.withTenantKey('tenant key'));
|
||||
```
|
||||
|
||||
#### `Client`构造参数:
|
||||
|
||||
| 参数 | 描述 | 类型 | 必须 | 默认 |
|
||||
| ----------------- | ---------------------------------------------------------------------------------- | ---------------- | -- | -------------------------------------------------------------- |
|
||||
| appId | 应用的id | string | 是 | - |
|
||||
| appSecret | 应用的密码 | string | 是 | - |
|
||||
| domain | 应用的域,分为飞书(<https://open.feishu.cn)、lark(https://open.larksuite.com)、其它(需要传递完整的域名)> | Domain \| string | 否 | Domain.Feishu |
|
||||
| httpInstance | sdk发送请求的http实例。*sdk内部默认使用axios.create()构造出一个defaultHttpInstance来进行http调用。* | HttpInstance | 否 | defaultHttpInstance。*可以从sdk中import它,在其上添加interceptors来完成业务需求。* |
|
||||
| loggerLevel | 日志级别 | LoggerLevel | 否 | info |
|
||||
| logger | - | Logger | 否 | - |
|
||||
| cache | 缓存器 | Cache | 否 | - |
|
||||
| disableTokenCache | 是否禁用缓存,如若禁用,则token等不会进行缓存,每次需要使用时都会重新拉取 | boolean | 否 | false |
|
||||
| appType | 应用的类型,分为商店应用或者自建应用 | AppType | 否 | AppType.SelfBuild |
|
||||
| helpDeskId | 服务台id | string | 否 | - |
|
||||
| helpDeskToken | 服务台token | string | 否 | - |
|
||||
|
||||
#### 分页
|
||||
|
||||
针对返回值以分页形式呈现的接口,对其提供了迭代器方式的封装(方法名后缀为WithIterator),提高易用性,消弭了根据page\_token来反复获取数据的繁琐操作,如获取用户列表:
|
||||
|
||||
```typescript
|
||||
// 每次处理20条数据
|
||||
for await (const items of await client.contact.user.listWithIterator({
|
||||
params: {
|
||||
department_id: '0',
|
||||
page_size: 20,
|
||||
},
|
||||
})) {
|
||||
console.log(items);
|
||||
}
|
||||
|
||||
// 也可用next来手动控制迭代,每次取20条数据
|
||||
const listIterator = await SDKClient.contact.user.listWithIterator({
|
||||
params: {
|
||||
department_id: '0',
|
||||
page_size: 20,
|
||||
},
|
||||
});
|
||||
const { value } = await listIterator[Symbol.asyncIterator]().next();
|
||||
console.log(value);
|
||||
```
|
||||
|
||||
*当然也可以使用无迭代器封装的版本,这时候需要自己每次根据返回的page\_token来手动进行分页调用。*
|
||||
|
||||
#### 文件上传
|
||||
|
||||
和调用普通api的方式一样,按类型提示传递参数即可,内部封装了对文件上传的处理,如:
|
||||
|
||||
```typescript
|
||||
const res = await client.im.file.create({
|
||||
data: {
|
||||
file_type: 'mp4',
|
||||
file_name: 'test.mp4',
|
||||
file: fs.readFileSync('file path'),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### 文件下载
|
||||
|
||||
对返回的二进制流进行了封装,消弭了对流本身的处理,只需调用writeFile方法即可将数据写入文件,如:
|
||||
|
||||
```typescript
|
||||
const resp = await client.im.file.get({
|
||||
path: {
|
||||
file_key: 'file key',
|
||||
},
|
||||
});
|
||||
await resp.writeFile(`filepath.suffix`);
|
||||
```
|
||||
|
||||
如果想要自定义对流的处理,可以调用getReadableStream方法获取到流,如将流写入文件:
|
||||
|
||||
```typescript
|
||||
import * as fs from 'fs';
|
||||
|
||||
const resp = await client.im.file.get({
|
||||
path: {
|
||||
file_key: 'file key',
|
||||
},
|
||||
});
|
||||
const readableStream = resp.getReadableStream();
|
||||
const writableStream = fs.createWriteStream('file url');
|
||||
readableStream.pipe(writableStream);
|
||||
```
|
||||
|
||||
> 注意:流只能被消费一次,即如果使用了writeFile消费了流,则getReadableStream获取流会报错/获取到的流为空;如需消费多次流,可以使用getReadableStream获取流,然后读取流中的数据做缓存,将缓存的数据给消费方使用。
|
||||
|
||||
#### 普通调用
|
||||
|
||||
某些老版本的开放接口,无法生成对应的语义化调用方法,需要使用client上的request方法来进行手动调用:
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const client = new lark.Client({
|
||||
appId: 'app id',
|
||||
appSecret: 'app secret',
|
||||
appType: lark.AppType.SelfBuild,
|
||||
domain: lark.Domain.Feishu,
|
||||
});
|
||||
|
||||
const res = await client.request({
|
||||
method: 'POST',
|
||||
url: 'xxx',
|
||||
data: {},
|
||||
params: {},
|
||||
});
|
||||
```
|
||||
|
||||
#### 消息卡片
|
||||
|
||||
在发送[消息卡片](https://open.feishu.cn/document/ukTMukTMukTM/uczM3QjL3MzN04yNzcDN)信息时,会先在[消息卡片搭建工具](https://open.feishu.cn/document/ukTMukTMukTM/uYzM3QjL2MzN04iNzcDN/message-card-builder)中搭建出消息卡片的模版,拿到生成的模版json,用数据替换其中内容相关的部分,将结果作为支持消息卡片api的参数来使用。如发送一个简单的具有`title`和`content`的消息卡片:
|
||||
|
||||
```typescript
|
||||
client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'your receive_id',
|
||||
content: JSON.stringify({
|
||||
"config": {
|
||||
"wide_screen_mode": true
|
||||
},
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": "Card Content"
|
||||
}
|
||||
],
|
||||
"header": {
|
||||
"template": "blue",
|
||||
"title": {
|
||||
"content": "Card Title",
|
||||
"tag": "plain_text"
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
msg_type: 'interactive'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||

|
||||
这样使用会有一个问题:**如果消息卡片内容比较丰富,生成的模版json比较大,与之相关需要数据填充的内容部分也会比较多,手动维护比较繁琐**。针对这个问题,开放平台提供了[模版消息](https://open.feishu.cn/document/tools-and-resources/message-card-builder#3e1f2c7c)的能力,发送消息卡片时只需要提供模版id和模版的数据内容即可。sdk对这个能力进行了调用上的封装,支持消息卡片的接口会同步的增加一个ByCard的调用方式,只需要传递`template_id`和`template_variable`即可。如上面的调用可以改写成:
|
||||
|
||||
```typescript
|
||||
client.im.message.createByCard({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'your receive_id',
|
||||
template_id: 'your template_id',
|
||||
template_variable: {
|
||||
content: "Card Content",
|
||||
title: "Card Title"
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
如果想要快速体验消息卡片,可以使用sdk中内置的一个基础卡片:
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'your receive_id',
|
||||
content: lark.messageCard.defaultCard({
|
||||
title: 'Card Title',
|
||||
content: 'Card Content'
|
||||
}),
|
||||
msg_type: 'interactive'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
效果同上:
|
||||

|
||||
|
||||
#### 配置请求选项
|
||||
|
||||
如果想在api调用过程中修改请求的参数,如携带一些header,自定义tenantToken等,则可以使用请求方法的第二个参数来进行修改:
|
||||
|
||||
```typescript
|
||||
await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'receive_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text',
|
||||
},
|
||||
}, {
|
||||
headers: {
|
||||
customizedHeaderKey: 'customizedHeaderValue'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
SDK亦将常用的修改操作封装成了方法,可以使用:
|
||||
|
||||
| 方法 | 描述 |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------- |
|
||||
| withTenantKey | 设置tenant key |
|
||||
| withTenantToken | 设置tenant token |
|
||||
| withHelpDeskCredential | 是否在请求中带入[服务台token](https://open.feishu.cn/document/ukTMukTMukTM/ugDOyYjL4gjM24CO4IjN) |
|
||||
| withUserAccessToken | 设置access token |
|
||||
| withAll | 将上述方法的结果合并起来 |
|
||||
|
||||
```typescript
|
||||
await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'receive_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text',
|
||||
},
|
||||
}, lark.withTenantToken('tenant token'));
|
||||
|
||||
await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: 'receive_id',
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text',
|
||||
},
|
||||
}, lark.withAll([
|
||||
lark.withTenantToken('tenant token'),
|
||||
lark.withTenantKey('tenant key')
|
||||
]));
|
||||
```
|
||||
|
||||
### 处理事件
|
||||
|
||||
飞书开放平台开放的所有事件列表,可点击[这里查看](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-list)。
|
||||
|
||||
针对事件处理的场景,我们所关心的仅是**监听何种事件**,以及事件发生后我们**做些什么**,其它诸如数据解密等工作是我们不想关心的。SDK提供了直观的方式来描述这部分逻辑:
|
||||
|
||||
1. 构造事件处理器`EventDispatcher`的实例;
|
||||
2. 在实例上注册需要监听的事件及其处理函数;
|
||||
3. 将实例和服务进行绑定;
|
||||
|
||||
`EventDispatcher`内部会进行数据解密等操作,如果没有传递相关参数,则会自动忽略。
|
||||
|
||||
```typescript
|
||||
import http from 'http';
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const eventDispatcher = new lark.EventDispatcher({
|
||||
encryptKey: 'encrypt key'
|
||||
}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const chatId = data.message.chat_id;
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: chatId,
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
});
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
const server = http.createServer();
|
||||
server.on('request', lark.adaptDefault('/webhook/event', eventDispatcher));
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
#### `EventDispatcher`构造参数
|
||||
|
||||
| 参数 | 描述 | 类型 | 必须 | 默认 |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ----------- | -- | --------------------- |
|
||||
| [encryptKey](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case) | 推送数据加密的key,开启加密推送时需要使用来进行数据解密 | string | 否 | - |
|
||||
| loggerLevel | 日志级别 | LoggerLevel | 否 | lark.LoggerLevel.info |
|
||||
| logger | - | Logger | 否 | - |
|
||||
| cache | 缓存器 | Cache | 否 | - |
|
||||
|
||||
> 注:有一些事件是v1.0版本且已经不在维护了,SDK保留了对其的支持,强烈建议使用与之功能相一致的新版事件代替。鼠标移动到相应事件订阅函数上即可看到相关文档:
|
||||
> 
|
||||
|
||||
#### 和express结合
|
||||
|
||||
SDK提供了针对experss的适配器,用于将eventDispatcher转化为express的中间件,可无缝与使用express编写的服务相结合(*示例中的bodyParser的使用不是必须的,但社区大多用其来格式化body数据*):
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
import express from 'express';
|
||||
import bodyParser from 'body-parser';
|
||||
|
||||
const server = express();
|
||||
server.use(bodyParser.json());
|
||||
|
||||
const eventDispatcher = new lark.EventDispatcher({
|
||||
encryptKey: 'encryptKey',
|
||||
}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const chatId = data.message.chat_id;
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: chatId,
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
});
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
server.use('/webhook/event', lark.adaptExpress(eventDispatcher));
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
#### 和koa结合
|
||||
|
||||
SDK提供了针对koa的适配器,用于将eventDispatcher转化为koa的中间件,可无缝与使用koa编写的服务相结合(*示例中的koaBody的使用不是必须的,但社区大多用其来格式化body数据*):
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
import Koa from 'koa';
|
||||
import koaBody from 'koa-body';
|
||||
|
||||
const server = new Koa();
|
||||
server.use(koaBody());
|
||||
|
||||
const eventDispatcher = new lark.EventDispatcher({
|
||||
encryptKey: 'encryptKey',
|
||||
}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const open_chat_id = data.message.chat_id;
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: open_chat_id,
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
server.use(lark.adaptKoa('/webhook/event', eventDispatcher));
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
#### 和koa-router结合
|
||||
|
||||
在使用koa来编写服务时,大多情况下会配合使用koa-router来对路由进行处理,因此SDK也提供了针对这一情况的适配:
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import koaBody from 'koa-body';
|
||||
|
||||
const server = new Koa();
|
||||
const router = new Router();
|
||||
server.use(koaBody());
|
||||
|
||||
const eventDispatcher = new lark.EventDispatcher({
|
||||
encryptKey: 'encryptKey',
|
||||
}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const open_chat_id = data.message.chat_id;
|
||||
|
||||
const res = await client.im.message.create({
|
||||
params: {
|
||||
receive_id_type: 'chat_id',
|
||||
},
|
||||
data: {
|
||||
receive_id: open_chat_id,
|
||||
content: JSON.stringify({text: 'hello world'}),
|
||||
msg_type: 'text'
|
||||
},
|
||||
});
|
||||
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
router.post('/webhook/event', lark.adaptKoaRouter(eventDispatcher));
|
||||
server.use(router.routes());
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
#### 自定义适配器
|
||||
|
||||
如果要适配其它库编写的服务,目前需要自己来封装相应的适配器。将接收到的事件数据和请求头传递给实例化的 `eventDispatcher` 的 invoke 方法进行事件的处理即可:
|
||||
|
||||
```typescript
|
||||
const data = server.getData();
|
||||
const headers = server.getHeaders();
|
||||
const assigned = Object.assign(
|
||||
Object.create({ headers }),
|
||||
data,
|
||||
);
|
||||
const result = await dispatcher.invoke(assigned);
|
||||
server.sendResult(result);
|
||||
```
|
||||
|
||||
#### challenge校验
|
||||
|
||||
在配置事件请求地址时,开放平台会向请求地址推送一个`application/json`格式的 POST请求,该POST请求用于验证所配置的请求地址的合法性,请求体中会携带一个`challenge`字段,**应用需要在 1 秒内,将接收到的challenge值原样返回给飞书开放平台**。详见:[文档](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/request-url-configuration-case)
|
||||
|
||||
上面sdk提供出的适配器内部封装了这部分验证的逻辑,将options参数中的`autoChallenge`字段设为true即可启用:
|
||||
|
||||
```typescript
|
||||
// adaptDefault
|
||||
lark.adaptDefault('/webhook/event', eventDispatcher, {
|
||||
autoChallenge: true,
|
||||
});
|
||||
// express
|
||||
lark.adaptExpress(eventDispatcher, {
|
||||
autoChallenge: true,
|
||||
});
|
||||
// koa
|
||||
lark.adaptKoa('/webhook/event', eventDispatcher, {
|
||||
autoChallenge: true,
|
||||
});
|
||||
// koa-router
|
||||
router.post(
|
||||
'/webhook/event',
|
||||
lark.adaptKoaRouter(eventDispatcher, {
|
||||
autoChallenge: true,
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
### 使用长链模式处理事件
|
||||
|
||||
官方文档:[文档](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/event-subscription-guide/long-connection-mode#62c8b8c8)
|
||||
|
||||
开发者通过集成飞书 SDK 与开放平台建立一条 WebSocket 全双工通道,当有事件回调发生时,开放平台会通过该通道向开发者发送消息。与传统的 Webhook 模式相比,长连接模式大大降低了接入成本,将原先 1 周左右的开发周期降低到 5 分钟。具体优势如下:
|
||||
|
||||
- 测试阶段**无需使用内网穿透工具**,通过长连接模式在本地开发环境中即可接收事件回调。
|
||||
- 只在建连时进行鉴权,后续事件推送均为明文数据,无需开发者再处理解密和验签逻辑。
|
||||
- 只需保证运行环境具备访问公网能力即可,无需提供公网 IP 或域名。
|
||||
- 无需部署防火墙和配置白名单。
|
||||
|
||||
> 注意事项
|
||||
|
||||
1. 与 Webhook 相同, 长连接模式下开发者接收到消息后,需要在 3 秒内处理完成,否则会触发超时重推。
|
||||
2. 消息推送为 集群模式,不支持广播,即如果同一应用部署了多个客户端,那么只有其中随机一个客户端会收到消息。
|
||||
3. 目前长连接模式仅支持事件订阅,不支持回调订阅。
|
||||
|
||||
SDK支持了该功能集成,`1.24.0`及之后的版本可用,示例代码:
|
||||
|
||||
```typescript
|
||||
import * as Lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const baseConfig = {
|
||||
appId: 'xxx',
|
||||
appSecret: 'xxx'
|
||||
}
|
||||
|
||||
const client = new Lark.Client(baseConfig);
|
||||
|
||||
const wsClient = new Lark.WSClient({...baseConfig, loggerLevel: Lark.LoggerLevel.info});
|
||||
|
||||
wsClient.start({
|
||||
eventDispatcher: new Lark.EventDispatcher({}).register({
|
||||
'im.message.receive_v1': async (data) => {
|
||||
const {
|
||||
message: { chat_id, content}
|
||||
} = data;
|
||||
await client.im.v1.message.create({
|
||||
params: {
|
||||
receive_id_type: "chat_id"
|
||||
},
|
||||
data: {
|
||||
receive_id: chat_id,
|
||||
content: Lark.messageCard.defaultCard({
|
||||
title: `reply: ${JSON.parse(content).text}`,
|
||||
content: 'hello'
|
||||
}),
|
||||
msg_type: 'interactive'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Channel 模块
|
||||
|
||||
`Channel` 是在 `WSClient` / `Client` 之上封装的高层模块,一站式封装飞书机器人接入的传输、消息归一化、安全策略、出站发送、流式回复、媒体上传、卡片交互等杂活,适合会话式机器人(AI 对话、流式回复、卡片按钮等)场景。
|
||||
|
||||
```typescript
|
||||
import { createLarkChannel } from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const channel = createLarkChannel({ appId, appSecret });
|
||||
|
||||
channel.on('message', async (msg) => {
|
||||
await channel.send(
|
||||
msg.chatId,
|
||||
{ markdown: `收到:${msg.content}` },
|
||||
{ replyTo: msg.messageId },
|
||||
);
|
||||
});
|
||||
|
||||
await channel.connect();
|
||||
```
|
||||
|
||||
完整用法(事件监听、发送消息、流式回复、底层能力、错误处理、配置项等)见 [docs/channel.zh.md](./docs/channel.zh.md)。
|
||||
|
||||
### [消息卡片](https://open.feishu.cn/document/ukTMukTMukTM/uczM3QjL3MzN04yNzcDN)
|
||||
|
||||
对消息卡片的处理亦是对事件处理的一种,两者的不同点仅在于消息卡片的处理器用于响应用户与消息卡片交互所产生的事件,若处理器有返回值(*返回值的数据结构理应为符合[消息卡片结构](https://open.feishu.cn/document/ukTMukTMukTM/uEjNwUjLxYDM14SM2ATN)所定义的结构*),则返回值被用来更新被响应的消息卡片:
|
||||
|
||||
```typescript
|
||||
import http from 'http';
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
import type { InteractiveCardActionEvent, InteractiveCard } from '@larksuiteoapi/node-sdk';
|
||||
|
||||
const cardDispatcher = new lark.CardActionHandler(
|
||||
{
|
||||
encryptKey: 'encrypt key',
|
||||
verificationToken: 'verification token'
|
||||
},
|
||||
async (data: InteractiveCardActionEvent) => {
|
||||
console.log(data);
|
||||
const newCard: InteractiveCard = {
|
||||
// your new interactive card content
|
||||
header: {},
|
||||
elements: []
|
||||
};
|
||||
return newCard;
|
||||
}
|
||||
);
|
||||
|
||||
const server = http.createServer();
|
||||
server.on('request', lark.adaptDefault('/webhook/card', cardDispatcher));
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
#### `CardActionHandler`构造参数
|
||||
|
||||
| 参数 | 描述 | 类型 | 必须 | 默认 |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ----------- | -- | ---------------- |
|
||||
| [encryptKey](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case) | 推送数据加密的key,开启加密推送时需要使用来进行数据解密 | string | 否 | - |
|
||||
| [verificationToken](https://open.feishu.cn/document/ukTMukTMukTM/uYzMxEjL2MTMx4iNzETM) | 安全校验,开启消息的安全校验时需要使用 | string | 否 | - |
|
||||
| loggerLevel | 日志级别 | LoggerLevel | 否 | LoggerLevel.info |
|
||||
| logger | - | Logger | 否 | - |
|
||||
| cache | 缓存器 | Cache | 否 | - |
|
||||
|
||||
### 一键创建应用
|
||||
|
||||
SDK提供了`registerApp`方法,基于 OAuth 2.0 Device Authorization Grant(RFC 8628)协议实现一键创建应用。该方法会返回一个验证链接,用户在飞书/Lark中打开该链接完成授权后,即可自动注册应用并获取凭据,无需手动到开发者后台创建。
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
try {
|
||||
const result = await lark.registerApp({
|
||||
onQRCodeReady(info) {
|
||||
console.log(`请扫码: ${info.url}`);
|
||||
console.log(`链接将在 ${info.expireIn} 秒后过期`);
|
||||
},
|
||||
onStatusChange(info) {
|
||||
// 处理状态变化:'polling' | 'slow_down' | 'domain_switched'
|
||||
},
|
||||
});
|
||||
|
||||
console.log('App ID:', result.client_id);
|
||||
console.log('App Secret:', result.client_secret);
|
||||
|
||||
// 用获取到的凭据初始化 Client
|
||||
const client = new lark.Client({
|
||||
appId: result.client_id,
|
||||
appSecret: result.client_secret,
|
||||
});
|
||||
} catch (e) {
|
||||
// e.code: 'access_denied' | 'expired_token' | 'abort' | ...
|
||||
// e.description: 错误描述
|
||||
console.error(e.code, e.description);
|
||||
}
|
||||
```
|
||||
|
||||
#### `registerApp`参数
|
||||
|
||||
| 参数 | 描述 | 类型 | 必须 | 默认 |
|
||||
| -------------- | ---------------------------------------------------------------------------------------- | ----------- | -- | ------------------------ |
|
||||
| domain | 自定义认证域名(仅 host 部分) | string | 否 | `accounts.feishu.cn` |
|
||||
| larkDomain | 自定义 Lark 认证域名(仅 host 部分),检测到 Lark 租户时自动切换 | string | 否 | `accounts.larksuite.com` |
|
||||
| source | 来源标识,拼入二维码 URL 的 `from` 参数,格式为 `node-sdk/{source}` | string | 否 | - |
|
||||
| signal | 用于取消轮询的 `AbortSignal` | AbortSignal | 否 | - |
|
||||
| onQRCodeReady | 验证链接就绪时的回调,参数为 `{ url, expireIn }`。可将 URL 渲染为二维码供用户扫码,或直接作为链接展示 | function | 是 | - |
|
||||
| onStatusChange | 轮询状态变化时的回调,参数为 `{ status, interval? }`。status 取值:`polling`、`slow_down`、`domain_switched` | function | 否 | - |
|
||||
| appPreset | 预设应用信息(头像、名称、描述)。所有字段都是可选的,用户扫码后仍可在页面手动修改 | AppPreset | 否 | - |
|
||||
| appPreset.avatar | 应用头像 URL,支持 1-6 个;传多个时默认选中第一个。支持 png / jpg / jpeg / webp / gif(gif 自动取一帧,不保留动图) | string \| string[] | 否 | - |
|
||||
| appPreset.name | 应用名称,支持 `{user}` 占位符(替换为扫码用户名称) | string | 否 | - |
|
||||
| appPreset.desc | 应用描述,支持 `{user}` 占位符 | string | 否 | - |
|
||||
|
||||
#### 返回值
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
| ------------------------ | ---------- | --------------------- |
|
||||
| client\_id | string | App ID |
|
||||
| client\_secret | string | App Secret |
|
||||
| user\_info | object(可选) | 扫码用户信息 |
|
||||
| user\_info.open\_id | string(可选) | 扫码用户的 open\_id |
|
||||
| user\_info.tenant\_brand | string(可选) | `"feishu"` 或 `"lark"` |
|
||||
|
||||
#### 错误处理
|
||||
|
||||
抛出的错误对象包含 `code` 和 `description` 字段:
|
||||
|
||||
| code | 描述 |
|
||||
| --------------- | ----------------- |
|
||||
| `access_denied` | 用户拒绝了授权 |
|
||||
| `expired_token` | 二维码过期或轮询超时 |
|
||||
| `abort` | 通过 AbortSignal 取消 |
|
||||
|
||||
### 工具方法
|
||||
|
||||
#### AESCipher
|
||||
|
||||
解密。如果配置了[加密推送](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/encrypt-key-encryption-configuration-case),开放平台会推送加密的数据,这时候需要对数据进行解密处理,调用此方法可以便捷的进行解密。(一般情况下,SDK中内置了解密逻辑,不需要手动进行处理)。
|
||||
|
||||
```typescript
|
||||
import * as lark from '@larksuiteoapi/node-sdk';
|
||||
|
||||
new lark.AESCipher('encrypt key').decrypt('content');
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
[快速开发自动回复机器人](https://github.com/larksuite/lark-samples/blob/main/react_and_nodejs/robot/README.zh.md)
|
||||
|
||||
## Blog
|
||||
|
||||
[ISV(商店应用)开发指南](https://bytedance.feishu.cn/docx/RUZKdGwdyoH4KexMJgDcITQnn0b)
|
||||
|
||||
## 许可协议
|
||||
|
||||
MIT
|
||||
|
||||
## 联系我们
|
||||
|
||||
点击[服务端SDK](https://open.feishu.cn/document/ukTMukTMukTM/uETO1YjLxkTN24SM5UjN) 页面右上角【这篇文档是否对你有帮助?】提交反馈😘
|
||||
Generated
Vendored
+93667
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+93713
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "@larksuiteoapi/node-sdk",
|
||||
"version": "1.66.0",
|
||||
"description": "larksuite open sdk for nodejs",
|
||||
"keywords": [
|
||||
"feishu",
|
||||
"larksuite",
|
||||
"sdk",
|
||||
"nodejs"
|
||||
],
|
||||
"types": "./types",
|
||||
"main": "./lib/index.js",
|
||||
"module": "./es/index.js",
|
||||
"files": [
|
||||
"lib",
|
||||
"es",
|
||||
"types",
|
||||
"README.zh.md",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -r lib es types & rollup -c",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch --testPathPattern=utils/__tests__"
|
||||
},
|
||||
"author": "mazhe.nerd",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/larksuite/node-sdk",
|
||||
"url": "https://github.com/larksuite/node-sdk/issues",
|
||||
"dependencies": {
|
||||
"axios": "~1.13.3",
|
||||
"lodash.identity": "^3.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.pickby": "^4.6.0",
|
||||
"protobufjs": "^7.2.6",
|
||||
"qs": "^6.14.2",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@koa/router": "^12.0.0",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rollup/plugin-typescript": "^8.3.2",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/lodash.pick": "^4.4.7",
|
||||
"@types/qs": "^6.9.16",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@typescript-eslint/parser": "^5.27.1",
|
||||
"body-parser": "^1.20.1",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"express": "^4.18.2",
|
||||
"jest": "^28.1.1",
|
||||
"koa": "^2.13.4",
|
||||
"koa-body": "^5.0.0",
|
||||
"prettier": "2.6.2",
|
||||
"rollup": "^2.75.5",
|
||||
"rollup-plugin-dts": "^4.2.2",
|
||||
"rollup-plugin-license": "^2.8.1",
|
||||
"ts-jest": "^28.0.5",
|
||||
"ts-node": "^10.8.1",
|
||||
"tsconfig-paths": "^4.0.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+301397
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2016, Daniel Wirtz All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of its author, nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
@protobufjs/aspromise
|
||||
=====================
|
||||
[](https://www.npmjs.com/package/@protobufjs/aspromise)
|
||||
|
||||
Returns a promise from a node-style callback function.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
* **asPromise(fn: `function`, ctx: `Object`, ...params: `*`): `Promise<*>`**<br />
|
||||
Returns a promise from a node-style callback function.
|
||||
|
||||
**License:** [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
export = asPromise;
|
||||
|
||||
type asPromiseCallback = (error: Error | null, ...params: any[]) => {};
|
||||
|
||||
/**
|
||||
* Returns a promise from a node-style callback function.
|
||||
* @memberof util
|
||||
* @param {asPromiseCallback} fn Function to call
|
||||
* @param {*} ctx Function context
|
||||
* @param {...*} params Function arguments
|
||||
* @returns {Promise<*>} Promisified function
|
||||
*/
|
||||
declare function asPromise(fn: asPromiseCallback, ctx: any, ...params: any[]): Promise<any>;
|
||||
Generated
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
module.exports = asPromise;
|
||||
|
||||
/**
|
||||
* Callback as used by {@link util.asPromise}.
|
||||
* @typedef asPromiseCallback
|
||||
* @type {function}
|
||||
* @param {Error|null} error Error, if any
|
||||
* @param {...*} params Additional arguments
|
||||
* @returns {undefined}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a promise from a node-style callback function.
|
||||
* @memberof util
|
||||
* @param {asPromiseCallback} fn Function to call
|
||||
* @param {*} ctx Function context
|
||||
* @param {...*} params Function arguments
|
||||
* @returns {Promise<*>} Promisified function
|
||||
*/
|
||||
function asPromise(fn, ctx/*, varargs */) {
|
||||
var params = new Array(arguments.length - 1),
|
||||
offset = 0,
|
||||
index = 2,
|
||||
pending = true;
|
||||
while (index < arguments.length)
|
||||
params[offset++] = arguments[index++];
|
||||
return new Promise(function executor(resolve, reject) {
|
||||
params[offset] = function callback(err/*, varargs */) {
|
||||
if (pending) {
|
||||
pending = false;
|
||||
if (err)
|
||||
reject(err);
|
||||
else {
|
||||
var params = new Array(arguments.length - 1),
|
||||
offset = 0;
|
||||
while (offset < params.length)
|
||||
params[offset++] = arguments[offset];
|
||||
resolve.apply(null, params);
|
||||
}
|
||||
}
|
||||
};
|
||||
try {
|
||||
fn.apply(ctx || null, params);
|
||||
} catch (err) {
|
||||
if (pending) {
|
||||
pending = false;
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@protobufjs/aspromise",
|
||||
"description": "Returns a promise from a node-style callback function.",
|
||||
"version": "1.1.2",
|
||||
"author": "Daniel Wirtz <dcode+protobufjs@dcode.io>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dcodeIO/protobuf.js.git"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"devDependencies": {
|
||||
"istanbul": "^0.4.5",
|
||||
"tape": "^4.6.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tape tests/*.js",
|
||||
"coverage": "istanbul cover node_modules/tape/bin/tape tests/*.js"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+130
@@ -0,0 +1,130 @@
|
||||
var tape = require("tape");
|
||||
|
||||
var asPromise = require("..");
|
||||
|
||||
tape.test("aspromise", function(test) {
|
||||
|
||||
test.test(this.name + " - resolve", function(test) {
|
||||
|
||||
function fn(arg1, arg2, callback) {
|
||||
test.equal(this, ctx, "function should be called with this = ctx");
|
||||
test.equal(arg1, 1, "function should be called with arg1 = 1");
|
||||
test.equal(arg2, 2, "function should be called with arg2 = 2");
|
||||
callback(null, arg2);
|
||||
}
|
||||
|
||||
var ctx = {};
|
||||
|
||||
var promise = asPromise(fn, ctx, 1, 2);
|
||||
promise.then(function(arg2) {
|
||||
test.equal(arg2, 2, "promise should be resolved with arg2 = 2");
|
||||
test.end();
|
||||
}).catch(function(err) {
|
||||
test.fail("promise should not be rejected (" + err + ")");
|
||||
});
|
||||
});
|
||||
|
||||
test.test(this.name + " - reject", function(test) {
|
||||
|
||||
function fn(arg1, arg2, callback) {
|
||||
test.equal(this, ctx, "function should be called with this = ctx");
|
||||
test.equal(arg1, 1, "function should be called with arg1 = 1");
|
||||
test.equal(arg2, 2, "function should be called with arg2 = 2");
|
||||
callback(arg1);
|
||||
}
|
||||
|
||||
var ctx = {};
|
||||
|
||||
var promise = asPromise(fn, ctx, 1, 2);
|
||||
promise.then(function() {
|
||||
test.fail("promise should not be resolved");
|
||||
}).catch(function(err) {
|
||||
test.equal(err, 1, "promise should be rejected with err = 1");
|
||||
test.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.test(this.name + " - resolve twice", function(test) {
|
||||
|
||||
function fn(arg1, arg2, callback) {
|
||||
test.equal(this, ctx, "function should be called with this = ctx");
|
||||
test.equal(arg1, 1, "function should be called with arg1 = 1");
|
||||
test.equal(arg2, 2, "function should be called with arg2 = 2");
|
||||
callback(null, arg2);
|
||||
callback(null, arg1);
|
||||
}
|
||||
|
||||
var ctx = {};
|
||||
var count = 0;
|
||||
|
||||
var promise = asPromise(fn, ctx, 1, 2);
|
||||
promise.then(function(arg2) {
|
||||
test.equal(arg2, 2, "promise should be resolved with arg2 = 2");
|
||||
if (++count > 1)
|
||||
test.fail("promise should not be resolved twice");
|
||||
test.end();
|
||||
}).catch(function(err) {
|
||||
test.fail("promise should not be rejected (" + err + ")");
|
||||
});
|
||||
});
|
||||
|
||||
test.test(this.name + " - reject twice", function(test) {
|
||||
|
||||
function fn(arg1, arg2, callback) {
|
||||
test.equal(this, ctx, "function should be called with this = ctx");
|
||||
test.equal(arg1, 1, "function should be called with arg1 = 1");
|
||||
test.equal(arg2, 2, "function should be called with arg2 = 2");
|
||||
callback(arg1);
|
||||
callback(arg2);
|
||||
}
|
||||
|
||||
var ctx = {};
|
||||
var count = 0;
|
||||
|
||||
var promise = asPromise(fn, ctx, 1, 2);
|
||||
promise.then(function() {
|
||||
test.fail("promise should not be resolved");
|
||||
}).catch(function(err) {
|
||||
test.equal(err, 1, "promise should be rejected with err = 1");
|
||||
if (++count > 1)
|
||||
test.fail("promise should not be rejected twice");
|
||||
test.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.test(this.name + " - reject error", function(test) {
|
||||
|
||||
function fn(callback) {
|
||||
test.ok(arguments.length === 1 && typeof callback === "function", "function should be called with just a callback");
|
||||
throw 3;
|
||||
}
|
||||
|
||||
var promise = asPromise(fn, null);
|
||||
promise.then(function() {
|
||||
test.fail("promise should not be resolved");
|
||||
}).catch(function(err) {
|
||||
test.equal(err, 3, "promise should be rejected with err = 3");
|
||||
test.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.test(this.name + " - reject and error", function(test) {
|
||||
|
||||
function fn(callback) {
|
||||
callback(3);
|
||||
throw 4;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
|
||||
var promise = asPromise(fn, null);
|
||||
promise.then(function() {
|
||||
test.fail("promise should not be resolved");
|
||||
}).catch(function(err) {
|
||||
test.equal(err, 3, "promise should be rejected with err = 3");
|
||||
if (++count > 1)
|
||||
test.fail("promise should not be rejected twice");
|
||||
test.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2016, Daniel Wirtz All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of its author, nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
@protobufjs/base64
|
||||
==================
|
||||
[](https://www.npmjs.com/package/@protobufjs/base64)
|
||||
|
||||
A minimal base64 implementation for number arrays.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
* **base64.length(string: `string`): `number`**<br />
|
||||
Calculates the byte length of a base64 encoded string.
|
||||
|
||||
* **base64.encode(buffer: `Uint8Array`, start: `number`, end: `number`): `string`**<br />
|
||||
Encodes a buffer to a base64 encoded string.
|
||||
|
||||
* **base64.decode(string: `string`, buffer: `Uint8Array`, offset: `number`): `number`**<br />
|
||||
Decodes a base64 encoded string to a buffer.
|
||||
|
||||
**License:** [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
Generated
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Calculates the byte length of a base64 encoded string.
|
||||
* @param {string} string Base64 encoded string
|
||||
* @returns {number} Byte length
|
||||
*/
|
||||
export function length(string: string): number;
|
||||
|
||||
/**
|
||||
* Encodes a buffer to a base64 encoded string.
|
||||
* @param {Uint8Array} buffer Source buffer
|
||||
* @param {number} start Source start
|
||||
* @param {number} end Source end
|
||||
* @returns {string} Base64 encoded string
|
||||
*/
|
||||
export function encode(buffer: Uint8Array, start: number, end: number): string;
|
||||
|
||||
/**
|
||||
* Decodes a base64 encoded string to a buffer.
|
||||
* @param {string} string Source string
|
||||
* @param {Uint8Array} buffer Destination buffer
|
||||
* @param {number} offset Destination offset
|
||||
* @returns {number} Number of bytes written
|
||||
* @throws {Error} If encoding is invalid
|
||||
*/
|
||||
export function decode(string: string, buffer: Uint8Array, offset: number): number;
|
||||
|
||||
/**
|
||||
* Tests if the specified string appears to be base64 encoded.
|
||||
* @param {string} string String to test
|
||||
* @returns {boolean} `true` if it appears to be base64 encoded, otherwise false
|
||||
*/
|
||||
export function test(string: string): boolean;
|
||||
Generated
Vendored
+139
@@ -0,0 +1,139 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* A minimal base64 implementation for number arrays.
|
||||
* @memberof util
|
||||
* @namespace
|
||||
*/
|
||||
var base64 = exports;
|
||||
|
||||
/**
|
||||
* Calculates the byte length of a base64 encoded string.
|
||||
* @param {string} string Base64 encoded string
|
||||
* @returns {number} Byte length
|
||||
*/
|
||||
base64.length = function length(string) {
|
||||
var p = string.length;
|
||||
if (!p)
|
||||
return 0;
|
||||
var n = 0;
|
||||
while (--p % 4 > 1 && string.charAt(p) === "=")
|
||||
++n;
|
||||
return Math.ceil(string.length * 3) / 4 - n;
|
||||
};
|
||||
|
||||
// Base64 encoding table
|
||||
var b64 = new Array(64);
|
||||
|
||||
// Base64 decoding table
|
||||
var s64 = new Array(123);
|
||||
|
||||
// 65..90, 97..122, 48..57, 43, 47
|
||||
for (var i = 0; i < 64;)
|
||||
s64[b64[i] = i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i - 59 | 43] = i++;
|
||||
|
||||
/**
|
||||
* Encodes a buffer to a base64 encoded string.
|
||||
* @param {Uint8Array} buffer Source buffer
|
||||
* @param {number} start Source start
|
||||
* @param {number} end Source end
|
||||
* @returns {string} Base64 encoded string
|
||||
*/
|
||||
base64.encode = function encode(buffer, start, end) {
|
||||
var parts = null,
|
||||
chunk = [];
|
||||
var i = 0, // output index
|
||||
j = 0, // goto index
|
||||
t; // temporary
|
||||
while (start < end) {
|
||||
var b = buffer[start++];
|
||||
switch (j) {
|
||||
case 0:
|
||||
chunk[i++] = b64[b >> 2];
|
||||
t = (b & 3) << 4;
|
||||
j = 1;
|
||||
break;
|
||||
case 1:
|
||||
chunk[i++] = b64[t | b >> 4];
|
||||
t = (b & 15) << 2;
|
||||
j = 2;
|
||||
break;
|
||||
case 2:
|
||||
chunk[i++] = b64[t | b >> 6];
|
||||
chunk[i++] = b64[b & 63];
|
||||
j = 0;
|
||||
break;
|
||||
}
|
||||
if (i > 8191) {
|
||||
(parts || (parts = [])).push(String.fromCharCode.apply(String, chunk));
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
if (j) {
|
||||
chunk[i++] = b64[t];
|
||||
chunk[i++] = 61;
|
||||
if (j === 1)
|
||||
chunk[i++] = 61;
|
||||
}
|
||||
if (parts) {
|
||||
if (i)
|
||||
parts.push(String.fromCharCode.apply(String, chunk.slice(0, i)));
|
||||
return parts.join("");
|
||||
}
|
||||
return String.fromCharCode.apply(String, chunk.slice(0, i));
|
||||
};
|
||||
|
||||
var invalidEncoding = "invalid encoding";
|
||||
|
||||
/**
|
||||
* Decodes a base64 encoded string to a buffer.
|
||||
* @param {string} string Source string
|
||||
* @param {Uint8Array} buffer Destination buffer
|
||||
* @param {number} offset Destination offset
|
||||
* @returns {number} Number of bytes written
|
||||
* @throws {Error} If encoding is invalid
|
||||
*/
|
||||
base64.decode = function decode(string, buffer, offset) {
|
||||
var start = offset;
|
||||
var j = 0, // goto index
|
||||
t; // temporary
|
||||
for (var i = 0; i < string.length;) {
|
||||
var c = string.charCodeAt(i++);
|
||||
if (c === 61 && j > 1)
|
||||
break;
|
||||
if ((c = s64[c]) === undefined)
|
||||
throw Error(invalidEncoding);
|
||||
switch (j) {
|
||||
case 0:
|
||||
t = c;
|
||||
j = 1;
|
||||
break;
|
||||
case 1:
|
||||
buffer[offset++] = t << 2 | (c & 48) >> 4;
|
||||
t = c;
|
||||
j = 2;
|
||||
break;
|
||||
case 2:
|
||||
buffer[offset++] = (t & 15) << 4 | (c & 60) >> 2;
|
||||
t = c;
|
||||
j = 3;
|
||||
break;
|
||||
case 3:
|
||||
buffer[offset++] = (t & 3) << 6 | c;
|
||||
j = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j === 1)
|
||||
throw Error(invalidEncoding);
|
||||
return offset - start;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests if the specified string appears to be base64 encoded.
|
||||
* @param {string} string String to test
|
||||
* @returns {boolean} `true` if probably base64 encoded, otherwise false
|
||||
*/
|
||||
base64.test = function test(string) {
|
||||
return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(string);
|
||||
};
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@protobufjs/base64",
|
||||
"description": "A minimal base64 implementation for number arrays.",
|
||||
"version": "1.1.2",
|
||||
"author": "Daniel Wirtz <dcode+protobufjs@dcode.io>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dcodeIO/protobuf.js.git"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"devDependencies": {
|
||||
"istanbul": "^0.4.5",
|
||||
"tape": "^4.6.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tape tests/*.js",
|
||||
"coverage": "istanbul cover node_modules/tape/bin/tape tests/*.js"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
var tape = require("tape");
|
||||
|
||||
var base64 = require("..");
|
||||
|
||||
var strings = {
|
||||
"": "",
|
||||
"a": "YQ==",
|
||||
"ab": "YWI=",
|
||||
"abcdefg": "YWJjZGVmZw==",
|
||||
"abcdefgh": "YWJjZGVmZ2g=",
|
||||
"abcdefghi": "YWJjZGVmZ2hp"
|
||||
};
|
||||
|
||||
tape.test("base64", function(test) {
|
||||
|
||||
Object.keys(strings).forEach(function(str) {
|
||||
var enc = strings[str];
|
||||
|
||||
test.equal(base64.test(enc), true, "should detect '" + enc + "' to be base64 encoded");
|
||||
|
||||
var len = base64.length(enc);
|
||||
test.equal(len, str.length, "should calculate '" + enc + "' as " + str.length + " bytes");
|
||||
|
||||
var buf = new Array(len);
|
||||
var len2 = base64.decode(enc, buf, 0);
|
||||
test.equal(len2, len, "should decode '" + enc + "' to " + len + " bytes");
|
||||
|
||||
test.equal(String.fromCharCode.apply(String, buf), str, "should decode '" + enc + "' to '" + str + "'");
|
||||
|
||||
var enc2 = base64.encode(buf, 0, buf.length);
|
||||
test.equal(enc2, enc, "should encode '" + str + "' to '" + enc + "'");
|
||||
|
||||
});
|
||||
|
||||
test.throws(function() {
|
||||
var buf = new Array(10);
|
||||
base64.decode("YQ!", buf, 0);
|
||||
}, Error, "should throw if encoding is invalid");
|
||||
|
||||
test.throws(function() {
|
||||
var buf = new Array(10);
|
||||
base64.decode("Y", buf, 0);
|
||||
}, Error, "should throw if string is truncated");
|
||||
|
||||
test.end();
|
||||
});
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2016, Daniel Wirtz All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of its author, nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
@protobufjs/codegen
|
||||
===================
|
||||
[](https://www.npmjs.com/package/@protobufjs/codegen)
|
||||
|
||||
A minimalistic code generation utility.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
* **codegen([functionParams: `string[]`], [functionName: string]): `Codegen`**<br />
|
||||
Begins generating a function.
|
||||
|
||||
* **codegen.verbose = `false`**<br />
|
||||
When set to true, codegen will log generated code to console. Useful for debugging.
|
||||
|
||||
Invoking **codegen** returns an appender function that appends code to the function's body and returns itself:
|
||||
|
||||
* **Codegen(formatString: `string`, [...formatParams: `any`]): Codegen**<br />
|
||||
Appends code to the function's body. The format string can contain placeholders specifying the types of inserted format parameters:
|
||||
|
||||
* `%d`: Number (integer or floating point value)
|
||||
* `%f`: Floating point value
|
||||
* `%i`: Integer value
|
||||
* `%j`: JSON.stringify'ed value
|
||||
* `%s`: String value
|
||||
* `%%`: Percent sign<br />
|
||||
|
||||
* **Codegen([scope: `Object.<string,*>`]): `Function`**<br />
|
||||
Finishes the function and returns it.
|
||||
|
||||
* **Codegen.toString([functionNameOverride: `string`]): `string`**<br />
|
||||
Returns the function as a string.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
```js
|
||||
var codegen = require("@protobufjs/codegen");
|
||||
|
||||
var add = codegen(["a", "b"], "add") // A function with parameters "a" and "b" named "add"
|
||||
("// awesome comment") // adds the line to the function's body
|
||||
("return a + b - c + %d", 1) // replaces %d with 1 and adds the line to the body
|
||||
({ c: 1 }); // adds "c" with a value of 1 to the function's scope
|
||||
|
||||
console.log(add.toString()); // function add(a, b) { return a + b - c + 1 }
|
||||
console.log(add(1, 2)); // calculates 1 + 2 - 1 + 1 = 3
|
||||
```
|
||||
|
||||
**License:** [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
export = codegen;
|
||||
|
||||
/**
|
||||
* Appends code to the function's body.
|
||||
* @param [formatStringOrScope] Format string or, to finish the function, an object of additional scope variables, if any
|
||||
* @param [formatParams] Format parameters
|
||||
* @returns Itself or the generated function if finished
|
||||
* @throws {Error} If format parameter counts do not match
|
||||
*/
|
||||
type Codegen = (formatStringOrScope?: (string|{ [k: string]: any }), ...formatParams: any[]) => (Codegen|Function);
|
||||
|
||||
/**
|
||||
* Begins generating a function.
|
||||
* @param functionParams Function parameter names
|
||||
* @param [functionName] Function name if not anonymous
|
||||
* @returns Appender that appends code to the function's body
|
||||
*/
|
||||
declare function codegen(functionParams: string[], functionName?: string): Codegen;
|
||||
|
||||
/**
|
||||
* Begins generating a function.
|
||||
* @param [functionName] Function name if not anonymous
|
||||
* @returns Appender that appends code to the function's body
|
||||
*/
|
||||
declare function codegen(functionName?: string): Codegen;
|
||||
|
||||
declare namespace codegen {
|
||||
|
||||
/** When set to `true`, codegen will log generated code to console. Useful for debugging. */
|
||||
let verbose: boolean;
|
||||
}
|
||||
Generated
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
"use strict";
|
||||
module.exports = codegen;
|
||||
|
||||
var reservedRe = /^(?:do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$/;
|
||||
|
||||
/**
|
||||
* Begins generating a function.
|
||||
* @memberof util
|
||||
* @param {string[]} functionParams Function parameter names
|
||||
* @param {string} [functionName] Function name if not anonymous
|
||||
* @returns {Codegen} Appender that appends code to the function's body
|
||||
*/
|
||||
function codegen(functionParams, functionName) {
|
||||
|
||||
/* istanbul ignore if */
|
||||
if (typeof functionParams === "string") {
|
||||
functionName = functionParams;
|
||||
functionParams = undefined;
|
||||
}
|
||||
|
||||
var body = [];
|
||||
|
||||
/**
|
||||
* Appends code to the function's body or finishes generation.
|
||||
* @typedef Codegen
|
||||
* @type {function}
|
||||
* @param {string|Object.<string,*>} [formatStringOrScope] Format string or, to finish the function, an object of additional scope variables, if any
|
||||
* @param {...*} [formatParams] Format parameters
|
||||
* @returns {Codegen|Function} Itself or the generated function if finished
|
||||
* @throws {Error} If format parameter counts do not match
|
||||
*/
|
||||
|
||||
function Codegen(formatStringOrScope) {
|
||||
// note that explicit array handling below makes this ~50% faster
|
||||
|
||||
// finish the function
|
||||
if (typeof formatStringOrScope !== "string") {
|
||||
var source = toString();
|
||||
if (codegen.verbose)
|
||||
console.log("codegen: " + source); // eslint-disable-line no-console
|
||||
source = "return " + source;
|
||||
if (formatStringOrScope) {
|
||||
var scopeKeys = Object.keys(formatStringOrScope),
|
||||
scopeParams = new Array(scopeKeys.length + 1),
|
||||
scopeValues = new Array(scopeKeys.length),
|
||||
scopeOffset = 0;
|
||||
while (scopeOffset < scopeKeys.length) {
|
||||
scopeParams[scopeOffset] = scopeKeys[scopeOffset];
|
||||
scopeValues[scopeOffset] = formatStringOrScope[scopeKeys[scopeOffset++]];
|
||||
}
|
||||
scopeParams[scopeOffset] = source;
|
||||
return Function.apply(null, scopeParams).apply(null, scopeValues); // eslint-disable-line no-new-func
|
||||
}
|
||||
return Function(source)(); // eslint-disable-line no-new-func
|
||||
}
|
||||
|
||||
// otherwise append to body
|
||||
var formatParams = new Array(arguments.length - 1),
|
||||
formatOffset = 0;
|
||||
while (formatOffset < formatParams.length)
|
||||
formatParams[formatOffset] = arguments[++formatOffset];
|
||||
formatOffset = 0;
|
||||
formatStringOrScope = formatStringOrScope.replace(/%([%dfijs])/g, function replace($0, $1) {
|
||||
var value = formatParams[formatOffset++];
|
||||
switch ($1) {
|
||||
case "d": case "f": return String(Number(value));
|
||||
case "i": return String(Math.floor(value));
|
||||
case "j": return JSON.stringify(value);
|
||||
case "s": return String(value);
|
||||
}
|
||||
return "%";
|
||||
});
|
||||
if (formatOffset !== formatParams.length)
|
||||
throw Error("parameter count mismatch");
|
||||
body.push(formatStringOrScope);
|
||||
return Codegen;
|
||||
}
|
||||
|
||||
function toString(functionNameOverride) {
|
||||
return "function " + safeFunctionName(functionNameOverride || functionName) + "(" + (functionParams && functionParams.join(",") || "") + "){\n " + body.join("\n ") + "\n}";
|
||||
}
|
||||
|
||||
Codegen.toString = toString;
|
||||
return Codegen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins generating a function.
|
||||
* @memberof util
|
||||
* @function codegen
|
||||
* @param {string} [functionName] Function name if not anonymous
|
||||
* @returns {Codegen} Appender that appends code to the function's body
|
||||
* @variation 2
|
||||
*/
|
||||
|
||||
/**
|
||||
* When set to `true`, codegen will log generated code to console. Useful for debugging.
|
||||
* @name util.codegen.verbose
|
||||
* @type {boolean}
|
||||
*/
|
||||
codegen.verbose = false;
|
||||
|
||||
function safeFunctionName(name) {
|
||||
if (!name)
|
||||
return "";
|
||||
name = String(name).replace(/[^\w$]/g, "");
|
||||
if (!name)
|
||||
return "";
|
||||
if (/^\d/.test(name))
|
||||
name = "_" + name;
|
||||
return reservedRe.test(name) ? name + "_" : name;
|
||||
}
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@protobufjs/codegen",
|
||||
"description": "A minimalistic code generation utility.",
|
||||
"version": "2.0.5",
|
||||
"author": "Daniel Wirtz <dcode+protobufjs@dcode.io>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dcodeIO/protobuf.js.git"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
var codegen = require("..");
|
||||
|
||||
// new require("benchmark").Suite().add("add", function() {
|
||||
|
||||
var add = codegen(["a", "b"], "add")
|
||||
("// awesome comment")
|
||||
("return a + b - c + %d", 1)
|
||||
({ c: 1 });
|
||||
|
||||
if (add(1, 2) !== 3)
|
||||
throw Error("failed");
|
||||
|
||||
// }).on("cycle", function(event) { process.stdout.write(String(event.target) + "\n"); }).run();
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## [1.1.1](https://github.com/protobufjs/protobuf.js/compare/eventemitter-v1.1.0...eventemitter-v1.1.1) (2026-05-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Backport misc utility hardening ([#2280](https://github.com/protobufjs/protobuf.js/issues/2280)) ([8a45c13](https://github.com/protobufjs/protobuf.js/commit/8a45c13d22ec2d05ab1b7935fcb5331ea59a9cd0))
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2016, Daniel Wirtz All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of its author, nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
@protobufjs/eventemitter
|
||||
========================
|
||||
[](https://www.npmjs.com/package/@protobufjs/eventemitter)
|
||||
|
||||
A minimal event emitter.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
* **new EventEmitter()**<br />
|
||||
Constructs a new event emitter instance.
|
||||
|
||||
* **EventEmitter#on(evt: `string`, fn: `function`, [ctx: `Object`]): `EventEmitter`**<br />
|
||||
Registers an event listener.
|
||||
|
||||
* **EventEmitter#off([evt: `string`], [fn: `function`]): `EventEmitter`**<br />
|
||||
Removes an event listener or any matching listeners if arguments are omitted.
|
||||
|
||||
* **EventEmitter#emit(evt: `string`, ...args: `*`): `EventEmitter`**<br />
|
||||
Emits an event by calling its listeners with the specified arguments.
|
||||
|
||||
**License:** [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
export = EventEmitter;
|
||||
|
||||
type EventEmitterListener = (...args: any[]) => {};
|
||||
|
||||
/**
|
||||
* Constructs a new event emitter instance.
|
||||
* @classdesc A minimal event emitter.
|
||||
* @memberof util
|
||||
* @constructor
|
||||
*/
|
||||
declare class EventEmitter {
|
||||
|
||||
/**
|
||||
* Constructs a new event emitter instance.
|
||||
* @classdesc A minimal event emitter.
|
||||
* @memberof util
|
||||
* @constructor
|
||||
*/
|
||||
constructor();
|
||||
|
||||
/**
|
||||
* Registers an event listener.
|
||||
* @param {string} evt Event name
|
||||
* @param {EventEmitterListener} fn Listener
|
||||
* @param {*} [ctx] Listener context
|
||||
* @returns {this} `this`
|
||||
*/
|
||||
public on(evt: string, fn: EventEmitterListener, ctx?: any): EventEmitter;
|
||||
|
||||
/**
|
||||
* Removes an event listener or any matching listeners if arguments are omitted.
|
||||
* @param {string} [evt] Event name. Removes all listeners if omitted.
|
||||
* @param {EventEmitterListener} [fn] Listener to remove. Removes all listeners of `evt` if omitted.
|
||||
* @returns {this} `this`
|
||||
*/
|
||||
public off(evt?: string, fn?: EventEmitterListener): EventEmitter;
|
||||
|
||||
/**
|
||||
* Emits an event by calling its listeners with the specified arguments.
|
||||
* @param {string} evt Event name
|
||||
* @param {...*} args Arguments
|
||||
* @returns {this} `this`
|
||||
*/
|
||||
public emit(evt: string, ...args: any[]): EventEmitter;
|
||||
}
|
||||
Generated
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
"use strict";
|
||||
module.exports = EventEmitter;
|
||||
|
||||
/**
|
||||
* Constructs a new event emitter instance.
|
||||
* @classdesc A minimal event emitter.
|
||||
* @memberof util
|
||||
* @constructor
|
||||
*/
|
||||
function EventEmitter() {
|
||||
|
||||
/**
|
||||
* Registered listeners.
|
||||
* @type {Object.<string,*>}
|
||||
* @private
|
||||
*/
|
||||
this._listeners = Object.create(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener as used by {@link util.EventEmitter}.
|
||||
* @typedef EventEmitterListener
|
||||
* @type {function}
|
||||
* @param {...*} args Arguments
|
||||
* @returns {undefined}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Registers an event listener.
|
||||
* @param {string} evt Event name
|
||||
* @param {EventEmitterListener} fn Listener
|
||||
* @param {*} [ctx] Listener context
|
||||
* @returns {this} `this`
|
||||
*/
|
||||
EventEmitter.prototype.on = function on(evt, fn, ctx) {
|
||||
(this._listeners[evt] || (this._listeners[evt] = [])).push({
|
||||
fn : fn,
|
||||
ctx : ctx || this
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes an event listener or any matching listeners if arguments are omitted.
|
||||
* @param {string} [evt] Event name. Removes all listeners if omitted.
|
||||
* @param {EventEmitterListener} [fn] Listener to remove. Removes all listeners of `evt` if omitted.
|
||||
* @returns {this} `this`
|
||||
*/
|
||||
EventEmitter.prototype.off = function off(evt, fn) {
|
||||
if (evt === undefined)
|
||||
this._listeners = Object.create(null);
|
||||
else {
|
||||
if (fn === undefined)
|
||||
this._listeners[evt] = [];
|
||||
else {
|
||||
var listeners = this._listeners[evt];
|
||||
if (!listeners)
|
||||
return this;
|
||||
for (var i = 0; i < listeners.length;)
|
||||
if (listeners[i].fn === fn)
|
||||
listeners.splice(i, 1);
|
||||
else
|
||||
++i;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits an event by calling its listeners with the specified arguments.
|
||||
* @param {string} evt Event name
|
||||
* @param {...*} args Arguments
|
||||
* @returns {this} `this`
|
||||
*/
|
||||
EventEmitter.prototype.emit = function emit(evt) {
|
||||
var listeners = this._listeners[evt];
|
||||
if (listeners) {
|
||||
var args = [],
|
||||
i = 1;
|
||||
for (; i < arguments.length;)
|
||||
args.push(arguments[i++]);
|
||||
for (i = 0; i < listeners.length;)
|
||||
listeners[i].fn.apply(listeners[i++].ctx, args);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@protobufjs/eventemitter",
|
||||
"description": "A minimal event emitter.",
|
||||
"version": "1.1.1",
|
||||
"author": "Daniel Wirtz <dcode+protobufjs@dcode.io>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dcodeIO/protobuf.js.git"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"devDependencies": {
|
||||
"istanbul": "^0.4.5",
|
||||
"tape": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tape tests/*.js",
|
||||
"coverage": "istanbul cover node_modules/tape/bin/tape tests/*.js"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
var tape = require("tape");
|
||||
|
||||
var EventEmitter = require("..");
|
||||
|
||||
tape.test("eventemitter", function(test) {
|
||||
|
||||
var ee = new EventEmitter();
|
||||
var fn;
|
||||
var ctx = {};
|
||||
|
||||
test.equal(Object.getPrototypeOf(ee._listeners), null, "should not inherit listener lookup keys");
|
||||
|
||||
test.doesNotThrow(function() {
|
||||
ee.emit("a", 1);
|
||||
ee.off();
|
||||
ee.off("a");
|
||||
ee.off("a", function() {});
|
||||
}, "should not throw if no listeners are registered");
|
||||
|
||||
test.equal(ee.on("a", function(arg1) {
|
||||
test.equal(this, ctx, "should be called with this = ctx");
|
||||
test.equal(arg1, 1, "should be called with arg1 = 1");
|
||||
}, ctx), ee, "should return itself when registering events");
|
||||
ee.emit("a", 1);
|
||||
|
||||
ee.off("a");
|
||||
test.same(Object.keys(ee._listeners), [ "a" ], "should keep the event key when calling off(evt)");
|
||||
test.same(ee._listeners.a, [], "should remove all listeners of the respective event when calling off(evt)");
|
||||
|
||||
ee.off();
|
||||
test.equal(Object.getPrototypeOf(ee._listeners), null, "should keep the listener table isolated when just calling off()");
|
||||
test.same(Object.keys(ee._listeners), [], "should remove all listeners when just calling off()");
|
||||
|
||||
ee.on("a", fn = function(arg1) {
|
||||
test.equal(this, ctx, "should be called with this = ctx");
|
||||
test.equal(arg1, 1, "should be called with arg1 = 1");
|
||||
}, ctx).emit("a", 1);
|
||||
|
||||
ee.off("a", fn);
|
||||
test.same(Object.keys(ee._listeners), [ "a" ], "should keep the event key when calling off(evt, fn)");
|
||||
test.same(ee._listeners.a, [], "should remove the exact listener when calling off(evt, fn)");
|
||||
|
||||
ee.on("a", function() {
|
||||
test.equal(this, ee, "should be called with this = ee");
|
||||
}).emit("a");
|
||||
|
||||
test.doesNotThrow(function() {
|
||||
ee.off("a", fn);
|
||||
}, "should not throw if no such listener is found");
|
||||
|
||||
test.test(test.name + " - special event names", function(test) {
|
||||
var ee = new EventEmitter();
|
||||
var calls = 0;
|
||||
|
||||
test.doesNotThrow(function() {
|
||||
ee.off("__proto__", function() {});
|
||||
}, "should not throw when removing an absent special event listener");
|
||||
|
||||
ee.on("__proto__", function(arg) {
|
||||
++calls;
|
||||
test.equal(arg, 1, "should pass arguments for __proto__ events");
|
||||
});
|
||||
ee.on("constructor", function(arg) {
|
||||
++calls;
|
||||
test.equal(arg, 2, "should pass arguments for constructor events");
|
||||
});
|
||||
ee.emit("__proto__", 1);
|
||||
ee.emit("constructor", 2);
|
||||
|
||||
test.equal(calls, 2, "should dispatch special event names");
|
||||
test.equal(Object.getPrototypeOf(ee._listeners), null, "should keep the listener table isolated");
|
||||
test.ok(Object.prototype.hasOwnProperty.call(ee._listeners, "__proto__"), "should store __proto__ as an own event key");
|
||||
test.ok(Object.prototype.hasOwnProperty.call(ee._listeners, "constructor"), "should store constructor as an own event key");
|
||||
|
||||
ee.off("__proto__");
|
||||
ee.off("constructor");
|
||||
test.same(ee._listeners.__proto__, [], "should clear __proto__ listeners");
|
||||
test.same(ee._listeners.constructor, [], "should clear constructor listeners");
|
||||
test.end();
|
||||
});
|
||||
|
||||
test.end();
|
||||
});
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## [1.1.1](https://github.com/protobufjs/protobuf.js/compare/fetch-v1.1.0...fetch-v1.1.1) (2026-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Backport bundler-safe optional module lookups ([#2254](https://github.com/protobufjs/protobuf.js/issues/2254)) ([0853a62](https://github.com/protobufjs/protobuf.js/commit/0853a625680f9247596b84ef48082b8f4e554797))
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2016, Daniel Wirtz All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of its author, nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
@protobufjs/fetch
|
||||
=================
|
||||
[](https://www.npmjs.com/package/@protobufjs/fetch)
|
||||
|
||||
Fetches the contents of a file accross node and browsers.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
* **fetch(path: `string`, [options: { binary: boolean } ], [callback: `function(error: ?Error, [contents: string])`]): `Promise<string|Uint8Array>|undefined`**
|
||||
Fetches the contents of a file.
|
||||
|
||||
**License:** [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
Generated
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
export = fetch;
|
||||
|
||||
/**
|
||||
* Node-style callback as used by {@link util.fetch}.
|
||||
* @typedef FetchCallback
|
||||
* @type {function}
|
||||
* @param {?Error} error Error, if any, otherwise `null`
|
||||
* @param {string} [contents] File contents, if there hasn't been an error
|
||||
* @returns {undefined}
|
||||
*/
|
||||
type FetchCallback = (error: Error, contents?: string) => void;
|
||||
|
||||
/**
|
||||
* Options as used by {@link util.fetch}.
|
||||
* @typedef IFetchOptions
|
||||
* @type {Object}
|
||||
* @property {boolean} [binary=false] Whether expecting a binary response
|
||||
* @property {boolean} [xhr=false] If `true`, forces the use of XMLHttpRequest
|
||||
*/
|
||||
|
||||
interface IFetchOptions {
|
||||
binary?: boolean;
|
||||
xhr?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the contents of a file.
|
||||
* @memberof util
|
||||
* @param {string} filename File path or url
|
||||
* @param {IFetchOptions} options Fetch options
|
||||
* @param {FetchCallback} callback Callback function
|
||||
* @returns {undefined}
|
||||
*/
|
||||
declare function fetch(filename: string, options: IFetchOptions, callback: FetchCallback): void;
|
||||
|
||||
/**
|
||||
* Fetches the contents of a file.
|
||||
* @name util.fetch
|
||||
* @function
|
||||
* @param {string} path File path or url
|
||||
* @param {FetchCallback} callback Callback function
|
||||
* @returns {undefined}
|
||||
* @variation 2
|
||||
*/
|
||||
declare function fetch(path: string, callback: FetchCallback): void;
|
||||
|
||||
/**
|
||||
* Fetches the contents of a file.
|
||||
* @name util.fetch
|
||||
* @function
|
||||
* @param {string} path File path or url
|
||||
* @param {IFetchOptions} [options] Fetch options
|
||||
* @returns {Promise<string|Uint8Array>} Promise
|
||||
* @variation 3
|
||||
*/
|
||||
declare function fetch(path: string, options?: IFetchOptions): Promise<(string|Uint8Array)>;
|
||||
Generated
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
"use strict";
|
||||
module.exports = fetch;
|
||||
|
||||
var asPromise = require("@protobufjs/aspromise"),
|
||||
fs = require("./util/fs");
|
||||
|
||||
/**
|
||||
* Node-style callback as used by {@link util.fetch}.
|
||||
* @typedef FetchCallback
|
||||
* @type {function}
|
||||
* @param {?Error} error Error, if any, otherwise `null`
|
||||
* @param {string} [contents] File contents, if there hasn't been an error
|
||||
* @returns {undefined}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options as used by {@link util.fetch}.
|
||||
* @interface IFetchOptions
|
||||
* @property {boolean} [binary=false] Whether expecting a binary response
|
||||
* @property {boolean} [xhr=false] If `true`, forces the use of XMLHttpRequest
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetches the contents of a file.
|
||||
* @memberof util
|
||||
* @param {string} filename File path or url
|
||||
* @param {IFetchOptions} options Fetch options
|
||||
* @param {FetchCallback} callback Callback function
|
||||
* @returns {undefined}
|
||||
*/
|
||||
function fetch(filename, options, callback) {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = {};
|
||||
} else if (!options)
|
||||
options = {};
|
||||
|
||||
if (!callback)
|
||||
return asPromise(fetch, this, filename, options); // eslint-disable-line no-invalid-this
|
||||
|
||||
// if a node-like filesystem is present, try it first but fall back to XHR if nothing is found.
|
||||
if (!options.xhr && fs && fs.readFile)
|
||||
return fs.readFile(filename, function fetchReadFileCallback(err, contents) {
|
||||
return err && typeof XMLHttpRequest !== "undefined"
|
||||
? fetch.xhr(filename, options, callback)
|
||||
: err
|
||||
? callback(err)
|
||||
: callback(null, options.binary ? contents : contents.toString("utf8"));
|
||||
});
|
||||
|
||||
// use the XHR version otherwise.
|
||||
return fetch.xhr(filename, options, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the contents of a file.
|
||||
* @name util.fetch
|
||||
* @function
|
||||
* @param {string} path File path or url
|
||||
* @param {FetchCallback} callback Callback function
|
||||
* @returns {undefined}
|
||||
* @variation 2
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetches the contents of a file.
|
||||
* @name util.fetch
|
||||
* @function
|
||||
* @param {string} path File path or url
|
||||
* @param {IFetchOptions} [options] Fetch options
|
||||
* @returns {Promise<string|Uint8Array>} Promise
|
||||
* @variation 3
|
||||
*/
|
||||
|
||||
/**/
|
||||
fetch.xhr = function fetch_xhr(filename, options, callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange /* works everywhere */ = function fetchOnReadyStateChange() {
|
||||
|
||||
if (xhr.readyState !== 4)
|
||||
return undefined;
|
||||
|
||||
// local cors security errors return status 0 / empty string, too. afaik this cannot be
|
||||
// reliably distinguished from an actually empty file for security reasons. feel free
|
||||
// to send a pull request if you are aware of a solution.
|
||||
if (xhr.status !== 0 && xhr.status !== 200)
|
||||
return callback(Error("status " + xhr.status));
|
||||
|
||||
// if binary data is expected, make sure that some sort of array is returned, even if
|
||||
// ArrayBuffers are not supported. the binary string fallback, however, is unsafe.
|
||||
if (options.binary) {
|
||||
var buffer = xhr.response;
|
||||
if (!buffer) {
|
||||
buffer = [];
|
||||
for (var i = 0; i < xhr.responseText.length; ++i)
|
||||
buffer.push(xhr.responseText.charCodeAt(i) & 255);
|
||||
}
|
||||
return callback(null, typeof Uint8Array !== "undefined" ? new Uint8Array(buffer) : buffer);
|
||||
}
|
||||
return callback(null, xhr.responseText);
|
||||
};
|
||||
|
||||
if (options.binary) {
|
||||
// ref: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data#Receiving_binary_data_in_older_browsers
|
||||
if ("overrideMimeType" in xhr)
|
||||
xhr.overrideMimeType("text/plain; charset=x-user-defined");
|
||||
xhr.responseType = "arraybuffer";
|
||||
}
|
||||
|
||||
xhr.open("GET", filename);
|
||||
xhr.send();
|
||||
};
|
||||
Generated
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@protobufjs/fetch",
|
||||
"description": "Fetches the contents of a file accross node and browsers.",
|
||||
"version": "1.1.1",
|
||||
"author": "Daniel Wirtz <dcode+protobufjs@dcode.io>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dcodeIO/protobuf.js.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": {
|
||||
"fs": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "^0.4.5",
|
||||
"tape": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tape tests/*.js",
|
||||
"coverage": "istanbul cover node_modules/tape/bin/tape tests/*.js"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
file.txt
|
||||
Generated
Vendored
+158
@@ -0,0 +1,158 @@
|
||||
var tape = require("tape");
|
||||
|
||||
var fetch = require("..");
|
||||
|
||||
tape.test("fetch", function(test) {
|
||||
|
||||
if (typeof Promise !== "undefined")
|
||||
test.test(test.name + " - promise", function(test) {
|
||||
var promise = fetch("NOTFOUND");
|
||||
test.plan(2);
|
||||
test.ok(promise instanceof Promise, "should return a promise if callback has been omitted");
|
||||
promise
|
||||
.then(function() {
|
||||
test.fail("should not resolve");
|
||||
})
|
||||
.catch(function(err) {
|
||||
test.ok(err, "should reject with an error");
|
||||
});
|
||||
});
|
||||
|
||||
test.test(test.name + " - node fs", function(test) {
|
||||
|
||||
test.test(test.name + " - string", function(test) {
|
||||
test.plan(2);
|
||||
fetch(require.resolve("./data/file.txt"), function(err, contents) {
|
||||
test.notOk(err, "should not return an error");
|
||||
test.equal(contents, "file.txt", "should return contents as a string");
|
||||
});
|
||||
});
|
||||
|
||||
test.test(test.name + " - binary", function(test) {
|
||||
test.plan(2);
|
||||
fetch(require.resolve("./data/file.txt"), { binary: true }, function(err, contents) {
|
||||
test.notOk(err, "should not return an error");
|
||||
test.same(contents, new Buffer("file.txt", "utf8"), "should return contents as a Buffer");
|
||||
});
|
||||
});
|
||||
|
||||
test.test(test.name + " - fallback", function(test) {
|
||||
test.plan(2);
|
||||
global.XMLHttpRequest = fakeXHR(0);
|
||||
fetch("file.txt", function(err, contents) {
|
||||
delete global.XMLHttpRequest;
|
||||
test.notOk(err, "should not return an error");
|
||||
test.same(contents, "file.txt", "should return contents as a string");
|
||||
});
|
||||
});
|
||||
|
||||
test.end();
|
||||
});
|
||||
|
||||
test.test(test.name + " - XMLHttpRequest", function(test) {
|
||||
|
||||
test.test(test.name + " - 404", function(test) {
|
||||
global.XMLHttpRequest = fakeXHR(404);
|
||||
fetch("file.txt", { xhr: true }, function(err) {
|
||||
delete global.XMLHttpRequest;
|
||||
test.ok(err, "should return an error");
|
||||
test.end();
|
||||
});
|
||||
});
|
||||
|
||||
test.test(test.name + " - string", function(test) {
|
||||
global.XMLHttpRequest = fakeXHR(0);
|
||||
test.plan(2);
|
||||
fetch("file.txt", { xhr: true }, function(err, contents) {
|
||||
delete global.XMLHttpRequest;
|
||||
test.notOk(err, "should not return an error");
|
||||
test.equal(contents, "file.txt", "should return contents as a string");
|
||||
});
|
||||
});
|
||||
|
||||
test.test(test.name + " - binary", function(test) {
|
||||
global.XMLHttpRequest = fakeXHR(200);
|
||||
test.plan(2);
|
||||
fetch("file.txt", { xhr: true, binary: true }, function(err, contents) {
|
||||
delete global.XMLHttpRequest;
|
||||
test.notOk(err, "should not return an error");
|
||||
test.same(contents, new Uint8Array([0x66, 0x69, 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74]), "should return contents as an Uint8Array");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test.test(test.name + " - XMLHttpRequest (ancient)", function(test) {
|
||||
|
||||
test.test(test.name + " - string", function(test) {
|
||||
global.XMLHttpRequest = fakeXHR(0, true);
|
||||
test.plan(2);
|
||||
fetch("file.txt", { xhr: true }, function(err, contents) {
|
||||
delete global.XMLHttpRequest;
|
||||
test.notOk(err, "should not return an error");
|
||||
test.equal(contents, "file.txt", "should return contents as a string");
|
||||
});
|
||||
});
|
||||
|
||||
test.test(test.name + " - binary", function(test) {
|
||||
global.XMLHttpRequest = fakeXHR(200, true);
|
||||
var U8 = global.Uint8Array;
|
||||
delete global.Uint8Array;
|
||||
test.plan(2);
|
||||
fetch("file.txt", { xhr: true, binary: true }, function(err, contents) {
|
||||
delete global.XMLHttpRequest;
|
||||
global.Uint8Array = U8;
|
||||
test.notOk(err, "should not return an error");
|
||||
test.same(contents, [0x66, 0x69, 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74], "should return contents as an Array");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function fakeXHR(status, ancient) {
|
||||
|
||||
var UNSENT = 0,
|
||||
OPENED = 1,
|
||||
HEADERS_RECEIVED = 2,
|
||||
LOADING = 3,
|
||||
DONE = 4;
|
||||
|
||||
function XMLHttpRequest() {
|
||||
this.status = 0;
|
||||
this.readyState = UNSENT;
|
||||
}
|
||||
if (!ancient)
|
||||
XMLHttpRequest.prototype.overrideMimeType = function(mimeType) {
|
||||
this._mimeType = mimeType;
|
||||
};
|
||||
XMLHttpRequest.prototype.open = function open(method, path) {
|
||||
this._method = method;
|
||||
this._path = path;
|
||||
this.readyState = OPENED;
|
||||
};
|
||||
XMLHttpRequest.prototype.send = function send() {
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.onreadystatechange(); // opened
|
||||
self.readyState = HEADERS_RECEIVED;
|
||||
self.onreadystatechange();
|
||||
self.readyState = LOADING;
|
||||
self.onreadystatechange();
|
||||
self.readyState = DONE;
|
||||
self.status = status;
|
||||
if (self.responseType === "arraybuffer" && !ancient) {
|
||||
var buf = new Buffer(self._path, "utf8");
|
||||
var abuf = new ArrayBuffer(buf.length);
|
||||
var view = new Uint8Array(abuf);
|
||||
for (var i = 0; i < buf.length; ++i)
|
||||
view[i] = buf[i];
|
||||
self.response = abuf;
|
||||
} else
|
||||
self.responseText = self._path;
|
||||
self.onreadystatechange();
|
||||
});
|
||||
};
|
||||
return XMLHttpRequest;
|
||||
}
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
|
||||
var fs = null;
|
||||
try {
|
||||
fs = require(/* webpackIgnore: true */ "fs");
|
||||
if (!fs || !fs.readFile || !fs.readFileSync)
|
||||
fs = null;
|
||||
} catch (e) {
|
||||
// `fs` is unavailable in browsers and browser-like bundles.
|
||||
}
|
||||
module.exports = fs;
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2016, Daniel Wirtz All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of its author, nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
@protobufjs/float
|
||||
=================
|
||||
[](https://www.npmjs.com/package/@protobufjs/float)
|
||||
|
||||
Reads / writes floats / doubles from / to buffers in both modern and ancient browsers. Fast.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
* **writeFloatLE(val: `number`, buf: `Uint8Array`, pos: `number`)**<br />
|
||||
Writes a 32 bit float to a buffer using little endian byte order.
|
||||
|
||||
* **writeFloatBE(val: `number`, buf: `Uint8Array`, pos: `number`)**<br />
|
||||
Writes a 32 bit float to a buffer using big endian byte order.
|
||||
|
||||
* **readFloatLE(buf: `Uint8Array`, pos: `number`): `number`**<br />
|
||||
Reads a 32 bit float from a buffer using little endian byte order.
|
||||
|
||||
* **readFloatBE(buf: `Uint8Array`, pos: `number`): `number`**<br />
|
||||
Reads a 32 bit float from a buffer using big endian byte order.
|
||||
|
||||
* **writeDoubleLE(val: `number`, buf: `Uint8Array`, pos: `number`)**<br />
|
||||
Writes a 64 bit double to a buffer using little endian byte order.
|
||||
|
||||
* **writeDoubleBE(val: `number`, buf: `Uint8Array`, pos: `number`)**<br />
|
||||
Writes a 64 bit double to a buffer using big endian byte order.
|
||||
|
||||
* **readDoubleLE(buf: `Uint8Array`, pos: `number`): `number`**<br />
|
||||
Reads a 64 bit double from a buffer using little endian byte order.
|
||||
|
||||
* **readDoubleBE(buf: `Uint8Array`, pos: `number`): `number`**<br />
|
||||
Reads a 64 bit double from a buffer using big endian byte order.
|
||||
|
||||
Performance
|
||||
-----------
|
||||
There is a simple benchmark included comparing raw read/write performance of this library (float), float's fallback for old browsers, the [ieee754](https://www.npmjs.com/package/ieee754) module and node's [buffer](https://nodejs.org/api/buffer.html). On an i7-2600k running node 6.9.1 it yields:
|
||||
|
||||
```
|
||||
benchmarking writeFloat performance ...
|
||||
|
||||
float x 42,741,625 ops/sec ±1.75% (81 runs sampled)
|
||||
float (fallback) x 11,272,532 ops/sec ±1.12% (85 runs sampled)
|
||||
ieee754 x 8,653,337 ops/sec ±1.18% (84 runs sampled)
|
||||
buffer x 12,412,414 ops/sec ±1.41% (83 runs sampled)
|
||||
buffer (noAssert) x 13,471,149 ops/sec ±1.09% (84 runs sampled)
|
||||
|
||||
float was fastest
|
||||
float (fallback) was 73.5% slower
|
||||
ieee754 was 79.6% slower
|
||||
buffer was 70.9% slower
|
||||
buffer (noAssert) was 68.3% slower
|
||||
|
||||
benchmarking readFloat performance ...
|
||||
|
||||
float x 44,382,729 ops/sec ±1.70% (84 runs sampled)
|
||||
float (fallback) x 20,925,938 ops/sec ±0.86% (87 runs sampled)
|
||||
ieee754 x 17,189,009 ops/sec ±1.01% (87 runs sampled)
|
||||
buffer x 10,518,437 ops/sec ±1.04% (83 runs sampled)
|
||||
buffer (noAssert) x 11,031,636 ops/sec ±1.15% (87 runs sampled)
|
||||
|
||||
float was fastest
|
||||
float (fallback) was 52.5% slower
|
||||
ieee754 was 61.0% slower
|
||||
buffer was 76.1% slower
|
||||
buffer (noAssert) was 75.0% slower
|
||||
|
||||
benchmarking writeDouble performance ...
|
||||
|
||||
float x 38,624,906 ops/sec ±0.93% (83 runs sampled)
|
||||
float (fallback) x 10,457,811 ops/sec ±1.54% (85 runs sampled)
|
||||
ieee754 x 7,681,130 ops/sec ±1.11% (83 runs sampled)
|
||||
buffer x 12,657,876 ops/sec ±1.03% (83 runs sampled)
|
||||
buffer (noAssert) x 13,372,795 ops/sec ±0.84% (85 runs sampled)
|
||||
|
||||
float was fastest
|
||||
float (fallback) was 73.1% slower
|
||||
ieee754 was 80.1% slower
|
||||
buffer was 67.3% slower
|
||||
buffer (noAssert) was 65.3% slower
|
||||
|
||||
benchmarking readDouble performance ...
|
||||
|
||||
float x 40,527,888 ops/sec ±1.05% (84 runs sampled)
|
||||
float (fallback) x 18,696,480 ops/sec ±0.84% (86 runs sampled)
|
||||
ieee754 x 14,074,028 ops/sec ±1.04% (87 runs sampled)
|
||||
buffer x 10,092,367 ops/sec ±1.15% (84 runs sampled)
|
||||
buffer (noAssert) x 10,623,793 ops/sec ±0.96% (84 runs sampled)
|
||||
|
||||
float was fastest
|
||||
float (fallback) was 53.8% slower
|
||||
ieee754 was 65.3% slower
|
||||
buffer was 75.1% slower
|
||||
buffer (noAssert) was 73.8% slower
|
||||
```
|
||||
|
||||
To run it yourself:
|
||||
|
||||
```
|
||||
$> npm run bench
|
||||
```
|
||||
|
||||
**License:** [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
Generated
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
"use strict";
|
||||
|
||||
var float = require(".."),
|
||||
ieee754 = require("ieee754"),
|
||||
newSuite = require("./suite");
|
||||
|
||||
var F32 = Float32Array;
|
||||
var F64 = Float64Array;
|
||||
delete global.Float32Array;
|
||||
delete global.Float64Array;
|
||||
var floatFallback = float({});
|
||||
global.Float32Array = F32;
|
||||
global.Float64Array = F64;
|
||||
|
||||
var buf = new Buffer(8);
|
||||
|
||||
newSuite("writeFloat")
|
||||
.add("float", function() {
|
||||
float.writeFloatLE(0.1, buf, 0);
|
||||
})
|
||||
.add("float (fallback)", function() {
|
||||
floatFallback.writeFloatLE(0.1, buf, 0);
|
||||
})
|
||||
.add("ieee754", function() {
|
||||
ieee754.write(buf, 0.1, 0, true, 23, 4);
|
||||
})
|
||||
.add("buffer", function() {
|
||||
buf.writeFloatLE(0.1, 0);
|
||||
})
|
||||
.add("buffer (noAssert)", function() {
|
||||
buf.writeFloatLE(0.1, 0, true);
|
||||
})
|
||||
.run();
|
||||
|
||||
newSuite("readFloat")
|
||||
.add("float", function() {
|
||||
float.readFloatLE(buf, 0);
|
||||
})
|
||||
.add("float (fallback)", function() {
|
||||
floatFallback.readFloatLE(buf, 0);
|
||||
})
|
||||
.add("ieee754", function() {
|
||||
ieee754.read(buf, 0, true, 23, 4);
|
||||
})
|
||||
.add("buffer", function() {
|
||||
buf.readFloatLE(0);
|
||||
})
|
||||
.add("buffer (noAssert)", function() {
|
||||
buf.readFloatLE(0, true);
|
||||
})
|
||||
.run();
|
||||
|
||||
newSuite("writeDouble")
|
||||
.add("float", function() {
|
||||
float.writeDoubleLE(0.1, buf, 0);
|
||||
})
|
||||
.add("float (fallback)", function() {
|
||||
floatFallback.writeDoubleLE(0.1, buf, 0);
|
||||
})
|
||||
.add("ieee754", function() {
|
||||
ieee754.write(buf, 0.1, 0, true, 52, 8);
|
||||
})
|
||||
.add("buffer", function() {
|
||||
buf.writeDoubleLE(0.1, 0);
|
||||
})
|
||||
.add("buffer (noAssert)", function() {
|
||||
buf.writeDoubleLE(0.1, 0, true);
|
||||
})
|
||||
.run();
|
||||
|
||||
newSuite("readDouble")
|
||||
.add("float", function() {
|
||||
float.readDoubleLE(buf, 0);
|
||||
})
|
||||
.add("float (fallback)", function() {
|
||||
floatFallback.readDoubleLE(buf, 0);
|
||||
})
|
||||
.add("ieee754", function() {
|
||||
ieee754.read(buf, 0, true, 52, 8);
|
||||
})
|
||||
.add("buffer", function() {
|
||||
buf.readDoubleLE(0);
|
||||
})
|
||||
.add("buffer (noAssert)", function() {
|
||||
buf.readDoubleLE(0, true);
|
||||
})
|
||||
.run();
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
module.exports = newSuite;
|
||||
|
||||
var benchmark = require("benchmark"),
|
||||
chalk = require("chalk");
|
||||
|
||||
var padSize = 27;
|
||||
|
||||
function newSuite(name) {
|
||||
var benches = [];
|
||||
return new benchmark.Suite(name)
|
||||
.on("add", function(event) {
|
||||
benches.push(event.target);
|
||||
})
|
||||
.on("start", function() {
|
||||
process.stdout.write("benchmarking " + name + " performance ...\n\n");
|
||||
})
|
||||
.on("cycle", function(event) {
|
||||
process.stdout.write(String(event.target) + "\n");
|
||||
})
|
||||
.on("complete", function() {
|
||||
if (benches.length > 1) {
|
||||
var fastest = this.filter("fastest"), // eslint-disable-line no-invalid-this
|
||||
fastestHz = getHz(fastest[0]);
|
||||
process.stdout.write("\n" + chalk.white(pad(fastest[0].name, padSize)) + " was " + chalk.green("fastest") + "\n");
|
||||
benches.forEach(function(bench) {
|
||||
if (fastest.indexOf(bench) === 0)
|
||||
return;
|
||||
var hz = hz = getHz(bench);
|
||||
var percent = (1 - hz / fastestHz) * 100;
|
||||
process.stdout.write(chalk.white(pad(bench.name, padSize)) + " was " + chalk.red(percent.toFixed(1) + "% slower") + "\n");
|
||||
});
|
||||
}
|
||||
process.stdout.write("\n");
|
||||
});
|
||||
}
|
||||
|
||||
function getHz(bench) {
|
||||
return 1 / (bench.stats.mean + bench.stats.moe);
|
||||
}
|
||||
|
||||
function pad(str, len, l) {
|
||||
while (str.length < len)
|
||||
str = l ? str + " " : " " + str;
|
||||
return str;
|
||||
}
|
||||
Generated
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Writes a 32 bit float to a buffer using little endian byte order.
|
||||
* @name writeFloatLE
|
||||
* @function
|
||||
* @param {number} val Value to write
|
||||
* @param {Uint8Array} buf Target buffer
|
||||
* @param {number} pos Target buffer offset
|
||||
* @returns {undefined}
|
||||
*/
|
||||
export function writeFloatLE(val: number, buf: Uint8Array, pos: number): void;
|
||||
|
||||
/**
|
||||
* Writes a 32 bit float to a buffer using big endian byte order.
|
||||
* @name writeFloatBE
|
||||
* @function
|
||||
* @param {number} val Value to write
|
||||
* @param {Uint8Array} buf Target buffer
|
||||
* @param {number} pos Target buffer offset
|
||||
* @returns {undefined}
|
||||
*/
|
||||
export function writeFloatBE(val: number, buf: Uint8Array, pos: number): void;
|
||||
|
||||
/**
|
||||
* Reads a 32 bit float from a buffer using little endian byte order.
|
||||
* @name readFloatLE
|
||||
* @function
|
||||
* @param {Uint8Array} buf Source buffer
|
||||
* @param {number} pos Source buffer offset
|
||||
* @returns {number} Value read
|
||||
*/
|
||||
export function readFloatLE(buf: Uint8Array, pos: number): number;
|
||||
|
||||
/**
|
||||
* Reads a 32 bit float from a buffer using big endian byte order.
|
||||
* @name readFloatBE
|
||||
* @function
|
||||
* @param {Uint8Array} buf Source buffer
|
||||
* @param {number} pos Source buffer offset
|
||||
* @returns {number} Value read
|
||||
*/
|
||||
export function readFloatBE(buf: Uint8Array, pos: number): number;
|
||||
|
||||
/**
|
||||
* Writes a 64 bit double to a buffer using little endian byte order.
|
||||
* @name writeDoubleLE
|
||||
* @function
|
||||
* @param {number} val Value to write
|
||||
* @param {Uint8Array} buf Target buffer
|
||||
* @param {number} pos Target buffer offset
|
||||
* @returns {undefined}
|
||||
*/
|
||||
export function writeDoubleLE(val: number, buf: Uint8Array, pos: number): void;
|
||||
|
||||
/**
|
||||
* Writes a 64 bit double to a buffer using big endian byte order.
|
||||
* @name writeDoubleBE
|
||||
* @function
|
||||
* @param {number} val Value to write
|
||||
* @param {Uint8Array} buf Target buffer
|
||||
* @param {number} pos Target buffer offset
|
||||
* @returns {undefined}
|
||||
*/
|
||||
export function writeDoubleBE(val: number, buf: Uint8Array, pos: number): void;
|
||||
|
||||
/**
|
||||
* Reads a 64 bit double from a buffer using little endian byte order.
|
||||
* @name readDoubleLE
|
||||
* @function
|
||||
* @param {Uint8Array} buf Source buffer
|
||||
* @param {number} pos Source buffer offset
|
||||
* @returns {number} Value read
|
||||
*/
|
||||
export function readDoubleLE(buf: Uint8Array, pos: number): number;
|
||||
|
||||
/**
|
||||
* Reads a 64 bit double from a buffer using big endian byte order.
|
||||
* @name readDoubleBE
|
||||
* @function
|
||||
* @param {Uint8Array} buf Source buffer
|
||||
* @param {number} pos Source buffer offset
|
||||
* @returns {number} Value read
|
||||
*/
|
||||
export function readDoubleBE(buf: Uint8Array, pos: number): number;
|
||||
Generated
Vendored
+335
@@ -0,0 +1,335 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = factory(factory);
|
||||
|
||||
/**
|
||||
* Reads / writes floats / doubles from / to buffers.
|
||||
* @name util.float
|
||||
* @namespace
|
||||
*/
|
||||
|
||||
/**
|
||||
* Writes a 32 bit float to a buffer using little endian byte order.
|
||||
* @name util.float.writeFloatLE
|
||||
* @function
|
||||
* @param {number} val Value to write
|
||||
* @param {Uint8Array} buf Target buffer
|
||||
* @param {number} pos Target buffer offset
|
||||
* @returns {undefined}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Writes a 32 bit float to a buffer using big endian byte order.
|
||||
* @name util.float.writeFloatBE
|
||||
* @function
|
||||
* @param {number} val Value to write
|
||||
* @param {Uint8Array} buf Target buffer
|
||||
* @param {number} pos Target buffer offset
|
||||
* @returns {undefined}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reads a 32 bit float from a buffer using little endian byte order.
|
||||
* @name util.float.readFloatLE
|
||||
* @function
|
||||
* @param {Uint8Array} buf Source buffer
|
||||
* @param {number} pos Source buffer offset
|
||||
* @returns {number} Value read
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reads a 32 bit float from a buffer using big endian byte order.
|
||||
* @name util.float.readFloatBE
|
||||
* @function
|
||||
* @param {Uint8Array} buf Source buffer
|
||||
* @param {number} pos Source buffer offset
|
||||
* @returns {number} Value read
|
||||
*/
|
||||
|
||||
/**
|
||||
* Writes a 64 bit double to a buffer using little endian byte order.
|
||||
* @name util.float.writeDoubleLE
|
||||
* @function
|
||||
* @param {number} val Value to write
|
||||
* @param {Uint8Array} buf Target buffer
|
||||
* @param {number} pos Target buffer offset
|
||||
* @returns {undefined}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Writes a 64 bit double to a buffer using big endian byte order.
|
||||
* @name util.float.writeDoubleBE
|
||||
* @function
|
||||
* @param {number} val Value to write
|
||||
* @param {Uint8Array} buf Target buffer
|
||||
* @param {number} pos Target buffer offset
|
||||
* @returns {undefined}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reads a 64 bit double from a buffer using little endian byte order.
|
||||
* @name util.float.readDoubleLE
|
||||
* @function
|
||||
* @param {Uint8Array} buf Source buffer
|
||||
* @param {number} pos Source buffer offset
|
||||
* @returns {number} Value read
|
||||
*/
|
||||
|
||||
/**
|
||||
* Reads a 64 bit double from a buffer using big endian byte order.
|
||||
* @name util.float.readDoubleBE
|
||||
* @function
|
||||
* @param {Uint8Array} buf Source buffer
|
||||
* @param {number} pos Source buffer offset
|
||||
* @returns {number} Value read
|
||||
*/
|
||||
|
||||
// Factory function for the purpose of node-based testing in modified global environments
|
||||
function factory(exports) {
|
||||
|
||||
// float: typed array
|
||||
if (typeof Float32Array !== "undefined") (function() {
|
||||
|
||||
var f32 = new Float32Array([ -0 ]),
|
||||
f8b = new Uint8Array(f32.buffer),
|
||||
le = f8b[3] === 128;
|
||||
|
||||
function writeFloat_f32_cpy(val, buf, pos) {
|
||||
f32[0] = val;
|
||||
buf[pos ] = f8b[0];
|
||||
buf[pos + 1] = f8b[1];
|
||||
buf[pos + 2] = f8b[2];
|
||||
buf[pos + 3] = f8b[3];
|
||||
}
|
||||
|
||||
function writeFloat_f32_rev(val, buf, pos) {
|
||||
f32[0] = val;
|
||||
buf[pos ] = f8b[3];
|
||||
buf[pos + 1] = f8b[2];
|
||||
buf[pos + 2] = f8b[1];
|
||||
buf[pos + 3] = f8b[0];
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
exports.writeFloatLE = le ? writeFloat_f32_cpy : writeFloat_f32_rev;
|
||||
/* istanbul ignore next */
|
||||
exports.writeFloatBE = le ? writeFloat_f32_rev : writeFloat_f32_cpy;
|
||||
|
||||
function readFloat_f32_cpy(buf, pos) {
|
||||
f8b[0] = buf[pos ];
|
||||
f8b[1] = buf[pos + 1];
|
||||
f8b[2] = buf[pos + 2];
|
||||
f8b[3] = buf[pos + 3];
|
||||
return f32[0];
|
||||
}
|
||||
|
||||
function readFloat_f32_rev(buf, pos) {
|
||||
f8b[3] = buf[pos ];
|
||||
f8b[2] = buf[pos + 1];
|
||||
f8b[1] = buf[pos + 2];
|
||||
f8b[0] = buf[pos + 3];
|
||||
return f32[0];
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
exports.readFloatLE = le ? readFloat_f32_cpy : readFloat_f32_rev;
|
||||
/* istanbul ignore next */
|
||||
exports.readFloatBE = le ? readFloat_f32_rev : readFloat_f32_cpy;
|
||||
|
||||
// float: ieee754
|
||||
})(); else (function() {
|
||||
|
||||
function writeFloat_ieee754(writeUint, val, buf, pos) {
|
||||
var sign = val < 0 ? 1 : 0;
|
||||
if (sign)
|
||||
val = -val;
|
||||
if (val === 0)
|
||||
writeUint(1 / val > 0 ? /* positive */ 0 : /* negative 0 */ 2147483648, buf, pos);
|
||||
else if (isNaN(val))
|
||||
writeUint(2143289344, buf, pos);
|
||||
else if (val > 3.4028234663852886e+38) // +-Infinity
|
||||
writeUint((sign << 31 | 2139095040) >>> 0, buf, pos);
|
||||
else if (val < 1.1754943508222875e-38) // denormal
|
||||
writeUint((sign << 31 | Math.round(val / 1.401298464324817e-45)) >>> 0, buf, pos);
|
||||
else {
|
||||
var exponent = Math.floor(Math.log(val) / Math.LN2),
|
||||
mantissa = Math.round(val * Math.pow(2, -exponent) * 8388608) & 8388607;
|
||||
writeUint((sign << 31 | exponent + 127 << 23 | mantissa) >>> 0, buf, pos);
|
||||
}
|
||||
}
|
||||
|
||||
exports.writeFloatLE = writeFloat_ieee754.bind(null, writeUintLE);
|
||||
exports.writeFloatBE = writeFloat_ieee754.bind(null, writeUintBE);
|
||||
|
||||
function readFloat_ieee754(readUint, buf, pos) {
|
||||
var uint = readUint(buf, pos),
|
||||
sign = (uint >> 31) * 2 + 1,
|
||||
exponent = uint >>> 23 & 255,
|
||||
mantissa = uint & 8388607;
|
||||
return exponent === 255
|
||||
? mantissa
|
||||
? NaN
|
||||
: sign * Infinity
|
||||
: exponent === 0 // denormal
|
||||
? sign * 1.401298464324817e-45 * mantissa
|
||||
: sign * Math.pow(2, exponent - 150) * (mantissa + 8388608);
|
||||
}
|
||||
|
||||
exports.readFloatLE = readFloat_ieee754.bind(null, readUintLE);
|
||||
exports.readFloatBE = readFloat_ieee754.bind(null, readUintBE);
|
||||
|
||||
})();
|
||||
|
||||
// double: typed array
|
||||
if (typeof Float64Array !== "undefined") (function() {
|
||||
|
||||
var f64 = new Float64Array([-0]),
|
||||
f8b = new Uint8Array(f64.buffer),
|
||||
le = f8b[7] === 128;
|
||||
|
||||
function writeDouble_f64_cpy(val, buf, pos) {
|
||||
f64[0] = val;
|
||||
buf[pos ] = f8b[0];
|
||||
buf[pos + 1] = f8b[1];
|
||||
buf[pos + 2] = f8b[2];
|
||||
buf[pos + 3] = f8b[3];
|
||||
buf[pos + 4] = f8b[4];
|
||||
buf[pos + 5] = f8b[5];
|
||||
buf[pos + 6] = f8b[6];
|
||||
buf[pos + 7] = f8b[7];
|
||||
}
|
||||
|
||||
function writeDouble_f64_rev(val, buf, pos) {
|
||||
f64[0] = val;
|
||||
buf[pos ] = f8b[7];
|
||||
buf[pos + 1] = f8b[6];
|
||||
buf[pos + 2] = f8b[5];
|
||||
buf[pos + 3] = f8b[4];
|
||||
buf[pos + 4] = f8b[3];
|
||||
buf[pos + 5] = f8b[2];
|
||||
buf[pos + 6] = f8b[1];
|
||||
buf[pos + 7] = f8b[0];
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
exports.writeDoubleLE = le ? writeDouble_f64_cpy : writeDouble_f64_rev;
|
||||
/* istanbul ignore next */
|
||||
exports.writeDoubleBE = le ? writeDouble_f64_rev : writeDouble_f64_cpy;
|
||||
|
||||
function readDouble_f64_cpy(buf, pos) {
|
||||
f8b[0] = buf[pos ];
|
||||
f8b[1] = buf[pos + 1];
|
||||
f8b[2] = buf[pos + 2];
|
||||
f8b[3] = buf[pos + 3];
|
||||
f8b[4] = buf[pos + 4];
|
||||
f8b[5] = buf[pos + 5];
|
||||
f8b[6] = buf[pos + 6];
|
||||
f8b[7] = buf[pos + 7];
|
||||
return f64[0];
|
||||
}
|
||||
|
||||
function readDouble_f64_rev(buf, pos) {
|
||||
f8b[7] = buf[pos ];
|
||||
f8b[6] = buf[pos + 1];
|
||||
f8b[5] = buf[pos + 2];
|
||||
f8b[4] = buf[pos + 3];
|
||||
f8b[3] = buf[pos + 4];
|
||||
f8b[2] = buf[pos + 5];
|
||||
f8b[1] = buf[pos + 6];
|
||||
f8b[0] = buf[pos + 7];
|
||||
return f64[0];
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
exports.readDoubleLE = le ? readDouble_f64_cpy : readDouble_f64_rev;
|
||||
/* istanbul ignore next */
|
||||
exports.readDoubleBE = le ? readDouble_f64_rev : readDouble_f64_cpy;
|
||||
|
||||
// double: ieee754
|
||||
})(); else (function() {
|
||||
|
||||
function writeDouble_ieee754(writeUint, off0, off1, val, buf, pos) {
|
||||
var sign = val < 0 ? 1 : 0;
|
||||
if (sign)
|
||||
val = -val;
|
||||
if (val === 0) {
|
||||
writeUint(0, buf, pos + off0);
|
||||
writeUint(1 / val > 0 ? /* positive */ 0 : /* negative 0 */ 2147483648, buf, pos + off1);
|
||||
} else if (isNaN(val)) {
|
||||
writeUint(0, buf, pos + off0);
|
||||
writeUint(2146959360, buf, pos + off1);
|
||||
} else if (val > 1.7976931348623157e+308) { // +-Infinity
|
||||
writeUint(0, buf, pos + off0);
|
||||
writeUint((sign << 31 | 2146435072) >>> 0, buf, pos + off1);
|
||||
} else {
|
||||
var mantissa;
|
||||
if (val < 2.2250738585072014e-308) { // denormal
|
||||
mantissa = val / 5e-324;
|
||||
writeUint(mantissa >>> 0, buf, pos + off0);
|
||||
writeUint((sign << 31 | mantissa / 4294967296) >>> 0, buf, pos + off1);
|
||||
} else {
|
||||
var exponent = Math.floor(Math.log(val) / Math.LN2);
|
||||
if (exponent === 1024)
|
||||
exponent = 1023;
|
||||
mantissa = val * Math.pow(2, -exponent);
|
||||
writeUint(mantissa * 4503599627370496 >>> 0, buf, pos + off0);
|
||||
writeUint((sign << 31 | exponent + 1023 << 20 | mantissa * 1048576 & 1048575) >>> 0, buf, pos + off1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.writeDoubleLE = writeDouble_ieee754.bind(null, writeUintLE, 0, 4);
|
||||
exports.writeDoubleBE = writeDouble_ieee754.bind(null, writeUintBE, 4, 0);
|
||||
|
||||
function readDouble_ieee754(readUint, off0, off1, buf, pos) {
|
||||
var lo = readUint(buf, pos + off0),
|
||||
hi = readUint(buf, pos + off1);
|
||||
var sign = (hi >> 31) * 2 + 1,
|
||||
exponent = hi >>> 20 & 2047,
|
||||
mantissa = 4294967296 * (hi & 1048575) + lo;
|
||||
return exponent === 2047
|
||||
? mantissa
|
||||
? NaN
|
||||
: sign * Infinity
|
||||
: exponent === 0 // denormal
|
||||
? sign * 5e-324 * mantissa
|
||||
: sign * Math.pow(2, exponent - 1075) * (mantissa + 4503599627370496);
|
||||
}
|
||||
|
||||
exports.readDoubleLE = readDouble_ieee754.bind(null, readUintLE, 0, 4);
|
||||
exports.readDoubleBE = readDouble_ieee754.bind(null, readUintBE, 4, 0);
|
||||
|
||||
})();
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
// uint helpers
|
||||
|
||||
function writeUintLE(val, buf, pos) {
|
||||
buf[pos ] = val & 255;
|
||||
buf[pos + 1] = val >>> 8 & 255;
|
||||
buf[pos + 2] = val >>> 16 & 255;
|
||||
buf[pos + 3] = val >>> 24;
|
||||
}
|
||||
|
||||
function writeUintBE(val, buf, pos) {
|
||||
buf[pos ] = val >>> 24;
|
||||
buf[pos + 1] = val >>> 16 & 255;
|
||||
buf[pos + 2] = val >>> 8 & 255;
|
||||
buf[pos + 3] = val & 255;
|
||||
}
|
||||
|
||||
function readUintLE(buf, pos) {
|
||||
return (buf[pos ]
|
||||
| buf[pos + 1] << 8
|
||||
| buf[pos + 2] << 16
|
||||
| buf[pos + 3] << 24) >>> 0;
|
||||
}
|
||||
|
||||
function readUintBE(buf, pos) {
|
||||
return (buf[pos ] << 24
|
||||
| buf[pos + 1] << 16
|
||||
| buf[pos + 2] << 8
|
||||
| buf[pos + 3]) >>> 0;
|
||||
}
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@protobufjs/float",
|
||||
"description": "Reads / writes floats / doubles from / to buffers in both modern and ancient browsers.",
|
||||
"version": "1.0.2",
|
||||
"author": "Daniel Wirtz <dcode+protobufjs@dcode.io>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dcodeIO/protobuf.js.git"
|
||||
},
|
||||
"dependencies": {},
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"devDependencies": {
|
||||
"benchmark": "^2.1.4",
|
||||
"chalk": "^1.1.3",
|
||||
"ieee754": "^1.1.8",
|
||||
"istanbul": "^0.4.5",
|
||||
"tape": "^4.6.3"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tape tests/*.js",
|
||||
"coverage": "istanbul cover node_modules/tape/bin/tape tests/*.js",
|
||||
"bench": "node bench"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
var tape = require("tape");
|
||||
|
||||
var float = require("..");
|
||||
|
||||
tape.test("float", function(test) {
|
||||
|
||||
// default
|
||||
test.test(test.name + " - typed array", function(test) {
|
||||
runTest(float, test);
|
||||
});
|
||||
|
||||
// ieee754
|
||||
test.test(test.name + " - fallback", function(test) {
|
||||
var F32 = global.Float32Array,
|
||||
F64 = global.Float64Array;
|
||||
delete global.Float32Array;
|
||||
delete global.Float64Array;
|
||||
runTest(float({}), test);
|
||||
global.Float32Array = F32;
|
||||
global.Float64Array = F64;
|
||||
});
|
||||
});
|
||||
|
||||
function runTest(float, test) {
|
||||
|
||||
var common = [
|
||||
0,
|
||||
-0,
|
||||
Infinity,
|
||||
-Infinity,
|
||||
0.125,
|
||||
1024.5,
|
||||
-4096.5,
|
||||
NaN
|
||||
];
|
||||
|
||||
test.test(test.name + " - using 32 bits", function(test) {
|
||||
common.concat([
|
||||
3.4028234663852886e+38,
|
||||
1.1754943508222875e-38,
|
||||
1.1754946310819804e-39
|
||||
])
|
||||
.forEach(function(value) {
|
||||
var strval = value === 0 && 1 / value < 0 ? "-0" : value.toString();
|
||||
test.ok(
|
||||
checkValue(value, 4, float.readFloatLE, float.writeFloatLE, Buffer.prototype.writeFloatLE),
|
||||
"should write and read back " + strval + " (32 bit LE)"
|
||||
);
|
||||
test.ok(
|
||||
checkValue(value, 4, float.readFloatBE, float.writeFloatBE, Buffer.prototype.writeFloatBE),
|
||||
"should write and read back " + strval + " (32 bit BE)"
|
||||
);
|
||||
});
|
||||
test.end();
|
||||
});
|
||||
|
||||
test.test(test.name + " - using 64 bits", function(test) {
|
||||
common.concat([
|
||||
1.7976931348623157e+308,
|
||||
2.2250738585072014e-308,
|
||||
2.2250738585072014e-309
|
||||
])
|
||||
.forEach(function(value) {
|
||||
var strval = value === 0 && 1 / value < 0 ? "-0" : value.toString();
|
||||
test.ok(
|
||||
checkValue(value, 8, float.readDoubleLE, float.writeDoubleLE, Buffer.prototype.writeDoubleLE),
|
||||
"should write and read back " + strval + " (64 bit LE)"
|
||||
);
|
||||
test.ok(
|
||||
checkValue(value, 8, float.readDoubleBE, float.writeDoubleBE, Buffer.prototype.writeDoubleBE),
|
||||
"should write and read back " + strval + " (64 bit BE)"
|
||||
);
|
||||
});
|
||||
test.end();
|
||||
});
|
||||
|
||||
test.end();
|
||||
}
|
||||
|
||||
function checkValue(value, size, read, write, write_comp) {
|
||||
var buffer = new Buffer(size);
|
||||
write(value, buffer, 0);
|
||||
var value_comp = read(buffer, 0);
|
||||
var strval = value === 0 && 1 / value < 0 ? "-0" : value.toString();
|
||||
if (value !== value) {
|
||||
if (value_comp === value_comp)
|
||||
return false;
|
||||
} else if (value_comp !== value)
|
||||
return false;
|
||||
|
||||
var buffer_comp = new Buffer(size);
|
||||
write_comp.call(buffer_comp, value, 0);
|
||||
for (var i = 0; i < size; ++i)
|
||||
if (buffer[i] !== buffer_comp[i]) {
|
||||
console.error(">", buffer, buffer_comp);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## [1.1.2](https://github.com/protobufjs/protobuf.js/compare/inquire-v1.1.1...inquire-v1.1.2) (2026-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Backport bundler-safe optional module lookups ([#2254](https://github.com/protobufjs/protobuf.js/issues/2254)) ([0853a62](https://github.com/protobufjs/protobuf.js/commit/0853a625680f9247596b84ef48082b8f4e554797))
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2016, Daniel Wirtz All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of its author, nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
@protobufjs/inquire
|
||||
===================
|
||||
[](https://www.npmjs.com/package/@protobufjs/inquire)
|
||||
|
||||
Requires a module only if available and hides the require call from bundlers.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
* **inquire(moduleName: `string`): `?Object`**<br />
|
||||
Requires a module only if available.
|
||||
|
||||
**License:** [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
export = inquire;
|
||||
|
||||
/**
|
||||
* Requires a module only if available.
|
||||
* @memberof util
|
||||
* @param {string} moduleName Module to require
|
||||
* @returns {?Object} Required module if available and not empty, otherwise `null`
|
||||
* @deprecated Legacy optional require helper. Will be removed in a future release.
|
||||
*/
|
||||
declare function inquire(moduleName: string): object;
|
||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
module.exports = inquire;
|
||||
|
||||
/**
|
||||
* Requires a module only if available.
|
||||
* @memberof util
|
||||
* @param {string} moduleName Module to require
|
||||
* @returns {?Object} Required module if available and not empty, otherwise `null`
|
||||
* @deprecated Legacy optional require helper. Will be removed in a future release.
|
||||
*/
|
||||
function inquire(moduleName) {
|
||||
try {
|
||||
if (typeof require !== "function") {
|
||||
return null;
|
||||
}
|
||||
var mod = require(moduleName);
|
||||
if (mod && (mod.length || Object.keys(mod).length)) return mod;
|
||||
return null;
|
||||
} catch (err) {
|
||||
// ignore
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// maybe worth a shot to prevent renaming issues:
|
||||
// see: https://github.com/webpack/webpack/blob/master/lib/dependencies/CommonJsRequireDependencyParserPlugin.js
|
||||
// triggers on:
|
||||
// - expression require.cache
|
||||
// - expression require (???)
|
||||
// - call require
|
||||
// - call require:commonjs:item
|
||||
// - call require:commonjs:context
|
||||
|
||||
Object.defineProperty(Function.prototype, "__self", { get: function() { return this; } });
|
||||
var r = require.__self;
|
||||
delete Function.prototype.__self;
|
||||
*/
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@protobufjs/inquire",
|
||||
"description": "Requires a module only if available and hides the require call from bundlers.",
|
||||
"version": "1.1.2",
|
||||
"author": "Daniel Wirtz <dcode+protobufjs@dcode.io>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dcodeIO/protobuf.js.git"
|
||||
},
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"devDependencies": {
|
||||
"istanbul": "^0.4.5",
|
||||
"tape": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tape tests/*.js",
|
||||
"coverage": "istanbul cover node_modules/tape/bin/tape tests/*.js"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
module.exports = [1];
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
module.exports = [];
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
module.exports = { a: 1 };
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
var tape = require("tape");
|
||||
|
||||
var inquire = require("..");
|
||||
|
||||
tape.test("inquire", function(test) {
|
||||
|
||||
test.equal(inquire("buffer").Buffer, Buffer, "should be able to require \"buffer\"");
|
||||
|
||||
test.equal(inquire("%invalid"), null, "should not be able to require \"%invalid\"");
|
||||
|
||||
test.equal(inquire("./tests/data/emptyObject"), null, "should return null when requiring a module exporting an empty object");
|
||||
|
||||
test.equal(inquire("./tests/data/emptyArray"), null, "should return null when requiring a module exporting an empty array");
|
||||
|
||||
test.same(inquire("./tests/data/object"), { a: 1 }, "should return the object if a non-empty object");
|
||||
|
||||
test.same(inquire("./tests/data/array"), [ 1 ], "should return the module if a non-empty array");
|
||||
|
||||
test.end();
|
||||
});
|
||||
Generated
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
Copyright (c) 2016, Daniel Wirtz All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of its author, nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user