@spzhongwin/skill-logger-plugin 1.0.15 → 1.0.16

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.
Files changed (56) hide show
  1. package/dist/index.js +193 -3109
  2. package/openclaw.plugin.json +50 -50
  3. package/package.json +34 -34
  4. package/src/active-skills.test.ts +32 -32
  5. package/src/active-skills.ts +77 -77
  6. package/src/config-sync.test.ts +165 -165
  7. package/src/config-sync.ts +544 -544
  8. package/src/expert-skill-layout.test.ts +196 -0
  9. package/src/expert-skill-layout.ts +233 -0
  10. package/src/hooks.test.ts +228 -228
  11. package/src/hooks.ts +494 -494
  12. package/src/http.ts +61 -61
  13. package/src/identity.ts +88 -88
  14. package/src/index.test.ts +53 -53
  15. package/src/index.ts +218 -197
  16. package/src/integration.test.ts +119 -119
  17. package/src/matcher.test.ts +170 -170
  18. package/src/matcher.ts +393 -393
  19. package/src/paths.test.ts +57 -57
  20. package/src/paths.ts +84 -84
  21. package/src/reporter.test.ts +139 -139
  22. package/src/reporter.ts +303 -303
  23. package/src/sample-config.json +72 -72
  24. package/src/semver.test.ts +33 -23
  25. package/src/semver.ts +65 -60
  26. package/src/skill-version.ts +53 -53
  27. package/src/types.ts +202 -202
  28. package/src/updater.test.ts +431 -314
  29. package/src/updater.ts +584 -532
  30. package/src/ws-client.test.ts +158 -128
  31. package/src/ws-client.ts +805 -773
  32. package/test-ws.ts +17 -17
  33. package/tsconfig.json +18 -18
  34. package/dist/active-skills.js +0 -67
  35. package/dist/active-skills.test.js +0 -29
  36. package/dist/config-sync.js +0 -439
  37. package/dist/config-sync.test.js +0 -145
  38. package/dist/hooks.js +0 -337
  39. package/dist/hooks.test.js +0 -123
  40. package/dist/http.js +0 -54
  41. package/dist/identity.js +0 -56
  42. package/dist/index.test.js +0 -39
  43. package/dist/integration.test.js +0 -102
  44. package/dist/matcher.js +0 -362
  45. package/dist/matcher.test.js +0 -139
  46. package/dist/paths.js +0 -62
  47. package/dist/paths.test.js +0 -49
  48. package/dist/reporter.js +0 -267
  49. package/dist/reporter.test.js +0 -128
  50. package/dist/semver.js +0 -64
  51. package/dist/semver.test.js +0 -21
  52. package/dist/skill-version.js +0 -23
  53. package/dist/types.js +0 -9
  54. package/dist/updater.js +0 -352
  55. package/dist/updater.test.js +0 -212
  56. package/dist/ws-client.js +0 -484
