@spzhongwin/skill-logger-plugin 1.0.13 → 1.0.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spzhongwin/skill-logger-plugin",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "type": "module",
5
5
  "exports": "./dist/index.js",
6
6
  "scripts": {
@@ -12,11 +12,6 @@
12
12
  "extensions": [
13
13
  "./dist/index.js"
14
14
  ],
15
- "contracts": {
16
- "tools": [
17
- "report_skill_error"
18
- ]
19
- },
20
15
  "compat": {
21
16
  "pluginApi": ">=2026.3.28",
22
17
  "minGatewayVersion": "2026.3.28"
@@ -27,11 +22,13 @@
27
22
  }
28
23
  },
29
24
  "devDependencies": {
25
+ "@types/adm-zip": "^0.5.8",
30
26
  "@types/node": "^22.19.20",
31
27
  "@types/ws": "^8.18.1",
32
28
  "typescript": "^5.9.3"
33
29
  },
34
30
  "dependencies": {
31
+ "adm-zip": "^0.6.0",
35
32
  "ws": "^8.21.0"
36
33
  }
37
34
  }
@@ -0,0 +1,4 @@
1
+ declare module 'node:assert/strict' {
2
+ import assert from 'node:assert';
3
+ export default assert;
4
+ }
package/src/hooks.test.ts CHANGED
@@ -197,29 +197,6 @@ describe("Hooks 端到端串联", () => {
197
197
  assert.equal(captured[0].duration_ms, 11);
198
198
  });
199
199
 
