@js-eyes/protocol 2.7.0 → 2.8.2

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/index.js CHANGED
@@ -24,13 +24,12 @@ const FORWARDABLE_ACTIONS = [
24
24
  ];
25
25
 
26
26
  const SENSITIVE_TOOL_NAMES = Object.freeze([
27
- 'js_eyes_execute_script',
28
- 'js_eyes_get_cookies',
29
- 'js_eyes_get_cookies_by_domain',
30
- 'js_eyes_inject_css',
31
- 'js_eyes_upload_file',
32
- 'js_eyes_upload_file_to_tab',
33
- 'js_eyes_install_skill',
27
+ 'browser/execute-script',
28
+ 'browser/get-cookies',
29
+ 'browser/get-cookies-by-domain',
30
+ 'browser/inject-css',
31
+ 'browser/upload-file',
32
+ 'skills/plan-install',
34
33
  ]);
35
34
 
36
35
  const LOOPBACK_HOSTS = Object.freeze(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1', '0:0:0:0:0:0:0:1']);
@@ -79,13 +78,12 @@ const DEFAULT_SECURITY_CONFIG = Object.freeze({
79
78
  taint: { ...DEFAULT_TAINT_CONFIG },
80
79
  profile: { ...DEFAULT_PROFILE_CONFIG },
81
80
  toolPolicies: {
82
- js_eyes_execute_script: 'confirm',
83
- js_eyes_get_cookies: 'confirm',
84
- js_eyes_get_cookies_by_domain: 'confirm',
85
- js_eyes_inject_css: 'confirm',
86
- js_eyes_upload_file: 'confirm',
87
- js_eyes_upload_file_to_tab: 'confirm',
88
- js_eyes_install_skill: 'confirm',
81
+ 'browser/execute-script': 'confirm',
82
+ 'browser/get-cookies': 'confirm',
83
+ 'browser/get-cookies-by-domain': 'confirm',
84
+ 'browser/inject-css': 'confirm',
85
+ 'browser/upload-file': 'confirm',
86
+ 'skills/plan-install': 'confirm',
89
87
  },
90
88
  sensitiveCookieDomains: [
91
89
  'bank',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@js-eyes/protocol",
3
- "version": "2.7.0",
3
+ "version": "2.8.2",
4
4
  "description": "Shared protocol constants for JS Eyes runtime packages",
5
5
  "main": "index.js",
6
6
  "files": [
package/skill-registry.js CHANGED
@@ -3,7 +3,12 @@
3
3
  /**
4
4
  * SkillRegistry — js-eyes 技能运行时注册表
5
5
  *
6
- * 通过「工具级 dispatcher 间接层」实现零重启热加载:
6
+ * 通过两种绑定模式实现零重启热加载:
7
+ * - routerMode=true(OpenClaw 单工具模式):只维护 actionBindings,由 `js-eyes`
8
+ * 总线工具按 `skill/<skillId>/<action>` 委派执行,不向宿主注册子技能工具;
9
+ * - routerMode=false(兼容/测试模式):保留工具级 dispatcher 间接层。
10
+ *
11
+ * 兼容 dispatcher 模式:
7
12
  * - 插件启动时 api.registerTool(name, dispatcher) 仅对每个 tool name 注册一次稳定闭包;
8
13
  * - 每个 dispatcher 在调用时从 toolBindings.get(name) 查当前实现并委派;
9
14
  * - 热加载只改 toolBindings 映射,不触碰宿主注册表。
@@ -15,6 +20,8 @@
15
20
  * 让 OpenClaw / LLM 看到正确的入参约束(required / properties 等)。热加载时若 contract
16
21
  * 改了 schema,会按**引用** mutate 已注册的 dispatcher 对象;若某些 OpenClaw 宿主对 tool
17
22
  * 对象做了深拷贝/快照,则 mutate 不会生效,但首次注册的 schema 仍然是正确的。
23
+ * routerMode 下 OpenClaw 只看到 `js-eyes` 的总线 schema,子技能 schema 作为内部 definition
24
+ * 保留给后续 introspection / 文档输出使用。
18
25
  */
19
26
 
20
27
  const fs = require('fs');
@@ -30,6 +37,7 @@ function isSkillEnabled(...args) { return skillsApi.isSkillEnabled(...args); }
30
37
  function loadSkillContract(...args) { return skillsApi.loadSkillContract(...args); }
31
38
  function readSkillIntegrity(...args) { return skillsApi.readSkillIntegrity(...args); }
32
39
  function resolveSkillSources(...args) { return skillsApi.resolveSkillSources(...args); }
40
+ function skillToolActionName(...args) { return skillsApi.skillToolActionName(...args); }
33
41
  function verifySkillIntegrity(...args) { return skillsApi.verifySkillIntegrity(...args); }
34
42
 
35
43
  const { verifyExtraDir: verifyExtraSkillDir } = require('./extra-integrity');
@@ -129,6 +137,7 @@ function createSkillRegistry(options = {}) {
129
137
  pluginConfig = {},
130
138
  wrapSensitiveTool = null,
131
139
  builtinToolNames = [],
140
+ routerMode = false,
132
141
  // When true (default when a watcher is active), we set suppressNextReload
133
142
  // after our own setConfigValue writes so the chokidar echo doesn't cause a
134
143
  // duplicate reload. Leave this false in test harnesses that drive reload()
@@ -136,11 +145,11 @@ function createSkillRegistry(options = {}) {
136
145
  suppressSelfWrites = true,
137
146
  } = options;
138
147
 
139
- if (!api || typeof api.registerTool !== 'function') {
148
+ if (!routerMode && (!api || typeof api.registerTool !== 'function')) {
140
149
  throw new Error('createSkillRegistry: api.registerTool is required');
141
150
  }
142
151
 
143
- const logger = makeLogger(options.logger || api.logger);
152
+ const logger = makeLogger(options.logger || (api && api.logger));
144
153
  const configLoader = typeof options.configLoader === 'function'
145
154
  ? options.configLoader
146
155
  : () => ({});
@@ -159,6 +168,8 @@ function createSkillRegistry(options = {}) {
159
168
  const skills = new Map();
160
169
  // toolName -> { skillId, definition, optional }
161
170
  const toolBindings = new Map();
171
+ // action -> { skillId, toolName, definition, optional }
172
+ const actionBindings = new Map();
162
173
  // toolName -> dispatcher object (registered once per name; kept by reference so we can
163
174
  // mutate `description`/`parameters`/`label` on hot-reload to keep OpenClaw-visible schema in sync)
164
175
  const dispatchers = new Map();
@@ -266,6 +277,11 @@ function createSkillRegistry(options = {}) {
266
277
  removed.push(name);
267
278
  }
268
279
  }
280
+ for (const [action, binding] of actionBindings) {
281
+ if (binding.skillId === skillId) {
282
+ actionBindings.delete(action);
283
+ }
284
+ }
269
285
  return removed;
270
286
  }
271
287
 
@@ -347,6 +363,7 @@ function createSkillRegistry(options = {}) {
347
363
  contract,
348
364
  adapter,
349
365
  toolNames: toolDefs.map((t) => t.toolName),
366
+ actionNames: toolDefs.map((t) => skillToolActionName(skill.id, t.toolName)),
350
367
  // runtime.dispose is used by hot-unload to drain WS, clear intervals, etc.
351
368
  dispose: async () => {
352
369
  const runtime = adapter && adapter.runtime;
@@ -365,17 +382,35 @@ function createSkillRegistry(options = {}) {
365
382
 
366
383
  function applyBindings(state) {
367
384
  const failedDispatchers = [];
385
+ const localActions = new Set();
368
386
  for (const entry of state.toolDefs) {
369
- const dispatched = ensureDispatcher(entry.toolName, entry.optional, entry.definition);
370
- if (!dispatched.ok) {
387
+ if (!routerMode) {
388
+ const dispatched = ensureDispatcher(entry.toolName, entry.optional, entry.definition);
389
+ if (!dispatched.ok) {
390
+ failedDispatchers.push(entry.toolName);
391
+ continue;
392
+ }
393
+ }
394
+ const actionName = skillToolActionName(state.id, entry.toolName);
395
+ if (localActions.has(actionName)) {
396
+ logger.warn(
397
+ `[js-eyes] Skill "${state.id}" skipped tool "${entry.toolName}" because action "${actionName}" duplicates another tool after slug normalization`,
398
+ );
371
399
  failedDispatchers.push(entry.toolName);
372
400
  continue;
373
401
  }
402
+ localActions.add(actionName);
374
403
  toolBindings.set(entry.toolName, {
375
404
  skillId: state.id,
376
405
  definition: entry.definition,
377
406
  optional: entry.optional,
378
407
  });
408
+ actionBindings.set(actionName, {
409
+ skillId: state.id,
410
+ toolName: entry.toolName,
411
+ definition: entry.definition,
412
+ optional: entry.optional,
413
+ });
379
414
  }
380
415
  return { failedDispatchers };
381
416
  }
@@ -663,12 +698,27 @@ function createSkillRegistry(options = {}) {
663
698
  sourcePath: s.sourcePath,
664
699
  skillDir: s.skillDir,
665
700
  tools: s.toolNames.slice(),
701
+ actions: s.actionNames.slice(),
666
702
  })),
667
703
  toolBindings: Array.from(toolBindings.keys()),
704
+ actionBindings: Array.from(actionBindings.keys()),
668
705
  dispatchersRegistered: Array.from(dispatchers.keys()),
669
706
  };
670
707
  }
671
708
 
709
+ async function executeAction(action, toolCallId, params) {
710
+ const binding = actionBindings.get(action);
711
+ if (!binding) {
712
+ return { content: [{ type: 'text', text: DEFAULT_UNAVAILABLE_MESSAGE(action) }] };
713
+ }
714
+ return binding.definition.execute(toolCallId, params);
715
+ }
716
+
717
+ function getActionDefinition(action) {
718
+ const binding = actionBindings.get(action);
719
+ return binding ? binding.definition : null;
720
+ }
721
+
672
722
  return {
673
723
  init,
674
724
  reload,
@@ -676,9 +726,12 @@ function createSkillRegistry(options = {}) {
676
726
  disposeAll,
677
727
  snapshot,
678
728
  getState,
729
+ executeAction,
730
+ getActionDefinition,
679
731
  // Testing / plugin integration helpers
680
732
  _internals: {
681
733
  toolBindings,
734
+ actionBindings,
682
735
  skills,
683
736
  dispatchers,
684
737
  setSuppressNextReload(v) { suppressNextReload = Boolean(v); },
package/skills.js CHANGED
@@ -13,6 +13,20 @@ const SKILL_CONTRACT_FILE = 'skill.contract.js';
13
13
  const INTEGRITY_FILE = '.integrity.json';
14
14
  const INSTALL_MANIFEST_FILE = 'skills-install.json';
15
15
 
16
+ function toolNameToActionSegment(name) {
17
+ return String(name || '')
18
+ .trim()
19
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
20
+ .replace(/[^a-zA-Z0-9]+/g, '-')
21
+ .replace(/^-+|-+$/g, '')
22
+ .toLowerCase();
23
+ }
24
+
25
+ function skillToolActionName(skillId, toolName) {
26
+ const action = toolNameToActionSegment(toolName);
27
+ return `skill/${skillId}/${action || 'run'}`;
28
+ }
29
+
16
30
  function loadSkillContract(skillDir) {
17
31
  const contractPath = path.resolve(skillDir, SKILL_CONTRACT_FILE);
18
32
  if (!fs.existsSync(contractPath)) return null;
@@ -142,8 +156,10 @@ function normalizeSkillMetadata(skillDir) {
142
156
  ? cli.commands.map((command) => command.name)
143
157
  : [];
144
158
 
159
+ const id = contract?.id || pkg.name || path.basename(skillDir);
160
+
145
161
  return {
146
- id: contract?.id || pkg.name || path.basename(skillDir),
162
+ id,
147
163
  name: contract?.name || pkg.name || path.basename(skillDir),
148
164
  version: contract?.version || pkg.version || '1.0.0',
149
165
  description: contract?.description || pkg.description || '',
@@ -151,6 +167,7 @@ function normalizeSkillMetadata(skillDir) {
151
167
  cliEntry: cli.entry ? path.resolve(skillDir, cli.entry) : path.join(skillDir, 'index.js'),
152
168
  commands,
153
169
  tools,
170
+ actions: tools.map((tool) => skillToolActionName(id, tool)),
154
171
  runtime: contract?.runtime || {},
155
172
  contract,
156
173
  };
@@ -728,6 +745,8 @@ Object.assign(module.exports, {
728
745
  resolveSkillsDir,
729
746
  resolveOpenClawPluginEntry,
730
747
  runSkillCli,
748
+ skillToolActionName,
749
+ toolNameToActionSegment,
731
750
  verifySkillIntegrity,
732
751
  writeIntegrityManifest,
733
752
  });