package/src/index.ts CHANGED
@@ -1,197 +1,218 @@
1
- /**
2
- * OpenClaw 插件入口(瘦装配层)。
3
- *
4
- * 把各职责模块实例化并注册到 openclaw 的生命周期 hook:
5
- * - before_tool_call / after_tool_call:观测工具调用,记 skill_trigger / function_call
6
- * - gateway_start:加载配置、首次对账、起 3 分钟定时器(上报 + 配置对账)
7
- * - gateway_stop:停定时器、尽力最后上报一次
8
- * - before_install:skill/plugin 安装后重新对账,拉新配置
9
- *
10
- * 配置(上报地址/鉴权等)优先从插件 API 初始化配置读取,也兼容 hook 的
11
- * `event.context.pluginConfig` / `ctx.pluginConfig` 增量注入。
12
- */
13
- import fs from "node:fs";
14
- import path from "node:path";
15
- import { fileURLToPath } from "node:url";
16
- import os from "node:os";
17
- import type { PluginConfig } from "./types.ts";
18
- import { resolvePaths } from "./paths.ts";
19
- import { ActiveSkills } from "./active-skills.ts";
20
- import { SkillUpdater } from "./updater.ts";
21
- import { ConfigSync } from "./config-sync.ts";
22
- import { Reporter } from "./reporter.ts";
23
-
24
- // 启动长连接中枢
25
- import { GatewayWsClient } from "./ws-client.ts";
26
- let wsClient: GatewayWsClient | undefined;
27
-
28
- import { Hooks, isSkillMdReadPath } from "./hooks.ts";
29
-
30
- // 供单测复用(保留历史测试)。
31
- export { isSkillMdReadPath };
32
-
33
- /** 与 openclaw 插件 SDK 对齐的最小结构类型(结构化 typing,避免硬依赖 SDK 包类型)。 */
34
- type PluginApi = {
35
- pluginConfig?: PluginConfig;
36
- on: (
37
- hookName: string,
38
- handler: (event: Record<string, unknown>, ctx: Record<string, unknown>) => any
39
- ) => void;
40
- };
41
-
42
- /** 本地扫描对账周期:发现新增 agent/skill(纯本地 I/O),固定 3 分钟。 */
43
- const RECONCILE_INTERVAL_MS = 3 * 60 * 1000;
44
-
45
- /** 从 hook 事件/上下文里取本插件的运行期配置。 */
46
- function extractPluginConfig(
47
- event: Record<string, unknown>,
48
- ctx: Record<string, unknown>
49
- ): PluginConfig | undefined {
50
- const fromEvent = (event.context as Record<string, unknown> | undefined)?.pluginConfig;
51
- const fromCtx = ctx?.pluginConfig;
52
- return (fromEvent ?? fromCtx) as PluginConfig | undefined;
53
- }
54
-
55
- export function extractApiPluginConfig(api: Pick<PluginApi, "pluginConfig">): PluginConfig {
56
- return api.pluginConfig ?? {};
57
- }
58
-
59
- const definition = {
60
- id: "skill-logger-plugin",
61
- name: "Skill Logger",
62
- description:
63
- "追踪 openclaw skill 内功能点(脚本/命令/工具/HTTP)使用与报错,落本地并批量上报",
64
- register(api: PluginApi) {
65
- let pkgVersion = "unknown";
66
- try {
67
- const dir = path.dirname(fileURLToPath(import.meta.url));
68
- const pkgPath = path.join(dir, "..", "package.json");
69
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
70
- if (pkg.version) pkgVersion = pkg.version;
71
- } catch {
72
- // 忽略文件读取异常
73
- }
74
-
75
- const paths = resolvePaths();
76
- let currentConfig: PluginConfig = extractApiPluginConfig(api);
77
- currentConfig.pluginVersion = pkgVersion;
78
- const getConfig = () => currentConfig;
79
- const mergeConfig = (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
80
- const incoming = extractPluginConfig(event, ctx);
81
- if (incoming) currentConfig = { ...currentConfig, ...incoming };
82
- };
83
-
84
- const activeSkills = new ActiveSkills();
85
- const updater = new SkillUpdater({ getConfig, cooldownStatePath: paths.cooldownStatePath });
86
- const configSync = new ConfigSync({ paths, getConfig, updater });
87
- const reporter = new Reporter({ paths, getConfig });
88
- const hooks = new Hooks(reporter, configSync, activeSkills, getConfig);
89
-
90
- let reconcileTimer: ReturnType<typeof setInterval> | undefined;
91
- const sessionUpdatedSkills = new Set<string>();
92
-
93
- api.on("message_received", (event, ctx) => {
94
- mergeConfig(event, ctx);
95
- hooks.onMessageReceived(event, ctx);
96
- });
97
-
98
- api.on("before_prompt_build", () => {
99
- let appendStr = "";
100
-
101
- if (sessionUpdatedSkills.size > 0) {
102
- appendStr = `【系统环境实时通知】:在当前对话期间,以下技能已被更新或重装:[${Array.from(sessionUpdatedSkills).join(", ")}]。如果你之前调用它遇到了报错,请立即抛弃旧的经验,重新阅读它的说明并以最新结果为准!`;
103
- }
104
- return { appendSystemContext: appendStr.trim() };
105
- });
106
-
107
- api.on("before_tool_call", (event, ctx) => {
108
- mergeConfig(event, ctx);
109
- hooks.onBeforeToolCall(event, ctx);
110
- });
111
-
112
- api.on("after_tool_call", (event, ctx) => {
113
- mergeConfig(event, ctx);
114
- hooks.onAfterToolCall(event);
115
- });
116
-
117
- api.on("session_end", (_event, ctx) => {
118
- hooks.onSessionEnd(ctx);
119
- });
120
-
121
- api.on("gateway_start", (event, ctx) => {
122
- mergeConfig(event, ctx);
123
-
124
- // =========================================================
125
- // 启动星型中枢长连接网络
126
- // =========================================================
127
- if (!wsClient) {
128
- const currentConfig = getConfig();
129
-
130
- // 优先使用配置文件中的 pluginId,否则回退到 OS hostname
131
- const uniqueGatewayId = currentConfig.pluginId || process.env.GATEWAY_ID || `gateway-${os.hostname()}`;
132
-
133
- // 智能推导 WebSocket 服务地址
134
- let finalWsUrl = currentConfig.wsServerUrl || process.env.CENTRAL_WS_URL;
135
- if (!finalWsUrl && currentConfig.platformBaseUrl) {
136
- try {
137
- const url = new URL(currentConfig.platformBaseUrl);
138
- finalWsUrl = `${url.protocol === 'https:' ? 'wss:' : 'ws:'}//${url.host}/gateway/ws`;
139
- } catch (e) {
140
- // 容错处理
141
- finalWsUrl = currentConfig.platformBaseUrl.replace(/^http/, 'ws').replace(/\/api\/?$/, '').replace(/\/$/, '') + '/gateway/ws';
142
- }
143
- }
144
- if (!finalWsUrl) {
145
- finalWsUrl = "wss://aishuo.co/gateway/ws";
146
- }
147
-
148
- wsClient = new GatewayWsClient({
149
- serverUrl: finalWsUrl,
150
- gatewayId: uniqueGatewayId,
151
- authToken: currentConfig.authToken, // openclaw.json config 节点动态读取鉴权 token
152
- updater: updater,
153
- enableFileLog: currentConfig.enableFileLog // 将日志开关透传给客户端模块
154
- });
155
- wsClient.connect();
156
- }
157
- // =========================================================
158
-
159
- // 启动即跑一次:先 load 缓存 本地对账(扫描+匹配配置)
160
- void configSync
161
- .load()
162
- .then(() => configSync.reconcile())
163
- .catch((err) => console.warn("[skill-logger-plugin] 启动初始化异常", err));
164
- reporter.startTimer();
165
- // 定时器①:本地扫描对账,固定 3 分钟。
166
- if (!reconcileTimer) {
167
- reconcileTimer = setInterval(() => void configSync.reconcile(), RECONCILE_INTERVAL_MS);
168
- if (typeof reconcileTimer.unref === "function") reconcileTimer.unref();
169
- }
170
- });
171
-
172
- api.on("gateway_stop", async () => {
173
- // 停止时销毁连接
174
- if (wsClient) {
175
- wsClient.destroy();
176
- wsClient = undefined;
177
- }
178
-
179
- if (reconcileTimer) {
180
- clearInterval(reconcileTimer);
181
- reconcileTimer = undefined;
182
- }
183
- await hooks.flushAllPending();
184
- await reporter.stopTimer();
185
- });
186
-
187
- api.on("before_install", (event, ctx) => {
188
- mergeConfig(event, ctx);
189
- // 安装后立即对账拉取最新配置,不执行自动更新(由 WSS 指令统一控制更新动作)
190
- void configSync
191
- .reconcile()
192
- .catch((err) => console.warn("[skill-logger-plugin] before_install 处理异常", err));
193
- });
194
- },
195
- };
196
-
197
- export default definition;
1
+ /**
2
+ * OpenClaw 插件入口(瘦装配层)。
3
+ *
4
+ * 把各职责模块实例化并注册到 openclaw 的生命周期 hook:
5
+ * - before_tool_call / after_tool_call:观测工具调用,记 skill_trigger / function_call
6
+ * - gateway_start:加载配置、首次对账、起 3 分钟定时器(上报 + 配置对账)
7
+ * - gateway_stop:停定时器、尽力最后上报一次
8
+ * - before_install:skill/plugin 安装后重新对账,拉新配置
9
+ *
10
+ * 配置(上报地址/鉴权等)优先从插件 API 初始化配置读取,也兼容 hook 的
11
+ * `event.context.pluginConfig` / `ctx.pluginConfig` 增量注入。
12
+ */
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import os from "node:os";
17
+ import type { PluginConfig } from "./types.ts";
18
+ import { openclawHome, resolvePaths } from "./paths.ts";
19
+ import { ActiveSkills } from "./active-skills.ts";
20
+ import { SkillUpdater } from "./updater.ts";
21
+ import { ConfigSync } from "./config-sync.ts";
22
+ import { Reporter } from "./reporter.ts";
23
+ import { repairNestedExpertSkillLayouts } from "./expert-skill-layout.ts";
24
+
25
+ // 启动长连接中枢
26
+ import { GatewayWsClient } from "./ws-client.ts";
27
+ let wsClient: GatewayWsClient | undefined;
28
+
29
+ import { Hooks, isSkillMdReadPath } from "./hooks.ts";
30
+
31
+ // 供单测复用(保留历史测试)。
32
+ export { isSkillMdReadPath };
33
+
34
+ /** openclaw 插件 SDK 对齐的最小结构类型(结构化 typing,避免硬依赖 SDK 包类型)。 */
35
+ type PluginApi = {
36
+ pluginConfig?: PluginConfig;
37
+ on: (
38
+ hookName: string,
39
+ handler: (event: Record<string, unknown>, ctx: Record<string, unknown>) => any
40
+ ) => void;
41
+ };
42
+
43
+ /** 本地扫描对账周期:发现新增 agent/skill(纯本地 I/O),固定 3 分钟。 */
44
+ const RECONCILE_INTERVAL_MS = 3 * 60 * 1000;
45
+
46
+ /** 从 hook 事件/上下文里取本插件的运行期配置。 */
47
+ function extractPluginConfig(
48
+ event: Record<string, unknown>,
49
+ ctx: Record<string, unknown>
50
+ ): PluginConfig | undefined {
51
+ const fromEvent = (event.context as Record<string, unknown> | undefined)?.pluginConfig;
52
+ const fromCtx = ctx?.pluginConfig;
53
+ return (fromEvent ?? fromCtx) as PluginConfig | undefined;
54
+ }
55
+
56
+ export function extractApiPluginConfig(api: Pick<PluginApi, "pluginConfig">): PluginConfig {
57
+ return api.pluginConfig ?? {};
58
+ }
59
+
60
+ const definition = {
61
+ id: "skill-logger-plugin",
62
+ name: "Skill Logger",
63
+ description:
64
+ "追踪 openclaw skill 内功能点(脚本/命令/工具/HTTP)使用与报错,落本地并批量上报",
65
+ register(api: PluginApi) {
66
+ let pkgVersion = "unknown";
67
+ try {
68
+ const dir = path.dirname(fileURLToPath(import.meta.url));
69
+ const pkgPath = path.join(dir, "..", "package.json");
70
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
71
+ if (pkg.version) pkgVersion = pkg.version;
72
+ } catch {
73
+ // 忽略文件读取异常
74
+ }
75
+
76
+ const paths = resolvePaths();
77
+ let currentConfig: PluginConfig = extractApiPluginConfig(api);
78
+ currentConfig.pluginVersion = pkgVersion;
79
+ const getConfig = () => currentConfig;
80
+ const mergeConfig = (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
81
+ const incoming = extractPluginConfig(event, ctx);
82
+ if (incoming) currentConfig = { ...currentConfig, ...incoming };
83
+ };
84
+
85
+ const activeSkills = new ActiveSkills();
86
+ const updater = new SkillUpdater({ getConfig, cooldownStatePath: paths.cooldownStatePath });
87
+ const configSync = new ConfigSync({ paths, getConfig, updater });
88
+ const reporter = new Reporter({ paths, getConfig });
89
+ const hooks = new Hooks(reporter, configSync, activeSkills, getConfig);
90
+
91
+ let expertSkillLayoutRepair: Promise<void> | undefined;
92
+ const repairExpertSkillLayouts = () => {
93
+ if (expertSkillLayoutRepair) return expertSkillLayoutRepair;
94
+ expertSkillLayoutRepair = (async () => {
95
+ const result = await repairNestedExpertSkillLayouts(openclawHome());
96
+ if (result.repaired.length > 0) {
97
+ console.log(`[skill-logger-plugin] 已修复 ${result.repaired.length} 个专家 Skill 嵌套目录`);
98
+ }
99
+ for (const skippedPath of result.skipped) {
100
+ console.warn(`[skill-logger-plugin] 专家 Skill 嵌套目录版本无法安全提升,已保留现场: ${skippedPath}`);
101
+ }
102
+ for (const error of result.errors) {
103
+ console.warn(`[skill-logger-plugin] 修复专家 Skill 目录失败: ${error.path}`, error.message);
104
+ }
105
+ })().finally(() => {
106
+ expertSkillLayoutRepair = undefined;
107
+ });
108
+ return expertSkillLayoutRepair;
109
+ };
110
+
111
+ let reconcileTimer: ReturnType<typeof setInterval> | undefined;
112
+ const sessionUpdatedSkills = new Set<string>();
113
+
114
+ api.on("message_received", (event, ctx) => {
115
+ mergeConfig(event, ctx);
116
+ hooks.onMessageReceived(event, ctx);
117
+ });
118
+
119
+ api.on("before_prompt_build", () => {
120
+ let appendStr = "";
121
+
122
+ if (sessionUpdatedSkills.size > 0) {
123
+ appendStr = `【系统环境实时通知】:在当前对话期间,以下技能已被更新或重装:[${Array.from(sessionUpdatedSkills).join(", ")}]。如果你之前调用它遇到了报错,请立即抛弃旧的经验,重新阅读它的说明并以最新结果为准!`;
124
+ }
125
+ return { appendSystemContext: appendStr.trim() };
126
+ });
127
+
128
+ api.on("before_tool_call", (event, ctx) => {
129
+ mergeConfig(event, ctx);
130
+ hooks.onBeforeToolCall(event, ctx);
131
+ });
132
+
133
+ api.on("after_tool_call", (event, ctx) => {
134
+ mergeConfig(event, ctx);
135
+ hooks.onAfterToolCall(event);
136
+ });
137
+
138
+ api.on("session_end", (_event, ctx) => {
139
+ hooks.onSessionEnd(ctx);
140
+ });
141
+
142
+ api.on("gateway_start", (event, ctx) => {
143
+ mergeConfig(event, ctx);
144
+
145
+ // =========================================================
146
+ // 启动星型中枢长连接网络
147
+ // =========================================================
148
+ if (!wsClient) {
149
+ const currentConfig = getConfig();
150
+
151
+ // 优先使用配置文件中的 pluginId,否则回退到 OS hostname
152
+ const uniqueGatewayId = currentConfig.pluginId || process.env.GATEWAY_ID || `gateway-${os.hostname()}`;
153
+
154
+ // 智能推导 WebSocket 服务地址
155
+ let finalWsUrl = currentConfig.wsServerUrl || process.env.CENTRAL_WS_URL;
156
+ if (!finalWsUrl && currentConfig.platformBaseUrl) {
157
+ try {
158
+ const url = new URL(currentConfig.platformBaseUrl);
159
+ finalWsUrl = `${url.protocol === 'https:' ? 'wss:' : 'ws:'}//${url.host}/gateway/ws`;
160
+ } catch (e) {
161
+ // 容错处理
162
+ finalWsUrl = currentConfig.platformBaseUrl.replace(/^http/, 'ws').replace(/\/api\/?$/, '').replace(/\/$/, '') + '/gateway/ws';
163
+ }
164
+ }
165
+ if (!finalWsUrl) {
166
+ finalWsUrl = "wss://aishuo.co/gateway/ws";
167
+ }
168
+
169
+ wsClient = new GatewayWsClient({
170
+ serverUrl: finalWsUrl,
171
+ gatewayId: uniqueGatewayId,
172
+ authToken: currentConfig.authToken, // openclaw.json 的 config 节点动态读取鉴权 token
173
+ updater: updater,
174
+ enableFileLog: currentConfig.enableFileLog // 将日志开关透传给客户端模块
175
+ });
176
+ }
177
+ // =========================================================
178
+
179
+ // 连接中控前先修复存量异常,避免服务端上线重投与目录迁移并发写入同一路径。
180
+ void repairExpertSkillLayouts()
181
+ .then(() => wsClient?.connect())
182
+ .then(() => configSync.load())
183
+ .then(() => configSync.reconcile())
184
+ .catch((err) => console.warn("[skill-logger-plugin] 启动初始化异常", err));
185
+ reporter.startTimer();
186
+ // 定时器①:本地扫描对账,固定 3 分钟。
187
+ if (!reconcileTimer) {
188
+ reconcileTimer = setInterval(() => void configSync.reconcile(), RECONCILE_INTERVAL_MS);
189
+ if (typeof reconcileTimer.unref === "function") reconcileTimer.unref();
190
+ }
191
+ });
192
+
193
+ api.on("gateway_stop", async () => {
194
+ // 停止时销毁连接
195
+ if (wsClient) {
196
+ wsClient.destroy();
197
+ wsClient = undefined;
198
+ }
199
+
200
+ if (reconcileTimer) {
201
+ clearInterval(reconcileTimer);
202
+ reconcileTimer = undefined;
203
+ }
204
+ await hooks.flushAllPending();
205
+ await reporter.stopTimer();
206
+ });
207
+
208
+ api.on("before_install", (event, ctx) => {
209
+ mergeConfig(event, ctx);
210
+ // 安装后立即对账拉取最新配置,不执行自动更新(由 WSS 指令统一控制更新动作)
211
+ void configSync
212
+ .reconcile()
213
+ .catch((err) => console.warn("[skill-logger-plugin] before_install 处理异常", err));
214
+ });
215
+ },
216
+ };
217
+
218
+ export default definition;