@colinlu50/openclaw-lark-stream 260323.3.0 → 260327.2.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.en.md +86 -58
- package/README.md +86 -65
- package/bin/openclaw-lark.js +297 -259
- package/dist/index.js +2833 -2350
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
package/bin/openclaw-lark.js
CHANGED
|
@@ -1,259 +1,297 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
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";
|
|
7
|
-
|
|
8
|
-
const SELF_PACKAGE = "@colinlu50/openclaw-lark-stream";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
console.log("\
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
console.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
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";
|
|
7
|
+
|
|
8
|
+
const SELF_PACKAGE = "@colinlu50/openclaw-lark-stream";
|
|
9
|
+
// Version-gated npm specs:
|
|
10
|
+
// - OpenClaw >= 2026.3.22 → latest build (new SDK)
|
|
11
|
+
// - OpenClaw < 2026.3.22 → pinned old build (legacy SDK)
|
|
12
|
+
const PACKAGE_NEW = "@colinlu50/openclaw-lark-stream"; // >= 2026.3.22
|
|
13
|
+
const PACKAGE_OLD = "@colinlu50/openclaw-lark-stream@260323.3.0"; // < 2026.3.22
|
|
14
|
+
const OPENCLAW_BREAKPOINT = { year: 2026, month: 3, day: 22 };
|
|
15
|
+
|
|
16
|
+
const STATE_DIR = process.env.OPENCLAW_STATE_DIR || join(process.env.HOME || process.env.USERPROFILE || "", ".openclaw");
|
|
17
|
+
const EXTENSIONS_DIR = join(STATE_DIR, "extensions");
|
|
18
|
+
const CONFIG_FILE = join(STATE_DIR, "openclaw.json");
|
|
19
|
+
const OFFICIAL_DIR = join(EXTENSIONS_DIR, "openclaw-lark");
|
|
20
|
+
const SELF_DIR = join(EXTENSIONS_DIR, "openclaw-lark-stream");
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const subcommand = args[0];
|
|
24
|
+
|
|
25
|
+
// ── install / update ──
|
|
26
|
+
if (subcommand === "install" || subcommand === "update") {
|
|
27
|
+
await runInstall();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── All other commands: show help ──
|
|
32
|
+
console.log(`Usage: npx ${SELF_PACKAGE} install`);
|
|
33
|
+
console.log(` npx ${SELF_PACKAGE} update`);
|
|
34
|
+
process.exit(0);
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Install flow
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
async function runInstall() {
|
|
41
|
+
// 1. Version check — determines which package version to install
|
|
42
|
+
const npmSpec = resolveNpmSpec();
|
|
43
|
+
|
|
44
|
+
// 2. Clean stale state
|
|
45
|
+
cleanPluginState();
|
|
46
|
+
|
|
47
|
+
// 3. Install the appropriate plugin version
|
|
48
|
+
console.log(`\nInstalling ${npmSpec}...`);
|
|
49
|
+
try {
|
|
50
|
+
execSync(`openclaw plugins install ${npmSpec}`, { stdio: "inherit" });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`\n❌ Failed to install ${npmSpec}.`);
|
|
53
|
+
console.error(error.message || error);
|
|
54
|
+
console.error(`\nYou can retry with: openclaw plugins install ${npmSpec}`);
|
|
55
|
+
process.exit(error.status ?? 1);
|
|
56
|
+
}
|
|
57
|
+
console.log(`\n✅ Plugin installed successfully.`);
|
|
58
|
+
|
|
59
|
+
// 4. Ensure plugins.allow includes our plugin ID
|
|
60
|
+
ensurePluginAllowed("openclaw-lark-stream");
|
|
61
|
+
|
|
62
|
+
// 5. Bot configuration (interactive)
|
|
63
|
+
await configureBotIfNeeded();
|
|
64
|
+
|
|
65
|
+
// 6. Restart gateway
|
|
66
|
+
console.log("\nRestarting gateway...");
|
|
67
|
+
try {
|
|
68
|
+
execSync("openclaw gateway restart", { stdio: "inherit" });
|
|
69
|
+
} catch {
|
|
70
|
+
console.log("Gateway restart failed. You can manually run: openclaw gateway restart");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log("\n🎉 All done!");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Version detection — returns the correct npm spec to install
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Detect the installed OpenClaw version and return the correct npm spec:
|
|
82
|
+
* - >= 2026.3.22 → latest package (new SDK)
|
|
83
|
+
* - < 2026.3.22 → pinned legacy package
|
|
84
|
+
*/
|
|
85
|
+
function resolveNpmSpec() {
|
|
86
|
+
let verString;
|
|
87
|
+
try {
|
|
88
|
+
verString = execSync("openclaw -v", { encoding: "utf8" }).trim();
|
|
89
|
+
console.log(`OpenClaw version: ${verString}`);
|
|
90
|
+
} catch {
|
|
91
|
+
console.error("❌ OpenClaw not found. Install it first: npm install -g openclaw");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ver = parseOpenClawVersion(verString);
|
|
96
|
+
if (!ver) {
|
|
97
|
+
console.warn(`⚠️ Could not parse OpenClaw version "${verString}", installing latest.`);
|
|
98
|
+
return PACKAGE_NEW;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isNew = isVersionAtLeast(ver, OPENCLAW_BREAKPOINT);
|
|
102
|
+
if (isNew) {
|
|
103
|
+
console.log(`✅ OpenClaw >= 2026.3.22 detected — installing latest plugin version.`);
|
|
104
|
+
return PACKAGE_NEW;
|
|
105
|
+
} else {
|
|
106
|
+
console.log(`ℹ️ OpenClaw < 2026.3.22 detected — installing legacy plugin version.`);
|
|
107
|
+
console.log(` (To use the latest plugin, upgrade OpenClaw: npm install -g openclaw)`);
|
|
108
|
+
return PACKAGE_OLD;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Parse "YYYY.M.D" or "OpenClaw YYYY.M.D ..." into { year, month, day }. */
|
|
113
|
+
function parseOpenClawVersion(str) {
|
|
114
|
+
const m = /(\d{4})\.(\d+)\.(\d+)/.exec(str);
|
|
115
|
+
if (!m) return null;
|
|
116
|
+
return { year: parseInt(m[1], 10), month: parseInt(m[2], 10), day: parseInt(m[3], 10) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** True if ver >= { year, month, day }. */
|
|
120
|
+
function isVersionAtLeast(ver, min) {
|
|
121
|
+
if (ver.year !== min.year) return ver.year > min.year;
|
|
122
|
+
if (ver.month !== min.month) return ver.month > min.month;
|
|
123
|
+
return ver.day >= min.day;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Bot configuration
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
async function configureBotIfNeeded() {
|
|
131
|
+
const cfg = readConfig();
|
|
132
|
+
const existing = cfg.channels?.feishu;
|
|
133
|
+
|
|
134
|
+
if (existing?.appId) {
|
|
135
|
+
console.log(`\nFound existing bot config (App ID: ${existing.appId}).`);
|
|
136
|
+
const reuse = await ask("Use existing bot config? (Y/n): ");
|
|
137
|
+
if (reuse.toLowerCase() !== "n") {
|
|
138
|
+
// Keep bot credentials but ensure streaming is enabled
|
|
139
|
+
ensureStreamingConfig(cfg);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log("\n── Feishu Bot Setup ──");
|
|
145
|
+
console.log("You need a Feishu bot app. Create one at: https://open.feishu.cn/app\n");
|
|
146
|
+
|
|
147
|
+
const appId = await ask("App ID: ");
|
|
148
|
+
const appSecret = await ask("App Secret: ");
|
|
149
|
+
|
|
150
|
+
if (!appId || !appSecret) {
|
|
151
|
+
console.log("Skipped. You can configure manually in ~/.openclaw/openclaw.json");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Ask for domain
|
|
156
|
+
const domainChoice = await ask("Domain - feishu or lark? (feishu): ");
|
|
157
|
+
const domain = domainChoice === "lark" ? "lark" : "feishu";
|
|
158
|
+
|
|
159
|
+
// Write config
|
|
160
|
+
if (!cfg.channels) cfg.channels = {};
|
|
161
|
+
cfg.channels.feishu = {
|
|
162
|
+
...(cfg.channels.feishu || {}),
|
|
163
|
+
enabled: true,
|
|
164
|
+
appId,
|
|
165
|
+
appSecret,
|
|
166
|
+
connectionMode: "websocket",
|
|
167
|
+
domain,
|
|
168
|
+
streaming: true,
|
|
169
|
+
defaultAccount: "main",
|
|
170
|
+
replyMode: {
|
|
171
|
+
direct: "streaming",
|
|
172
|
+
group: "streaming",
|
|
173
|
+
default: "streaming",
|
|
174
|
+
},
|
|
175
|
+
accounts: {
|
|
176
|
+
...(cfg.channels?.feishu?.accounts || {}),
|
|
177
|
+
main: { appId, appSecret },
|
|
178
|
+
},
|
|
179
|
+
dmPolicy: cfg.channels?.feishu?.dmPolicy || "pairing",
|
|
180
|
+
groupPolicy: cfg.channels?.feishu?.groupPolicy || "open",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
184
|
+
console.log(`\n✅ Bot configured (App ID: ${appId}).`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Helpers
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Ensure streaming is enabled on an existing config.
|
|
193
|
+
*/
|
|
194
|
+
function ensureStreamingConfig(cfg) {
|
|
195
|
+
if (!cfg.channels?.feishu) return;
|
|
196
|
+
let changed = false;
|
|
197
|
+
if (!cfg.channels.feishu.streaming) {
|
|
198
|
+
cfg.channels.feishu.streaming = true;
|
|
199
|
+
changed = true;
|
|
200
|
+
}
|
|
201
|
+
const rm = cfg.channels.feishu.replyMode || {};
|
|
202
|
+
if (rm.direct !== "streaming" || rm.group !== "streaming" || rm.default !== "streaming") {
|
|
203
|
+
cfg.channels.feishu.replyMode = { direct: "streaming", group: "streaming", default: "streaming" };
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
if (changed) {
|
|
207
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
208
|
+
console.log("Enabled streaming mode for all reply modes.");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ask(prompt) {
|
|
213
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
rl.question(prompt, (answer) => {
|
|
216
|
+
rl.close();
|
|
217
|
+
resolve(answer.trim());
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readConfig() {
|
|
223
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
226
|
+
} catch {
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Remove all plugin directories, staging leftovers, and stale config
|
|
233
|
+
* references so that openclaw has a clean state for the next install.
|
|
234
|
+
*/
|
|
235
|
+
/**
|
|
236
|
+
* Ensure the plugin ID is in plugins.allow so openclaw doesn't warn.
|
|
237
|
+
*/
|
|
238
|
+
function ensurePluginAllowed(pluginId) {
|
|
239
|
+
const cfg = readConfig();
|
|
240
|
+
if (!cfg.plugins) cfg.plugins = {};
|
|
241
|
+
if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = [];
|
|
242
|
+
if (!cfg.plugins.allow.includes(pluginId)) {
|
|
243
|
+
cfg.plugins.allow.push(pluginId);
|
|
244
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
245
|
+
console.log(`Added "${pluginId}" to plugins.allow.`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function cleanPluginState() {
|
|
250
|
+
for (const dir of [OFFICIAL_DIR, SELF_DIR]) {
|
|
251
|
+
if (existsSync(dir)) {
|
|
252
|
+
console.log(`Removing ${dir}...`);
|
|
253
|
+
rmSync(dir, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Remove leftover staging directories (.openclaw-install-stage-*)
|
|
257
|
+
if (existsSync(EXTENSIONS_DIR)) {
|
|
258
|
+
try {
|
|
259
|
+
for (const entry of readdirSync(EXTENSIONS_DIR)) {
|
|
260
|
+
if (entry.startsWith(".openclaw-install-stage-")) {
|
|
261
|
+
const p = join(EXTENSIONS_DIR, entry);
|
|
262
|
+
console.log(`Removing staging dir ${p}...`);
|
|
263
|
+
rmSync(p, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch { /* ignore */ }
|
|
267
|
+
}
|
|
268
|
+
cleanConfigReferences("openclaw-lark");
|
|
269
|
+
cleanConfigReferences("openclaw-lark-stream");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cleanConfigReferences(pluginId) {
|
|
273
|
+
if (!existsSync(CONFIG_FILE)) return;
|
|
274
|
+
try {
|
|
275
|
+
const cfg = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
276
|
+
let changed = false;
|
|
277
|
+
if (cfg.plugins?.entries?.[pluginId]) {
|
|
278
|
+
delete cfg.plugins.entries[pluginId];
|
|
279
|
+
changed = true;
|
|
280
|
+
}
|
|
281
|
+
if (cfg.plugins?.installs?.[pluginId]) {
|
|
282
|
+
delete cfg.plugins.installs[pluginId];
|
|
283
|
+
changed = true;
|
|
284
|
+
}
|
|
285
|
+
if (Array.isArray(cfg.plugins?.allow)) {
|
|
286
|
+
const idx = cfg.plugins.allow.indexOf(pluginId);
|
|
287
|
+
if (idx !== -1) {
|
|
288
|
+
cfg.plugins.allow.splice(idx, 1);
|
|
289
|
+
changed = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (changed) {
|
|
293
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
294
|
+
console.log(`Cleaned "${pluginId}" references from config.`);
|
|
295
|
+
}
|
|
296
|
+
} catch { /* ignore */ }
|
|
297
|
+
}
|