@colinlu50/openclaw-lark-stream 2026.3.29 → 2026.3.31
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.en.md +2 -0
- package/README.md +16 -11
- package/bin/openclaw-lark.js +163 -81
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -2,6 +2,8 @@ English | [中文](./README.md)
|
|
|
2
2
|
|
|
3
3
|
# OpenClaw Lark/Feishu Plugin — Stream Card Fork
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
Fork of the official [openclaw-larksuite](https://github.com/larksuite/openclaw-larksuite) plugin with **streaming block output** and **tool call indicators**.
|
|
6
8
|
|
|
7
9
|
## What's Changed
|
package/README.md
CHANGED
|
@@ -2,27 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
# OpenClaw 飞书插件 — 流式卡片 Fork
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
基于官方 [openclaw-larksuite](https://github.com/larksuite/openclaw-larksuite) 插件的 Fork,支持**流式分块输出**和**工具调用状态展示**。
|
|
6
8
|
|
|
7
|
-
## 改动说明
|
|
9
|
+
## ✨ 改动说明
|
|
8
10
|
|
|
9
11
|
官方插件在 LLM 生成完一个 block 后才一次性推送结果。本 Fork 实现了:
|
|
10
12
|
|
|
11
|
-
-
|
|
13
|
+
- **实时流式输出** — 每个 block 的内容在生成过程中逐步追加到流式卡片
|
|
12
14
|
- **工具调用状态** — agent 调用工具时,卡片顶部实时显示当前工具,完成后自动折叠为摘要面板
|
|
13
15
|
|
|
14
|
-
##
|
|
16
|
+
## 📢 News
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
- **2026.3.23** — 发布第一版,支持实时流式输出和工具调用状态展示
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
openclaw plugins install @colinlu50/openclaw-lark-stream
|
|
20
|
-
```
|
|
20
|
+
## 📦 安装
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
需要 [OpenClaw](https://openclaw.ai)(>= 2026.2.26)和 Node.js(>= v22)。
|
|
23
|
+
|
|
24
|
+
> 查看 OpenClaw 版本:`openclaw -v`,如版本过低请先升级:
|
|
25
|
+
> ```bash
|
|
26
|
+
> npm install -g openclaw
|
|
27
|
+
> ```
|
|
23
28
|
|
|
24
29
|
```bash
|
|
25
|
-
openclaw
|
|
30
|
+
npx -y @colinlu50/openclaw-lark-stream install
|
|
26
31
|
```
|
|
27
32
|
|
|
28
33
|
> 如果之前安装过官方插件,先卸载:
|
|
@@ -39,7 +44,7 @@ cd openclaw-lark-stream && npm install && npm run build
|
|
|
39
44
|
openclaw gateway restart
|
|
40
45
|
```
|
|
41
46
|
|
|
42
|
-
## 配置
|
|
47
|
+
## ⚙️ 配置
|
|
43
48
|
|
|
44
49
|
卡片底栏默认显示耗时和完成状态,如需关闭:
|
|
45
50
|
|
|
@@ -51,6 +56,6 @@ openclaw config set channels.feishu.footer.status false # 隐藏完成状态
|
|
|
51
56
|
- **elapsed** — 卡片底栏显示总响应耗时(如 `耗时 3.2s`)
|
|
52
57
|
- **status** — 卡片底栏显示完成状态(`已完成` / `出错` / `已停止`)
|
|
53
58
|
|
|
54
|
-
## 许可证
|
|
59
|
+
## 📄 许可证
|
|
55
60
|
|
|
56
61
|
MIT — 与上游项目相同。
|
package/bin/openclaw-lark.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
6
7
|
|
|
7
8
|
const SELF_PACKAGE = "@colinlu50/openclaw-lark-stream";
|
|
8
9
|
const STATE_DIR = process.env.OPENCLAW_STATE_DIR || join(process.env.HOME || process.env.USERPROFILE || "", ".openclaw");
|
|
9
10
|
const EXTENSIONS_DIR = join(STATE_DIR, "extensions");
|
|
10
11
|
const CONFIG_FILE = join(STATE_DIR, "openclaw.json");
|
|
11
|
-
// Tools installs official plugin as "openclaw-lark"; our manifest uses "openclaw-lark-stream"
|
|
12
12
|
const OFFICIAL_DIR = join(EXTENSIONS_DIR, "openclaw-lark");
|
|
13
13
|
const SELF_DIR = join(EXTENSIONS_DIR, "openclaw-lark-stream");
|
|
14
14
|
|
|
@@ -16,58 +16,177 @@ const args = process.argv.slice(2);
|
|
|
16
16
|
const subcommand = args[0];
|
|
17
17
|
|
|
18
18
|
// ── install / update ──
|
|
19
|
-
// 1) Let @larksuite/openclaw-lark-tools run the full interactive setup
|
|
20
|
-
// (version check, bot config, gateway restart, etc.)
|
|
21
|
-
// 2) Then swap the installed official code with our fork
|
|
22
19
|
if (subcommand === "install" || subcommand === "update") {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
await runInstall();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── All other commands: show help ──
|
|
25
|
+
console.log(`Usage: npx ${SELF_PACKAGE} install`);
|
|
26
|
+
console.log(` npx ${SELF_PACKAGE} update`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Install flow
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
async function runInstall() {
|
|
34
|
+
// 1. Version check
|
|
35
|
+
checkOpenClawVersion();
|
|
31
36
|
|
|
32
|
-
//
|
|
37
|
+
// 2. Clean stale state
|
|
38
|
+
cleanPluginState();
|
|
39
|
+
|
|
40
|
+
// 3. Install our plugin
|
|
41
|
+
console.log(`\nInstalling ${SELF_PACKAGE}...`);
|
|
33
42
|
try {
|
|
34
|
-
|
|
35
|
-
if (existsSync(dir)) {
|
|
36
|
-
console.log(`\nRemoving ${dir}...`);
|
|
37
|
-
rmSync(dir, { recursive: true, force: true });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Remove stale "openclaw-lark" references from config so openclaw doesn't
|
|
41
|
-
// fail validation when the directory is gone.
|
|
42
|
-
cleanConfigReferences("openclaw-lark");
|
|
43
|
-
|
|
44
|
-
console.log(`\nInstalling ${SELF_PACKAGE}...`);
|
|
45
|
-
// Use execSync with shell so that .cmd shims are resolved on Windows
|
|
46
|
-
execSync(`openclaw plugins install ${SELF_PACKAGE}`, {
|
|
47
|
-
stdio: "inherit",
|
|
48
|
-
});
|
|
49
|
-
console.log(`\n✅ ${SELF_PACKAGE} installed successfully.`);
|
|
50
|
-
console.log("Run: openclaw gateway restart");
|
|
43
|
+
execSync(`openclaw plugins install ${SELF_PACKAGE}`, { stdio: "inherit" });
|
|
51
44
|
} catch (error) {
|
|
52
45
|
console.error(`\n❌ Failed to install ${SELF_PACKAGE}.`);
|
|
53
46
|
console.error(error.message || error);
|
|
54
|
-
console.error(
|
|
47
|
+
console.error(`\nYou can retry with: openclaw plugins install ${SELF_PACKAGE}`);
|
|
55
48
|
process.exit(error.status ?? 1);
|
|
56
49
|
}
|
|
57
|
-
|
|
50
|
+
console.log(`\n✅ Plugin installed successfully.`);
|
|
51
|
+
|
|
52
|
+
// 4. Bot configuration (interactive)
|
|
53
|
+
await configureBotIfNeeded();
|
|
54
|
+
|
|
55
|
+
// 5. Restart gateway
|
|
56
|
+
console.log("\nRestarting gateway...");
|
|
57
|
+
try {
|
|
58
|
+
execSync("openclaw gateway restart", { stdio: "inherit" });
|
|
59
|
+
} catch {
|
|
60
|
+
console.log("Gateway restart failed. You can manually run: openclaw gateway restart");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log("\n🎉 All done!");
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Version check
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function checkOpenClawVersion() {
|
|
71
|
+
try {
|
|
72
|
+
const ver = execSync("openclaw -v", { encoding: "utf8" }).trim();
|
|
73
|
+
console.log(`OpenClaw version: ${ver}`);
|
|
74
|
+
} catch {
|
|
75
|
+
console.error("❌ OpenClaw not found. Install it first: npm install -g openclaw");
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Bot configuration
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
async function configureBotIfNeeded() {
|
|
85
|
+
const cfg = readConfig();
|
|
86
|
+
const existing = cfg.channels?.feishu;
|
|
87
|
+
|
|
88
|
+
if (existing?.appId) {
|
|
89
|
+
console.log(`\nFound existing bot config (App ID: ${existing.appId}).`);
|
|
90
|
+
const reuse = await ask("Use existing bot config? (Y/n): ");
|
|
91
|
+
if (reuse.toLowerCase() !== "n") {
|
|
92
|
+
console.log("Keeping existing config.");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log("\n── Feishu Bot Setup ──");
|
|
98
|
+
console.log("You need a Feishu bot app. Create one at: https://open.feishu.cn/app\n");
|
|
99
|
+
|
|
100
|
+
const appId = await ask("App ID: ");
|
|
101
|
+
const appSecret = await ask("App Secret: ");
|
|
102
|
+
|
|
103
|
+
if (!appId || !appSecret) {
|
|
104
|
+
console.log("Skipped. You can configure manually in ~/.openclaw/openclaw.json");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Ask for domain
|
|
109
|
+
const domainChoice = await ask("Domain - feishu or lark? (feishu): ");
|
|
110
|
+
const domain = domainChoice === "lark" ? "lark" : "feishu";
|
|
111
|
+
|
|
112
|
+
// Write config
|
|
113
|
+
if (!cfg.channels) cfg.channels = {};
|
|
114
|
+
cfg.channels.feishu = {
|
|
115
|
+
...(cfg.channels.feishu || {}),
|
|
116
|
+
enabled: true,
|
|
117
|
+
appId,
|
|
118
|
+
appSecret,
|
|
119
|
+
connectionMode: "websocket",
|
|
120
|
+
domain,
|
|
121
|
+
streaming: true,
|
|
122
|
+
defaultAccount: "main",
|
|
123
|
+
replyMode: {
|
|
124
|
+
direct: "streaming",
|
|
125
|
+
group: "streaming",
|
|
126
|
+
default: "streaming",
|
|
127
|
+
},
|
|
128
|
+
accounts: {
|
|
129
|
+
...(cfg.channels?.feishu?.accounts || {}),
|
|
130
|
+
main: { appId, appSecret },
|
|
131
|
+
},
|
|
132
|
+
dmPolicy: cfg.channels?.feishu?.dmPolicy || "pairing",
|
|
133
|
+
groupPolicy: cfg.channels?.feishu?.groupPolicy || "open",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
137
|
+
console.log(`\n✅ Bot configured (App ID: ${appId}).`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Helpers
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
function ask(prompt) {
|
|
145
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
rl.question(prompt, (answer) => {
|
|
148
|
+
rl.close();
|
|
149
|
+
resolve(answer.trim());
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readConfig() {
|
|
155
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
158
|
+
} catch {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
65
161
|
}
|
|
66
162
|
|
|
67
163
|
/**
|
|
68
|
-
* Remove
|
|
69
|
-
*
|
|
164
|
+
* Remove all plugin directories, staging leftovers, and stale config
|
|
165
|
+
* references so that openclaw has a clean state for the next install.
|
|
70
166
|
*/
|
|
167
|
+
function cleanPluginState() {
|
|
168
|
+
for (const dir of [OFFICIAL_DIR, SELF_DIR]) {
|
|
169
|
+
if (existsSync(dir)) {
|
|
170
|
+
console.log(`Removing ${dir}...`);
|
|
171
|
+
rmSync(dir, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Remove leftover staging directories (.openclaw-install-stage-*)
|
|
175
|
+
if (existsSync(EXTENSIONS_DIR)) {
|
|
176
|
+
try {
|
|
177
|
+
for (const entry of readdirSync(EXTENSIONS_DIR)) {
|
|
178
|
+
if (entry.startsWith(".openclaw-install-stage-")) {
|
|
179
|
+
const p = join(EXTENSIONS_DIR, entry);
|
|
180
|
+
console.log(`Removing staging dir ${p}...`);
|
|
181
|
+
rmSync(p, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch { /* ignore */ }
|
|
185
|
+
}
|
|
186
|
+
cleanConfigReferences("openclaw-lark");
|
|
187
|
+
cleanConfigReferences("openclaw-lark-stream");
|
|
188
|
+
}
|
|
189
|
+
|
|
71
190
|
function cleanConfigReferences(pluginId) {
|
|
72
191
|
if (!existsSync(CONFIG_FILE)) return;
|
|
73
192
|
try {
|
|
@@ -90,44 +209,7 @@ function cleanConfigReferences(pluginId) {
|
|
|
90
209
|
}
|
|
91
210
|
if (changed) {
|
|
92
211
|
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
93
|
-
console.log(`Cleaned "${pluginId}" references from
|
|
212
|
+
console.log(`Cleaned "${pluginId}" references from config.`);
|
|
94
213
|
}
|
|
95
|
-
} catch {
|
|
96
|
-
// Config parse failure — let openclaw handle it
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function runTools(fwdArgs) {
|
|
101
|
-
let version = "latest";
|
|
102
|
-
const vIdx = fwdArgs.indexOf("--tools-version");
|
|
103
|
-
if (vIdx !== -1) {
|
|
104
|
-
version = fwdArgs[vIdx + 1];
|
|
105
|
-
fwdArgs.splice(vIdx, 2);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const allArgs = ["--yes", `@larksuite/openclaw-lark-tools@${version}`, ...fwdArgs];
|
|
109
|
-
|
|
110
|
-
if (process.platform === "win32") {
|
|
111
|
-
const npxCli = join(
|
|
112
|
-
dirname(process.execPath),
|
|
113
|
-
"node_modules",
|
|
114
|
-
"npm",
|
|
115
|
-
"bin",
|
|
116
|
-
"npx-cli.js",
|
|
117
|
-
);
|
|
118
|
-
execFileSync(process.execPath, [npxCli, ...allArgs], {
|
|
119
|
-
stdio: "inherit",
|
|
120
|
-
env: {
|
|
121
|
-
...process.env,
|
|
122
|
-
NODE_OPTIONS: [
|
|
123
|
-
process.env.NODE_OPTIONS,
|
|
124
|
-
"--disable-warning=DEP0190",
|
|
125
|
-
]
|
|
126
|
-
.filter(Boolean)
|
|
127
|
-
.join(" "),
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
} else {
|
|
131
|
-
execFileSync("npx", allArgs, { stdio: "inherit" });
|
|
132
|
-
}
|
|
214
|
+
} catch { /* ignore */ }
|
|
133
215
|
}
|