@aiyiran/myclaw 1.0.138 → 1.0.140
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/gateway.js +170 -0
- package/index.js +32 -29
- package/inject-minimax.js +1 -1
- package/inject-search.js +1 -1
- package/package.json +1 -1
- package/patch-manifest.json +28 -12
- package/publish.sh +1 -3
- package/skills/tavily-search/.clawhub/origin.json +7 -0
- package/skills/tavily-search/SKILL.md +92 -0
- package/skills/tavily-search/_meta.json +6 -0
- package/skills/tavily-search/scripts/search.mjs +150 -0
- package/build-manifest.js +0 -128
package/gateway.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* gateway.js
|
|
5
|
+
*
|
|
6
|
+
* Gateway 生命周期管理(跨平台: Mac/Linux/Windows/容器)
|
|
7
|
+
*
|
|
8
|
+
* 提供:
|
|
9
|
+
* stop() — 先礼后兵,确保彻底关闭
|
|
10
|
+
* start() — 后台启动 + 健康检查
|
|
11
|
+
* restart() — stop + start
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const PORT = 18789;
|
|
17
|
+
const isWin = process.platform === 'win32';
|
|
18
|
+
|
|
19
|
+
// ANSI 颜色
|
|
20
|
+
const colors = {
|
|
21
|
+
green: '\x1b[32m',
|
|
22
|
+
yellow: '\x1b[33m',
|
|
23
|
+
red: '\x1b[31m',
|
|
24
|
+
nc: '\x1b[0m',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 彻底停止 Gateway(先礼后兵)
|
|
29
|
+
*/
|
|
30
|
+
function stop() {
|
|
31
|
+
// Step 1: 优雅关闭
|
|
32
|
+
console.log('[停止] Step 1 — 优雅关闭...');
|
|
33
|
+
try {
|
|
34
|
+
execSync('openclaw gateway stop', { stdio: 'pipe', timeout: 10000 });
|
|
35
|
+
console.log('[停止] ' + colors.green + 'Gateway 已发送停止信号' + colors.nc);
|
|
36
|
+
} catch {
|
|
37
|
+
console.log('[停止] Gateway 未在运行或已停止');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 等 2 秒让优雅关闭执行
|
|
41
|
+
execSync(isWin ? 'timeout /t 2 /nobreak >nul' : 'sleep 2', { stdio: 'ignore' });
|
|
42
|
+
|
|
43
|
+
// Step 2: 检查残留进程并强杀
|
|
44
|
+
console.log('[停止] Step 2 — 清理残留进程...');
|
|
45
|
+
|
|
46
|
+
// 2a. 杀端口占用者
|
|
47
|
+
try {
|
|
48
|
+
if (isWin) {
|
|
49
|
+
const netstat = execSync('netstat -ano | findstr :' + PORT + ' | findstr LISTENING', {
|
|
50
|
+
encoding: 'utf8', stdio: 'pipe', timeout: 5000
|
|
51
|
+
}).trim();
|
|
52
|
+
const pids = [...new Set(netstat.split('\n').map(l => l.trim().split(/\s+/).pop()).filter(p => p && p !== '0'))];
|
|
53
|
+
for (const pid of pids) {
|
|
54
|
+
try {
|
|
55
|
+
execSync('taskkill /F /PID ' + pid, { stdio: 'pipe', timeout: 5000 });
|
|
56
|
+
console.log('[停止] 🔪 强杀端口占用进程 PID=' + pid);
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const pids = execSync('lsof -i :' + PORT + ' -t 2>/dev/null', {
|
|
61
|
+
encoding: 'utf8', stdio: 'pipe', timeout: 5000
|
|
62
|
+
}).trim();
|
|
63
|
+
if (pids) {
|
|
64
|
+
for (const pid of pids.split('\n').filter(Boolean)) {
|
|
65
|
+
try {
|
|
66
|
+
execSync('kill -9 ' + pid, { stdio: 'pipe', timeout: 3000 });
|
|
67
|
+
console.log('[停止] 🔪 强杀端口占用进程 PID=' + pid);
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// 没有端口占用,正常
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2b. 杀所有 openclaw 相关进程(防止僵尸进程)
|
|
77
|
+
try {
|
|
78
|
+
if (!isWin) {
|
|
79
|
+
const myPid = process.pid;
|
|
80
|
+
const procs = execSync('pgrep -f "openclaw" 2>/dev/null || true', {
|
|
81
|
+
encoding: 'utf8', stdio: 'pipe', timeout: 5000
|
|
82
|
+
}).trim();
|
|
83
|
+
if (procs) {
|
|
84
|
+
for (const pid of procs.split('\n').filter(Boolean)) {
|
|
85
|
+
if (pid === String(myPid)) continue;
|
|
86
|
+
try {
|
|
87
|
+
execSync('kill -9 ' + pid, { stdio: 'pipe', timeout: 3000 });
|
|
88
|
+
console.log('[停止] 🔪 清理残留 openclaw 进程 PID=' + pid);
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
|
|
95
|
+
console.log('[停止] ' + colors.green + '清理完成' + colors.nc);
|
|
96
|
+
|
|
97
|
+
// Step 3: 轮询等待端口释放
|
|
98
|
+
const MAX_WAIT = 10;
|
|
99
|
+
let portFree = false;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < MAX_WAIT; i++) {
|
|
102
|
+
try {
|
|
103
|
+
if (isWin) {
|
|
104
|
+
execSync('netstat -ano | findstr :' + PORT + ' | findstr LISTENING', {
|
|
105
|
+
stdio: 'pipe', timeout: 2000
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
execSync('lsof -i :' + PORT + ' -t 2>/dev/null', { stdio: 'pipe', timeout: 2000 });
|
|
109
|
+
}
|
|
110
|
+
process.stdout.write('[等待] 端口 ' + PORT + ' 释放中... (' + (i + 1) + '/' + MAX_WAIT + 's)\r');
|
|
111
|
+
execSync(isWin ? 'timeout /t 1 /nobreak >nul' : 'sleep 1', { stdio: 'ignore' });
|
|
112
|
+
} catch {
|
|
113
|
+
portFree = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (portFree) {
|
|
119
|
+
console.log('[等待] ' + colors.green + '端口已释放' + colors.nc + ' ');
|
|
120
|
+
} else {
|
|
121
|
+
console.log('[等待] ' + colors.yellow + '端口释放超时' + colors.nc);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return portFree;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 启动 Gateway(后台 + 健康检查)
|
|
129
|
+
*/
|
|
130
|
+
function start() {
|
|
131
|
+
console.log('[启动] 正在启动 Gateway...');
|
|
132
|
+
|
|
133
|
+
if (isWin) {
|
|
134
|
+
execSync('start /B openclaw gateway > %TEMP%\\openclaw-gateway.log 2>&1', {
|
|
135
|
+
stdio: 'ignore', shell: true,
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
execSync('nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &', {
|
|
139
|
+
stdio: 'ignore', shell: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 等待启动完成
|
|
144
|
+
execSync(isWin ? 'timeout /t 3 /nobreak >nul' : 'sleep 3', { stdio: 'ignore' });
|
|
145
|
+
|
|
146
|
+
// 健康检查
|
|
147
|
+
try {
|
|
148
|
+
execSync('openclaw health', { stdio: 'pipe', timeout: 5000 });
|
|
149
|
+
console.log('[启动] ' + colors.green + 'Gateway 启动成功!' + colors.nc);
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log('控制台: ' + colors.yellow + 'http://127.0.0.1:' + PORT + colors.nc);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
console.log('[启动] ' + colors.yellow + 'Gateway 正在启动中...' + colors.nc);
|
|
155
|
+
const logPath = isWin ? '%TEMP%\\openclaw-gateway.log' : '/tmp/openclaw-gateway.log';
|
|
156
|
+
console.log('日志: ' + colors.yellow + 'tail -f ' + logPath + colors.nc);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 重启 Gateway(stop + start)
|
|
163
|
+
*/
|
|
164
|
+
function restart() {
|
|
165
|
+
stop();
|
|
166
|
+
console.log('');
|
|
167
|
+
return start();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { stop, start, restart, PORT };
|
package/index.js
CHANGED
|
@@ -720,6 +720,36 @@ function runPatch() {
|
|
|
720
720
|
console.log('[myclaw-config] ⚠️ 配置补丁失败: ' + err.message);
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
+
// 5. 自定义注入脚本执行引擎 (Manifest `run` 数组)
|
|
724
|
+
// 此处读取 manifest.json 中的 "run" 数组,并按顺序 require 执行对应的模块。
|
|
725
|
+
// 通过修改 manifest 即可控制额外脚本(如 inject-image, inject-search)是否跟随 patch 自动执行,
|
|
726
|
+
// 从而避免硬编码具体要注入哪些模块。
|
|
727
|
+
try {
|
|
728
|
+
const { loadManifest } = require('./patch-agent');
|
|
729
|
+
const m = loadManifest();
|
|
730
|
+
if (m && Array.isArray(m.run) && m.run.length > 0) {
|
|
731
|
+
console.log('');
|
|
732
|
+
for (const entry of m.run) {
|
|
733
|
+
const strategy = entry.strategy || 'auto';
|
|
734
|
+
const desc = entry.description || entry.module;
|
|
735
|
+
if (strategy === 'off') {
|
|
736
|
+
console.log('[myclaw-run] ⏭️ 跳过: ' + desc + ' [off]');
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
console.log('[myclaw-run] ▶️ 执行: ' + desc);
|
|
741
|
+
const mod = require(entry.module);
|
|
742
|
+
mod.run([]);
|
|
743
|
+
console.log('[myclaw-run] ✅ 完成: ' + desc);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
console.log('[myclaw-run] ⚠️ 失败: ' + desc + ' — ' + err.message);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
} catch (err) {
|
|
750
|
+
// manifest 加载失败,跳过
|
|
751
|
+
}
|
|
752
|
+
|
|
723
753
|
if (uiResult.success || skillResult.success || (agentResult && agentResult.success)) {
|
|
724
754
|
console.log('');
|
|
725
755
|
console.log(bar);
|
|
@@ -746,6 +776,7 @@ function runUnpatch() {
|
|
|
746
776
|
}
|
|
747
777
|
|
|
748
778
|
function runRestart() {
|
|
779
|
+
const gateway = require('./gateway');
|
|
749
780
|
const bar = '----------------------------------------';
|
|
750
781
|
|
|
751
782
|
console.log('');
|
|
@@ -754,35 +785,7 @@ function runRestart() {
|
|
|
754
785
|
console.log('');
|
|
755
786
|
|
|
756
787
|
try {
|
|
757
|
-
|
|
758
|
-
try {
|
|
759
|
-
execSync('openclaw gateway stop', { stdio: 'pipe', timeout: 10000 });
|
|
760
|
-
console.log('[停止] ' + colors.green + 'Gateway 已停止' + colors.nc);
|
|
761
|
-
} catch {
|
|
762
|
-
console.log('[停止] Gateway 未在运行或已停止');
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
console.log('');
|
|
766
|
-
console.log('[启动] 正在启动 Gateway...');
|
|
767
|
-
// 使用 nohup + 后台启动,避免阻塞当前进程
|
|
768
|
-
execSync('nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &', {
|
|
769
|
-
stdio: 'ignore',
|
|
770
|
-
shell: true,
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
// 等待 2 秒让 Gateway 启动
|
|
774
|
-
execSync('sleep 2', { stdio: 'ignore' });
|
|
775
|
-
|
|
776
|
-
// 检查是否启动成功
|
|
777
|
-
try {
|
|
778
|
-
execSync('openclaw health', { stdio: 'pipe', timeout: 5000 });
|
|
779
|
-
console.log('[启动] ' + colors.green + 'Gateway 启动成功!' + colors.nc);
|
|
780
|
-
console.log('');
|
|
781
|
-
console.log('控制台: ' + colors.yellow + 'http://127.0.0.1:18789' + colors.nc);
|
|
782
|
-
} catch {
|
|
783
|
-
console.log('[启动] ' + colors.yellow + 'Gateway 正在启动中...' + colors.nc);
|
|
784
|
-
console.log('日志: ' + colors.yellow + 'tail -f /tmp/openclaw-gateway.log' + colors.nc);
|
|
785
|
-
}
|
|
788
|
+
gateway.restart();
|
|
786
789
|
} catch (err) {
|
|
787
790
|
console.error('[' + colors.red + '错误' + colors.nc + '] 重启失败: ' + err.message);
|
|
788
791
|
}
|
package/inject-minimax.js
CHANGED
package/inject-search.js
CHANGED
|
@@ -28,7 +28,7 @@ function run(cliArgs) {
|
|
|
28
28
|
if (!config.plugins) config.plugins = {};
|
|
29
29
|
if (!config.plugins.entries) config.plugins.entries = {};
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// 备注:不支持逗号分隔的多 key 轮换
|
|
32
32
|
config.plugins.entries.tavily = {
|
|
33
33
|
enabled: true,
|
|
34
34
|
config: {
|
package/package.json
CHANGED
package/patch-manifest.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_doc": "MyClaw 注入清单 (
|
|
3
|
-
"
|
|
2
|
+
"_doc": "MyClaw 注入清单 (手动维护)。strategy: auto | on | off | delete",
|
|
3
|
+
"_doc_order": "执行顺序: 1.UI注入(patch.js) → 2.skills → 3.agents → 4.config → 5.run",
|
|
4
|
+
"_doc_skills": "Step 2: 将 myclaw/skills/ 下的技能包复制到 ~/.openclaw/workspace/skills/",
|
|
5
|
+
"skills": [
|
|
6
|
+
{
|
|
7
|
+
"name": "minimax-inject",
|
|
8
|
+
"strategy": "off",
|
|
9
|
+
"description": "minimax-inject (agent 可调用的注入技能)"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "tavily-search",
|
|
13
|
+
"strategy": "off",
|
|
14
|
+
"description": "tavily-search (Tavily 搜索技能包)"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"_doc_agents": "Step 3: 将 agent-list/ 下的智能体分发到 ~/.openclaw/ 并注册到 openclaw.json",
|
|
4
18
|
"agents": [
|
|
5
19
|
{
|
|
6
20
|
"id": "danci",
|
|
@@ -15,13 +29,7 @@
|
|
|
15
29
|
"description": "kakaxi"
|
|
16
30
|
}
|
|
17
31
|
],
|
|
18
|
-
"
|
|
19
|
-
{
|
|
20
|
-
"name": "minimax-inject",
|
|
21
|
-
"strategy": "off",
|
|
22
|
-
"description": "minimax-inject"
|
|
23
|
-
}
|
|
24
|
-
],
|
|
32
|
+
"_doc_config": "Step 4: 使用 dot-notation 补丁修改 openclaw.json 的系统配置,深度合并",
|
|
25
33
|
"config": {
|
|
26
34
|
"strategy": "on",
|
|
27
35
|
"patches": {
|
|
@@ -30,8 +38,16 @@
|
|
|
30
38
|
"tools.exec.ask": "off",
|
|
31
39
|
"tools.exec.security": "full",
|
|
32
40
|
"plugins.entries.tavily.enabled": true,
|
|
33
|
-
"plugins.entries.tavily.config.webSearch.apiKey": "tvly-dev-
|
|
41
|
+
"plugins.entries.tavily.config.webSearch.apiKey": "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",
|
|
34
42
|
"update.checkOnStart": false
|
|
35
43
|
}
|
|
36
|
-
}
|
|
37
|
-
|
|
44
|
+
},
|
|
45
|
+
"_doc_run": "Step 5: 按顺序 require(module).run([]) 执行自定义脚本,不触发 Gateway 重启",
|
|
46
|
+
"run": [
|
|
47
|
+
{
|
|
48
|
+
"module": "./inject-image",
|
|
49
|
+
"strategy": "auto",
|
|
50
|
+
"description": "图片模型注入 (vveai)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
package/publish.sh
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tavily-search
|
|
3
|
+
description: Web search using Tavily's LLM-optimized API. Returns relevant results with content snippets, scores, and metadata.
|
|
4
|
+
homepage: https://tavily.com
|
|
5
|
+
metadata: {"openclaw":{"emoji":"🔍","requires":{"bins":["node"],"env":["TAVILY_API_KEY"]},"primaryEnv":"TAVILY_API_KEY"}}
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Tavily Search
|
|
9
|
+
|
|
10
|
+
Search the web and get relevant results optimized for LLM consumption.
|
|
11
|
+
|
|
12
|
+
## Authentication
|
|
13
|
+
|
|
14
|
+
Get your API key at https://tavily.com and add to your OpenClaw config:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"skills": {
|
|
19
|
+
"entries": {
|
|
20
|
+
"tavily-search": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"apiKey": "tvly-YOUR_API_KEY_HERE"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or set the environment variable:
|
|
30
|
+
```bash
|
|
31
|
+
export TAVILY_API_KEY="tvly-YOUR_API_KEY_HERE"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
### Using the Script
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node {baseDir}/scripts/search.mjs "query"
|
|
40
|
+
node {baseDir}/scripts/search.mjs "query" -n 10
|
|
41
|
+
node {baseDir}/scripts/search.mjs "query" --deep
|
|
42
|
+
node {baseDir}/scripts/search.mjs "query" --topic news
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Examples
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Basic search
|
|
49
|
+
node {baseDir}/scripts/search.mjs "python async patterns"
|
|
50
|
+
|
|
51
|
+
# With more results
|
|
52
|
+
node {baseDir}/scripts/search.mjs "React hooks tutorial" -n 10
|
|
53
|
+
|
|
54
|
+
# Advanced search
|
|
55
|
+
node {baseDir}/scripts/search.mjs "machine learning" --deep
|
|
56
|
+
|
|
57
|
+
# News search
|
|
58
|
+
node {baseDir}/scripts/search.mjs "AI news" --topic news
|
|
59
|
+
|
|
60
|
+
# Domain-filtered search
|
|
61
|
+
node {baseDir}/scripts/search.mjs "Python docs" --include-domains docs.python.org
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Options
|
|
65
|
+
|
|
66
|
+
| Option | Description | Default |
|
|
67
|
+
|--------|-------------|---------|
|
|
68
|
+
| `-n <count>` | Number of results (1-20) | 10 |
|
|
69
|
+
| `--depth <mode>` | Search depth: `ultra-fast`, `fast`, `basic`, `advanced` | `basic` |
|
|
70
|
+
| `--topic <topic>` | Topic: `general` or `news` | `general` |
|
|
71
|
+
| `--time-range <range>` | Time range: `day`, `week`, `month`, `year` | - |
|
|
72
|
+
| `--include-domains <domains>` | Comma-separated domains to include | - |
|
|
73
|
+
| `--exclude-domains <domains>` | Comma-separated domains to exclude | - |
|
|
74
|
+
| `--raw-content` | Include full page content | false |
|
|
75
|
+
| `--json` | Output raw JSON | false |
|
|
76
|
+
|
|
77
|
+
## Search Depth
|
|
78
|
+
|
|
79
|
+
| Depth | Latency | Relevance | Use Case |
|
|
80
|
+
|-------|---------|-----------|----------|
|
|
81
|
+
| `ultra-fast` | Lowest | Lower | Real-time chat, autocomplete |
|
|
82
|
+
| `fast` | Low | Good | Need chunks but latency matters |
|
|
83
|
+
| `basic` | Medium | High | General-purpose, balanced |
|
|
84
|
+
| `advanced` | Higher | Highest | Precision matters, research |
|
|
85
|
+
|
|
86
|
+
## Tips
|
|
87
|
+
|
|
88
|
+
- **Keep queries under 400 characters** - Think search query, not prompt
|
|
89
|
+
- **Break complex queries into sub-queries** - Better results than one massive query
|
|
90
|
+
- **Use `--include-domains`** to focus on trusted sources
|
|
91
|
+
- **Use `--time-range`** for recent information
|
|
92
|
+
- **Filter by `score`** (0-1) to get highest relevance results
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
function usage() {
|
|
4
|
+
console.error(`Usage: search.mjs "query" [options]
|
|
5
|
+
|
|
6
|
+
Options:
|
|
7
|
+
-n <count> Number of results (1-20, default: 10)
|
|
8
|
+
--depth <mode> Search depth: ultra-fast, fast, basic, advanced (default: basic)
|
|
9
|
+
--topic <topic> Topic: general or news (default: general)
|
|
10
|
+
--time-range <range> Time range: day, week, month, year
|
|
11
|
+
--include-domains <list> Comma-separated domains to include
|
|
12
|
+
--exclude-domains <list> Comma-separated domains to exclude
|
|
13
|
+
--raw-content Include full page content
|
|
14
|
+
--json Output raw JSON
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
search.mjs "python async patterns"
|
|
18
|
+
search.mjs "React hooks tutorial" -n 10
|
|
19
|
+
search.mjs "AI news" --topic news --time-range week
|
|
20
|
+
search.mjs "Python docs" --include-domains docs.python.org,realpython.com`);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const args = process.argv.slice(2);
|
|
25
|
+
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") usage();
|
|
26
|
+
|
|
27
|
+
const query = args[0];
|
|
28
|
+
let maxResults = 10;
|
|
29
|
+
let searchDepth = "basic";
|
|
30
|
+
let topic = "general";
|
|
31
|
+
let timeRange = null;
|
|
32
|
+
let includeDomains = [];
|
|
33
|
+
let excludeDomains = [];
|
|
34
|
+
let includeRawContent = false;
|
|
35
|
+
let outputJson = false;
|
|
36
|
+
|
|
37
|
+
for (let i = 1; i < args.length; i++) {
|
|
38
|
+
const a = args[i];
|
|
39
|
+
if (a === "-n") {
|
|
40
|
+
maxResults = Number.parseInt(args[i + 1] ?? "10", 10);
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (a === "--depth") {
|
|
45
|
+
searchDepth = args[i + 1] ?? "basic";
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (a === "--topic") {
|
|
50
|
+
topic = args[i + 1] ?? "general";
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (a === "--time-range") {
|
|
55
|
+
timeRange = args[i + 1];
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (a === "--include-domains") {
|
|
60
|
+
includeDomains = (args[i + 1] ?? "").split(",").map(d => d.trim()).filter(Boolean);
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (a === "--exclude-domains") {
|
|
65
|
+
excludeDomains = (args[i + 1] ?? "").split(",").map(d => d.trim()).filter(Boolean);
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (a === "--raw-content") {
|
|
70
|
+
includeRawContent = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (a === "--json") {
|
|
74
|
+
outputJson = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
console.error(`Unknown arg: ${a}`);
|
|
78
|
+
usage();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const apiKey = (process.env.TAVILY_API_KEY ?? "").trim();
|
|
82
|
+
if (!apiKey) {
|
|
83
|
+
console.error("Error: TAVILY_API_KEY not set");
|
|
84
|
+
console.error("Get your API key at https://tavily.com");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const body = {
|
|
89
|
+
query: query,
|
|
90
|
+
max_results: Math.max(1, Math.min(maxResults, 20)),
|
|
91
|
+
search_depth: searchDepth,
|
|
92
|
+
topic: topic,
|
|
93
|
+
include_raw_content: includeRawContent,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (timeRange) body.time_range = timeRange;
|
|
97
|
+
if (includeDomains.length > 0) body.include_domains = includeDomains;
|
|
98
|
+
if (excludeDomains.length > 0) body.exclude_domains = excludeDomains;
|
|
99
|
+
|
|
100
|
+
const resp = await fetch("https://api.tavily.com/search", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
105
|
+
},
|
|
106
|
+
body: JSON.stringify(body),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!resp.ok) {
|
|
110
|
+
const text = await resp.text().catch(() => "");
|
|
111
|
+
throw new Error(`Tavily Search failed (${resp.status}): ${text}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data = await resp.json();
|
|
115
|
+
|
|
116
|
+
if (outputJson) {
|
|
117
|
+
console.log(JSON.stringify(data, null, 2));
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Print AI answer if available
|
|
122
|
+
if (data.answer) {
|
|
123
|
+
console.log("## Answer\n");
|
|
124
|
+
console.log(data.answer);
|
|
125
|
+
console.log("\n---\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Print results
|
|
129
|
+
const results = (data.results ?? []).slice(0, maxResults);
|
|
130
|
+
console.log(`## Sources (${results.length} results)\n`);
|
|
131
|
+
|
|
132
|
+
for (const r of results) {
|
|
133
|
+
const title = String(r?.title ?? "").trim();
|
|
134
|
+
const url = String(r?.url ?? "").trim();
|
|
135
|
+
const content = String(r?.content ?? "").trim();
|
|
136
|
+
const score = r?.score ? ` (relevance: ${(r.score * 100).toFixed(0)}%)` : "";
|
|
137
|
+
|
|
138
|
+
if (!title || !url) continue;
|
|
139
|
+
|
|
140
|
+
console.log(`- **${title}**${score}`);
|
|
141
|
+
console.log(` ${url}`);
|
|
142
|
+
if (content) {
|
|
143
|
+
console.log(` ${content.slice(0, 300)}${content.length > 300 ? "..." : ""}`);
|
|
144
|
+
}
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (data.response_time) {
|
|
149
|
+
console.log(`\nResponse time: ${data.response_time}s`);
|
|
150
|
+
}
|
package/build-manifest.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ============================================================================
|
|
5
|
-
* build-manifest.js - 自动构建 patch-manifest.json
|
|
6
|
-
* ============================================================================
|
|
7
|
-
*
|
|
8
|
-
* 扫描 agent-list/ 和 skills/ 目录,自动生成统一注入清单。
|
|
9
|
-
* 已有的 strategy 设置会保留(不会被覆盖)。
|
|
10
|
-
*
|
|
11
|
-
* 在 publish.sh 中自动调用,无需手动维护 manifest。
|
|
12
|
-
* ============================================================================
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const path = require('path');
|
|
17
|
-
|
|
18
|
-
const MANIFEST_PATH = path.join(__dirname, 'patch-manifest.json');
|
|
19
|
-
const AGENT_LIST_DIR = path.join(__dirname, 'agent-list');
|
|
20
|
-
const SKILLS_DIR = path.join(__dirname, 'skills');
|
|
21
|
-
|
|
22
|
-
// 默认 config patches(硬编码,不从目录扫描)
|
|
23
|
-
const DEFAULT_CONFIG = {
|
|
24
|
-
strategy: 'on',
|
|
25
|
-
patches: {
|
|
26
|
-
'session.reset.mode': 'idle',
|
|
27
|
-
'session.reset.idleMinutes': 90720,
|
|
28
|
-
'tools.exec.ask': 'off',
|
|
29
|
-
'tools.exec.security': 'full',
|
|
30
|
-
// 'gateway.auth.token': 'aiyiran',// 为了志新docker关闭掉
|
|
31
|
-
'plugins.entries.tavily.enabled': true,
|
|
32
|
-
'plugins.entries.tavily.config.webSearch.apiKey': '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,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',
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function buildManifest() {
|
|
37
|
-
// 读取已有 manifest(保留手动设置的 strategy)
|
|
38
|
-
let existing = { agents: [], skills: [], config: DEFAULT_CONFIG };
|
|
39
|
-
if (fs.existsSync(MANIFEST_PATH)) {
|
|
40
|
-
try {
|
|
41
|
-
existing = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
|
|
42
|
-
} catch { }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 建立已有策略映射(保留手动覆盖)
|
|
46
|
-
const saved = {};
|
|
47
|
-
for (const list of [existing.agents || [], existing.skills || []]) {
|
|
48
|
-
for (const item of list) {
|
|
49
|
-
const key = item.id || item.name;
|
|
50
|
-
if (key && item.strategy) saved[key] = item.strategy;
|
|
51
|
-
if (key && item.description) saved[key + '_desc'] = item.description;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// === 1. 扫描 agent-list/ ===
|
|
56
|
-
const agents = [];
|
|
57
|
-
if (fs.existsSync(AGENT_LIST_DIR)) {
|
|
58
|
-
const dirs = fs.readdirSync(AGENT_LIST_DIR, { withFileTypes: true })
|
|
59
|
-
.filter(d => d.isDirectory() && d.name.startsWith('workspace-'))
|
|
60
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
61
|
-
|
|
62
|
-
for (const d of dirs) {
|
|
63
|
-
const agentId = d.name.replace(/^workspace-/, '');
|
|
64
|
-
agents.push({
|
|
65
|
-
id: agentId,
|
|
66
|
-
workspace: d.name,
|
|
67
|
-
strategy: saved[agentId] || 'auto',
|
|
68
|
-
description: saved[agentId + '_desc'] || agentId,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// === 2. 扫描 skills/ ===
|
|
74
|
-
const skills = [];
|
|
75
|
-
if (fs.existsSync(SKILLS_DIR)) {
|
|
76
|
-
const dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
77
|
-
.filter(d => d.isDirectory())
|
|
78
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
79
|
-
|
|
80
|
-
for (const d of dirs) {
|
|
81
|
-
// 必须有 SKILL.md 才是合法 skill
|
|
82
|
-
const skillMd = path.join(SKILLS_DIR, d.name, 'SKILL.md');
|
|
83
|
-
if (!fs.existsSync(skillMd)) continue;
|
|
84
|
-
|
|
85
|
-
skills.push({
|
|
86
|
-
name: d.name,
|
|
87
|
-
strategy: saved[d.name] || 'auto',
|
|
88
|
-
description: saved[d.name + '_desc'] || d.name,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// === 3. 合并 config(以 DEFAULT_CONFIG 为基础,保留旧 manifest 中手动添加的) ===
|
|
94
|
-
const config = {
|
|
95
|
-
...DEFAULT_CONFIG,
|
|
96
|
-
patches: {
|
|
97
|
-
...DEFAULT_CONFIG.patches,
|
|
98
|
-
...(existing.config?.patches || {}),
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
// 确保 DEFAULT_CONFIG 中的新字段一定存在(防止旧 manifest 覆盖丢失)
|
|
102
|
-
for (const [k, v] of Object.entries(DEFAULT_CONFIG.patches)) {
|
|
103
|
-
config.patches[k] = v;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const manifest = {
|
|
107
|
-
_doc: 'MyClaw 注入清单 (auto-generated)。strategy: auto | on | off | delete',
|
|
108
|
-
_generated: new Date().toISOString(),
|
|
109
|
-
agents,
|
|
110
|
-
skills,
|
|
111
|
-
config,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
115
|
-
|
|
116
|
-
console.log('📋 patch-manifest.json 已生成');
|
|
117
|
-
console.log(' 智能体: ' + agents.length + ' 个');
|
|
118
|
-
for (const a of agents) {
|
|
119
|
-
console.log(' • ' + a.id + ' [' + a.strategy + ']');
|
|
120
|
-
}
|
|
121
|
-
console.log(' 技能: ' + skills.length + ' 个');
|
|
122
|
-
for (const s of skills) {
|
|
123
|
-
console.log(' • ' + s.name + ' [' + s.strategy + ']');
|
|
124
|
-
}
|
|
125
|
-
console.log(' 配置: ' + Object.keys(config.patches || {}).length + ' 项 [' + (config.strategy || 'on') + ']');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
buildManifest();
|