@vibe-lark/larkpal 0.1.91-issue68.0 → 0.1.91-issue70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/main.mjs +281 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,9 +93,60 @@ lark-cli auth login
|
|
|
93
93
|
| `LARKPAL_ENCRYPTION_KEY` | 用户凭证加密密钥(32字节 hex) | 未设置(明文,仅限开发) |
|
|
94
94
|
| `LARKPAL_PLUGINS` | 声明式加载的插件列表 | 空 |
|
|
95
95
|
| `LARKPAL_CHAT_JWT_SECRET` | Chat API JWT 签名密钥 | fallback 到 `LARKPAL_API_SECRET` |
|
|
96
|
+
| `LARKPAL_BUSINESS_EVENT_CALLBACK_URL` | 飞书长连接业务事件回调地址,用于固定菜单和业务卡片 action | 空 |
|
|
96
97
|
|
|
97
98
|
会话工作目录默认写入 `LARKPAL_WORKSPACE` 下的隐藏目录:飞书会话为 `.chats/`,通用 Agent 会话为 `.sessions/`,Chat API 历史为 `.chat-history/`。
|
|
98
99
|
|
|
100
|
+
### Business Event Callback
|
|
101
|
+
|
|
102
|
+
当配置 `LARKPAL_BUSINESS_EVENT_CALLBACK_URL` 时,LarkPal 会把内置处理器未消费的业务事件 POST 到该地址。典型来源包括飞书固定机器人菜单 `application.bot.menu_v6` 和业务自定义 `card.action.trigger`。回调响应可以让 LarkPal 在触发会话中发送可见反馈;业务系统拥有 workflow 语义和卡片内容,LarkPal 只负责 Feishu transport。
|
|
103
|
+
|
|
104
|
+
**文本消息响应**
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{ "kind": "message", "text": "配方草稿已创建,请继续填写。" }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
兼容格式:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{ "message": { "text": "配方草稿已创建,请继续填写。" } }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**互动卡片响应**
|
|
117
|
+
|
|
118
|
+
LarkPal 原样发送 `card` payload,不重写业务 action id。
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"kind": "card",
|
|
123
|
+
"card": {
|
|
124
|
+
"schema": "2.0",
|
|
125
|
+
"body": { "elements": [] }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
兼容格式:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{ "card": { "schema": "2.0", "body": { "elements": [] } } }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**轻量提示响应**
|
|
137
|
+
|
|
138
|
+
对于不支持直接 toast 的事件(如固定菜单),LarkPal 会降级发送一条可见文本消息,避免用户点击后无反馈。
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{ "kind": "toast", "toast": { "type": "warning", "content": "请先上传实验文档。" } }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
兼容格式:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "toast": { "type": "warning", "content": "请先上传实验文档。" } }
|
|
148
|
+
```
|
|
149
|
+
|
|
99
150
|
### larkpal-agent Runtime 专用(LARKPAL_RUNTIME=larkpal-agent 时必填)
|
|
100
151
|
|
|
101
152
|
| 变量名 | 用途 | 示例 |
|
package/dist/main.mjs
CHANGED
|
@@ -7009,6 +7009,175 @@ function clearChatInfoCache(accountId) {
|
|
|
7009
7009
|
}
|
|
7010
7010
|
}
|
|
7011
7011
|
//#endregion
|
|
7012
|
+
//#region src/core/lark-ws-event-normalizer.ts
|
|
7013
|
+
const REDACTED_HEADER_PATTERN = /authorization|token|secret|signature|encrypt|cookie|credential/i;
|
|
7014
|
+
function normalizeLarkWsEventFrame(frame) {
|
|
7015
|
+
const rewrittenTypeFrame = readHeader(frame.headers, ["type"]) === "card" ? {
|
|
7016
|
+
...frame,
|
|
7017
|
+
headers: (frame.headers ?? []).map((header) => header.key === "type" ? {
|
|
7018
|
+
...header,
|
|
7019
|
+
value: "event"
|
|
7020
|
+
} : header)
|
|
7021
|
+
} : frame;
|
|
7022
|
+
if (rewrittenTypeFrame.payload && rewrittenTypeFrame.payload.byteLength > 0) return rewrittenTypeFrame;
|
|
7023
|
+
const eventType = readHeader(rewrittenTypeFrame.headers, [
|
|
7024
|
+
"event_type",
|
|
7025
|
+
"event-type",
|
|
7026
|
+
"eventType",
|
|
7027
|
+
"x-lark-event-type",
|
|
7028
|
+
"x-lark-event_type"
|
|
7029
|
+
]);
|
|
7030
|
+
if (eventType !== "application.bot.menu_v6") return rewrittenTypeFrame;
|
|
7031
|
+
const eventKey = readHeader(rewrittenTypeFrame.headers, [
|
|
7032
|
+
"event_key",
|
|
7033
|
+
"event-key",
|
|
7034
|
+
"eventKey",
|
|
7035
|
+
"menu_event_key",
|
|
7036
|
+
"menu-event-key",
|
|
7037
|
+
"menuEventKey"
|
|
7038
|
+
]);
|
|
7039
|
+
if (!eventKey) return rewrittenTypeFrame;
|
|
7040
|
+
const payload = buildBotMenuPayload(rewrittenTypeFrame.headers, eventType, eventKey);
|
|
7041
|
+
return {
|
|
7042
|
+
...rewrittenTypeFrame,
|
|
7043
|
+
headers: ensureDispatchHeaders(rewrittenTypeFrame.headers),
|
|
7044
|
+
payload: new TextEncoder().encode(JSON.stringify(payload))
|
|
7045
|
+
};
|
|
7046
|
+
}
|
|
7047
|
+
function getLarkWsFrameDiagnostics(frame) {
|
|
7048
|
+
const diagnostics = {
|
|
7049
|
+
msgType: readHeader(frame.headers, ["type"]),
|
|
7050
|
+
eventType: readHeader(frame.headers, [
|
|
7051
|
+
"event_type",
|
|
7052
|
+
"event-type",
|
|
7053
|
+
"eventType",
|
|
7054
|
+
"x-lark-event-type",
|
|
7055
|
+
"x-lark-event_type"
|
|
7056
|
+
]),
|
|
7057
|
+
eventKey: readHeader(frame.headers, [
|
|
7058
|
+
"event_key",
|
|
7059
|
+
"event-key",
|
|
7060
|
+
"eventKey",
|
|
7061
|
+
"menu_event_key",
|
|
7062
|
+
"menu-event-key"
|
|
7063
|
+
]),
|
|
7064
|
+
headersCount: frame.headers?.length ?? 0,
|
|
7065
|
+
hasData: frame.data != null,
|
|
7066
|
+
hasPayload: !!frame.payload && frame.payload.byteLength > 0,
|
|
7067
|
+
dataPreview: previewUnknown(frame.data),
|
|
7068
|
+
payloadPreview: previewPayload(frame.payload)
|
|
7069
|
+
};
|
|
7070
|
+
if (!diagnostics.eventType || !diagnostics.hasPayload) diagnostics.headers = sanitizeHeaders(frame.headers);
|
|
7071
|
+
return diagnostics;
|
|
7072
|
+
}
|
|
7073
|
+
function buildBotMenuPayload(headers, eventType, eventKey) {
|
|
7074
|
+
const tenantKey = readHeader(headers, [
|
|
7075
|
+
"tenant_key",
|
|
7076
|
+
"tenant-key",
|
|
7077
|
+
"tenantKey",
|
|
7078
|
+
"x-tenant-key"
|
|
7079
|
+
]);
|
|
7080
|
+
const operatorOpenId = readHeader(headers, [
|
|
7081
|
+
"operator_open_id",
|
|
7082
|
+
"operator-open-id",
|
|
7083
|
+
"operatorOpenId",
|
|
7084
|
+
"open_id",
|
|
7085
|
+
"open-id"
|
|
7086
|
+
]);
|
|
7087
|
+
const operatorUserId = readHeader(headers, [
|
|
7088
|
+
"operator_user_id",
|
|
7089
|
+
"operator-user-id",
|
|
7090
|
+
"operatorUserId",
|
|
7091
|
+
"user_id",
|
|
7092
|
+
"user-id"
|
|
7093
|
+
]);
|
|
7094
|
+
const operatorUnionId = readHeader(headers, [
|
|
7095
|
+
"operator_union_id",
|
|
7096
|
+
"operator-union-id",
|
|
7097
|
+
"operatorUnionId",
|
|
7098
|
+
"union_id"
|
|
7099
|
+
]);
|
|
7100
|
+
const openChatId = readHeader(headers, [
|
|
7101
|
+
"open_chat_id",
|
|
7102
|
+
"open-chat-id",
|
|
7103
|
+
"openChatId",
|
|
7104
|
+
"chat_id",
|
|
7105
|
+
"chat-id"
|
|
7106
|
+
]);
|
|
7107
|
+
return omitUndefined({
|
|
7108
|
+
schema: "2.0",
|
|
7109
|
+
header: omitUndefined({
|
|
7110
|
+
event_type: eventType,
|
|
7111
|
+
tenant_key: tenantKey
|
|
7112
|
+
}),
|
|
7113
|
+
event: omitUndefined({
|
|
7114
|
+
event_key: eventKey,
|
|
7115
|
+
operator: operatorOpenId || operatorUserId || operatorUnionId ? { operator_id: omitUndefined({
|
|
7116
|
+
open_id: operatorOpenId,
|
|
7117
|
+
user_id: operatorUserId,
|
|
7118
|
+
union_id: operatorUnionId
|
|
7119
|
+
}) } : void 0,
|
|
7120
|
+
open_chat_id: openChatId
|
|
7121
|
+
})
|
|
7122
|
+
});
|
|
7123
|
+
}
|
|
7124
|
+
function readHeader(headers, names) {
|
|
7125
|
+
const wanted = new Set(names.map(normalizeHeaderName));
|
|
7126
|
+
for (const header of headers ?? []) {
|
|
7127
|
+
const key = header.key;
|
|
7128
|
+
const value = header.value;
|
|
7129
|
+
if (!key || typeof value !== "string" || !value.trim()) continue;
|
|
7130
|
+
if (wanted.has(normalizeHeaderName(key))) return value;
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
function normalizeHeaderName(name) {
|
|
7134
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
7135
|
+
}
|
|
7136
|
+
function sanitizeHeaders(headers) {
|
|
7137
|
+
return (headers ?? []).filter((header) => typeof header.key === "string").map((header) => ({
|
|
7138
|
+
key: header.key,
|
|
7139
|
+
value: REDACTED_HEADER_PATTERN.test(header.key) ? "[redacted]" : previewString(header.value)
|
|
7140
|
+
}));
|
|
7141
|
+
}
|
|
7142
|
+
function ensureDispatchHeaders(headers) {
|
|
7143
|
+
const nextHeaders = [...headers ?? []];
|
|
7144
|
+
const ensureHeader = (key, value) => {
|
|
7145
|
+
if (!readHeader(nextHeaders, [key])) nextHeaders.push({
|
|
7146
|
+
key,
|
|
7147
|
+
value
|
|
7148
|
+
});
|
|
7149
|
+
};
|
|
7150
|
+
ensureHeader("message_id", `synthetic-${Date.now()}`);
|
|
7151
|
+
ensureHeader("sum", "1");
|
|
7152
|
+
ensureHeader("seq", "0");
|
|
7153
|
+
ensureHeader("trace_id", "synthetic-bot-menu");
|
|
7154
|
+
return nextHeaders;
|
|
7155
|
+
}
|
|
7156
|
+
function previewPayload(payload) {
|
|
7157
|
+
if (!payload || payload.byteLength === 0) return void 0;
|
|
7158
|
+
try {
|
|
7159
|
+
return previewString(new TextDecoder().decode(payload));
|
|
7160
|
+
} catch {
|
|
7161
|
+
return `[binary:${payload.byteLength}]`;
|
|
7162
|
+
}
|
|
7163
|
+
}
|
|
7164
|
+
function previewUnknown(value) {
|
|
7165
|
+
if (value == null) return void 0;
|
|
7166
|
+
if (typeof value === "string") return previewString(value);
|
|
7167
|
+
try {
|
|
7168
|
+
return previewString(JSON.stringify(value));
|
|
7169
|
+
} catch {
|
|
7170
|
+
return String(value).slice(0, 200);
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
function previewString(value) {
|
|
7174
|
+
if (typeof value !== "string") return void 0;
|
|
7175
|
+
return value.slice(0, 200);
|
|
7176
|
+
}
|
|
7177
|
+
function omitUndefined(value) {
|
|
7178
|
+
return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== void 0));
|
|
7179
|
+
}
|
|
7180
|
+
//#endregion
|
|
7012
7181
|
//#region src/core/lark-client.ts
|
|
7013
7182
|
/**
|
|
7014
7183
|
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
@@ -7331,24 +7500,13 @@ var LarkClient = class LarkClient {
|
|
|
7331
7500
|
const wsClientAny = this._wsClient;
|
|
7332
7501
|
const origHandleEventData = wsClientAny.handleEventData.bind(wsClientAny);
|
|
7333
7502
|
wsClientAny.handleEventData = (data) => {
|
|
7334
|
-
const
|
|
7335
|
-
const
|
|
7503
|
+
const normalizedData = normalizeLarkWsEventFrame(data);
|
|
7504
|
+
const diagnostics = getLarkWsFrameDiagnostics(normalizedData);
|
|
7336
7505
|
log$24.info("WS 收到原始事件", {
|
|
7337
7506
|
accountId: this.accountId,
|
|
7338
|
-
|
|
7339
|
-
eventType,
|
|
7340
|
-
headersCount: data.headers?.length,
|
|
7341
|
-
hasData: !!data.data,
|
|
7342
|
-
dataPreview: typeof data.data === "string" ? data.data.slice(0, 200) : JSON.stringify(data.data)?.slice(0, 200)
|
|
7343
|
-
});
|
|
7344
|
-
if (msgType === "card") return origHandleEventData({
|
|
7345
|
-
...data,
|
|
7346
|
-
headers: data.headers.map((h) => h.key === "type" ? {
|
|
7347
|
-
...h,
|
|
7348
|
-
value: "event"
|
|
7349
|
-
} : h)
|
|
7507
|
+
...diagnostics
|
|
7350
7508
|
});
|
|
7351
|
-
return origHandleEventData(
|
|
7509
|
+
return origHandleEventData(normalizedData);
|
|
7352
7510
|
};
|
|
7353
7511
|
await this.waitForAbort(dispatcher, abortSignal);
|
|
7354
7512
|
}
|
|
@@ -18179,7 +18337,15 @@ async function routeBusinessEvent(params) {
|
|
|
18179
18337
|
if (response.status === 204) return void 0;
|
|
18180
18338
|
if (!(response.headers.get("content-type") ?? "").includes("application/json")) return void 0;
|
|
18181
18339
|
const payload = await response.json();
|
|
18182
|
-
|
|
18340
|
+
if (!isObject(payload)) return void 0;
|
|
18341
|
+
const normalizedResponse = normalizeBusinessCallbackResponse(payload);
|
|
18342
|
+
await sendVisibleBusinessResponse({
|
|
18343
|
+
cfg: params.cfg,
|
|
18344
|
+
accountId: params.accountId,
|
|
18345
|
+
envelope,
|
|
18346
|
+
response: normalizedResponse
|
|
18347
|
+
});
|
|
18348
|
+
return normalizedResponse.raw;
|
|
18183
18349
|
} catch (err) {
|
|
18184
18350
|
log.warn("business event callback failed", {
|
|
18185
18351
|
accountId: params.accountId,
|
|
@@ -18229,6 +18395,105 @@ function extractBotMenuKey(rawEvent) {
|
|
|
18229
18395
|
const event = asRecord(raw.event);
|
|
18230
18396
|
return readString(raw.event_key, event.event_key, raw.menu_event_key, event.menu_event_key);
|
|
18231
18397
|
}
|
|
18398
|
+
async function sendVisibleBusinessResponse(params) {
|
|
18399
|
+
const target = params.envelope.conversationId;
|
|
18400
|
+
if (!target) {
|
|
18401
|
+
log.warn("business callback response has no Feishu send target", {
|
|
18402
|
+
accountId: params.accountId,
|
|
18403
|
+
eventType: params.envelope.eventType,
|
|
18404
|
+
responseKind: params.response.kind,
|
|
18405
|
+
trigger: params.envelope.trigger
|
|
18406
|
+
});
|
|
18407
|
+
return;
|
|
18408
|
+
}
|
|
18409
|
+
try {
|
|
18410
|
+
if (params.response.kind === "card" && params.response.card) {
|
|
18411
|
+
await sendCardFeishu({
|
|
18412
|
+
cfg: params.cfg,
|
|
18413
|
+
accountId: params.accountId,
|
|
18414
|
+
to: target,
|
|
18415
|
+
card: params.response.card
|
|
18416
|
+
});
|
|
18417
|
+
log.info("business callback card response sent", {
|
|
18418
|
+
accountId: params.accountId,
|
|
18419
|
+
eventType: params.envelope.eventType,
|
|
18420
|
+
responseKind: params.response.kind,
|
|
18421
|
+
target,
|
|
18422
|
+
trigger: params.envelope.trigger
|
|
18423
|
+
});
|
|
18424
|
+
return;
|
|
18425
|
+
}
|
|
18426
|
+
const text = params.response.kind === "message" ? params.response.text : params.response.toast?.content;
|
|
18427
|
+
if (text) {
|
|
18428
|
+
await sendMessageFeishu({
|
|
18429
|
+
cfg: params.cfg,
|
|
18430
|
+
accountId: params.accountId,
|
|
18431
|
+
to: target,
|
|
18432
|
+
text
|
|
18433
|
+
});
|
|
18434
|
+
log.info("business callback message response sent", {
|
|
18435
|
+
accountId: params.accountId,
|
|
18436
|
+
eventType: params.envelope.eventType,
|
|
18437
|
+
responseKind: params.response.kind,
|
|
18438
|
+
target,
|
|
18439
|
+
trigger: params.envelope.trigger
|
|
18440
|
+
});
|
|
18441
|
+
}
|
|
18442
|
+
} catch (err) {
|
|
18443
|
+
log.warn("business callback visible response send failed", {
|
|
18444
|
+
accountId: params.accountId,
|
|
18445
|
+
eventType: params.envelope.eventType,
|
|
18446
|
+
responseKind: params.response.kind,
|
|
18447
|
+
target,
|
|
18448
|
+
trigger: params.envelope.trigger,
|
|
18449
|
+
error: err instanceof Error ? err.message : String(err)
|
|
18450
|
+
});
|
|
18451
|
+
}
|
|
18452
|
+
}
|
|
18453
|
+
function normalizeBusinessCallbackResponse(payload) {
|
|
18454
|
+
const kind = typeof payload.kind === "string" ? payload.kind : void 0;
|
|
18455
|
+
const message = asRecord(payload.message);
|
|
18456
|
+
const toast = asRecord(payload.toast);
|
|
18457
|
+
const card = normalizeCardPayload(payload);
|
|
18458
|
+
const text = readString(payload.text, message.text, message.content);
|
|
18459
|
+
if (kind === "card" && card) return {
|
|
18460
|
+
kind: "card",
|
|
18461
|
+
card,
|
|
18462
|
+
raw: payload
|
|
18463
|
+
};
|
|
18464
|
+
if (card && !kind) return {
|
|
18465
|
+
kind: "card",
|
|
18466
|
+
card,
|
|
18467
|
+
raw: payload
|
|
18468
|
+
};
|
|
18469
|
+
if (kind === "message" && text) return {
|
|
18470
|
+
kind: "message",
|
|
18471
|
+
text,
|
|
18472
|
+
raw: payload
|
|
18473
|
+
};
|
|
18474
|
+
if (text && !kind) return {
|
|
18475
|
+
kind: "message",
|
|
18476
|
+
text,
|
|
18477
|
+
raw: payload
|
|
18478
|
+
};
|
|
18479
|
+
if ((kind === "toast" || !kind) && isObject(toast)) return {
|
|
18480
|
+
kind: "toast",
|
|
18481
|
+
toast: {
|
|
18482
|
+
type: readString(toast.type),
|
|
18483
|
+
content: readString(toast.content)
|
|
18484
|
+
},
|
|
18485
|
+
raw: payload
|
|
18486
|
+
};
|
|
18487
|
+
return {
|
|
18488
|
+
kind: "none",
|
|
18489
|
+
raw: payload
|
|
18490
|
+
};
|
|
18491
|
+
}
|
|
18492
|
+
function normalizeCardPayload(payload) {
|
|
18493
|
+
if (isObject(payload.card)) return payload.card;
|
|
18494
|
+
const card = asRecord(payload.card);
|
|
18495
|
+
if (isObject(card.data)) return card.data;
|
|
18496
|
+
}
|
|
18232
18497
|
function extractOperator(raw) {
|
|
18233
18498
|
const operator = asRecord(raw.operator);
|
|
18234
18499
|
const operatorId = asRecord(operator.operator_id);
|