@tencent-connect/openclaw-qqbot 1.6.4-alpha.18 → 1.6.4-alpha.20

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.
@@ -34,6 +34,7 @@ function getFrameworkVersion() {
34
34
  if (_frameworkVersion !== null)
35
35
  return _frameworkVersion;
36
36
  try {
37
+ // 先尝试 PATH 中的 CLI
37
38
  for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
38
39
  try {
39
40
  const out = execFileSync(cli, ["--version"], { timeout: 3000, encoding: "utf8" }).trim();
@@ -47,6 +48,15 @@ function getFrameworkVersion() {
47
48
  continue;
48
49
  }
49
50
  }
51
+ // 尝试 findCli() 找到的完整路径
52
+ const cliPath = findCli();
53
+ if (cliPath) {
54
+ const out = execCliSync(cliPath, ["--version"]);
55
+ if (out) {
56
+ _frameworkVersion = out;
57
+ return _frameworkVersion;
58
+ }
59
+ }
50
60
  }
51
61
  catch {
52
62
  // fallback
@@ -260,7 +270,12 @@ function saveUpgradeGreetingTarget(accountId, appId, openid) {
260
270
  }
261
271
  // ============ 热更新 ============
262
272
  /**
263
- * 找到 CLI 命令名(openclaw / clawdbot / moltbot)
273
+ * 找到 CLI 命令名或完整路径(openclaw / clawdbot / moltbot)
274
+ *
275
+ * 查找策略:
276
+ * 1. 系统 PATH(where / which)
277
+ * 2. 打包环境(HoldClaw / QQAIO):从当前文件路径向上推断 CLI 位置
278
+ * 3. ~/.openclaw/bin/ 等常见安装路径
264
279
  */
265
280
  function findCli() {
266
281
  const whichCmd = isWindows() ? "where" : "which";
@@ -273,10 +288,90 @@ function findCli() {
273
288
  continue;
274
289
  }
275
290
  }
291
+ // 打包环境 fallback:从当前文件路径推断 CLI
292
+ // 典型路径: .../gateway/node_modules/openclaw-qqbot/dist/src/slash-commands.js
293
+ // CLI 位于: .../gateway/node_modules/openclaw/openclaw.mjs
294
+ // 或者: .../gateway/node_modules/.bin/openclaw
295
+ try {
296
+ const currentFile = fileURLToPath(import.meta.url);
297
+ const currentDir = path.dirname(currentFile);
298
+ // 向上查找 node_modules 目录
299
+ let dir = currentDir;
300
+ for (let i = 0; i < 10; i++) {
301
+ const basename = path.basename(dir);
302
+ if (basename === "node_modules") {
303
+ // 检查 .bin 下的 CLI
304
+ for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
305
+ const binName = isWindows() ? `${cli}.cmd` : cli;
306
+ const binPath = path.join(dir, ".bin", binName);
307
+ if (fs.existsSync(binPath))
308
+ return binPath;
309
+ }
310
+ // 检查 openclaw/openclaw.mjs(直接通过 node 调用)
311
+ for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
312
+ const mjsPath = path.join(dir, cli, `${cli}.mjs`);
313
+ if (fs.existsSync(mjsPath))
314
+ return mjsPath;
315
+ }
316
+ break;
317
+ }
318
+ const parent = path.dirname(dir);
319
+ if (parent === dir)
320
+ break;
321
+ dir = parent;
322
+ }
323
+ }
324
+ catch {
325
+ // ignore
326
+ }
327
+ // ~/.openclaw/bin/ 等常见安装路径
328
+ const homeDir = getHomeDir();
329
+ for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
330
+ const ext = isWindows() ? ".exe" : "";
331
+ const candidates = [
332
+ path.join(homeDir, `.${cli}`, "bin", `${cli}${ext}`),
333
+ path.join(homeDir, `.${cli}`, `${cli}${ext}`),
334
+ ];
335
+ for (const p of candidates) {
336
+ if (fs.existsSync(p))
337
+ return p;
338
+ }
339
+ }
276
340
  return null;
277
341
  }
278
342
  /**
279
- * 找到升级脚本路径(兼容源码运行、dist 运行、已安装扩展目录)
343
+ * 同步执行 CLI 命令。
344
+ * 当 cliPath 是 .mjs 文件时,自动通过 process.execPath (node) 调用。
345
+ */
346
+ function execCliSync(cliPath, args) {
347
+ try {
348
+ if (cliPath.endsWith(".mjs")) {
349
+ return execFileSync(process.execPath, [cliPath, ...args], {
350
+ timeout: 5000, encoding: "utf8", stdio: "pipe",
351
+ }).trim() || null;
352
+ }
353
+ return execFileSync(cliPath, args, {
354
+ timeout: 5000, encoding: "utf8", stdio: "pipe",
355
+ }).trim() || null;
356
+ }
357
+ catch {
358
+ return null;
359
+ }
360
+ }
361
+ /**
362
+ * 异步执行 CLI 命令。
363
+ * 当 cliPath 是 .mjs 文件时,自动通过 process.execPath (node) 调用。
364
+ */
365
+ function execCliAsync(cliPath, args, opts, cb) {
366
+ if (cliPath.endsWith(".mjs")) {
367
+ execFile(process.execPath, [cliPath, ...args], opts, cb);
368
+ }
369
+ else {
370
+ execFile(cliPath, args, opts, cb);
371
+ }
372
+ }
373
+ /**
374
+ * 找到升级脚本路径(兼容源码运行、dist 运行、已安装扩展目录、打包环境)
280
375
  * Windows 优先查找 .ps1,Mac/Linux 查找 .sh
281
376
  */