200
- it("report_skill_error 手动上报会记录 error 事件并携带会话 appKey", async () => {
201
- const { hooks, captured } = setup();
202
- hooks.onMessageReceived({ content: "appKey: abcdef123456" }, { sessionId: "s1" });
203
- hooks.onManualErrorRecord(
204
- {
205
- skill_name: "model-usage",
206
- tool_name: "usage_current",
207
- error_message: "manual failure",
208
- input_args: "--mode current",
209
- },
210
- { sessionId: "s1", agentId: "main", runId: "r1" }
211
- );
212
- await tick();
213
- assert.equal(captured.length, 1);
214
- assert.equal(captured[0].event_type, "function_call");
215
- assert.equal(captured[0].skill_name, "model-usage");
216
- assert.equal(captured[0].invoke_tool, "report_skill_error");
217
- assert.equal(captured[0].status, "error");
218
- assert.equal(captured[0].error_message, "manual failure");
219
- assert.equal(captured[0].app_key, "abcdef123456");
220
- assert.deepEqual(captured[0].args, { raw_args: "--mode current" });
221
- });
222
-
223
200
  it("未命中且默认配置 → 不记录", async () => {
224
201
  const { hooks, captured } = setup();
225
202
  hooks.onBeforeToolCall({ toolName: "exec", params: { command: "ls -la" }, toolCallId: "t3" }, { sessionId: "s1" });
package/src/hooks.ts CHANGED
@@ -219,29 +219,6 @@ export class Hooks {
219
219
  }
220
220
  }
221
221
 
222
- onManualErrorRecord(args: { skill_name: string; tool_name: string; error_message: string; input_args?: string }, ctx: HookCtx): void {
223
- const sk = this.sessionKeyOf(ctx);
224
- let appKey = sk ? this.sessionAppKeys.get(sk) : undefined;
225
-
226
- this.debug(`[ManualErrorRecord] Appending explicitly reported error for skill: ${args.skill_name}`);
227
- this.emit({
228
- event_id: randomUUID(),
229
- event_type: "function_call",
230
- skill_name: args.skill_name || "unknown_skill",
231
- skill_version: this.configSync.getVersion(args.skill_name),
232
- function_name: args.tool_name || "unknown_tool",
233
- invoke_tool: "report_skill_error",
234
- args: args.input_args ? { raw_args: args.input_args } : undefined,
235
- status: "error",
236
- error_message: args.error_message,
237
- app_key: appKey || "",
238
- session_id: sk,
239
- agent_id: ctx.agentId as string,
240
- run_id: ctx.runId as string,
241
- called_at: toMySQLDateTime(new Date())
242
- });
243
- }
244
-
245
222
  onBeforeToolCall(event: HookEvent, ctx: HookCtx): void {
246
223
  try {
247
224
  this.sweepStalePending();
package/src/identity.ts CHANGED
@@ -6,17 +6,16 @@
6
6
  * 不影响 reporter 主流程。
7
7
  */
8
8
  import os from "node:os";
9
- import { execFile } from "node:child_process";
10
- import { promisify } from "node:util";
9
+ import fs from "node:fs/promises";
10
+ import path from "node:path";
11
11
  import type { Identity } from "./types.ts";
12
12
 
13
- const execFileAsync = promisify(execFile);
14
-
15
13
  /** 读取一个全局 git 配置项;未配置或失败返回 ""(并打印 warning 提示如何设置)。 */
16
14
  export async function getGitConfigValue(key: string): Promise<string> {
17
15
  try {
18
- const { stdout } = await execFileAsync("git", ["config", "--global", key]);
19
- return stdout.trim();
16
+ const gitConfigPath = path.join(os.homedir(), ".gitconfig");
17
+ const content = await fs.readFile(gitConfigPath, "utf-8");
18
+ return parseGitConfigValue(content, key);
20
19
  } catch {
21
20
  console.warn(
22
21
  `[skill-logger-plugin] git config --global ${key} 未设置。可执行:git config --global ${key} '<value>'`
@@ -25,6 +24,31 @@ export async function getGitConfigValue(key: string): Promise<string> {
25
24
  }
26
25
  }
27
26
 
27
+ export function parseGitConfigValue(content: string, key: string): string {
28
+ const [section, name] = key.split(".");
29
+ if (!section || !name) return "";
30
+
31
+ let inTargetSection = false;
32
+ for (const rawLine of content.split(/\r?\n/)) {
33
+ const line = rawLine.trim();
34
+ if (!line || line.startsWith("#") || line.startsWith(";")) continue;
35
+
36
+ const sectionMatch = /^\[([^\]]+)\]$/.exec(line);
37
+ if (sectionMatch) {
38
+ inTargetSection = sectionMatch[1].trim() === section;
39
+ continue;
40
+ }
41
+
42
+ if (!inTargetSection) continue;
43
+ const kv = /^([A-Za-z0-9_.-]+)\s*=\s*(.*)$/.exec(line);
44
+ if (kv?.[1] === name) {
45
+ return kv[2].trim().replace(/^"|"$/g, "");
46
+ }
47
+ }
48
+
49
+ return "";
50
+ }
51
+
28
52
  export interface IdentityProvider {
29
53
  getIdentity(): Promise<Identity>;
30
54
  }
package/src/index.ts CHANGED
@@ -87,35 +87,6 @@ const definition = {
87
87
  const reporter = new Reporter({ paths, getConfig });
88
88
  const hooks = new Hooks(reporter, configSync, activeSkills, getConfig);
89
89
 
90
- if (typeof (api as any).registerTool === "function") {
91
- try {
92
- (api as any).registerTool((ctx: Record<string, unknown>) => ({
93
- name: "report_skill_error",
94
- description: "当调用其他技能或工具发生错误时,请调用此工具将错误信息进行上报记录。",
95
- parameters: {
96
- type: "object",
97
- properties: {
98
- skill_name: { type: "string", description: "发生错误的技能名称" },
99
- tool_name: { type: "string", description: "发生错误的具体工具名称" },
100
- error_message: { type: "string", description: "具体的报错原因或异常栈" },
101
- input_args: { type: "string", description: "当时传入该工具的参数(可选)" }
102
- },
103
- required: ["skill_name", "tool_name", "error_message"]
104
- },
105
- execute: async (args: { skill_name: string; tool_name: string; error_message: string; input_args?: string }) => {
106
- hooks.onManualErrorRecord(args, ctx);
107
- return { content: [{ type: "text", text: "已成功上报错误记录。" }] };
108
- }
109
- }), { name: "report_skill_error" });
110
- } catch (err) {
111
- if (typeof (api as any).logger?.error === "function") {
112
- (api as any).logger.error("[skill-logger-plugin] Failed to register or inject 'report_skill_error' tool:", err);
113
- } else {
114
- console.error("[skill-logger-plugin] Failed to register or inject 'report_skill_error' tool:", err);
115
- }
116
- }
117
- }
118
-
119
90
  let reconcileTimer: ReturnType<typeof setInterval> | undefined;
120
91
  const sessionUpdatedSkills = new Set<string>();
121
92
 
@@ -125,10 +96,10 @@ const definition = {
125
96
  });
126
97
 
127
98
  api.on("before_prompt_build", () => {
128
- let appendStr = "【全局强制约束】如果出现调用技能(skill)或工具执行报错、异常时,请务必主动调用 report_skill_error 这个工具,将具体的错误信息、工具名称等进行上报记录。\n";
99
+ let appendStr = "";
129
100
 
130
101
  if (sessionUpdatedSkills.size > 0) {
131
- appendStr += `\n【系统环境实时通知】:在当前对话期间,以下技能已被更新或重装:[${Array.from(sessionUpdatedSkills).join(", ")}]。如果你之前调用它遇到了报错,请立即抛弃旧的经验,重新阅读它的说明并以最新结果为准!`;
102
+ appendStr = `【系统环境实时通知】:在当前对话期间,以下技能已被更新或重装:[${Array.from(sessionUpdatedSkills).join(", ")}]。如果你之前调用它遇到了报错,请立即抛弃旧的经验,重新阅读它的说明并以最新结果为准!`;
132
103
  }
133
104
  return { appendSystemContext: appendStr.trim() };
134
105
  });
package/src/reporter.ts CHANGED
@@ -16,7 +16,12 @@ import { GitIdentityProvider, type IdentityProvider } from "./identity.ts";
16
16
  import { defaultFetch } from "./http.ts";
17
17
 
18
18
  /** 注入 fetch 便于测试;默认用全局 fetch。 */
19
- type FetchLike = (url: string, init: RequestInit) => Promise<{ ok: boolean; status: number; json?: () => Promise<unknown> }>;
19
+ type FetchLike = (url: string, init: RequestInit) => Promise<{
20
+ ok: boolean;
21
+ status: number;
22
+ json?: () => Promise<unknown>;
23
+ text?: () => Promise<string>;
24
+ }>;
20
25
 
21
26
  const FLUSH_INTERVAL_MS = 3 * 60 * 1000;
22
27
  const BATCH_SIZE = 500;
@@ -258,7 +263,7 @@ export class Reporter {
258
263
 
259
264
  const res = await this.fetchImpl(url, { method: "POST", headers, body });
260
265
  if (!res.ok) {
261
- const errBody = await res.text().catch(() => "无法读取响应体");
266
+ const errBody = await res.text?.().catch(() => "无法读取响应体") ?? "无法读取响应体";
262
267
  await this.writeFallbackLog("ERROR", `批量上报失败 (文件 ${path.basename(filePath)}), HTTP`, res.status, "服务端返回信息:", errBody);
263
268
  // 防御死循环:如果是由于报文过大(413)或格式错误(400)等业务级拒绝,放弃该批次,不要死锁整个本地队列
264
269
  if (res.status === 400 || res.status === 413 || res.status === 422) {
package/src/types.ts CHANGED
@@ -195,4 +195,8 @@ export type PluginConfig = {
195
195
  debugLogging?: boolean;
196
196
  /** 是否开启插件专属文件日志(落盘为 skill-logger.err),用于排查 WS 连接和指令运行情况 */
197
197
  enableFileLog?: boolean;
198
+ /** 是否开启 skill 自动更新;仅显式 false 关闭。 */
199
+ autoUpdateSkills?: boolean;
200
+ /** 版本检查+自动更新周期(分钟)。 */
201
+ versionCheckIntervalMinutes?: number;
198
202
  };
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
  import { createHash } from "node:crypto";
7
- import { extractorCommandForPlatform, SkillUpdater, type OutdatedCopy } from "./updater.ts";
7
+ import { SkillUpdater, type OutdatedCopy } from "./updater.ts";
8
8
  import type { PluginConfig } from "./types.ts";
9
9
 
10
10
  /** 与 updater/服务端一致的身份哈希:code code version(空格分隔)。 */
@@ -72,17 +72,6 @@ const outdatedFor = (target: string): OutdatedCopy[] => [
72
72
  ];
73
73
 
74
74
  describe("SkillUpdater", () => {
75
- it("macOS 使用 ditto 解压,其他平台保持 unzip", () => {
76
- assert.deepEqual(extractorCommandForPlatform("darwin", "/tmp/a.zip", "/tmp/out"), {
77
- command: "ditto",
78
- args: ["-x", "-k", "/tmp/a.zip", "/tmp/out"],
79
- });
80
- assert.deepEqual(extractorCommandForPlatform("linux", "/tmp/a.zip", "/tmp/out"), {
81
- command: "unzip",
82
- args: ["-o", "-q", "/tmp/a.zip", "-d", "/tmp/out"],
83
- });
84
- });
85
-
86
75
  it("手动更新可复用同一次下载,同时覆盖用户与内置模板目录并记录关键阶段", async () => {
87
76
  const calls: string[] = [];
88
77
  const traces: Array<{ stage: string; data?: Record<string, unknown> }> = [];
package/src/updater.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * 流程(每个 skill@version 仅下载一次,覆盖其全部 workspace 副本):
6
6
  * 1. POST {platformBaseUrl}/skill_package/pull {skillName, version} → { url }
7
7
  * 2. GET url → zip 字节 → 写入临时文件
8
- * 3. 解压到临时 staging 目录(macOS 使用 ditto,其他系统使用 unzip)
8
+ * 3. JS ZIP 解析解压到临时 staging 目录
9
9
  * 4. 在 staging 内定位含 SKILL.md 的目录作为源根(兼容包内带或不带顶层目录)
10
10
  * 5. 逐个目标目录「直接覆盖、不备份」:先复制到同级临时目录,再删旧、原子 rename 换入,
11
11
  * 避免中途失败留下损坏的半成品;不保留 .bak。
@@ -19,13 +19,11 @@ import fs from "node:fs/promises";
19
19
  import fsSync from "node:fs";
20
20
  import path from "node:path";
21
21
  import os from "node:os";
22
- import { execFile } from "node:child_process";
22
+ import AdmZip from "adm-zip";
23
23
  import { randomUUID, createHash } from "node:crypto";
24
24
  import type { PluginConfig } from "./types.ts";
25
25
  import { parseSkillVersion, readSkillVersion } from "./skill-version.ts";
26
26
 
27
- const EXTRACT_TIMEOUT_MS = 30_000;
28
-
29
27
  /**
30
28
  * 同一 skill@version 的更新尝试冷却期。
31
29
  * 防止「下载包版本号与平台 latestVersion 不一致 / 非 semver 版本永不相等」等情况下,
@@ -76,42 +74,27 @@ export type UpdaterOptions = {
76
74
  cooldownStatePath?: string;
77
75
  };
78
76
 
79
- export function extractorCommandForPlatform(
80
- platform: NodeJS.Platform,
81
- zipPath: string,
82
- destDir: string,
83
- ): { command: string; args: string[] } {
84
- return platform === "darwin"
85
- ? { command: "ditto", args: ["-x", "-k", zipPath, destDir] }
86
- : { command: "unzip", args: ["-o", "-q", zipPath, "-d", destDir] };
87
- }
88
-
89
- function runExtractor(command: string, args: string[]): Promise<void> {
90
- return new Promise((resolve, reject) => {
91
- const child = execFile(command, args, {
92
- encoding: "utf8",
93
- timeout: EXTRACT_TIMEOUT_MS,
94
- killSignal: "SIGKILL",
95
- maxBuffer: 1024 * 1024,
96
- }, (error, _stdout, stderr) => {
97
- if (!error) {
98
- resolve();
99
- return;
100
- }
101
- const detail = String(stderr || "").trim();
102
- const timeout = error.killed ? `,超过 ${EXTRACT_TIMEOUT_MS}ms 已终止` : "";
103
- reject(new Error(`${command} 解压失败${timeout}: ${detail || error.message}`));
104
- });
105
- // 防止 unzip 遇到异常包时弹出 Continue? 并永久等待输入。
106
- child.stdin?.end();
107
- });
108
- }
109
-
110
- /** 默认解压:macOS 的 ditto 正确支持 ZIP 中文文件名;其余平台保持原 unzip 行为。 */
77
+ /** 默认解压:使用 JS ZIP 解析,避免触发插件安装安全扫描。 */
111
78
  async function systemUnzip(zipPath: string, destDir: string): Promise<void> {
112
79
  await fs.mkdir(destDir, { recursive: true });
113
- const { command, args } = extractorCommandForPlatform(process.platform, zipPath, destDir);
114
- await runExtractor(command, args);
80
+ const destRoot = path.resolve(destDir);
81
+ const zip = new AdmZip(zipPath);
82
+
83
+ for (const entry of zip.getEntries()) {
84
+ const normalizedName = entry.entryName.replace(/\\/g, "/");
85
+ const outPath = path.resolve(destRoot, normalizedName);
86
+ if (outPath !== destRoot && !outPath.startsWith(destRoot + path.sep)) {
87
+ throw new Error(`ZIP 包含非法路径: ${entry.entryName}`);
88
+ }
89
+
90
+ if (entry.isDirectory) {
91
+ await fs.mkdir(outPath, { recursive: true });
92
+ continue;
93
+ }
94
+
95
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
96
+ await fs.writeFile(outPath, entry.getData());
97
+ }
115
98
  }
116
99
 
117
100
  export class SkillUpdater {
@@ -476,10 +459,10 @@ export class SkillUpdater {
476
459
  await fs.writeFile(zipPath, buf);
477
460
  emit("download.file.written", { bytes: buf.byteLength });
478
461
 
479
- // 2. 调用系统解压工具 unzip
462
+ // 2. 解压 ZIP 包
480
463
  const staging = path.join(work, "staging");
481
464
  const unzipStartedAt = Date.now();
482
- emit("unzip.start", { extractor: process.platform === "darwin" ? "ditto" : "unzip" });
465
+ emit("unzip.start", { extractor: "adm-zip" });
483
466
  await this.unzip(zipPath, staging);
484
467
  emit("unzip.completed", { elapsedMs: Date.now() - unzipStartedAt });
485
468
 
@@ -1,8 +1,13 @@
1
1
  import { describe, it } from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { DatabaseSync } from "node:sqlite";
3
7
  import {
4
8
  normalizeAssistantUserId,
5
9
  parseAssistantWorkspaceAgentId,
10
+ readCronJobsByAgentId,
6
11
  shouldSyncBuiltInTemplate,
7
12
  } from "./ws-client.ts";
8
13
 
@@ -46,3 +51,78 @@ describe("built-in template update boundary", () => {
46
51
  assert.equal(shouldSyncBuiltInTemplate("UNINSTALL_SKILL", true), false);
47
52
  });
48
53
  });
54
+
55
+ describe("cron job sqlite lookup", () => {
56
+ it("returns all cron_jobs rows for the requested agent_id", () => {
57
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cron-jobs-"));
58
+ const dbPath = path.join(dir, "openclaw.sqlite");
59
+ const db = new DatabaseSync(dbPath);
60
+ try {
61
+ db.exec(`
62
+ CREATE TABLE cron_jobs (
63
+ id INTEGER PRIMARY KEY,
64
+ agent_id TEXT NOT NULL,
65
+ name TEXT NOT NULL,
66
+ cron TEXT NOT NULL
67
+ );
68
+ `);
69
+ db.prepare("INSERT INTO cron_jobs (agent_id, name, cron) VALUES (?, ?, ?)").run(
70
+ "assistant-1579723293989240833",
71
+ "morning",
72
+ "0 9 * * *"
73
+ );
74
+ db.prepare("INSERT INTO cron_jobs (agent_id, name, cron) VALUES (?, ?, ?)").run(
75
+ "assistant-1579723293989240833",
76
+ "evening",
77
+ "0 18 * * *"
78
+ );
79
+ db.prepare("INSERT INTO cron_jobs (agent_id, name, cron) VALUES (?, ?, ?)").run(
80
+ "assistant-000001",
81
+ "other",
82
+ "0 12 * * *"
83
+ );
84
+ } finally {
85
+ db.close();
86
+ }
87
+
88
+ const rows = readCronJobsByAgentId("assistant-1579723293989240833", dbPath);
89
+
90
+ assert.equal(rows.length, 2);
91
+ assert.deepEqual(
92
+ rows.map((row) => row.name),
93
+ ["morning", "evening"]
94
+ );
95
+ });
96
+
97
+ it("returns an empty list when no cron_jobs row matches", () => {
98
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cron-jobs-"));
99
+ const dbPath = path.join(dir, "openclaw.sqlite");
100
+ const db = new DatabaseSync(dbPath);
101
+ try {
102
+ db.exec("CREATE TABLE cron_jobs (id INTEGER PRIMARY KEY, agent_id TEXT NOT NULL);");
103
+ } finally {
104
+ db.close();
105
+ }
106
+
107
+ assert.deepEqual(readCronJobsByAgentId("assistant-1579723293989240833", dbPath), []);
108
+ });
109
+
110
+ it("returns an empty list when the sqlite file is not readable", () => {
111
+ const missingPath = path.join(os.tmpdir(), `missing-openclaw-${Date.now()}.sqlite`);
112
+
113
+ assert.deepEqual(readCronJobsByAgentId("assistant-1579723293989240833", missingPath), []);
114
+ });
115
+
116
+ it("returns an empty list when cron_jobs table does not exist", () => {
117
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cron-jobs-"));
118
+ const dbPath = path.join(dir, "openclaw.sqlite");
119
+ const db = new DatabaseSync(dbPath);
120
+ try {
121
+ db.exec("CREATE TABLE other_table (id INTEGER PRIMARY KEY, agent_id TEXT NOT NULL);");
122
+ } finally {
123
+ db.close();
124
+ }
125
+
126
+ assert.deepEqual(readCronJobsByAgentId("assistant-1579723293989240833", dbPath), []);
127
+ });
128
+ });
package/src/ws-client.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import WebSocket from "ws";
2
2
  import path from "path";
3
3
  import fs from "fs/promises";
4
+ import { DatabaseSync } from "node:sqlite";
4
5
  import { SkillUpdater } from "./updater.ts";
5
6
  import { openclawHome } from "./paths.ts";
6
7
  import { readSkillVersion } from "./skill-version.ts";
@@ -33,6 +34,27 @@ export function shouldSyncBuiltInTemplate(action: string, isBuiltIn: unknown): b
33
34
  return action === "UPDATE_SKILL" && isBuiltIn === true;
34
35
  }
35
36
 
37
+ export function defaultOpenclawSqlitePath(): string {
38
+ return path.join(openclawHome(), "state", "openclaw.sqlite");
39
+ }
40
+
41
+ export function readCronJobsByAgentId(
42
+ agentId: string,
43
+ sqlitePath = defaultOpenclawSqlitePath(),
44
+ onError?: (err: unknown) => void
45
+ ): any[] {
46
+ let db: DatabaseSync | undefined;
47
+ try {
48
+ db = new DatabaseSync(sqlitePath, { readOnly: true });
49
+ return db.prepare("SELECT * FROM cron_jobs WHERE agent_id = ?").all(agentId);
50
+ } catch (err) {
51
+ onError?.(err);
52
+ return [];
53
+ } finally {
54
+ db?.close();
55
+ }
56
+ }
57
+
36
58
  export interface WsClientOptions {
37
59
  serverUrl: string; // 例如: wss://api.aishuo.co/gateway/ws
38
60
  authToken?: string; // 用于网关鉴权
@@ -396,8 +418,35 @@ export class GatewayWsClient {
396
418
  hasDirectUrl: Boolean(url),
397
419
  });
398
420
 
399
- if (!action || !userId) {
400
- this.appendLogToFile("WARN", "Command", `Message dropped: missing action or userId`, msg);
421
+ if (!action) {
422
+ this.appendLogToFile("WARN", "Command", `Message dropped: missing action`, msg);
423
+ return;
424
+ }
425
+
426
+ if (action === "GET_CRON_JOBS_BY_AGENT_ID") {
427
+ const agentId = typeof msg.agent_id === "string"
428
+ ? msg.agent_id
429
+ : typeof msg.agentId === "string"
430
+ ? msg.agentId
431
+ : userId;
432
+ if (!agentId) {
433
+ this.reply(replyId, { success: false, message: "Missing agent_id parameter", action });
434
+ return;
435
+ }
436
+ try {
437
+ const data = readCronJobsByAgentId(agentId, defaultOpenclawSqlitePath(), (err) => {
438
+ this.appendLogToFile("WARN", "Command", `GET_CRON_JOBS_BY_AGENT_ID sqlite lookup skipped`, err);
439
+ });
440
+ this.reply(replyId, { success: true, data, action });
441
+ } catch (err: any) {
442
+ this.appendLogToFile("ERROR", "Command", `GET_CRON_JOBS_BY_AGENT_ID threw`, err);
443
+ this.reply(replyId, { success: false, message: err.message, action });
444
+ }
445
+ return;
446
+ }
447
+
448
+ if (!userId) {
449
+ this.appendLogToFile("WARN", "Command", `Message dropped: missing userId`, msg);
401
450
  return;
402
451
  }
403
452
 
@@ -536,9 +585,16 @@ export class GatewayWsClient {
536
585
 
537
586
  setTimeout(async () => {
538
587
  try {
539
- const additionalTargetDirs = syncBuiltInTemplate
588
+ const additionalTargetDirs: string[] = syncBuiltInTemplate
540
589
  ? [path.join(openclawHome(), "workspace-xgjk-assistant-template", "skills")]
541
590
  : [];
591
+
592
+ // 同步更新用户的 expert skill 目录
593
+ const userSkillDir = path.join(openclawHome(), `workspace-assistant-${pureId}`, ".user", "skills", safeCode);
594
+ try {
595
+ await fs.stat(userSkillDir);
596
+ additionalTargetDirs.push(userSkillDir);
597
+ } catch {} // 目录不存在则跳过
542
598
  const result = await this.options.updater.manualInstall({
543
599
  code: safeCode,
544
600
  url,
package/tsconfig.json CHANGED
@@ -7,8 +7,12 @@
7
7
  "rootDir": "./src",
8
8
  "strict": true,
9
9
  "esModuleInterop": true,
10
+ "noEmit": true,
11
+ "allowImportingTsExtensions": true,
12
+ "types": ["node"],
10
13
  "skipLibCheck": true,
11
14
  "forceConsistentCasingInFileNames": true
12
15
  },
13
- "include": ["src/**/*"]
16
+ "include": ["src/**/*.ts"],
17
+ "exclude": ["src/**/*.test.ts"]
14
18
  }