@spzhongwin/skill-logger-plugin 1.0.9 → 1.0.11
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 +73 -5
- package/openclaw.plugin.json +50 -50
- package/package.json +37 -37
- package/src/active-skills.test.ts +32 -32
- package/src/active-skills.ts +77 -77
- package/src/config-sync.test.ts +165 -165
- package/src/config-sync.ts +544 -544
- package/src/hooks.test.ts +251 -251
- package/src/hooks.ts +517 -517
- package/src/http.ts +61 -61
- package/src/identity.ts +64 -64
- package/src/index.test.ts +53 -53
- package/src/index.ts +226 -226
- package/src/integration.test.ts +119 -119
- package/src/matcher.test.ts +170 -170
- package/src/matcher.ts +393 -393
- package/src/paths.test.ts +57 -57
- package/src/paths.ts +84 -84
- package/src/reporter.test.ts +139 -139
- package/src/reporter.ts +298 -298
- package/src/sample-config.json +72 -72
- package/src/semver.test.ts +23 -23
- package/src/semver.ts +60 -60
- package/src/skill-version.ts +53 -53
- package/src/types.ts +198 -198
- package/src/updater.test.ts +237 -237
- package/src/updater.ts +433 -433
- package/src/ws-client.test.ts +37 -37
- package/src/ws-client.ts +642 -641
- package/test-ws.ts +17 -17
- package/tsconfig.json +14 -14
- package/dist/active-skills.js +0 -67
- package/dist/active-skills.test.js +0 -29
- package/dist/config-sync.js +0 -439
- package/dist/config-sync.test.js +0 -145
- package/dist/hooks.js +0 -337
- package/dist/hooks.test.js +0 -123
- package/dist/http.js +0 -54
- package/dist/identity.js +0 -56
- package/dist/index.test.js +0 -39
- package/dist/integration.test.js +0 -102
- package/dist/matcher.js +0 -362
- package/dist/matcher.test.js +0 -139
- package/dist/paths.js +0 -62
- package/dist/paths.test.js +0 -49
- package/dist/reporter.js +0 -267
- package/dist/reporter.test.js +0 -128
- package/dist/semver.js +0 -64
- package/dist/semver.test.js +0 -21
- package/dist/skill-version.js +0 -23
- package/dist/types.js +0 -9
- package/dist/updater.js +0 -352
- package/dist/updater.test.js +0 -212
- package/dist/ws-client.js +0 -484
package/dist/config-sync.test.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { ConfigSync, parseSkillVersion } from "./config-sync.ts";
|
|
7
|
-
let dir;
|
|
8
|
-
let extensionsDir;
|
|
9
|
-
let syncStatePath;
|
|
10
|
-
const sampleConfigs = [
|
|
11
|
-
{ skillName: "foo", version: "1", functions: [{ id: "run", name: "运行", match: { type: "script", script: "scripts/run.py" } }] },
|
|
12
|
-
{ skillName: "bar", version: "1", functions: [{ id: "x", name: "x", match: { type: "tool", toolName: "bar_x" } }] },
|
|
13
|
-
];
|
|
14
|
-
async function writeSkill(name, withScripts = true) {
|
|
15
|
-
const skillDir = path.join(extensionsDir, "myext", "skills", name);
|
|
16
|
-
await fs.mkdir(skillDir, { recursive: true });
|
|
17
|
-
await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\n---\n# ${name}\n`);
|
|
18
|
-
if (withScripts) {
|
|
19
|
-
await fs.mkdir(path.join(skillDir, "scripts"), { recursive: true });
|
|
20
|
-
await fs.writeFile(path.join(skillDir, "scripts", "run.py"), "print(1)\n");
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function makeSync() {
|
|
24
|
-
return new ConfigSync({
|
|
25
|
-
paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
|
|
26
|
-
getConfig: () => ({}), // 无 platformBaseUrl → 用静态桩
|
|
27
|
-
sampleConfigs,
|
|
28
|
-
// 测试注入固定扫描目录,跳过 openclaw.json 解析;skill 实际写在 extensionsDir 下。
|
|
29
|
-
resolveSkillDirs: () => [path.join(os.tmpdir(), "openclaw-test-skills")],
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
beforeEach(async () => {
|
|
33
|
-
dir = path.join(os.tmpdir(), `slp-sync-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
34
|
-
extensionsDir = path.join(dir, "extensions");
|
|
35
|
-
syncStatePath = path.join(dir, "sync.json");
|
|
36
|
-
await fs.mkdir(extensionsDir, { recursive: true });
|
|
37
|
-
});
|
|
38
|
-
afterEach(async () => {
|
|
39
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
40
|
-
});
|
|
41
|
-
describe("parseSkillVersion(中英文兼容、纯正则)", () => {
|
|
42
|
-
const fm = (body) => `---\n${body}\n---\n# skill\n`;
|
|
43
|
-
it("英文 version", () => assert.equal(parseSkillVersion(fm("name: x\nversion: 1.2.3")), "1.2.3"));
|
|
44
|
-
it("大写 Version", () => assert.equal(parseSkillVersion(fm("Version: 2.0.0")), "2.0.0"));
|
|
45
|
-
it("中文 版本号 + 全角冒号", () => assert.equal(parseSkillVersion(fm("版本号:3.1")), "3.1"));
|
|
46
|
-
it("中文 版本 + 半角冒号", () => assert.equal(parseSkillVersion(fm("版本: v4")), "v4"));
|
|
47
|
-
it("去除包裹引号", () => assert.equal(parseSkillVersion(fm('version: "1.0.0"')), "1.0.0"));
|
|
48
|
-
it("去除行尾注释", () => assert.equal(parseSkillVersion(fm("version: 1.0.0 # latest")), "1.0.0"));
|
|
49
|
-
it("无版本字段返回 undefined", () => assert.equal(parseSkillVersion(fm("name: x")), undefined));
|
|
50
|
-
it("无 frontmatter 时全文宽松查找", () => assert.equal(parseSkillVersion("版本号: 9.9\n正文"), "9.9"));
|
|
51
|
-
it("版本写在正文单独一行 → 能取到", () => assert.equal(parseSkillVersion(fm("name: x") + "\n版本号:7.7\n"), "7.7"));
|
|
52
|
-
it("Markdown 标题 ## Version 不误取(# 前缀)", () => assert.equal(parseSkillVersion(fm("name: x") + "\n## Version: 5.5\n"), undefined));
|
|
53
|
-
it("frontmatter 与正文都有版本 → frontmatter 优先", () => assert.equal(parseSkillVersion(fm("version: 1.0.0") + "\nversion: 2.0.0\n"), "1.0.0"));
|
|
54
|
-
});
|
|
55
|
-
describe("ConfigSync.scanInstalledSkills", () => {
|
|
56
|
-
it("扫描出 SKILL.md 并解析 name", async () => {
|
|
57
|
-
await writeSkill("foo");
|
|
58
|
-
const skills = await makeSync().scanInstalledSkills();
|
|
59
|
-
const foo = skills.find((s) => s.name === "foo");
|
|
60
|
-
assert.ok(foo);
|
|
61
|
-
assert.ok(foo.signature.length > 0);
|
|
62
|
-
});
|
|
63
|
-
it("跳过 node_modules", async () => {
|
|
64
|
-
const nm = path.join(extensionsDir, "myext", "node_modules", "pkg", "skills", "ghost");
|
|
65
|
-
await fs.mkdir(nm, { recursive: true });
|
|
66
|
-
await fs.writeFile(path.join(nm, "SKILL.md"), `---\nname: ghost\n---\n`);
|
|
67
|
-
const skills = await makeSync().scanInstalledSkills();
|
|
68
|
-
assert.equal(skills.find((s) => s.name === "ghost"), undefined);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
describe("ConfigSync.diffAgainstState", () => {
|
|
72
|
-
it("初次全部需拉取", async () => {
|
|
73
|
-
await writeSkill("foo");
|
|
74
|
-
const cs = makeSync();
|
|
75
|
-
const installed = await cs.scanInstalledSkills();
|
|
76
|
-
const { toFetch } = cs.diffAgainstState(installed);
|
|
77
|
-
assert.equal(toFetch.length, 1);
|
|
78
|
-
assert.equal(toFetch[0].name, "foo");
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
describe("ConfigSync.pullConfigs(静态桩)", () => {
|
|
82
|
-
it("仅返回请求到的 skill(ok=true)", async () => {
|
|
83
|
-
const got = await makeSync().pullConfigs([{ name: "foo" }]);
|
|
84
|
-
assert.equal(got.ok, true);
|
|
85
|
-
assert.equal(got.configs.length, 1);
|
|
86
|
-
assert.equal(got.configs[0].skillName, "foo");
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
describe("ConfigSync 多 workspace 映射与过期检测", () => {
|
|
90
|
-
async function writeSkillIn(wsDir, name, version) {
|
|
91
|
-
const skillDir = path.join(wsDir, "skills", name);
|
|
92
|
-
await fs.mkdir(skillDir, { recursive: true });
|
|
93
|
-
await fs.writeFile(path.join(skillDir, "SKILL.md"), `---\nname: ${name}\nversion: ${version}\n---\n`);
|
|
94
|
-
return skillDir;
|
|
95
|
-
}
|
|
96
|
-
it("同名 skill 在多个 workspace → 映射记录全部副本,去重后只返回一份", async () => {
|
|
97
|
-
const wsA = path.join(dir, "workspace-a");
|
|
98
|
-
const wsB = path.join(dir, "workspace-b");
|
|
99
|
-
await writeSkillIn(wsA, "demo", "1.0.0");
|
|
100
|
-
await writeSkillIn(wsB, "demo", "1.0.0");
|
|
101
|
-
const cs = new ConfigSync({
|
|
102
|
-
paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
|
|
103
|
-
getConfig: () => ({}),
|
|
104
|
-
sampleConfigs,
|
|
105
|
-
resolveSkillDirs: () => [path.join(wsA, "skills"), path.join(wsB, "skills")],
|
|
106
|
-
});
|
|
107
|
-
const list = await cs.scanInstalledSkills();
|
|
108
|
-
assert.equal(list.filter((s) => s.name === "demo").length, 1); // 去重
|
|
109
|
-
assert.equal(cs.getInstallations().get("demo")?.length, 2); // 映射保留两份
|
|
110
|
-
});
|
|
111
|
-
it("detectOutdated:本地版本落后于平台最新版本时被标记", async () => {
|
|
112
|
-
const wsA = path.join(dir, "workspace-a");
|
|
113
|
-
await writeSkillIn(wsA, "demo", "1.0.0");
|
|
114
|
-
const cs = new ConfigSync({
|
|
115
|
-
paths: { extensionsDir, syncStatePath, openclawConfigPath: path.join(dir, "openclaw.json") },
|
|
116
|
-
getConfig: () => ({}),
|
|
117
|
-
// 平台最新版本 2.0.0(桩无 latestVersion 字段 → 回退用 version 当最新)
|
|
118
|
-
sampleConfigs: [{ skillName: "demo", version: "2.0.0", functions: [] }],
|
|
119
|
-
resolveSkillDirs: () => [path.join(wsA, "skills")],
|
|
120
|
-
});
|
|
121
|
-
await cs.checkVersionsAndUpdate(); // 刷新 latestVersions + 检测(无 updater 注入则不覆盖文件)
|
|
122
|
-
const outdated = cs.detectOutdated();
|
|
123
|
-
assert.equal(outdated.length, 1);
|
|
124
|
-
assert.equal(outdated[0].skillName, "demo");
|
|
125
|
-
assert.equal(outdated[0].localVersion, "1.0.0");
|
|
126
|
-
assert.equal(outdated[0].latestVersion, "2.0.0");
|
|
127
|
-
// agent↔skill 映射应本地持久化到 sync.json
|
|
128
|
-
const state = JSON.parse(await fs.readFile(syncStatePath, "utf-8"));
|
|
129
|
-
assert.ok(state.installations.demo, "sync.json 应含 installations 映射");
|
|
130
|
-
assert.equal(state.installations.demo[0].version, "1.0.0");
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe("ConfigSync.reconcile 端到端", () => {
|
|
134
|
-
it("对账后索引含该 skill 功能点,并持久化", async () => {
|
|
135
|
-
await writeSkill("foo");
|
|
136
|
-
const cs = makeSync();
|
|
137
|
-
await cs.reconcile();
|
|
138
|
-
// foo 的 run.py 应进入 script 索引
|
|
139
|
-
assert.ok(cs.getIndex().scriptByBasename.has("run.py"));
|
|
140
|
-
// 持久化文件存在且含 foo
|
|
141
|
-
const state = JSON.parse(await fs.readFile(syncStatePath, "utf-8"));
|
|
142
|
-
assert.ok(state.active.foo);
|
|
143
|
-
assert.ok(state.configPool["foo@unknown"]);
|
|
144
|
-
});
|
|
145
|
-
});
|
package/dist/hooks.js
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* hook 逻辑:把 matcher / activeSkills / reporter / configSync 串起来。
|
|
3
|
-
*
|
|
4
|
-
* - before_tool_call:
|
|
5
|
-
* · read 了某 SKILL.md → skill_trigger 事件 + 标记激活 + 懒拉配置
|
|
6
|
-
* · 其它工具 → matcher 匹配;命中则按 toolCallId 暂存,等 after 补 status/耗时
|
|
7
|
-
* - after_tool_call:按 toolCallId 取暂存,补 status/error/duration,落 function_call 事件
|
|
8
|
-
*
|
|
9
|
-
* 所有写入经 setImmediate 异步排队,不阻塞 agent。异常一律吞掉。
|
|
10
|
-
*/
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
13
|
-
import { match } from "./matcher.ts";
|
|
14
|
-
/** pending 视为「after 不会再来」的超时:留足长跑命令(构建等)的余量。 */
|
|
15
|
-
const PENDING_TTL_MS = 30 * 60 * 1000;
|
|
16
|
-
/** pending 内存硬上限,超出按最旧补记并清理。 */
|
|
17
|
-
const PENDING_MAX = 5000;
|
|
18
|
-
/** MySQL DATETIME 兼容格式:YYYY-MM-DD HH:MM:SS */
|
|
19
|
-
function toMySQLDateTime(d) {
|
|
20
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
21
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
22
|
-
}
|
|
23
|
-
/** read 内联路径是否为名为 SKILL.md 的文件(按 basename 判断,规避 fooSKILL.md 误判)。 */
|
|
24
|
-
export function isSkillMdReadPath(filePath) {
|
|
25
|
-
return path.basename(filePath) === "SKILL.md";
|
|
26
|
-
}
|
|
27
|
-
function extractAppKey(event, ctx) {
|
|
28
|
-
try {
|
|
29
|
-
const content = JSON.stringify({ event, ctx });
|
|
30
|
-
// 终极增强正则:兼容 "app key"、兼容长达40个字符的中文修饰语、兼容各类分隔符(含无分隔符)、并精确提取 8 位以上的密钥字符
|
|
31
|
-
const match = content.match(/(?:app[-_\s]?key)(?:[^\n\r]{0,40}?(?:[:=:]|是|为|\bis\b|\bvalue\b))?[^a-zA-Z0-9_-]*([a-zA-Z0-9_-]{8,})/i);
|
|
32
|
-
return match ? match[1] : undefined;
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
export class Hooks {
|
|
39
|
-
pending = new Map();
|
|
40
|
-
sessionAppKeys = new Map();
|
|
41
|
-
reporter;
|
|
42
|
-
configSync;
|
|
43
|
-
activeSkills;
|
|
44
|
-
getConfig;
|
|
45
|
-
constructor(reporter, configSync, activeSkills, getConfig) {
|
|
46
|
-
this.reporter = reporter;
|
|
47
|
-
this.configSync = configSync;
|
|
48
|
-
this.activeSkills = activeSkills;
|
|
49
|
-
this.getConfig = getConfig;
|
|
50
|
-
}
|
|
51
|
-
get isDebug() {
|
|
52
|
-
return this.getConfig().debugLogging !== false;
|
|
53
|
-
}
|
|
54
|
-
debug(...args) {
|
|
55
|
-
if (this.isDebug) {
|
|
56
|
-
console.log("[skill-logger-plugin/hooks]", ...args);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/** 补插件维度元数据(plugin_id/name/version);上报端按插件归属统计时需要。 */
|
|
60
|
-
enrichPluginMeta(event) {
|
|
61
|
-
const config = this.getConfig();
|
|
62
|
-
if (config.pluginId)
|
|
63
|
-
event.plugin_id = config.pluginId;
|
|
64
|
-
if (config.pluginName)
|
|
65
|
-
event.plugin_name = config.pluginName;
|
|
66
|
-
if (config.pluginVersion)
|
|
67
|
-
event.plugin_version = config.pluginVersion;
|
|
68
|
-
}
|
|
69
|
-
emit(event) {
|
|
70
|
-
this.enrichPluginMeta(event);
|
|
71
|
-
setImmediate(() => void this.reporter.appendEvent(event));
|
|
72
|
-
}
|
|
73
|
-
buildPendingEvent(p, status, error, durationMs, appKey) {
|
|
74
|
-
return {
|
|
75
|
-
event_id: randomUUID(),
|
|
76
|
-
event_type: "function_call",
|
|
77
|
-
skill_name: p.res.skillName,
|
|
78
|
-
skill_version: p.res.skillVersion,
|
|
79
|
-
function_id: p.res.functionId,
|
|
80
|
-
function_name: p.res.functionName,
|
|
81
|
-
match_type: p.res.matchType,
|
|
82
|
-
invoke_tool: p.invokeTool,
|
|
83
|
-
command: p.command,
|
|
84
|
-
args: p.res.args,
|
|
85
|
-
status,
|
|
86
|
-
error_message: error,
|
|
87
|
-
duration_ms: durationMs,
|
|
88
|
-
app_key: appKey || "",
|
|
89
|
-
session_id: p.sessionId,
|
|
90
|
-
agent_id: p.agentId,
|
|
91
|
-
run_id: p.runId,
|
|
92
|
-
called_at: toMySQLDateTime(new Date()),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
sessionKeyOf(ctx) {
|
|
96
|
-
return ctx.sessionKey ?? ctx.sessionId ?? undefined;
|
|
97
|
-
}
|
|
98
|
-
onMessageReceived(event, ctx) {
|
|
99
|
-
const rawAppKey = extractAppKey(event, ctx);
|
|
100
|
-
const sk = this.sessionKeyOf(ctx);
|
|
101
|
-
if (sk && rawAppKey) {
|
|
102
|
-
this.sessionAppKeys.set(sk, rawAppKey);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
onManualErrorRecord(args, ctx) {
|
|
106
|
-
const sk = this.sessionKeyOf(ctx);
|
|
107
|
-
let appKey = sk ? this.sessionAppKeys.get(sk) : undefined;
|
|
108
|
-
this.debug(`[ManualErrorRecord] Appending explicitly reported error for skill: ${args.skill_name}`);
|
|
109
|
-
this.emit({
|
|
110
|
-
event_id: randomUUID(),
|
|
111
|
-
event_type: "function_call",
|
|
112
|
-
skill_name: args.skill_name || "unknown_skill",
|
|
113
|
-
skill_version: this.configSync.getVersion(args.skill_name),
|
|
114
|
-
function_name: args.tool_name || "unknown_tool",
|
|
115
|
-
invoke_tool: "report_skill_error",
|
|
116
|
-
args: args.input_args ? { raw_args: args.input_args } : undefined,
|
|
117
|
-
status: "error",
|
|
118
|
-
error_message: args.error_message,
|
|
119
|
-
app_key: appKey || "",
|
|
120
|
-
session_id: sk,
|
|
121
|
-
agent_id: ctx.agentId,
|
|
122
|
-
run_id: ctx.runId,
|
|
123
|
-
called_at: toMySQLDateTime(new Date())
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
onBeforeToolCall(event, ctx) {
|
|
127
|
-
try {
|
|
128
|
-
this.sweepStalePending();
|
|
129
|
-
const toolName = event.toolName;
|
|
130
|
-
const params = event.params ?? {};
|
|
131
|
-
const sk = this.sessionKeyOf(ctx);
|
|
132
|
-
let appKey = undefined;
|
|
133
|
-
// Session-level AppKey Caching (Map priority optimization)
|
|
134
|
-
if (sk) {
|
|
135
|
-
appKey = this.sessionAppKeys.get(sk);
|
|
136
|
-
}
|
|
137
|
-
if (!appKey) {
|
|
138
|
-
appKey = extractAppKey(event, ctx);
|
|
139
|
-
if (appKey && sk) {
|
|
140
|
-
this.sessionAppKeys.set(sk, appKey);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// 路径一:显式 skill 工具(SKILL.md frontmatter 含 command-dispatch: tool)
|
|
144
|
-
if (toolName === "skill") {
|
|
145
|
-
const skillName = typeof params.skill === "string" ? params.skill : "";
|
|
146
|
-
if (!skillName)
|
|
147
|
-
return;
|
|
148
|
-
this.debug(`Intercepted explicit 'skill' tool call for: ${skillName}`);
|
|
149
|
-
this.recordTrigger(skillName, "tool", "skill", ctx, appKey);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
// 路径二:内联扩展——模型 read 了某 SKILL.md
|
|
153
|
-
if (toolName === "read") {
|
|
154
|
-
const filePath = (params.path ?? params.file_path ?? "");
|
|
155
|
-
if (!isSkillMdReadPath(filePath))
|
|
156
|
-
return;
|
|
157
|
-
const rootDir = path.dirname(filePath);
|
|
158
|
-
// 归一到与匹配一致的规范名(SKILL.md frontmatter name),扫描未覆盖时回退目录名。
|
|
159
|
-
const skillName = this.configSync.resolveSkillName(rootDir) || path.basename(rootDir);
|
|
160
|
-
if (!skillName)
|
|
161
|
-
return;
|
|
162
|
-
this.debug(`Intercepted 'read' for SKILL.md. Attributed to skill: ${skillName}`);
|
|
163
|
-
this.recordTrigger(skillName, "inline", "read", ctx, appKey);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
// 其它工具(含 exec / MCP 工具 / fetch …)→ 匹配功能点
|
|
167
|
-
const active = this.activeSkills.getActive(this.sessionKeyOf(ctx));
|
|
168
|
-
const res = match({ toolName, params }, active, this.configSync.getIndex());
|
|
169
|
-
const command = typeof params.command === "string" ? params.command : undefined;
|
|
170
|
-
if (!res) {
|
|
171
|
-
this.debug(`Tool call '${toolName}' did not match any function config.`);
|
|
172
|
-
this.maybeRecordUnattributed(toolName, command, active, ctx, appKey);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
this.debug(`Tool call '${toolName}' matched function ID: ${res.functionId} (${res.skillName}@${res.skillVersion})`);
|
|
176
|
-
const toolCallId = event.toolCallId;
|
|
177
|
-
if (toolCallId) {
|
|
178
|
-
this.pending.set(toolCallId, {
|
|
179
|
-
res,
|
|
180
|
-
command,
|
|
181
|
-
invokeTool: toolName,
|
|
182
|
-
sessionId: this.sessionKeyOf(ctx),
|
|
183
|
-
agentId: ctx.agentId,
|
|
184
|
-
runId: ctx.runId,
|
|
185
|
-
appKey,
|
|
186
|
-
ts: Date.now(),
|
|
187
|
-
});
|
|
188
|
-
this.capPending();
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
// 无 toolCallId 无法与 after 关联 → 立即记一条(无 status/耗时)
|
|
192
|
-
this.emit(this.buildFunctionCall(res, command, toolName, undefined, undefined, undefined, ctx, appKey));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
catch (err) {
|
|
196
|
-
console.warn("[skill-logger-plugin] onBeforeToolCall 异常", err);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
onAfterToolCall(event) {
|
|
200
|
-
try {
|
|
201
|
-
const toolCallId = event.toolCallId;
|
|
202
|
-
if (!toolCallId)
|
|
203
|
-
return;
|
|
204
|
-
const p = this.pending.get(toolCallId);
|
|
205
|
-
if (!p)
|
|
206
|
-
return;
|
|
207
|
-
this.pending.delete(toolCallId);
|
|
208
|
-
const error = event.error;
|
|
209
|
-
const durationMs = typeof event.durationMs === "number" ? event.durationMs : undefined;
|
|
210
|
-
const appKey = p.appKey || extractAppKey(event, {});
|
|
211
|
-
this.debug(`After tool call [${toolCallId}]: Status ${error ? "error" : "success"}, Duration ${durationMs}ms`);
|
|
212
|
-
this.emitPending(p, error ? "error" : "success", error, durationMs, appKey);
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
console.warn("[skill-logger-plugin] onAfterToolCall 异常", err);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/** session 结束:清激活记录,并把该 session 仍未收到 after 的 pending 补记为 unknown。 */
|
|
219
|
-
onSessionEnd(ctx) {
|
|
220
|
-
try {
|
|
221
|
-
const sk = this.sessionKeyOf(ctx);
|
|
222
|
-
this.activeSkills.clearSession(sk);
|
|
223
|
-
if (sk)
|
|
224
|
-
this.sessionAppKeys.delete(sk);
|
|
225
|
-
// pending 入队时存的 sessionId 取自 sessionKeyOf(ctx),这里须用同一口径比对,
|
|
226
|
-
// 否则当 ctx 同时带 sessionKey 与 sessionId 且两者不同时会漏补记、残留内存。
|
|
227
|
-
for (const [id, p] of this.pending) {
|
|
228
|
-
if (p.sessionId === sk) {
|
|
229
|
-
this.emitPending(p, "unknown");
|
|
230
|
-
this.pending.delete(id);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
console.warn("[skill-logger-plugin] onSessionEnd 异常", err);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
/** gateway 停止:把所有残留 pending 补记为 unknown,避免丢失使用记录。 */
|
|
239
|
-
async flushAllPending() {
|
|
240
|
-
const writes = [];
|
|
241
|
-
for (const [id, p] of this.pending) {
|
|
242
|
-
const ev = this.buildPendingEvent(p, "unknown");
|
|
243
|
-
this.enrichPluginMeta(ev);
|
|
244
|
-
writes.push(this.reporter.appendEvent(ev));
|
|
245
|
-
this.pending.delete(id);
|
|
246
|
-
}
|
|
247
|
-
await Promise.all(writes);
|
|
248
|
-
}
|
|
249
|
-
/** 记 skill 触发事件 + 标记激活 + 懒拉配置。 */
|
|
250
|
-
recordTrigger(skillName, invokeMode, invokeTool, ctx, appKey) {
|
|
251
|
-
this.emit({
|
|
252
|
-
event_id: randomUUID(),
|
|
253
|
-
event_type: "skill_trigger",
|
|
254
|
-
skill_name: skillName,
|
|
255
|
-
skill_version: this.configSync.getVersion(skillName),
|
|
256
|
-
invoke_mode: invokeMode,
|
|
257
|
-
invoke_tool: invokeTool,
|
|
258
|
-
app_key: appKey || "",
|
|
259
|
-
session_id: this.sessionKeyOf(ctx),
|
|
260
|
-
agent_id: ctx.agentId,
|
|
261
|
-
run_id: ctx.runId,
|
|
262
|
-
called_at: toMySQLDateTime(new Date()),
|
|
263
|
-
});
|
|
264
|
-
this.activeSkills.markActive(this.sessionKeyOf(ctx), skillName);
|
|
265
|
-
void this.configSync.lazyCheck(skillName);
|
|
266
|
-
}
|
|
267
|
-
emitPending(p, status, error, durationMs, appKey) {
|
|
268
|
-
this.emit(this.buildPendingEvent(p, status, error, durationMs, appKey || p.appKey));
|
|
269
|
-
}
|
|
270
|
-
buildFunctionCall(res, command, invokeTool, status, error, durationMs, ctx, appKey) {
|
|
271
|
-
return {
|
|
272
|
-
event_id: randomUUID(),
|
|
273
|
-
event_type: "function_call",
|
|
274
|
-
skill_name: res.skillName,
|
|
275
|
-
skill_version: res.skillVersion,
|
|
276
|
-
function_id: res.functionId,
|
|
277
|
-
function_name: res.functionName,
|
|
278
|
-
match_type: res.matchType,
|
|
279
|
-
invoke_tool: invokeTool,
|
|
280
|
-
command,
|
|
281
|
-
args: res.args,
|
|
282
|
-
status,
|
|
283
|
-
error_message: error,
|
|
284
|
-
duration_ms: durationMs,
|
|
285
|
-
app_key: appKey || "",
|
|
286
|
-
session_id: this.sessionKeyOf(ctx),
|
|
287
|
-
agent_id: ctx.agentId,
|
|
288
|
-
run_id: ctx.runId,
|
|
289
|
-
called_at: toMySQLDateTime(new Date()),
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
/** 可选:无法归属到功能点时,若开启 recordUnattributed 且恰有一个激活 skill,记一条通用 exec。 */
|
|
293
|
-
maybeRecordUnattributed(toolName, command, active, ctx, appKey) {
|
|
294
|
-
if (this.getConfig().recordUnattributed === false)
|
|
295
|
-
return;
|
|
296
|
-
if (toolName !== "exec" || active.size !== 1)
|
|
297
|
-
return;
|
|
298
|
-
const skillName = [...active][0];
|
|
299
|
-
this.debug(`Recording unattributed function_call for skill: ${skillName}`);
|
|
300
|
-
this.emit({
|
|
301
|
-
event_id: randomUUID(),
|
|
302
|
-
event_type: "function_call",
|
|
303
|
-
skill_name: skillName,
|
|
304
|
-
match_type: undefined,
|
|
305
|
-
invoke_tool: toolName,
|
|
306
|
-
command,
|
|
307
|
-
app_key: appKey || "",
|
|
308
|
-
called_at: toMySQLDateTime(new Date()),
|
|
309
|
-
session_id: this.sessionKeyOf(ctx),
|
|
310
|
-
agent_id: ctx.agentId,
|
|
311
|
-
run_id: ctx.runId,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
/** 把超时的 pending 补记为 unknown 并清理。Map 按插入顺序≈时间顺序,遇到首个未超时即停。 */
|
|
315
|
-
sweepStalePending() {
|
|
316
|
-
if (this.pending.size === 0)
|
|
317
|
-
return;
|
|
318
|
-
const cutoff = Date.now() - PENDING_TTL_MS;
|
|
319
|
-
for (const [id, p] of this.pending) {
|
|
320
|
-
if (p.ts >= cutoff)
|
|
321
|
-
break;
|
|
322
|
-
this.emitPending(p, "unknown");
|
|
323
|
-
this.pending.delete(id);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
/** 内存硬上限保护:超出时把最旧的补记为 unknown 并清理。 */
|
|
327
|
-
capPending() {
|
|
328
|
-
while (this.pending.size > PENDING_MAX) {
|
|
329
|
-
const next = this.pending.entries().next();
|
|
330
|
-
if (next.done)
|
|
331
|
-
break;
|
|
332
|
-
const [id, p] = next.value;
|
|
333
|
-
this.emitPending(p, "unknown");
|
|
334
|
-
this.pending.delete(id);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
package/dist/hooks.test.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { Hooks } from "./hooks.ts";
|
|
4
|
-
import { ActiveSkills } from "./active-skills.ts";
|
|
5
|
-
import { buildIndex } from "./matcher.ts";
|
|
6
|
-
const sampleConfigs = [
|
|
7
|
-
{
|
|
8
|
-
skillName: "model-usage",
|
|
9
|
-
version: "1.0.0",
|
|
10
|
-
functions: [
|
|
11
|
-
{ id: "usage_current", name: "当前用量", match: { type: "script", script: "scripts/model_usage.py", argRules: [{ flag: "--mode", value: "current" }] } },
|
|
12
|
-
],
|
|
13
|
-
},
|
|
14
|
-
];
|
|
15
|
-
const tick = () => new Promise((r) => setImmediate(r));
|
|
16
|
-
function setup() {
|
|
17
|
-
const captured = [];
|
|
18
|
-
const reporter = { appendEvent: async (e) => void captured.push(e) };
|
|
19
|
-
const index = buildIndex(sampleConfigs);
|
|
20
|
-
const lazy = [];
|
|
21
|
-
const configSync = {
|
|
22
|
-
getIndex: () => index,
|
|
23
|
-
lazyCheck: async (name) => void lazy.push(name),
|
|
24
|
-
resolveSkillName: () => undefined,
|
|
25
|
-
getVersion: () => undefined,
|
|
26
|
-
};
|
|
27
|
-
const activeSkills = new ActiveSkills();
|
|
28
|
-
const hooks = new Hooks(reporter, configSync, activeSkills, () => ({}));
|
|
29
|
-
return { hooks, captured, lazy, activeSkills };
|
|
30
|
-
}
|
|
31
|
-
describe("Hooks 端到端串联", () => {
|
|
32
|
-
it("read SKILL.md → skill_trigger + 标记激活 + 懒拉", async () => {
|
|
33
|
-
const { hooks, captured, lazy, activeSkills } = setup();
|
|
34
|
-
hooks.onBeforeToolCall({ toolName: "read", params: { path: "/e/skills/model-usage/SKILL.md" } }, { sessionId: "s1", agentId: "main" });
|
|
35
|
-
await tick();
|
|
36
|
-
assert.equal(captured.length, 1);
|
|
37
|
-
assert.equal(captured[0].event_type, "skill_trigger");
|
|
38
|
-
assert.equal(captured[0].skill_name, "model-usage");
|
|
39
|
-
assert.equal(captured[0].invoke_mode, "inline");
|
|
40
|
-
assert.deepEqual(lazy, ["model-usage"]);
|
|
41
|
-
assert.ok(activeSkills.getActive("s1").has("model-usage"));
|
|
42
|
-
});
|
|
43
|
-
it("skill 工具直调(command-dispatch: tool)→ skill_trigger(tool)", async () => {
|
|
44
|
-
const { hooks, captured, activeSkills } = setup();
|
|
45
|
-
hooks.onBeforeToolCall({ toolName: "skill", params: { skill: "model-usage" } }, { sessionId: "s1" });
|
|
46
|
-
await tick();
|
|
47
|
-
assert.equal(captured.length, 1);
|
|
48
|
-
assert.equal(captured[0].event_type, "skill_trigger");
|
|
49
|
-
assert.equal(captured[0].invoke_mode, "tool");
|
|
50
|
-
assert.ok(activeSkills.getActive("s1").has("model-usage"));
|
|
51
|
-
});
|
|
52
|
-
it("after 未到达:flushAllPending 补记 unknown", async () => {
|
|
53
|
-
const { hooks, captured } = setup();
|
|
54
|
-
hooks.onBeforeToolCall({ toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t9" }, { sessionId: "s1" });
|
|
55
|
-
await tick();
|
|
56
|
-
assert.equal(captured.length, 0);
|
|
57
|
-
await hooks.flushAllPending();
|
|
58
|
-
await tick();
|
|
59
|
-
assert.equal(captured.length, 1);
|
|
60
|
-
assert.equal(captured[0].status, "unknown");
|
|
61
|
-
assert.equal(captured[0].function_id, "usage_current");
|
|
62
|
-
});
|
|
63
|
-
it("session_end 补记该 session 残留 pending 并清激活", async () => {
|
|
64
|
-
const { hooks, captured, activeSkills } = setup();
|
|
65
|
-
hooks.onBeforeToolCall({ toolName: "read", params: { path: "/e/skills/model-usage/SKILL.md" } }, { sessionId: "s1" });
|
|
66
|
-
hooks.onBeforeToolCall({ toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t10" }, { sessionId: "s1" });
|
|
67
|
-
await tick();
|
|
68
|
-
captured.length = 0;
|
|
69
|
-
hooks.onSessionEnd({ sessionId: "s1" });
|
|
70
|
-
await tick();
|
|
71
|
-
assert.equal(captured.filter((e) => e.status === "unknown").length, 1);
|
|
72
|
-
assert.deepEqual([...activeSkills.getActive("s1")], []);
|
|
73
|
-
});
|
|
74
|
-
it("exec 命中 → 暂存,after 成功 → function_call(success)", async () => {
|
|
75
|
-
const { hooks, captured } = setup();
|
|
76
|
-
hooks.onBeforeToolCall({ toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t1" }, { sessionId: "s1" });
|
|
77
|
-
await tick();
|
|
78
|
-
assert.equal(captured.length, 0); // 等 after
|
|
79
|
-
hooks.onAfterToolCall({ toolCallId: "t1", durationMs: 42 });
|
|
80
|
-
await tick();
|
|
81
|
-
assert.equal(captured.length, 1);
|
|
82
|
-
const ev = captured[0];
|
|
83
|
-
assert.equal(ev.event_type, "function_call");
|
|
84
|
-
assert.equal(ev.function_id, "usage_current");
|
|
85
|
-
assert.equal(ev.match_type, "script");
|
|
86
|
-
assert.equal(ev.status, "success");
|
|
87
|
-
assert.equal(ev.duration_ms, 42);
|
|
88
|
-
assert.equal(ev.args?.mode, "current");
|
|
89
|
-
});
|
|
90
|
-
it("after 带 error → function_call(error)", async () => {
|
|
91
|
-
const { hooks, captured } = setup();
|
|
92
|
-
hooks.onBeforeToolCall({ toolName: "exec", params: { command: "python /e/skills/model-usage/scripts/model_usage.py --mode current" }, toolCallId: "t2" }, { sessionId: "s1" });
|
|
93
|
-
await tick();
|
|
94
|
-
hooks.onAfterToolCall({ toolCallId: "t2", error: "boom" });
|
|
95
|
-
await tick();
|
|
96
|
-
assert.equal(captured[0].status, "error");
|
|
97
|
-
assert.equal(captured[0].error_message, "boom");
|
|
98
|
-
});
|
|
99
|
-
it("未命中且默认配置 → 不记录", async () => {
|
|
100
|
-
const { hooks, captured } = setup();
|
|
101
|
-
hooks.onBeforeToolCall({ toolName: "exec", params: { command: "ls -la" }, toolCallId: "t3" }, { sessionId: "s1" });
|
|
102
|
-
hooks.onAfterToolCall({ toolCallId: "t3" });
|
|
103
|
-
await tick();
|
|
104
|
-
assert.equal(captured.length, 0);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
describe("插件模块图可加载", () => {
|
|
108
|
-
it("import index.ts 并 register 注册全部生命周期 hook", async () => {
|
|
109
|
-
const mod = await import("./index.ts");
|
|
110
|
-
const registered = [];
|
|
111
|
-
mod.default.register({ on: (name) => registered.push(name) });
|
|
112
|
-
assert.deepEqual(registered.sort(), [
|
|
113
|
-
"after_tool_call",
|
|
114
|
-
"before_install",
|
|
115
|
-
"before_prompt_build",
|
|
116
|
-
"before_tool_call",
|
|
117
|
-
"gateway_start",
|
|
118
|
-
"gateway_stop",
|
|
119
|
-
"message_received",
|
|
120
|
-
"session_end",
|
|
121
|
-
]);
|
|
122
|
-
});
|
|
123
|
-
});
|
package/dist/http.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 默认 HTTP 客户端:使用 Node 原生 https 模块发起请求,
|
|
3
|
-
* 显式关闭 SSL 证书校验 (rejectUnauthorized: false),并支持超时。
|
|
4
|
-
* 避免自签证书导致的 UNABLE_TO_VERIFY_LEAF_SIGNATURE 异常。
|
|
5
|
-
*/
|
|
6
|
-
import https from "node:https";
|
|
7
|
-
import http from "node:http";
|
|
8
|
-
import { URL } from "node:url";
|
|
9
|
-
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
10
|
-
export function defaultFetch(timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
11
|
-
return (urlStr, init) => {
|
|
12
|
-
return new Promise((resolve, reject) => {
|
|
13
|
-
let parsedUrl;
|
|
14
|
-
try {
|
|
15
|
-
parsedUrl = new URL(urlStr);
|
|
16
|
-
}
|
|
17
|
-
catch (err) {
|
|
18
|
-
return reject(err);
|
|
19
|
-
}
|
|
20
|
-
const isHttps = parsedUrl.protocol === "https:";
|
|
21
|
-
const requestFn = isHttps ? https.request : http.request;
|
|
22
|
-
const headers = { ...init.headers };
|
|
23
|
-
if (init.body) {
|
|
24
|
-
headers["Content-Length"] = Buffer.byteLength(init.body);
|
|
25
|
-
}
|
|
26
|
-
const options = {
|
|
27
|
-
method: init.method || "GET",
|
|
28
|
-
headers,
|
|
29
|
-
timeout: timeoutMs,
|
|
30
|
-
rejectUnauthorized: false // <--- 核心改动:跳过 SSL 校验
|
|
31
|
-
};
|
|
32
|
-
const req = requestFn(parsedUrl, options, (res) => {
|
|
33
|
-
let body = "";
|
|
34
|
-
res.on("data", chunk => { body += chunk; });
|
|
35
|
-
res.on("end", () => {
|
|
36
|
-
resolve({
|
|
37
|
-
ok: res.statusCode ? res.statusCode >= 200 && res.statusCode < 300 : false,
|
|
38
|
-
status: res.statusCode || 0,
|
|
39
|
-
text: async () => body,
|
|
40
|
-
json: async () => JSON.parse(body)
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
req.on("error", reject);
|
|
45
|
-
req.on("timeout", () => {
|
|
46
|
-
req.destroy(new Error("Timeout"));
|
|
47
|
-
});
|
|
48
|
-
if (init.body) {
|
|
49
|
-
req.write(init.body);
|
|
50
|
-
}
|
|
51
|
-
req.end();
|
|
52
|
-
});
|
|
53
|
-
};
|
|
54
|
-
}
|