282
377
  function getUpgradeScriptPath() {
@@ -284,10 +379,24 @@ function getUpgradeScriptPath() {
284
379
  const currentDir = path.dirname(currentFile);
285
380
  const scriptName = isWindows() ? "upgrade-via-npm.ps1" : "upgrade-via-npm.sh";
286
381
  const candidates = [
382
+ // 源码运行: src/slash-commands.ts → ../../scripts/
383
+ // dist 运行: dist/src/slash-commands.js → ../../scripts/
287
384
  path.resolve(currentDir, "..", "..", "scripts", scriptName),
385
+ // npm 安装: node_modules/@tencent-connect/openclaw-qqbot/dist/src → ../../scripts
288
386
  path.resolve(currentDir, "..", "scripts", scriptName),
289
387
  path.resolve(process.cwd(), "scripts", scriptName),
290
388
  ];
389
+ // 向上查找包含 scripts/ 的祖先目录(适应各种嵌套深度的打包环境)
390
+ let dir = currentDir;
391
+ for (let i = 0; i < 6; i++) {
392
+ const candidate = path.join(dir, "scripts", scriptName);
393
+ if (!candidates.includes(candidate))
394
+ candidates.push(candidate);
395
+ const parent = path.dirname(dir);
396
+ if (parent === dir)
397
+ break;
398
+ dir = parent;
399
+ }
291
400
  const homeDir = getHomeDir();
292
401
  for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
293
402
  candidates.push(path.join(homeDir, `.${cli}`, "extensions", "openclaw-qqbot", "scripts", scriptName));
@@ -505,12 +614,12 @@ function fireHotUpgrade(targetVersion) {
505
614
  // 确保新进程启动时读到的是 npm source,不会被本地源码覆盖。
506
615
  switchPluginSourceToNpm();
507
616
  // 文件替换成功,立即触发 gateway restart
508
- execFile(cli, ["gateway", "restart"], { timeout: 30_000 }, (restartErr) => {
617
+ execCliAsync(cli, ["gateway", "restart"], { timeout: 30_000 }, (restartErr) => {
509
618
  if (restartErr) {
510
619
  // restart 失败,尝试 stop + start 作为 fallback
511
- execFile(cli, ["gateway", "stop"], { timeout: 10_000 }, () => {
620
+ execCliAsync(cli, ["gateway", "stop"], { timeout: 10_000 }, () => {
512
621
  setTimeout(() => {
513
- execFile(cli, ["gateway", "start"], { timeout: 30_000 }, () => { });
622
+ execCliAsync(cli, ["gateway", "start"], { timeout: 30_000 }, () => { });
514
623
  }, 1000);
515
624
  });
516
625
  }
@@ -724,14 +833,41 @@ registerCommand({
724
833
  return resultLines.join("\n");
725
834
  },
726
835
  });
836
+ /**
837
+ * 从 openclaw.json / clawdbot.json / moltbot.json 的 logging.file 配置中
838
+ * 提取用户自定义的日志文件路径(直接文件路径,非目录)。
839
+ */
840
+ function getConfiguredLogFiles() {
841
+ const homeDir = getHomeDir();
842
+ const files = [];
843
+ for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
844
+ try {
845
+ const cfgPath = path.join(homeDir, `.${cli}`, `${cli}.json`);
846
+ if (!fs.existsSync(cfgPath))
847
+ continue;
848
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
849
+ const logFile = cfg?.logging?.file;
850
+ if (logFile && typeof logFile === "string") {
851
+ files.push(path.resolve(logFile));
852
+ }
853
+ break;
854
+ }
855
+ catch {
856
+ // ignore
857
+ }
858
+ }
859
+ return files;
860
+ }
727
861
  /**
728
862
  * /bot-logs — 导出本地日志文件
729
863
  *
730
864
  * 日志定位策略(兼容腾讯云/各云厂商不同安装路径):
731
- * 1. 优先使用 *_STATE_DIR 环境变量(OPENCLAW/CLAWDBOT/MOLTBOT)
865
+ * 0. 优先从 openclaw.json 的 logging.file 配置中读取自定义日志路径(最精确)
866
+ * 1. 使用 *_STATE_DIR 环境变量(OPENCLAW/CLAWDBOT/MOLTBOT)
732
867
  * 2. 扫描常见状态目录:~/.openclaw, ~/.clawdbot, ~/.moltbot 及其 logs 子目录
733
868
  * 3. 扫描 home/cwd/AppData 下名称包含 openclaw/clawdbot/moltbot 的目录
734
- * 4. 在候选目录中选取最近更新的日志文件(gateway/openclaw/clawdbot/moltbot
869
+ * 4. 扫描 /var/log 下的 openclaw/clawdbot/moltbot 目录
870
+ * 5. 在候选目录中选取最近更新的日志文件(gateway/openclaw/clawdbot/moltbot)
735
871
  */
736
872
  function collectCandidateLogDirs() {
737
873
  const homeDir = getHomeDir();
@@ -748,6 +884,11 @@ function collectCandidateLogDirs() {
748
884
  pushDir(stateDir);
749
885
  pushDir(path.join(stateDir, "logs"));
750
886
  };
887
+ // 0. 从配置文件的 logging.file 提取目录
888
+ for (const logFile of getConfiguredLogFiles()) {
889
+ pushDir(path.dirname(logFile));
890
+ }
891
+ // 1. 环境变量 *_STATE_DIR
751
892
  for (const [key, value] of Object.entries(process.env)) {
752
893
  if (!value)
753
894
  continue;
@@ -755,10 +896,12 @@ function collectCandidateLogDirs() {
755
896
  pushStateDir(value);
756
897
  }
757
898
  }
899
+ // 2. 常见状态目录
758
900
  for (const name of [".openclaw", ".clawdbot", ".moltbot", "openclaw", "clawdbot", "moltbot"]) {
759
901
  pushDir(path.join(homeDir, name));
760
902
  pushDir(path.join(homeDir, name, "logs"));
761
903
  }
904
+ // 3. home/cwd/AppData 下包含 openclaw/clawdbot/moltbot 的子目录
762
905
  const searchRoots = new Set([
763
906
  homeDir,
764
907
  process.cwd(),
@@ -785,6 +928,12 @@ function collectCandidateLogDirs() {
785
928
  // 无权限或不存在,跳过
786
929
  }
787
930
  }
931
+ // 4. /var/log 下的常见日志目录(Linux 服务器部署场景)
932
+ if (!isWindows()) {
933
+ for (const name of ["openclaw", "clawdbot", "moltbot"]) {
934
+ pushDir(path.join("/var/log", name));
935
+ }
936
+ }
788
937
  return Array.from(dirs);
789
938
  }
790
939
  function collectRecentLogFiles(logDirs) {
@@ -805,6 +954,10 @@ function collectRecentLogFiles(logDirs) {
805
954
  // 文件不存在或无权限
806
955
  }
807
956
  };
957
+ // 优先级最高:用户在 openclaw.json logging.file 中显式配置的日志文件
958
+ for (const logFile of getConfiguredLogFiles()) {
959
+ pushFile(logFile, path.dirname(logFile));
960
+ }
808
961
  for (const dir of logDirs) {
809
962
  pushFile(path.join(dir, "gateway.log"), dir);
810
963
  pushFile(path.join(dir, "gateway.err.log"), dir);
@@ -843,8 +996,24 @@ registerCommand({
843
996
  const logDirs = collectCandidateLogDirs();
844
997
  const recentFiles = collectRecentLogFiles(logDirs).slice(0, 4);
845
998
  if (recentFiles.length === 0) {
846
- const searched = logDirs.map(d => ` - ${d}`).join("\n");
847
- return `⚠️ 未找到日志文件\n\n已搜索以下路径:\n${searched}`;
999
+ const existingDirs = logDirs.filter(d => { try {
1000
+ return fs.existsSync(d);
1001
+ }
1002
+ catch {
1003
+ return false;
1004
+ } });
1005
+ const searched = existingDirs.length > 0
1006
+ ? existingDirs.map(d => ` • ${d}`).join("\n")
1007
+ : logDirs.slice(0, 6).map(d => ` • ${d}`).join("\n") + (logDirs.length > 6 ? `\n …及其他 ${logDirs.length - 6} 个路径` : "");
1008
+ return [
1009
+ `⚠️ 未找到日志文件`,
1010
+ ``,
1011
+ `已搜索以下${existingDirs.length > 0 ? "已存在的" : ""}路径:`,
1012
+ searched,
1013
+ ``,
1014
+ `💡 如果日志在自定义路径,请在配置文件中添加:`,
1015
+ ` "logging": { "file": "/path/to/your/logfile.log" }`,
1016
+ ].join("\n");
848
1017
  }
849
1018
  const lines = [];
850
1019
  let totalIncluded = 0;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * 启动问候语系统:首次安装/版本更新 vs 普通重启
3
3
  */
4
- export declare function getStartupGreetingText(version: string): string;
4
+ export declare function getFirstLaunchGreetingText(): string;
5
+ export declare function getUpgradeGreetingText(version: string): string;
5
6
  export type StartupMarkerData = {
6
7
  version?: string;
7
8
  startedAt?: string;
@@ -13,10 +14,11 @@ export type StartupMarkerData = {
13
14
  export declare function readStartupMarker(): StartupMarkerData;
14
15
  export declare function writeStartupMarker(data: StartupMarkerData): void;
15
16
  /**
16
- * 判断是否需要发送"灵魂上线"问候:
17
- * - 首次安装 / 版本变更:可发送
18
- * - 同版本:不发送
19
- * - 同版本近期失败:冷却期内不重试,减少噪音
17
+ * 判断是否需要发送启动问候:
18
+ * - 首次启动(无 marker)→ "灵魂已上线"
19
+ * - 版本变更 → "已更新至 vX.Y.Z"
20
+ * - 同版本 → 不发送
21
+ * - 同版本近期失败 → 冷却期内不重试
20
22
  */
21
23
  export declare function getStartupGreetingPlan(): {
22
24
  shouldSend: boolean;
@@ -7,7 +7,10 @@ import { getQQBotDataDir } from "./utils/platform.js";
7
7
  import { getPluginVersion } from "./slash-commands.js";
8
8
  const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
9
9
  const STARTUP_GREETING_RETRY_COOLDOWN_MS = 10 * 60 * 1000;
10
- export function getStartupGreetingText(version) {
10
+ export function getFirstLaunchGreetingText() {
11
+ return `Haha,我的'灵魂'已上线,随时等你吩咐。`;
12
+ }
13
+ export function getUpgradeGreetingText(version) {
11
14
  return `🎉 QQBot 插件已更新至 v${version},在线等候你的吩咐。`;
12
15
  }
13
16
  export function readStartupMarker() {
@@ -31,10 +34,11 @@ export function writeStartupMarker(data) {
31
34
  }
32
35
  }
33
36
  /**
34
- * 判断是否需要发送"灵魂上线"问候:
35
- * - 首次安装 / 版本变更:可发送
36
- * - 同版本:不发送
37
- * - 同版本近期失败:冷却期内不重试,减少噪音
37
+ * 判断是否需要发送启动问候:
38
+ * - 首次启动(无 marker)→ "灵魂已上线"
39
+ * - 版本变更 → "已更新至 vX.Y.Z"
40
+ * - 同版本 → 不发送
41
+ * - 同版本近期失败 → 冷却期内不重试
38
42
  */
39
43
  export function getStartupGreetingPlan() {
40
44
  const currentVersion = getPluginVersion();
@@ -48,7 +52,11 @@ export function getStartupGreetingPlan() {
48
52
  return { shouldSend: false, version: currentVersion, reason: "cooldown" };
49
53
  }
50
54
  }
51
- return { shouldSend: true, greeting: getStartupGreetingText(currentVersion), version: currentVersion };
55
+ const isFirstLaunch = !marker.version;
56
+ const greeting = isFirstLaunch
57
+ ? getFirstLaunchGreetingText()
58
+ : getUpgradeGreetingText(currentVersion);
59
+ return { shouldSend: true, greeting, version: currentVersion };
52
60
  }
53
61
  export function markStartupGreetingSent(version) {
54
62
  writeStartupMarker({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.4-alpha.18",
3
+ "version": "1.6.4-alpha.20",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,46 @@
1
+
2
+ ========== gateway.log (last 44 of 44 lines) ==========
3
+ from: C:\Users\v_qachchen\.openclaw
4
+ [2026-03-20T15:55:33.816Z] state: stopped → starting
5
+ [2026-03-20T15:55:33.819Z] [extract] 检测到 runtime.tar.gz,开始解压...
6
+ [2026-03-20T15:55:37.282Z] [extract] runtime.tar.gz 解压完成 (3.5s)
7
+ [2026-03-20T15:55:37.286Z] [extract] 检测到 gateway.tar.gz,开始解压...
8
+ [2026-03-20T15:56:05.916Z] [extract] gateway.tar.gz 解压完成 (28.6s)
9
+ [2026-03-20T15:56:05.924Z] --- gateway start ---
10
+ [2026-03-20T15:56:05.926Z] platform=win32 arch=x64 packaged=true
11
+ [2026-03-20T15:56:05.927Z] resourcesPath=C:\Users\v_qachchen\AppData\Local\Programs\HoldClaw\resources\resources
12
+ [2026-03-20T15:56:05.928Z] nodeBin=C:\Users\v_qachchen\AppData\Local\Programs\HoldClaw\resources\resources\runtime\node.exe exists=true
13
+ [2026-03-20T15:56:05.928Z] entry=C:\Users\v_qachchen\AppData\Local\Programs\HoldClaw\resources\resources\gateway\node_modules\openclaw\openclaw.mjs exists=true
14
+ [2026-03-20T15:56:05.929Z] cwd=C:\Users\v_qachchen\.openclaw\workspace exists=true
15
+ [2026-03-20T15:56:05.929Z] token=a98d...06c4 port=19789
16
+ [2026-03-20T15:56:05.978Z] TTS 未配置,使用默认 SiliconFlow URL
17
+ [2026-03-20T15:56:05.979Z] spawn: C:\Users\v_qachchen\AppData\Local\Programs\HoldClaw\resources\resources\runtime\node.exe C:\Users\v_qachchen\AppData\Local\Programs\HoldClaw\resources\resources\gateway\node_modules\openclaw\openclaw.mjs gateway run --port 19789 --bind loopback
18
+ [2026-03-20T15:56:14.259Z] stdout: |
19
+ o Doctor warnings ------------------------------------------------------+
20
+ | |
21
+ | - channels.imessage.groupPolicy is "allowlist" but groupAllowFrom is |
22
+ | empty — this channel does not fall back to allowFrom, so all group |
23
+ | messages will be silently dropped. Add sender IDs to |
24
+ | channels.imessage.groupAllowFrom, or set groupPolicy to "open". |
25
+ | |
26
+ +------------------------------------------------------------------------+
27
+ [2026-03-20T15:56:44.457Z] stdout: 2026-03-20T15:56:44.457Z [canvas] host mounted at http://127.0.0.1:19789/__openclaw__/canvas/ (root C:\Users\v_qachchen\.openclaw\canvas)
28
+ [2026-03-20T15:56:44.786Z] stdout: 2026-03-20T15:56:44.784Z [heartbeat] started
29
+ [2026-03-20T15:56:44.791Z] stdout: 2026-03-20T15:56:44.791Z [health-monitor] started (interval: 300s, startup-grace: 60s, channel-connect-grace: 120s)
30
+ [2026-03-20T15:56:44.804Z] stdout: 2026-03-20T15:56:44.802Z [gateway] agent model: custom/hy-hunyuan-instruct
31
+ [2026-03-20T15:56:44.806Z] stdout: 2026-03-20T15:56:44.806Z [gateway] listening on ws://127.0.0.1:19789, ws://[::1]:19789 (PID 224544)
32
+ [2026-03-20T15:56:44.812Z] stdout: 2026-03-20T15:56:44.812Z [gateway] log file: \tmp\openclaw\openclaw-2026-03-20.log
33
+ [2026-03-20T15:56:44.929Z] stdout: 2026-03-20T15:56:44.929Z [browser/server] Browser control listening on http://127.0.0.1:19791/ (auth=token)
34
+ [2026-03-20T15:56:45.150Z] health check passed, child alive
35
+ [2026-03-20T15:56:45.151Z] state: starting → running
36
+ [2026-03-20T15:56:46.272Z] stdout: 2026-03-20T15:56:46.112Z [hooks:loader] Registered hook: boot-md -> gateway:startup
37
+ 2026-03-20T15:56:46.179Z [hooks:loader] Registered hook: bootstrap-extra-files -> agent:bootstrap
38
+ 2026-03-20T15:56:46.247Z [hooks:loader] Registered hook: command-logger -> command
39
+ [2026-03-20T15:56:46.273Z] stderr: 2026-03-20T15:56:46.166Z [ws] closed before connect conn=bd4ae208-a072-4475-8c6b-63daad0e7025 remote=127.0.0.1 fwd=n/a origin=file:// host=127.0.0.1:19789 ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HoldClaw/1.0.14 Chrome/144.0.7559.225 Electron/40.7.0 Safari/537.36 code=1006 reason=n/a
40
+ 2026-03-20T15:56:46.235Z [ws] unauthorized conn=723433fc-d3d5-4065-b919-924e0992dcfe remote=127.0.0.1 client=openclaw-control-ui webchat vdev reason=token_mismatch
41
+ 2026-03-20T15:56:46.252Z [ws] closed before connect conn=723433fc-d3d5-4065-b919-924e0992dcfe remote=127.0.0.1 fwd=n/a origin=file:// host=127.0.0.1:19789 ua=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HoldClaw/1.0.14 Chrome/144.0.7559.225 Electron/40.7.0 Safari/537.36 code=1008 reason=unauthorized: gateway token mismatch (open the dashboard URL and paste the token in Control UI settings)
42
+ [2026-03-20T15:56:46.318Z] stdout: 2026-03-20T15:56:46.302Z [hooks:loader] Registered hook: session-memory -> command:new, command:reset
43
+ 2026-03-20T15:56:46.308Z [hooks] loaded 4 internal hook handlers
44
+ [2026-03-20T15:56:46.637Z] stdout: 2026-03-20T15:56:46.637Z [gateway] update available (latest): v2026.3.13 (current v2026.3.2). Run: openclaw update
45
+ [2026-03-20T15:56:47.076Z] stdout: 2026-03-20T15:56:47.076Z [gateway] device pairing auto-approved device=bd0fa7fe507891dfe0875c53fd38d0582145228b1478b4e62ad102f07d9254df role=operator
46
+ [2026-03-20T15:56:47.081Z] stdout: 2026-03-20T15:56:47.081Z [ws] webchat connected conn=200f754d-9791-4dcc-bfaa-93c839311f65 remote=127.0.0.1 client=openclaw-control-ui webchat vdev
@@ -36,6 +36,7 @@ let _frameworkVersion: string | null = null;
36
36
  function getFrameworkVersion(): string {
37
37
  if (_frameworkVersion !== null) return _frameworkVersion;
38
38
  try {
39
+ // 先尝试 PATH 中的 CLI
39
40
  for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
40
41
  try {
41
42
  const out = execFileSync(cli, ["--version"], { timeout: 3000, encoding: "utf8" }).trim();
@@ -48,6 +49,15 @@ function getFrameworkVersion(): string {
48
49
  continue;
49
50
  }
50
51
  }
52
+ // 尝试 findCli() 找到的完整路径
53
+ const cliPath = findCli();
54
+ if (cliPath) {
55
+ const out = execCliSync(cliPath, ["--version"]);
56
+ if (out) {
57
+ _frameworkVersion = out;
58
+ return _frameworkVersion;
59
+ }
60
+ }
51
61
  } catch {
52
62
  // fallback
53
63
  }
@@ -346,7 +356,12 @@ function saveUpgradeGreetingTarget(accountId: string, appId: string, openid: str
346
356
  // ============ 热更新 ============
347
357
 
348
358
  /**
349
- * 找到 CLI 命令名(openclaw / clawdbot / moltbot)
359
+ * 找到 CLI 命令名或完整路径(openclaw / clawdbot / moltbot)
360
+ *
361
+ * 查找策略:
362
+ * 1. 系统 PATH(where / which)
363
+ * 2. 打包环境(HoldClaw / QQAIO):从当前文件路径向上推断 CLI 位置
364
+ * 3. ~/.openclaw/bin/ 等常见安装路径
350
365
  */
351
366
  function findCli(): string | null {
352
367
  const whichCmd = isWindows() ? "where" : "which";
@@ -358,11 +373,95 @@ function findCli(): string | null {
358
373
  continue;
359
374
  }
360
375
  }
376
+
377
+ // 打包环境 fallback:从当前文件路径推断 CLI
378
+ // 典型路径: .../gateway/node_modules/openclaw-qqbot/dist/src/slash-commands.js
379
+ // CLI 位于: .../gateway/node_modules/openclaw/openclaw.mjs
380
+ // 或者: .../gateway/node_modules/.bin/openclaw
381
+ try {
382
+ const currentFile = fileURLToPath(import.meta.url);
383
+ const currentDir = path.dirname(currentFile);
384
+
385
+ // 向上查找 node_modules 目录
386
+ let dir = currentDir;
387
+ for (let i = 0; i < 10; i++) {
388
+ const basename = path.basename(dir);
389
+ if (basename === "node_modules") {
390
+ // 检查 .bin 下的 CLI
391
+ for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
392
+ const binName = isWindows() ? `${cli}.cmd` : cli;
393
+ const binPath = path.join(dir, ".bin", binName);
394
+ if (fs.existsSync(binPath)) return binPath;
395
+ }
396
+ // 检查 openclaw/openclaw.mjs(直接通过 node 调用)
397
+ for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
398
+ const mjsPath = path.join(dir, cli, `${cli}.mjs`);
399
+ if (fs.existsSync(mjsPath)) return mjsPath;
400
+ }
401
+ break;
402
+ }
403
+ const parent = path.dirname(dir);
404
+ if (parent === dir) break;
405
+ dir = parent;
406
+ }
407
+ } catch {
408
+ // ignore
409
+ }
410
+
411
+ // ~/.openclaw/bin/ 等常见安装路径
412
+ const homeDir = getHomeDir();
413
+ for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
414
+ const ext = isWindows() ? ".exe" : "";
415
+ const candidates = [
416
+ path.join(homeDir, `.${cli}`, "bin", `${cli}${ext}`),
417
+ path.join(homeDir, `.${cli}`, `${cli}${ext}`),
418
+ ];
419
+ for (const p of candidates) {
420
+ if (fs.existsSync(p)) return p;
421
+ }
422
+ }
423
+
361
424
  return null;
362
425
  }
363
426
 
364
427
  /**
365
- * 找到升级脚本路径(兼容源码运行、dist 运行、已安装扩展目录)
428
+ * 同步执行 CLI 命令。
429
+ * 当 cliPath 是 .mjs 文件时,自动通过 process.execPath (node) 调用。
430
+ */
431
+ function execCliSync(cliPath: string, args: string[]): string | null {
432
+ try {
433
+ if (cliPath.endsWith(".mjs")) {
434
+ return execFileSync(process.execPath, [cliPath, ...args], {
435
+ timeout: 5000, encoding: "utf8", stdio: "pipe",
436
+ }).trim() || null;
437
+ }
438
+ return execFileSync(cliPath, args, {
439
+ timeout: 5000, encoding: "utf8", stdio: "pipe",
440
+ }).trim() || null;
441
+ } catch {
442
+ return null;
443
+ }
444
+ }
445
+
446
+ /**
447
+ * 异步执行 CLI 命令。
448
+ * 当 cliPath 是 .mjs 文件时,自动通过 process.execPath (node) 调用。
449
+ */
450
+ function execCliAsync(
451
+ cliPath: string,
452
+ args: string[],
453
+ opts: { timeout?: number; env?: NodeJS.ProcessEnv; windowsHide?: boolean },
454
+ cb: (error: Error | null, stdout: string, stderr: string) => void,
455
+ ): void {
456
+ if (cliPath.endsWith(".mjs")) {
457
+ execFile(process.execPath, [cliPath, ...args], opts, cb);
458
+ } else {
459
+ execFile(cliPath, args, opts, cb);
460
+ }
461
+ }
462
+
463
+ /**
464
+ * 找到升级脚本路径(兼容源码运行、dist 运行、已安装扩展目录、打包环境)
366
465
  * Windows 优先查找 .ps1,Mac/Linux 查找 .sh
367
466
  */
368
467
  function getUpgradeScriptPath(): string | null {
@@ -371,11 +470,24 @@ function getUpgradeScriptPath(): string | null {
371
470
  const scriptName = isWindows() ? "upgrade-via-npm.ps1" : "upgrade-via-npm.sh";
372
471
 
373
472
  const candidates = [
473
+ // 源码运行: src/slash-commands.ts → ../../scripts/
474
+ // dist 运行: dist/src/slash-commands.js → ../../scripts/
374
475
  path.resolve(currentDir, "..", "..", "scripts", scriptName),
476
+ // npm 安装: node_modules/@tencent-connect/openclaw-qqbot/dist/src → ../../scripts
375
477
  path.resolve(currentDir, "..", "scripts", scriptName),
376
478
  path.resolve(process.cwd(), "scripts", scriptName),
377
479
  ];
378
480
 
481
+ // 向上查找包含 scripts/ 的祖先目录(适应各种嵌套深度的打包环境)
482
+ let dir = currentDir;
483
+ for (let i = 0; i < 6; i++) {
484
+ const candidate = path.join(dir, "scripts", scriptName);
485
+ if (!candidates.includes(candidate)) candidates.push(candidate);
486
+ const parent = path.dirname(dir);
487
+ if (parent === dir) break;
488
+ dir = parent;
489
+ }
490
+
379
491
  const homeDir = getHomeDir();
380
492
  for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
381
493
  candidates.push(path.join(homeDir, `.${cli}`, "extensions", "openclaw-qqbot", "scripts", scriptName));
@@ -601,12 +713,12 @@ function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
601
713
  switchPluginSourceToNpm();
602
714
 
603
715
  // 文件替换成功,立即触发 gateway restart
604
- execFile(cli, ["gateway", "restart"], { timeout: 30_000 }, (restartErr) => {
716
+ execCliAsync(cli, ["gateway", "restart"], { timeout: 30_000 }, (restartErr) => {
605
717
  if (restartErr) {
606
718
  // restart 失败,尝试 stop + start 作为 fallback
607
- execFile(cli, ["gateway", "stop"], { timeout: 10_000 }, () => {
719
+ execCliAsync(cli, ["gateway", "stop"], { timeout: 10_000 }, () => {
608
720
  setTimeout(() => {
609
- execFile(cli, ["gateway", "start"], { timeout: 30_000 }, () => {});
721
+ execCliAsync(cli, ["gateway", "start"], { timeout: 30_000 }, () => {});
610
722
  }, 1000);
611
723
  });
612
724
  }
@@ -841,14 +953,40 @@ registerCommand({
841
953
  },
842
954
  });
843
955
 
956
+ /**
957
+ * 从 openclaw.json / clawdbot.json / moltbot.json 的 logging.file 配置中
958
+ * 提取用户自定义的日志文件路径(直接文件路径,非目录)。
959
+ */
960
+ function getConfiguredLogFiles(): string[] {
961
+ const homeDir = getHomeDir();
962
+ const files: string[] = [];
963
+ for (const cli of ["openclaw", "clawdbot", "moltbot"]) {
964
+ try {
965
+ const cfgPath = path.join(homeDir, `.${cli}`, `${cli}.json`);
966
+ if (!fs.existsSync(cfgPath)) continue;
967
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
968
+ const logFile = cfg?.logging?.file;
969
+ if (logFile && typeof logFile === "string") {
970
+ files.push(path.resolve(logFile));
971
+ }
972
+ break;
973
+ } catch {
974
+ // ignore
975
+ }
976
+ }
977
+ return files;
978
+ }
979
+
844
980
  /**
845
981
  * /bot-logs — 导出本地日志文件
846
982
  *
847
983
  * 日志定位策略(兼容腾讯云/各云厂商不同安装路径):
848
- * 1. 优先使用 *_STATE_DIR 环境变量(OPENCLAW/CLAWDBOT/MOLTBOT)
984
+ * 0. 优先从 openclaw.json 的 logging.file 配置中读取自定义日志路径(最精确)
985
+ * 1. 使用 *_STATE_DIR 环境变量(OPENCLAW/CLAWDBOT/MOLTBOT)
849
986
  * 2. 扫描常见状态目录:~/.openclaw, ~/.clawdbot, ~/.moltbot 及其 logs 子目录
850
987
  * 3. 扫描 home/cwd/AppData 下名称包含 openclaw/clawdbot/moltbot 的目录
851
- * 4. 在候选目录中选取最近更新的日志文件(gateway/openclaw/clawdbot/moltbot
988
+ * 4. 扫描 /var/log 下的 openclaw/clawdbot/moltbot 目录
989
+ * 5. 在候选目录中选取最近更新的日志文件(gateway/openclaw/clawdbot/moltbot)
852
990
  */
853
991
  function collectCandidateLogDirs(): string[] {
854
992
  const homeDir = getHomeDir();
@@ -866,6 +1004,12 @@ function collectCandidateLogDirs(): string[] {
866
1004
  pushDir(path.join(stateDir, "logs"));
867
1005
  };
868
1006
 
1007
+ // 0. 从配置文件的 logging.file 提取目录
1008
+ for (const logFile of getConfiguredLogFiles()) {
1009
+ pushDir(path.dirname(logFile));
1010
+ }
1011
+
1012
+ // 1. 环境变量 *_STATE_DIR
869
1013
  for (const [key, value] of Object.entries(process.env)) {
870
1014
  if (!value) continue;
871
1015
  if (/STATE_DIR$/i.test(key) && /(OPENCLAW|CLAWDBOT|MOLTBOT)/i.test(key)) {
@@ -873,11 +1017,13 @@ function collectCandidateLogDirs(): string[] {
873
1017
  }
874
1018
  }
875
1019
 
1020
+ // 2. 常见状态目录
876
1021
  for (const name of [".openclaw", ".clawdbot", ".moltbot", "openclaw", "clawdbot", "moltbot"]) {
877
1022
  pushDir(path.join(homeDir, name));
878
1023
  pushDir(path.join(homeDir, name, "logs"));
879
1024
  }
880
1025
 
1026
+ // 3. home/cwd/AppData 下包含 openclaw/clawdbot/moltbot 的子目录
881
1027
  const searchRoots = new Set<string>([
882
1028
  homeDir,
883
1029
  process.cwd(),
@@ -901,6 +1047,13 @@ function collectCandidateLogDirs(): string[] {
901
1047
  }
902
1048
  }
903
1049
 
1050
+ // 4. /var/log 下的常见日志目录(Linux 服务器部署场景)
1051
+ if (!isWindows()) {
1052
+ for (const name of ["openclaw", "clawdbot", "moltbot"]) {
1053
+ pushDir(path.join("/var/log", name));
1054
+ }
1055
+ }
1056
+
904
1057
  return Array.from(dirs);
905
1058
  }
906
1059
 
@@ -927,6 +1080,11 @@ function collectRecentLogFiles(logDirs: string[]): LogCandidate[] {
927
1080
  }
928
1081
  };
929
1082
 
1083
+ // 优先级最高:用户在 openclaw.json logging.file 中显式配置的日志文件
1084
+ for (const logFile of getConfiguredLogFiles()) {
1085
+ pushFile(logFile, path.dirname(logFile));
1086
+ }
1087
+
930
1088
  for (const dir of logDirs) {
931
1089
  pushFile(path.join(dir, "gateway.log"), dir);
932
1090
  pushFile(path.join(dir, "gateway.err.log"), dir);
@@ -965,8 +1123,19 @@ registerCommand({
965
1123
  const recentFiles = collectRecentLogFiles(logDirs).slice(0, 4);
966
1124
 
967
1125
  if (recentFiles.length === 0) {
968
- const searched = logDirs.map(d => ` - ${d}`).join("\n");
969
- return `⚠️ 未找到日志文件\n\n已搜索以下路径:\n${searched}`;
1126
+ const existingDirs = logDirs.filter(d => { try { return fs.existsSync(d); } catch { return false; } });
1127
+ const searched = existingDirs.length > 0
1128
+ ? existingDirs.map(d => ` • ${d}`).join("\n")
1129
+ : logDirs.slice(0, 6).map(d => ` • ${d}`).join("\n") + (logDirs.length > 6 ? `\n …及其他 ${logDirs.length - 6} 个路径` : "");
1130
+ return [
1131
+ `⚠️ 未找到日志文件`,
1132
+ ``,
1133
+ `已搜索以下${existingDirs.length > 0 ? "已存在的" : ""}路径:`,
1134
+ searched,
1135
+ ``,
1136
+ `💡 如果日志在自定义路径,请在配置文件中添加:`,
1137
+ ` "logging": { "file": "/path/to/your/logfile.log" }`,
1138
+ ].join("\n");
970
1139
  }
971
1140
 
972
1141
  const lines: string[] = [];
@@ -10,7 +10,11 @@ import { getPluginVersion } from "./slash-commands.js";
10
10
  const STARTUP_MARKER_FILE = path.join(getQQBotDataDir("data"), "startup-marker.json");
11
11
  const STARTUP_GREETING_RETRY_COOLDOWN_MS = 10 * 60 * 1000;
12
12
 
13
- export function getStartupGreetingText(version: string): string {
13
+ export function getFirstLaunchGreetingText(): string {
14
+ return `Haha,我的'灵魂'已上线,随时等你吩咐。`;
15
+ }
16
+
17
+ export function getUpgradeGreetingText(version: string): string {
14
18
  return `🎉 QQBot 插件已更新至 v${version},在线等候你的吩咐。`;
15
19
  }
16
20
 
@@ -44,10 +48,11 @@ export function writeStartupMarker(data: StartupMarkerData): void {
44
48
  }
45
49
 
46
50
  /**
47
- * 判断是否需要发送"灵魂上线"问候:
48
- * - 首次安装 / 版本变更:可发送
49
- * - 同版本:不发送
50
- * - 同版本近期失败:冷却期内不重试,减少噪音
51
+ * 判断是否需要发送启动问候:
52
+ * - 首次启动(无 marker)→ "灵魂已上线"
53
+ * - 版本变更 → "已更新至 vX.Y.Z"
54
+ * - 同版本 → 不发送
55
+ * - 同版本近期失败 → 冷却期内不重试
51
56
  */
52
57
  export function getStartupGreetingPlan(): { shouldSend: boolean; greeting?: string; version: string; reason?: string } {
53
58
  const currentVersion = getPluginVersion();
@@ -64,7 +69,12 @@ export function getStartupGreetingPlan(): { shouldSend: boolean; greeting?: stri
64
69
  }
65
70
  }
66
71
 
67
- return { shouldSend: true, greeting: getStartupGreetingText(currentVersion), version: currentVersion };
72
+ const isFirstLaunch = !marker.version;
73
+ const greeting = isFirstLaunch
74
+ ? getFirstLaunchGreetingText()
75
+ : getUpgradeGreetingText(currentVersion);
76
+
77
+ return { shouldSend: true, greeting, version: currentVersion };
68
78
  }
69
79
 
70
80
  export function markStartupGreetingSent(version: string): void {