dsh-lark-bot 0.16.0 → 0.16.1
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 +6 -1
- package/README_EN.md +8 -0
- package/dist/cli.js +91 -11
- package/dist/cli.js.map +1 -1
- package/dist/plugin.js +91 -11
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,11 @@ dsh --profile dsh-lark
|
|
|
85
85
|
|
|
86
86
|
③ 首次启动终端打印二维码 → 飞书 / Lark App 扫码创建或选择 PersonalAgent 应用 → 绑定后私聊直接发消息;群聊 / 话题默认 `@bot`,也可显式开启受白名单保护的无 @ 模式。
|
|
87
87
|
|
|
88
|
+
卡片按钮按 Card JSON 2.0 的 `behaviors.callback` 协议发送;扫码向导会显式申请
|
|
89
|
+
`card.action.trigger` 回调能力。这两项都是计划、审批、问答等交互卡片按钮正常工作的
|
|
90
|
+
必要条件。已有应用若是在旧版本向导中创建,请在飞书开放平台的“事件与回调 → 回调配置”中启用
|
|
91
|
+
卡片回调后重新发布应用,否则消息收发正常但按钮点击不会送达 bot。
|
|
92
|
+
|
|
88
93
|
`setup` 自动完成:定位本机 dsh → 预批准 pnpm 构建策略 → 标准 `dsh plugin add` → 默认安装「安全网守护」系统服务,一条命令完成全部安装。
|
|
89
94
|
|
|
90
95
|
> **无需公网 IP / 域名 / 服务器 / 内网穿透**(飞书 WebSocket 出站长连接),Linux / macOS / Windows 通用。
|
|
@@ -235,7 +240,7 @@ guardian 仍只救援其配置的主实例。
|
|
|
235
240
|
|
|
236
241
|
**任务中向你提问(问答卡)**:agent 需要你拍板、确认或补充信息时,通过 `lark_ask_user` 工具弹**问答卡**(单选 / 多选 / 自由文本)。可提交卡片,也可直接回复该卡片输入任意文字;单选/多选没有合适项时,回复文字就是补充答案。系统按被回复的 card messageId 精确匹配 pending 问题,回答后任务自动继续,等待期间运行超时看门狗暂停。(与 `/ask` 的“你主动提问”方向相反。)
|
|
237
242
|
|
|
238
|
-
|
|
243
|
+
计划、审批与问答卡提交后会立即显示成功提示、发送一条终态确认并撤回原卡,避免按钮仍停留在聊天中造成“未生效”的误解;失效卡会返回明确错误提示,入站点击与失效原因写入结构化日志。确认或撤回失败不会影响已经提交给 agent 的决策、审批结果或答案。本地人机决策回调会以 JSON 空白流保活,避免 Node HTTP 客户端在等待 5 分钟后切断仍有效的卡片。
|
|
239
244
|
|
|
240
245
|
**安全网守护**:独立于 dsh 进程、系统级常驻的最小守护进程(systemd / LaunchAgent / Windows 启动项),默认随 `setup` 安装。dsh 正常时静默;dsh 下线或无法 boot(如第三方插件破坏 profile 组合)时自动接管飞书通道,无需命令行即可自救:
|
|
241
246
|
|
package/README_EN.md
CHANGED
|
@@ -87,6 +87,12 @@ dsh --profile dsh-lark
|
|
|
87
87
|
|
|
88
88
|
③ On first boot the terminal prints a QR code → scan it with the Feishu / Lark app to create or choose a PersonalAgent app → after binding, DM the bot directly; groups/topics use `@bot` by default, with an explicitly enabled allowlist-protected no-@ mode available.
|
|
89
89
|
|
|
90
|
+
Buttons use Card JSON 2.0 `behaviors.callback`, and the QR flow explicitly requests the
|
|
91
|
+
`card.action.trigger` callback capability required by plan, approval, and
|
|
92
|
+
question-card buttons. If an existing app was created by an older flow, enable card callbacks under Developer
|
|
93
|
+
Console → Events & Callbacks → Callback Configuration and publish the app again. Without it, messages still work
|
|
94
|
+
while card clicks never reach the bot.
|
|
95
|
+
|
|
90
96
|
`setup` automatically: locates your local dsh → pre-approves pnpm's build policy (protobufjs) → runs the standard `dsh plugin --profile dsh-lark add dsh-lark-bot@<version>` (pinned to the running package) → installs the safety-net guardian system service. One command installs everything.
|
|
91
97
|
|
|
92
98
|
> **No public IP / domain / server / tunneling required** (Feishu outbound WebSocket long connection); works on Linux / macOS / Windows.
|
|
@@ -239,6 +245,8 @@ stopping it cancels and recalls only that session's pending card. The legacy hea
|
|
|
239
245
|
|
|
240
246
|
**Mid-task questions (question cards)**: when the agent needs a decision, confirmation, or missing information, it sends a **question card** via the `lark_ask_user` tool (single choice / multi choice / free text). Submit the form or reply directly to that card with any text—even when none of the listed choices fits. The replied card message id selects the exact pending question, the agent resumes automatically, and the run-timeout watchdog pauses while it waits. (The opposite direction of `/ask`, where you ask the agent.)
|
|
241
247
|
|
|
248
|
+
Plan, approval and question-card submissions immediately show a native toast, post a terminal confirmation and recall the original card. Stale cards return an explicit error toast, while received actions and stale reasons are written to structured logs. Confirmation/recall failures do not change the decision already delivered to the agent. Local human-decision callbacks stream insignificant JSON whitespace while waiting so Node's HTTP client cannot invalidate a live card after five minutes.
|
|
249
|
+
|
|
242
250
|
**Safety-net guardian**: a minimal system-level resident process (systemd / LaunchAgent / Windows startup), independent of the dsh process and installed **by default with `setup`**. Silent while dsh runs, it takes over the Feishu channel when dsh goes down or fails to boot (e.g. a third-party plugin breaks the profile composition), so you can self-heal without touching the command line:
|
|
243
251
|
|
|
244
252
|
- `/safemode`: enter **core-only safe mode** (only the official `dsh-base` + `dsh-headless` bundles, **no third-party plugins**) — prefers the SDK streaming engine, falls back to headless, and lets you locate / fix / disable the offending plugin right from the chat;
|
package/dist/cli.js
CHANGED
|
@@ -6230,7 +6230,9 @@ function localizedCard(input) {
|
|
|
6230
6230
|
if (JSON.stringify(zhCallbacks) !== JSON.stringify(enCallbacks)) {
|
|
6231
6231
|
throw new Error("Localized card callback values must be identical");
|
|
6232
6232
|
}
|
|
6233
|
-
const body2 =
|
|
6233
|
+
const body2 = withV2CallbackBehaviors(
|
|
6234
|
+
localizeNode(input.zhCn.body, input.enUs.body, "body", input.bilingualFallback === true)
|
|
6235
|
+
);
|
|
6234
6236
|
const header = input.zhCn.header ? localizeNode(
|
|
6235
6237
|
input.zhCn.header,
|
|
6236
6238
|
input.enUs.header ?? input.zhCn.header,
|
|
@@ -6255,6 +6257,24 @@ function localizedCard(input) {
|
|
|
6255
6257
|
body: body2
|
|
6256
6258
|
};
|
|
6257
6259
|
}
|
|
6260
|
+
function withV2CallbackBehaviors(value) {
|
|
6261
|
+
if (Array.isArray(value)) return value.map(withV2CallbackBehaviors);
|
|
6262
|
+
if (!value || typeof value !== "object") return value;
|
|
6263
|
+
const source = value;
|
|
6264
|
+
const mapped = Object.fromEntries(
|
|
6265
|
+
Object.entries(source).map(([key, child]) => [key, withV2CallbackBehaviors(child)])
|
|
6266
|
+
);
|
|
6267
|
+
if (source.tag !== "button" || source.value === void 0) return mapped;
|
|
6268
|
+
const { value: callbackValue, ...button2 } = mapped;
|
|
6269
|
+
const behaviors = Array.isArray(button2.behaviors) ? button2.behaviors : [];
|
|
6270
|
+
return {
|
|
6271
|
+
...button2,
|
|
6272
|
+
behaviors: [
|
|
6273
|
+
...behaviors,
|
|
6274
|
+
{ type: "callback", value: callbackValue }
|
|
6275
|
+
]
|
|
6276
|
+
};
|
|
6277
|
+
}
|
|
6258
6278
|
function bilingualMarkdown(zhCn, enUs) {
|
|
6259
6279
|
if (zhCn === enUs) return zhCn;
|
|
6260
6280
|
return `${zhCn}
|
|
@@ -12491,6 +12511,13 @@ ${msg.content}`,
|
|
|
12491
12511
|
message: processMessage,
|
|
12492
12512
|
cardAction: async (event) => {
|
|
12493
12513
|
const value = event.action.value && typeof event.action.value === "object" ? event.action.value : void 0;
|
|
12514
|
+
const command = typeof value?.cmd === "string" ? value.cmd : void 0;
|
|
12515
|
+
log.info("card-action", "received", {
|
|
12516
|
+
chatId: event.chatId,
|
|
12517
|
+
messageId: event.messageId,
|
|
12518
|
+
operatorId: event.operator?.openId,
|
|
12519
|
+
command
|
|
12520
|
+
});
|
|
12494
12521
|
const threadId = cardActionThreadId(event.raw);
|
|
12495
12522
|
const currentScope = isolatedScope({
|
|
12496
12523
|
chatId: event.chatId,
|
|
@@ -12564,7 +12591,15 @@ ${msg.content}`,
|
|
|
12564
12591
|
if (value?.cmd === "approve" && typeof value.id === "string" && deps.approvals) {
|
|
12565
12592
|
const outcome = value.outcome === "allow" ? "allowed-once" : "rejected";
|
|
12566
12593
|
const settled = deps.approvals.resolve(scope, value.id, outcome);
|
|
12567
|
-
if (!settled)
|
|
12594
|
+
if (!settled) {
|
|
12595
|
+
log.warn("card-action", "stale", { kind: "approval", scope, messageId: event.messageId });
|
|
12596
|
+
return {
|
|
12597
|
+
toast: {
|
|
12598
|
+
type: "error",
|
|
12599
|
+
content: "\u6B64\u5BA1\u6279\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247 / This approval card is stale; use the latest card"
|
|
12600
|
+
}
|
|
12601
|
+
};
|
|
12602
|
+
}
|
|
12568
12603
|
const allowed = outcome === "allowed-once";
|
|
12569
12604
|
void settleActionCard(
|
|
12570
12605
|
channel,
|
|
@@ -12592,7 +12627,15 @@ ${msg.content}`,
|
|
|
12592
12627
|
question.options
|
|
12593
12628
|
);
|
|
12594
12629
|
const settled = deps.questions.resolve(scope, value.id, answer);
|
|
12595
|
-
if (!settled)
|
|
12630
|
+
if (!settled) {
|
|
12631
|
+
log.warn("card-action", "stale", { kind: "question", scope, messageId: event.messageId });
|
|
12632
|
+
return {
|
|
12633
|
+
toast: {
|
|
12634
|
+
type: "error",
|
|
12635
|
+
content: "\u6B64\u95EE\u7B54\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247 / This question card is stale; use the latest card"
|
|
12636
|
+
}
|
|
12637
|
+
};
|
|
12638
|
+
}
|
|
12596
12639
|
void settleActionCard(
|
|
12597
12640
|
channel,
|
|
12598
12641
|
event.chatId,
|
|
@@ -12606,7 +12649,13 @@ ${msg.content}`,
|
|
|
12606
12649
|
toast: { type: "success", content: "\u56DE\u7B54\u5DF2\u63D0\u4EA4 / Answer submitted" }
|
|
12607
12650
|
};
|
|
12608
12651
|
}
|
|
12609
|
-
|
|
12652
|
+
log.warn("card-action", "stale", { kind: "question", scope, messageId: event.messageId });
|
|
12653
|
+
return {
|
|
12654
|
+
toast: {
|
|
12655
|
+
type: "error",
|
|
12656
|
+
content: "\u6B64\u95EE\u7B54\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247 / This question card is stale; use the latest card"
|
|
12657
|
+
}
|
|
12658
|
+
};
|
|
12610
12659
|
}
|
|
12611
12660
|
if (value?.cmd === "plan-submit" && typeof value.id === "string" && deps.plans) {
|
|
12612
12661
|
const decision = value.decision === "approved" ? "approved" : "revise";
|
|
@@ -12616,7 +12665,15 @@ ${msg.content}`,
|
|
|
12616
12665
|
decision,
|
|
12617
12666
|
...feedback ? { feedback } : {}
|
|
12618
12667
|
});
|
|
12619
|
-
if (!settled)
|
|
12668
|
+
if (!settled) {
|
|
12669
|
+
log.warn("card-action", "stale", { kind: "plan", scope, messageId: event.messageId });
|
|
12670
|
+
return {
|
|
12671
|
+
toast: {
|
|
12672
|
+
type: "error",
|
|
12673
|
+
content: "\u6B64\u8BA1\u5212\u5361\u5DF2\u5931\u6548\uFF0C\u8BF7\u4F7F\u7528\u6700\u65B0\u5361\u7247 / This plan card is stale; use the latest card"
|
|
12674
|
+
}
|
|
12675
|
+
};
|
|
12676
|
+
}
|
|
12620
12677
|
const approved = decision === "approved";
|
|
12621
12678
|
void settleActionCard(
|
|
12622
12679
|
channel,
|
|
@@ -12931,7 +12988,7 @@ var NotifyServer = class {
|
|
|
12931
12988
|
}
|
|
12932
12989
|
async handle(req, res) {
|
|
12933
12990
|
const respond = (status, body2) => {
|
|
12934
|
-
res.writeHead(status, { "content-type": "application/json" });
|
|
12991
|
+
if (!res.headersSent) res.writeHead(status, { "content-type": "application/json" });
|
|
12935
12992
|
res.end(`${JSON.stringify(body2)}
|
|
12936
12993
|
`);
|
|
12937
12994
|
};
|
|
@@ -12955,12 +13012,17 @@ var NotifyServer = class {
|
|
|
12955
13012
|
respond(400, { ok: false, error: "sessionId and question are required" });
|
|
12956
13013
|
return;
|
|
12957
13014
|
}
|
|
12958
|
-
const
|
|
12959
|
-
|
|
12960
|
-
|
|
12961
|
-
|
|
13015
|
+
const stopKeepAlive = startJsonKeepAlive(res);
|
|
13016
|
+
try {
|
|
13017
|
+
const result = await this.deps.ask(payload);
|
|
13018
|
+
if (!result.ok) {
|
|
13019
|
+
respond(404, { ok: false, ...result.error === void 0 ? {} : { error: result.error } });
|
|
13020
|
+
return;
|
|
13021
|
+
}
|
|
13022
|
+
respond(200, { ok: true, ...result.answer === void 0 ? {} : { answer: result.answer } });
|
|
13023
|
+
} finally {
|
|
13024
|
+
stopKeepAlive();
|
|
12962
13025
|
}
|
|
12963
|
-
respond(200, { ok: true, ...result.answer === void 0 ? {} : { answer: result.answer } });
|
|
12964
13026
|
return;
|
|
12965
13027
|
}
|
|
12966
13028
|
if (req.url === "/plan") {
|
|
@@ -12985,9 +13047,11 @@ var NotifyServer = class {
|
|
|
12985
13047
|
};
|
|
12986
13048
|
res.once("close", onResponseClose);
|
|
12987
13049
|
let result;
|
|
13050
|
+
const stopKeepAlive = startJsonKeepAlive(res);
|
|
12988
13051
|
try {
|
|
12989
13052
|
result = await this.deps.plan(payload, controller.signal);
|
|
12990
13053
|
} finally {
|
|
13054
|
+
stopKeepAlive();
|
|
12991
13055
|
req.off("aborted", abort);
|
|
12992
13056
|
res.off("close", onResponseClose);
|
|
12993
13057
|
}
|
|
@@ -13024,9 +13088,11 @@ var NotifyServer = class {
|
|
|
13024
13088
|
};
|
|
13025
13089
|
res.once("close", onResponseClose);
|
|
13026
13090
|
let result;
|
|
13091
|
+
const stopKeepAlive = startJsonKeepAlive(res);
|
|
13027
13092
|
try {
|
|
13028
13093
|
result = await this.deps.approval(payload, controller.signal);
|
|
13029
13094
|
} finally {
|
|
13095
|
+
stopKeepAlive();
|
|
13030
13096
|
req.off("aborted", abort);
|
|
13031
13097
|
res.off("close", onResponseClose);
|
|
13032
13098
|
}
|
|
@@ -13092,6 +13158,16 @@ var NotifyServer = class {
|
|
|
13092
13158
|
}
|
|
13093
13159
|
}
|
|
13094
13160
|
};
|
|
13161
|
+
var JSON_KEEP_ALIVE_MS = 3e4;
|
|
13162
|
+
function startJsonKeepAlive(res) {
|
|
13163
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
13164
|
+
res.write(" ");
|
|
13165
|
+
const timer = setInterval(() => {
|
|
13166
|
+
if (!res.destroyed && !res.writableEnded) res.write(" ");
|
|
13167
|
+
}, JSON_KEEP_ALIVE_MS);
|
|
13168
|
+
timer.unref?.();
|
|
13169
|
+
return () => clearInterval(timer);
|
|
13170
|
+
}
|
|
13095
13171
|
function readBody(req) {
|
|
13096
13172
|
return new Promise((resolve6, reject) => {
|
|
13097
13173
|
const chunks = [];
|
|
@@ -13619,6 +13695,10 @@ async function onboardPersonalAgent(deps = {}) {
|
|
|
13619
13695
|
print("");
|
|
13620
13696
|
const result = await register({
|
|
13621
13697
|
source: deps.source ?? DEFAULT_SOURCE,
|
|
13698
|
+
// Card callbacks are a separate application capability in Feishu/Lark.
|
|
13699
|
+
// A WebSocket can be healthy and receive messages while silently never
|
|
13700
|
+
// receiving card.action.trigger unless this callback is enabled.
|
|
13701
|
+
addons: { callbacks: { items: ["card.action.trigger"] } },
|
|
13622
13702
|
...deps.signal ? { signal: deps.signal } : {},
|
|
13623
13703
|
onQRCodeReady: (info) => {
|
|
13624
13704
|
print("\u8BF7\u4F7F\u7528\u98DE\u4E66 / Lark App \u626B\u63CF\u4EE5\u4E0B\u4E8C\u7EF4\u7801\uFF0C\u521B\u5EFA\u6216\u9009\u62E9 PersonalAgent \u5E94\u7528\uFF1A");
|