@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/dist/index.js +2909 -155
- package/package.json +3 -6
- package/src/global.d.ts +4 -0
- package/src/hooks.test.ts +0 -23
- package/src/hooks.ts +0 -23
- package/src/identity.ts +30 -6
- package/src/index.ts +2 -31
- package/src/reporter.ts +7 -2
- package/src/types.ts +4 -0
- package/src/updater.test.ts +1 -12
- package/src/updater.ts +23 -40
- package/src/ws-client.test.ts +80 -0
- package/src/ws-client.ts +59 -3
- package/tsconfig.json +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spzhongwin/skill-logger-plugin",
|
|
3
|
-
"version": "1.0.
|
|
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
|
}
|
package/src/global.d.ts
ADDED
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
|
|
10
|
-
import
|
|
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
|
|
19
|
-
|
|
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 = "
|
|
99
|
+
let appendStr = "";
|
|
129
100
|
|
|
130
101
|
if (sessionUpdatedSkills.size > 0) {
|
|
131
|
-
appendStr
|
|
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<{
|
|
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
|
};
|
package/src/updater.test.ts
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
114
|
-
|
|
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.
|
|
462
|
+
// 2. 解压 ZIP 包
|
|
480
463
|
const staging = path.join(work, "staging");
|
|
481
464
|
const unzipStartedAt = Date.now();
|
|
482
|
-
emit("unzip.start", { extractor:
|
|
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
|
|
package/src/ws-client.test.ts
CHANGED
|
@@ -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
|
|
400
|
-
this.appendLogToFile("WARN", "Command", `Message dropped: missing action
|
|
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
|
}
|