@spzhongwin/skill-logger-plugin 1.0.5 → 1.0.7

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 CHANGED
@@ -141,6 +141,9 @@ function parseSkillVersion(content) {
141
141
  // src/updater.ts
142
142
  var execFileAsync = promisify(execFile);
143
143
  var ATTEMPT_COOLDOWN_MS = 30 * 60 * 1e3;
144
+ function skillIdentityHash(code, version) {
145
+ return createHash("sha256").update(`${code} ${code} ${version}`).digest("hex");
146
+ }
144
147
  async function systemUnzip(zipPath, destDir) {
145
148
  await fs3.mkdir(destDir, { recursive: true });
146
149
  await execFileAsync("unzip", ["-o", "-q", zipPath, "-d", destDir]);
@@ -259,6 +262,13 @@ var SkillUpdater = class {
259
262
  console.warn(`[skill-logger-plugin] ${skillName}@${version} \u4E0B\u8F7D\u5305\u65E0\u7248\u672C\u53F7\uFF0C\u653E\u5F03\u8986\u76D6`);
260
263
  return;
261
264
  }
265
+ if (dl.sha256) {
266
+ const actualHash = skillIdentityHash(skillName, packageVersion);
267
+ if (actualHash !== dl.sha256) {
268
+ console.warn(`[skill-logger-plugin] ${skillName}@${version} \u8EAB\u4EFD\u54C8\u5E0C\u4E0D\u4E00\u81F4\uFF0C\u653E\u5F03\u8986\u76D6`);
269
+ return;
270
+ }
271
+ }
262
272
  const metaPath = path3.join(srcRoot, ".meta.json");
263
273
  if (!await this.exists(metaPath)) {
264
274
  this.debug(`[skill-logger-plugin] \u4E3A ${skillName}@${version} \u81EA\u52A8\u751F\u6210 .meta.json`);
@@ -299,8 +309,9 @@ var SkillUpdater = class {
299
309
  const data = await res.json();
300
310
  const url = typeof data?.url === "string" ? data.url : typeof data?.downloadUrl === "string" ? data.downloadUrl : void 0;
301
311
  const resolvedVersion = typeof data?.version === "string" ? data.version : void 0;
312
+ const sha256 = typeof data?.sha256 === "string" ? data.sha256 : void 0;
302
313
  if (!url) return void 0;
303
- return { url, version: resolvedVersion };
314
+ return { url, version: resolvedVersion, sha256 };
304
315
  }
305
316
  /** 在解压目录里定位含 SKILL.md 的目录(兼容包内是否带顶层目录)。深度上限 2。 */
306
317
  async locateSkillRoot(dir, depth) {
@@ -774,6 +785,44 @@ function defaultFetch(timeoutMs = DEFAULT_TIMEOUT_MS) {
774
785
  };
775
786
  }
776
787
 
788
+ // src/semver.ts
789
+ function parseCore(v) {
790
+ const core = v.trim().replace(/^[vV]/, "").split(/[-+]/, 1)[0];
791
+ if (!core) return null;
792
+ const parts = core.split(".");
793
+ const nums = [];
794
+ for (const p of parts) {
795
+ if (!/^\d+$/.test(p)) return null;
796
+ nums.push(Number(p));
797
+ }
798
+ return nums.length > 0 ? nums : null;
799
+ }
800
+ function compareVersions(a, b) {
801
+ const na = parseCore(a);
802
+ const nb = parseCore(b);
803
+ if (na && nb) {
804
+ const len = Math.max(na.length, nb.length);
805
+ for (let i = 0; i < len; i++) {
806
+ const x = na[i] ?? 0;
807
+ const y = nb[i] ?? 0;
808
+ if (x < y) return -1;
809
+ if (x > y) return 1;
810
+ }
811
+ return 0;
812
+ }
813
+ const sa = a.trim();
814
+ const sb = b.trim();
815
+ if (sa === sb) return 0;
816
+ return sa < sb ? -1 : 1;
817
+ }
818
+ function isOutdated(local, latest) {
819
+ if (!local || !latest) return false;
820
+ const na = parseCore(local);
821
+ const nb = parseCore(latest);
822
+ if (na && nb) return compareVersions(local, latest) < 0;
823
+ return local.trim() !== latest.trim();
824
+ }
825
+
777
826
  // src/config-sync.ts
778
827
  var MAX_SCAN_DEPTH = 6;
779
828
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".cache"]);
@@ -919,6 +968,60 @@ var ConfigSync = class {
919
968
  getInstallations() {
920
969
  return this.installations;
921
970
  }
971
+ /** 基于最近一次版本检查结果,找出所有本地版本落后的安装副本。 */
972
+ detectOutdated() {
973
+ const out = [];
974
+ for (const [skillName, copies] of this.installations) {
975
+ const cfg = this.configs.get(skillName);
976
+ const latestVersion = this.latestVersions.get(skillName) || cfg?.latestVersion || cfg?.version;
977
+ if (!latestVersion) continue;
978
+ for (const copy of copies) {
979
+ if (!isOutdated(copy.version, latestVersion)) continue;
980
+ out.push({
981
+ skillName,
982
+ rootDir: copy.rootDir,
983
+ localVersion: copy.version || "",
984
+ latestVersion
985
+ });
986
+ }
987
+ }
988
+ return out;
989
+ }
990
+ /** 扫描本地安装副本,拉取平台最新版本,检测落后副本并按配置触发自动更新。 */
991
+ async checkVersionsAndUpdate() {
992
+ if (this.checkingVersions) return;
993
+ this.checkingVersions = true;
994
+ try {
995
+ const installed = await this.scanInstalledSkills();
996
+ this.scanCache = { ts: Date.now(), skills: installed };
997
+ if (installed.length > 0) {
998
+ const { ok, configs } = await this.pullConfigs(
999
+ installed.map((s) => ({ name: s.name, version: s.version }))
1000
+ );
1001
+ if (ok) {
1002
+ for (const cfg of configs) {
1003
+ const latestVersion = cfg.latestVersion || cfg.version;
1004
+ if (latestVersion) this.latestVersions.set(cfg.skillName, latestVersion);
1005
+ if (cfg.version) {
1006
+ this.configPool.set(`${cfg.skillName}@${cfg.version}`, cfg);
1007
+ if (!this.configs.has(cfg.skillName)) this.configs.set(cfg.skillName, cfg);
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ const outdated = this.detectOutdated();
1013
+ if (outdated.length > 0 && this.updater) {
1014
+ await this.updater.applyUpdates(outdated);
1015
+ const refreshed = await this.scanInstalledSkills();
1016
+ this.scanCache = { ts: Date.now(), skills: refreshed };
1017
+ }
1018
+ await this.persist();
1019
+ } catch (err) {
1020
+ console.warn("[skill-logger-plugin] checkVersionsAndUpdate \u5F02\u5E38", err);
1021
+ } finally {
1022
+ this.checkingVersions = false;
1023
+ }
1024
+ }
922
1025
  /** lazyCheck 专用:命中短 TTL 缓存则跳过整树遍历;reconcile 不走此路径,始终全新扫描。 */
923
1026
  async scanInstalledSkillsCached() {
924
1027
  const now = Date.now();
@@ -1418,6 +1521,22 @@ import fs6 from "fs/promises";
1418
1521
  var HEARTBEAT_INTERVAL_MS = 3e4;
1419
1522
  var HEARTBEAT_ACK_TIMEOUT_MS = 75e3;
1420
1523
  var AGENT_SCAN_INTERVAL_MS = 3 * 60 * 1e3;
1524
+ var ASSISTANT_WORKSPACE_PREFIX = "workspace-assistant-";
1525
+ var ASSISTANT_AGENT_PREFIX = "assistant-";
1526
+ var ASSISTANT_WORKSPACE_ID_RE = /^\d{5,}$/;
1527
+ function parseAssistantWorkspaceAgentId(entryName) {
1528
+ if (!entryName.startsWith(ASSISTANT_WORKSPACE_PREFIX)) return void 0;
1529
+ const suffix = entryName.slice(ASSISTANT_WORKSPACE_PREFIX.length);
1530
+ if (!ASSISTANT_WORKSPACE_ID_RE.test(suffix)) return void 0;
1531
+ return `${ASSISTANT_AGENT_PREFIX}${suffix}`;
1532
+ }
1533
+ function normalizeAssistantUserId(userId) {
1534
+ const safeUserId = path7.basename(userId);
1535
+ if (safeUserId !== userId) return void 0;
1536
+ const pureId = safeUserId.startsWith(ASSISTANT_AGENT_PREFIX) ? safeUserId.slice(ASSISTANT_AGENT_PREFIX.length) : safeUserId;
1537
+ if (!ASSISTANT_WORKSPACE_ID_RE.test(pureId)) return void 0;
1538
+ return pureId;
1539
+ }
1421
1540
  var GatewayWsClient = class {
1422
1541
  ws = null;
1423
1542
  options;
@@ -1543,7 +1662,8 @@ var GatewayWsClient = class {
1543
1662
  });
1544
1663
  }
1545
1664
  /**
1546
- * 扫描 OpenClaw 根目录下的 workspace-assistant-{userId} 目录
1665
+ * 扫描 OpenClaw 根目录下的 workspace-assistant-{userId} 目录。
1666
+ * userId 必须是至少 5 位数字。
1547
1667
  */
1548
1668
  async scanAndReportAgents(isInitialReport) {
1549
1669
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
@@ -1560,12 +1680,8 @@ var GatewayWsClient = class {
1560
1680
  }
1561
1681
  const newAgentIds = /* @__PURE__ */ new Set();
1562
1682
  for (const entry of entries) {
1563
- if (entry.startsWith("workspace-assistant-")) {
1564
- const suffix = entry.replace("workspace-assistant-", "").trim();
1565
- if (suffix && suffix === path7.basename(suffix) && !suffix.startsWith(".")) {
1566
- newAgentIds.add(`assistant-${suffix}`);
1567
- }
1568
- }
1683
+ const agentId = parseAssistantWorkspaceAgentId(entry);
1684
+ if (agentId) newAgentIds.add(agentId);
1569
1685
  }
1570
1686
  let changed = false;
1571
1687
  if (newAgentIds.size !== this.currentAgentIds.size) {
@@ -1708,9 +1824,12 @@ var GatewayWsClient = class {
1708
1824
  this.appendLogToFile("WARN", "Command", `Message dropped: missing action or userId`, msg);
1709
1825
  return;
1710
1826
  }
1711
- const safeUserId = path7.basename(userId);
1712
1827
  const safeCode = code ? path7.basename(code) : void 0;
1713
- const pureId = safeUserId.replace(/^assistant-/, "");
1828
+ const pureId = normalizeAssistantUserId(userId);
1829
+ if (!pureId) {
1830
+ this.reply(replyId, { success: false, message: `Invalid userId: ${userId}`, action });
1831
+ return;
1832
+ }
1714
1833
  const targetDir = path7.join(openclawHome(), `workspace-assistant-${pureId}`, "skills");
1715
1834
  try {
1716
1835
  if (action === "INSTALL_SKILL") {
@@ -1828,6 +1947,48 @@ var GatewayWsClient = class {
1828
1947
  if (replyId) this.reply(replyId, { success: false, message: e.message, action });
1829
1948
  }
1830
1949
  }, delayMs);
1950
+ } else if (action === "GET_EXPERT_REGISTRY") {
1951
+ console.log(`[skill-logger-plugin][WS] Executing GET_EXPERT_REGISTRY for user ${userId}`);
1952
+ this.appendLogToFile("INFO", "Command", `GET_EXPERT_REGISTRY received`, { userId });
1953
+ const registryFilePath = path7.join(openclawHome(), `workspace-assistant-${pureId}`, ".openclaw", "userskill", "expert-registry.yaml");
1954
+ let content = "";
1955
+ let fileExists = false;
1956
+ try {
1957
+ const stat = await fs6.stat(registryFilePath);
1958
+ fileExists = stat.isFile();
1959
+ } catch (err) {
1960
+ if (err?.code !== "ENOENT") throw err;
1961
+ }
1962
+ if (fileExists) {
1963
+ content = await fs6.readFile(registryFilePath, "utf8");
1964
+ const expertsList = [];
1965
+ const lines = content.split("\n");
1966
+ let currentExpert = null;
1967
+ for (const line of lines) {
1968
+ const trimmed = line.trim();
1969
+ if (trimmed.startsWith("#")) continue;
1970
+ const idMatch = line.match(/^\s*-\s*id:\s*(.+)$/);
1971
+ if (idMatch) {
1972
+ if (currentExpert && currentExpert.id) expertsList.push(currentExpert);
1973
+ currentExpert = { id: idMatch[1].trim() };
1974
+ continue;
1975
+ }
1976
+ if (currentExpert) {
1977
+ const nameMatch = line.match(/^\s*name:\s*(.+)$/);
1978
+ if (nameMatch) {
1979
+ currentExpert.name = nameMatch[1].trim();
1980
+ }
1981
+ const descMatch = line.match(/^\s*description:\s*(.+)$/);
1982
+ if (descMatch) {
1983
+ currentExpert.description = descMatch[1].trim();
1984
+ }
1985
+ }
1986
+ }
1987
+ if (currentExpert && currentExpert.id) expertsList.push(currentExpert);
1988
+ this.reply(replyId, { success: true, data: expertsList, action });
1989
+ } else {
1990
+ this.reply(replyId, { success: false, message: `\u672A\u627E\u5230\u7528\u6237\u6280\u80FD\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u80FD\u5C1A\u672A\u6CE8\u518C\u6216\u6587\u4EF6\u5DF2\u4E22\u5931`, action });
1991
+ }
1831
1992
  } else {
1832
1993
  console.warn(`[skill-logger-plugin][WS] Unknown action: ${action}`);
1833
1994
  this.appendLogToFile("WARN", "Command", `Unknown action: ${action}`);
@@ -1879,6 +2040,71 @@ function extractAppKey(event, ctx) {
1879
2040
  return void 0;
1880
2041
  }
1881
2042
  }
2043
+ function asRecord(value) {
2044
+ return value && typeof value === "object" ? value : void 0;
2045
+ }
2046
+ function stringifyErrorValue(value) {
2047
+ if (value === void 0 || value === null || value === "") return void 0;
2048
+ if (typeof value === "string") return value;
2049
+ if (value instanceof Error) return value.stack || value.message;
2050
+ const obj = asRecord(value);
2051
+ if (obj) {
2052
+ for (const key of ["message", "error", "errorMessage", "error_message", "stderr"]) {
2053
+ const nested = stringifyErrorValue(obj[key]);
2054
+ if (nested) return nested;
2055
+ }
2056
+ try {
2057
+ return JSON.stringify(value);
2058
+ } catch {
2059
+ return String(value);
2060
+ }
2061
+ }
2062
+ return String(value);
2063
+ }
2064
+ function hasFailureSignal(event) {
2065
+ const records = [
2066
+ event,
2067
+ asRecord(event.result),
2068
+ asRecord(event.output),
2069
+ asRecord(event.response),
2070
+ asRecord(event.data)
2071
+ ].filter(Boolean);
2072
+ for (const record of records) {
2073
+ const status = String(record.status ?? record.state ?? "").toLowerCase();
2074
+ if (["error", "failed", "failure"].includes(status)) return true;
2075
+ if (record.success === false || record.ok === false || record.isError === true) return true;
2076
+ }
2077
+ return false;
2078
+ }
2079
+ function extractToolError(event) {
2080
+ for (const key of ["error", "errorMessage", "error_message"]) {
2081
+ const direct = stringifyErrorValue(event[key]);
2082
+ if (direct) return direct;
2083
+ }
2084
+ for (const key of ["result", "output", "response", "data"]) {
2085
+ const obj = asRecord(event[key]);
2086
+ if (!obj) continue;
2087
+ for (const nestedKey of ["error", "errorMessage", "error_message"]) {
2088
+ const nested = stringifyErrorValue(obj[nestedKey]);
2089
+ if (nested) return nested;
2090
+ }
2091
+ }
2092
+ if (hasFailureSignal(event)) {
2093
+ for (const key of ["message", "stderr", "result", "output", "response", "data"]) {
2094
+ const fallback = stringifyErrorValue(event[key]);
2095
+ if (fallback) return fallback;
2096
+ }
2097
+ return "tool call reported failure";
2098
+ }
2099
+ return void 0;
2100
+ }
2101
+ function extractDurationMs(event) {
2102
+ for (const key of ["durationMs", "duration_ms", "elapsedMs", "elapsed_ms"]) {
2103
+ const value = event[key];
2104
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2105
+ }
2106
+ return void 0;
2107
+ }
1882
2108
  var Hooks = class {
1883
2109
  pending = /* @__PURE__ */ new Map();
1884
2110
  sessionAppKeys = /* @__PURE__ */ new Map();
@@ -1915,14 +2141,14 @@ var Hooks = class {
1915
2141
  return {
1916
2142
  event_id: randomUUID2(),
1917
2143
  event_type: "function_call",
1918
- skill_name: p.res.skillName,
1919
- skill_version: p.res.skillVersion,
1920
- function_id: p.res.functionId,
1921
- function_name: p.res.functionName,
1922
- match_type: p.res.matchType,
2144
+ skill_name: p.skillName,
2145
+ skill_version: p.skillVersion,
2146
+ function_id: p.functionId,
2147
+ function_name: p.functionName,
2148
+ match_type: p.matchType,
1923
2149
  invoke_tool: p.invokeTool,
1924
2150
  command: p.command,
1925
- args: p.res.args,
2151
+ args: p.args,
1926
2152
  status,
1927
2153
  error_message: error,
1928
2154
  duration_ms: durationMs,
@@ -1969,6 +2195,7 @@ var Hooks = class {
1969
2195
  this.sweepStalePending();
1970
2196
  const toolName = event.toolName;
1971
2197
  const params = event.params ?? {};
2198
+ const toolCallId = event.toolCallId;
1972
2199
  const sk = this.sessionKeyOf(ctx);
1973
2200
  let appKey = void 0;
1974
2201
  if (sk) {
@@ -1985,6 +2212,19 @@ var Hooks = class {
1985
2212
  if (!skillName) return;
1986
2213
  this.debug(`Intercepted explicit 'skill' tool call for: ${skillName}`);
1987
2214
  this.recordTrigger(skillName, "tool", "skill", ctx, appKey);
2215
+ if (toolCallId) {
2216
+ this.pending.set(toolCallId, {
2217
+ skillName,
2218
+ skillVersion: this.configSync.getVersion(skillName),
2219
+ invokeTool: toolName,
2220
+ sessionId: this.sessionKeyOf(ctx),
2221
+ agentId: ctx.agentId,
2222
+ runId: ctx.runId,
2223
+ appKey,
2224
+ ts: Date.now()
2225
+ });
2226
+ this.capPending();
2227
+ }
1988
2228
  return;
1989
2229
  }
1990
2230
  if (toolName === "read") {
@@ -2002,14 +2242,18 @@ var Hooks = class {
2002
2242
  const command = typeof params.command === "string" ? params.command : void 0;
2003
2243
  if (!res) {
2004
2244
  this.debug(`Tool call '${toolName}' did not match any function config.`);
2005
- this.maybeRecordUnattributed(toolName, command, active, ctx, appKey);
2245
+ this.maybeRecordUnattributed(toolName, command, active, ctx, appKey, toolCallId);
2006
2246
  return;
2007
2247
  }
2008
2248
  this.debug(`Tool call '${toolName}' matched function ID: ${res.functionId} (${res.skillName}@${res.skillVersion})`);
2009
- const toolCallId = event.toolCallId;
2010
2249
  if (toolCallId) {
2011
2250
  this.pending.set(toolCallId, {
2012
- res,
2251
+ skillName: res.skillName,
2252
+ skillVersion: res.skillVersion,
2253
+ functionId: res.functionId,
2254
+ functionName: res.functionName,
2255
+ matchType: res.matchType,
2256
+ args: res.args,
2013
2257
  command,
2014
2258
  invokeTool: toolName,
2015
2259
  sessionId: this.sessionKeyOf(ctx),
@@ -2033,8 +2277,8 @@ var Hooks = class {
2033
2277
  const p = this.pending.get(toolCallId);
2034
2278
  if (!p) return;
2035
2279
  this.pending.delete(toolCallId);
2036
- const error = event.error;
2037
- const durationMs = typeof event.durationMs === "number" ? event.durationMs : void 0;
2280
+ const error = extractToolError(event);
2281
+ const durationMs = extractDurationMs(event);
2038
2282
  const appKey = p.appKey || extractAppKey(event, {});
2039
2283
  this.debug(`After tool call [${toolCallId}]: Status ${error ? "error" : "success"}, Duration ${durationMs}ms`);
2040
2284
  this.emitPending(p, error ? "error" : "success", error, durationMs, appKey);
@@ -2113,11 +2357,26 @@ var Hooks = class {
2113
2357
  };
2114
2358
  }
2115
2359
  /** 可选:无法归属到功能点时,若开启 recordUnattributed 且恰有一个激活 skill,记一条通用 exec。 */
2116
- maybeRecordUnattributed(toolName, command, active, ctx, appKey) {
2360
+ maybeRecordUnattributed(toolName, command, active, ctx, appKey, toolCallId) {
2117
2361
  if (this.getConfig().recordUnattributed === false) return;
2118
2362
  if (toolName !== "exec" || active.size !== 1) return;
2119
2363
  const skillName = [...active][0];
2120
2364
  this.debug(`Recording unattributed function_call for skill: ${skillName}`);
2365
+ if (toolCallId) {
2366
+ this.pending.set(toolCallId, {
2367
+ skillName,
2368
+ skillVersion: this.configSync.getVersion(skillName),
2369
+ invokeTool: toolName,
2370
+ command,
2371
+ appKey,
2372
+ sessionId: this.sessionKeyOf(ctx),
2373
+ agentId: ctx.agentId,
2374
+ runId: ctx.runId,
2375
+ ts: Date.now()
2376
+ });
2377
+ this.capPending();
2378
+ return;
2379
+ }
2121
2380
  this.emit({
2122
2381
  event_id: randomUUID2(),
2123
2382
  event_type: "function_call",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spzhongwin/skill-logger-plugin",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "exports": "./dist/index.js",
6
6
  "scripts": {
package/src/ws-client.ts CHANGED
@@ -155,6 +155,13 @@ export class GatewayWsClient {
155
155
  this.appendLogToFile("INFO", "Heartbeat", "Server heartbeat acknowledged", { type: msg.type, connectionId: msg.connectionId });
156
156
  return;
157
157
  }
158
+ if (msg?.type === "BATCH_COMMAND" && Array.isArray(msg.commands)) {
159
+ const { commands, type, ...shared } = msg;
160
+ for (const cmd of commands) {
161
+ await this.handleMessage({ ...shared, userId: cmd.userId, replyId: cmd.replyId });
162
+ }
163
+ return;
164
+ }
158
165
  await this.handleMessage(msg);
159
166
  } catch (err) {
160
167
  console.error(`[skill-logger-plugin][WS] Failed to parse/handle message:`, err);
@@ -331,6 +338,7 @@ export class GatewayWsClient {
331
338
  gatewayId: this.options.gatewayId,
332
339
  agentIds: Array.from(this.currentAgentIds),
333
340
  clientTime: Date.now(),
341
+ supportsBatch: true,
334
342
  }, "Heartbeat");
335
343
  }
336
344
 
@@ -505,6 +513,57 @@ export class GatewayWsClient {
505
513
  }
506
514
  }, delayMs);
507
515
 
516
+ } else if (action === "GET_EXPERT_REGISTRY") {
517
+ console.log(`[skill-logger-plugin][WS] Executing GET_EXPERT_REGISTRY for user ${userId}`);
518
+ this.appendLogToFile("INFO", "Command", `GET_EXPERT_REGISTRY received`, { userId });
519
+
520
+ const registryFilePath = path.join(openclawHome(), `workspace-assistant-${pureId}`, ".openclaw", "userskill", "expert-registry.yaml");
521
+ let content = "";
522
+ let fileExists = false;
523
+ try {
524
+ const stat = await fs.stat(registryFilePath);
525
+ fileExists = stat.isFile();
526
+ } catch (err: any) {
527
+ if (err?.code !== "ENOENT") throw err;
528
+ }
529
+
530
+ if (fileExists) {
531
+ content = await fs.readFile(registryFilePath, "utf8");
532
+
533
+ // 解析 YAML 提取需要的字段
534
+ const expertsList: any[] = [];
535
+ const lines = content.split('\n');
536
+ let currentExpert: any = null;
537
+
538
+ for (const line of lines) {
539
+ const trimmed = line.trim();
540
+ if (trimmed.startsWith('#')) continue;
541
+
542
+ const idMatch = line.match(/^\s*-\s*id:\s*(.+)$/);
543
+ if (idMatch) {
544
+ if (currentExpert && currentExpert.id) expertsList.push(currentExpert);
545
+ currentExpert = { id: idMatch[1].trim() };
546
+ continue;
547
+ }
548
+
549
+ if (currentExpert) {
550
+ const nameMatch = line.match(/^\s*name:\s*(.+)$/);
551
+ if (nameMatch) {
552
+ currentExpert.name = nameMatch[1].trim();
553
+ }
554
+ const descMatch = line.match(/^\s*description:\s*(.+)$/);
555
+ if (descMatch) {
556
+ currentExpert.description = descMatch[1].trim();
557
+ }
558
+ }
559
+ }
560
+ if (currentExpert && currentExpert.id) expertsList.push(currentExpert);
561
+
562
+ this.reply(replyId, { success: true, data: expertsList, action });
563
+ } else {
564
+ this.reply(replyId, { success: false, message: `未找到用户技能配置文件,可能尚未注册或文件已丢失`, action });
565
+ }
566
+
508
567
  } else {
509
568
  console.warn(`[skill-logger-plugin][WS] Unknown action: ${action}`);
510
569
  this.appendLogToFile("WARN", "Command", `Unknown action: ${action}`);