@cozeclaw/coze-openclaw-plugin 0.1.2 → 0.1.4
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/README.md +10 -11
- package/package.json +19 -2
- package/skills/coze-asr/SKILL.md +16 -4
- package/skills/coze-image-gen/SKILL.md +18 -7
- package/skills/coze-tts/SKILL.md +62 -10
- package/src/client.ts +3 -1
- package/src/shared/image-gen.ts +17 -2
- package/src/skill-cli.ts +77 -0
- package/index.test.ts +0 -63
- package/src/config.test.ts +0 -130
- package/src/plugin.test.ts +0 -62
- package/src/skill-cli.test.ts +0 -65
- package/src/tools/web-fetch.test.ts +0 -68
- package/src/tools/web-search.test.ts +0 -140
package/README.md
CHANGED
|
@@ -106,20 +106,19 @@ Bundled skills:
|
|
|
106
106
|
- `coze-asr`: speech to text
|
|
107
107
|
- `coze-image-gen`: image generation
|
|
108
108
|
|
|
109
|
-
Example commands:
|
|
110
|
-
|
|
111
|
-
```bash
|
|
112
|
-
node {baseDir}/scripts/tts.mjs --text "Hello from Coze"
|
|
113
|
-
node {baseDir}/scripts/asr.mjs --url "https://example.com/audio.mp3"
|
|
114
|
-
node {baseDir}/scripts/gen.mjs --prompt "A futuristic city at sunset"
|
|
115
|
-
```
|
|
116
|
-
|
|
117
109
|
## Notes
|
|
118
110
|
|
|
119
|
-
- The plugin requires `apiKey` in plugin config
|
|
111
|
+
- The plugin requires `apiKey` in plugin config
|
|
120
112
|
- Skill visibility depends on `plugins.entries.coze-openclaw-plugin.config.apiKey`
|
|
121
|
-
- `
|
|
122
|
-
|
|
113
|
+
- The published npm package only includes runtime files, bundled skills, `README.md`, and `LICENSE`
|
|
114
|
+
|
|
115
|
+
## Contributing
|
|
116
|
+
|
|
117
|
+
See `./CONTRIBUTING.md` for the supported contribution flow.
|
|
118
|
+
|
|
119
|
+
## Security
|
|
120
|
+
|
|
121
|
+
See `./SECURITY.md` for how to report security issues.
|
|
123
122
|
|
|
124
123
|
## License
|
|
125
124
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cozeclaw/coze-openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "OpenClaw Coze tools and bundled skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "vitest run src
|
|
7
|
+
"test": "vitest run ./src"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.ts",
|
|
11
|
+
"openclaw.plugin.json",
|
|
12
|
+
"src/client.ts",
|
|
13
|
+
"src/config.ts",
|
|
14
|
+
"src/skill-cli.ts",
|
|
15
|
+
"src/shared/asr.ts",
|
|
16
|
+
"src/shared/fetch.ts",
|
|
17
|
+
"src/shared/image-gen.ts",
|
|
18
|
+
"src/shared/search.ts",
|
|
19
|
+
"src/shared/tts.ts",
|
|
20
|
+
"src/tools/web-fetch.ts",
|
|
21
|
+
"src/tools/web-search.ts",
|
|
22
|
+
"skills",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
9
26
|
"devDependencies": {
|
|
10
27
|
"openclaw": "v2026.2.6",
|
|
11
28
|
"vitest": "^3.2.4"
|
package/skills/coze-asr/SKILL.md
CHANGED
|
@@ -12,16 +12,28 @@ Transcribe audio from a URL or local file using Coze ASR.
|
|
|
12
12
|
## Quick start
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
node {
|
|
16
|
-
node {
|
|
15
|
+
node {skillDir}/scripts/asr.mjs --url "https://example.com/audio.mp3"
|
|
16
|
+
node {skillDir}/scripts/asr.mjs --file ./recording.mp3
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Options
|
|
20
20
|
|
|
21
|
-
- `--url <url>` remote audio URL
|
|
22
|
-
- `--file <path>` local audio file
|
|
21
|
+
- `--url <url>`, `-u <url>` remote audio URL
|
|
22
|
+
- `--file <path>`, `-f <path>` local audio file
|
|
23
|
+
|
|
24
|
+
## Behavior
|
|
25
|
+
|
|
26
|
+
- One of `--url` or `--file` is required.
|
|
27
|
+
- If both `--url` and `--file` are provided, the local file input takes precedence.
|
|
28
|
+
- Local files are read and uploaded as base64 audio content.
|
|
29
|
+
- Supported audio formats follow the SDK/API capability: WAV, MP3, OGG Opus, M4A.
|
|
30
|
+
- Recommended limits from the SDK docs: duration up to 2 hours, size up to 100MB.
|
|
31
|
+
- The CLI prints recognized `text`, and may also print `duration` and `segments`.
|
|
32
|
+
- The CLI does not print full `utterances` details or raw response payload.
|
|
33
|
+
- This skill does not expose custom headers such as `--header`, `-H`, or mock mode.
|
|
23
34
|
|
|
24
35
|
## Notes
|
|
25
36
|
|
|
26
37
|
- The skill runtime requires `plugins.entries.coze-openclaw-plugin.config.apiKey`.
|
|
38
|
+
- `{skillDir}` means the directory containing this `SKILL.md`.
|
|
27
39
|
- Local files are read and uploaded as base64 audio content.
|
|
@@ -9,23 +9,34 @@ metadata: { "openclaw": { "emoji": "🎨", "requires": { "bins": ["node"], "conf
|
|
|
9
9
|
|
|
10
10
|
Generate one or more images from a prompt using Coze.
|
|
11
11
|
|
|
12
|
+
This skill runs as a Node CLI wrapper around the backend SDK. Do not use the SDK from client-side code.
|
|
13
|
+
|
|
12
14
|
## Quick start
|
|
13
15
|
|
|
14
16
|
```bash
|
|
15
|
-
node {
|
|
16
|
-
node {
|
|
17
|
-
node {
|
|
17
|
+
node {skillDir}/scripts/gen.mjs --prompt "A futuristic city at sunset"
|
|
18
|
+
node {skillDir}/scripts/gen.mjs --prompt "A serene mountain landscape" --count 2 --size 4K
|
|
19
|
+
node {skillDir}/scripts/gen.mjs --prompt "A hero's journey through magical lands" --sequential --max-sequential 5
|
|
20
|
+
node {skillDir}/scripts/gen.mjs --prompt "Transform into anime style" --image "https://example.com/input.jpg"
|
|
21
|
+
node {skillDir}/scripts/gen.mjs --prompt "A modern office workspace" --response-format url
|
|
18
22
|
```
|
|
19
23
|
|
|
20
24
|
## Options
|
|
21
25
|
|
|
22
26
|
- `--prompt <text>` required prompt text
|
|
23
|
-
- `--count <n>` number of
|
|
24
|
-
- `--size <size>` image size
|
|
27
|
+
- `--count <n>` number of independent generation requests, default `1`
|
|
28
|
+
- `--size <size>` image size: `2K`, `4K`, or `WIDTHxHEIGHT`, default `2K`
|
|
29
|
+
- `--image <url>` reference image URL; repeat the flag to pass multiple images
|
|
30
|
+
- `--response-format <format>` only supports `url`
|
|
31
|
+
- `--watermark <true|false>` whether to keep watermark
|
|
32
|
+
- `--optimize-prompt-mode <mode>` prompt optimization mode passed through to the SDK
|
|
25
33
|
- `--sequential` enable sequential image generation
|
|
26
|
-
- `--max-sequential <n>` max sequential frames, default `
|
|
34
|
+
- `--max-sequential <n>` max sequential frames, default `15`
|
|
35
|
+
- `--header <key:value>` custom HTTP header; repeatable, alias `-H`
|
|
27
36
|
|
|
28
37
|
## Notes
|
|
29
38
|
|
|
30
39
|
- The skill runtime requires `plugins.entries.coze-openclaw-plugin.config.apiKey`.
|
|
31
|
-
-
|
|
40
|
+
- `{skillDir}` means the directory containing this `SKILL.md`.
|
|
41
|
+
- Successful runs print generated URLs directly.
|
|
42
|
+
- This skill does not expose base64 output.
|
package/skills/coze-tts/SKILL.md
CHANGED
|
@@ -12,22 +12,74 @@ Generate speech audio URLs from text using Coze TTS.
|
|
|
12
12
|
## Quick start
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
node {
|
|
16
|
-
node {
|
|
17
|
-
node {
|
|
15
|
+
node {skillDir}/scripts/tts.mjs --text "Hello, welcome to our service"
|
|
16
|
+
node {skillDir}/scripts/tts.mjs --texts "Chapter 1" "Chapter 2" --speaker zh_male_m191_uranus_bigtts
|
|
17
|
+
node {skillDir}/scripts/tts.mjs --text "Fast announcement" --speech-rate 30 --format mp3 --sample-rate 48000
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
## Options
|
|
21
21
|
|
|
22
|
-
- `--text <text>` single text input
|
|
23
|
-
- `--texts <texts...>` multiple text inputs
|
|
24
|
-
- `--speaker <id>` speaker id
|
|
25
|
-
- `--format <fmt>` `mp3`, `pcm`, or `ogg_opus`
|
|
26
|
-
- `--sample-rate <hz>` sample rate
|
|
27
|
-
- `--speech-rate <n>` speech rate adjustment
|
|
28
|
-
- `--loudness-rate <n>` loudness adjustment
|
|
22
|
+
- `--text <text>` single text input. If both `--text` and `--texts` are provided, `--text` takes precedence.
|
|
23
|
+
- `--texts <texts...>` multiple text inputs. Values are read until the next `--flag`.
|
|
24
|
+
- `--speaker <id>` speaker id, default `zh_female_xiaohe_uranus_bigtts`
|
|
25
|
+
- `--format <fmt>` audio format: `mp3`, `pcm`, or `ogg_opus`. Default is SDK default (`mp3`).
|
|
26
|
+
- `--sample-rate <hz>` sample rate. Supported values: `8000`, `16000`, `22050`, `24000`, `32000`, `44100`, `48000`. Default is SDK default (`24000`).
|
|
27
|
+
- `--speech-rate <n>` speech rate adjustment, range `-50` to `100`, default `0`
|
|
28
|
+
- `--loudness-rate <n>` loudness adjustment, range `-50` to `100`, default `0`
|
|
29
|
+
|
|
30
|
+
## Behavior
|
|
31
|
+
|
|
32
|
+
- At least one of `--text` or `--texts` is required.
|
|
33
|
+
- This skill currently supports plain text input only. It does not expose `ssml`, `--header`, `-H`, or `--mock`.
|
|
34
|
+
- The CLI prints one audio URL per generated segment. It does not download audio files locally.
|
|
35
|
+
- The CLI does not print `audioSize`, even though the underlying SDK returns it.
|
|
36
|
+
- Invalid ranges or unsupported values are passed through to the SDK and may fail there.
|
|
37
|
+
|
|
38
|
+
## Sample Rates
|
|
39
|
+
|
|
40
|
+
Supported: `8000`, `16000`, `22050`, `24000`, `32000`, `44100`, `48000` Hz
|
|
41
|
+
|
|
42
|
+
- `8000-16000`: Phone quality
|
|
43
|
+
- `22050-24000`: Standard quality (default)
|
|
44
|
+
- `32000-48000`: High quality
|
|
45
|
+
|
|
46
|
+
## Tuning
|
|
47
|
+
|
|
48
|
+
- `speechRate`: range `-50` to `100`, default `0`. Negative values slow speech down, positive values speed it up.
|
|
49
|
+
- `loudnessRate`: range `-50` to `100`, default `0`. Negative values make output quieter, positive values make it louder.
|
|
50
|
+
|
|
51
|
+
## Voices
|
|
52
|
+
|
|
53
|
+
### General
|
|
54
|
+
|
|
55
|
+
- `zh_female_xiaohe_uranus_bigtts` `小荷`: 默认,通用女声
|
|
56
|
+
- `zh_female_vv_uranus_bigtts` `Vivi`: 中英双语女声
|
|
57
|
+
- `zh_male_m191_uranus_bigtts` `云舟`: 男声
|
|
58
|
+
- `zh_male_taocheng_uranus_bigtts` `小天`: 男声
|
|
59
|
+
|
|
60
|
+
### Audiobook / Reading
|
|
61
|
+
|
|
62
|
+
- `zh_female_xueayi_saturn_bigtts` `雪阿姨`: 儿童有声读物女声
|
|
63
|
+
|
|
64
|
+
### Video Dubbing
|
|
65
|
+
|
|
66
|
+
- `zh_male_dayi_saturn_bigtts` `大一`: 男声
|
|
67
|
+
- `zh_female_mizai_saturn_bigtts` `米仔`: 女声
|
|
68
|
+
- `zh_female_jitangnv_saturn_bigtts` `鸡汤女`: 励志女声
|
|
69
|
+
- `zh_female_meilinvyou_saturn_bigtts` `甜美女友`: 甜美女友
|
|
70
|
+
- `zh_female_santongyongns_saturn_bigtts` `三通女声`: 通用流畅女声
|
|
71
|
+
- `zh_male_ruyayichen_saturn_bigtts` `儒雅一尘`: 儒雅男声
|
|
72
|
+
|
|
73
|
+
### Roleplay
|
|
74
|
+
|
|
75
|
+
- `saturn_zh_female_keainvsheng_tob` `可爱女生`: 可爱女生
|
|
76
|
+
- `saturn_zh_female_tiaopigongzhu_tob` `俏皮公主`: 俏皮公主
|
|
77
|
+
- `saturn_zh_male_shuanglangshaonian_tob` `爽朗少年`: 爽朗少年
|
|
78
|
+
- `saturn_zh_male_tiancaitongzhuo_tob` `天才同桌`: 天才同桌
|
|
79
|
+
- `saturn_zh_female_cancan_tob` `灿灿`: 知性灿灿
|
|
29
80
|
|
|
30
81
|
## Notes
|
|
31
82
|
|
|
32
83
|
- The skill runtime requires `plugins.entries.coze-openclaw-plugin.config.apiKey`.
|
|
84
|
+
- `{skillDir}` means the directory containing this `SKILL.md`.
|
|
33
85
|
- The script prints one audio URL per generated segment.
|
package/src/client.ts
CHANGED
|
@@ -3,12 +3,14 @@ import type { CozeConfig } from "coze-coding-dev-sdk";
|
|
|
3
3
|
type CozeSdkModule = typeof import("coze-coding-dev-sdk");
|
|
4
4
|
type ConfigurableClientConstructor<TClient> = new (
|
|
5
5
|
config: InstanceType<CozeSdkModule["Config"]>,
|
|
6
|
+
customHeaders?: Record<string, string>,
|
|
6
7
|
) => TClient;
|
|
7
8
|
|
|
8
9
|
let sdkPromise: Promise<CozeSdkModule> | null = null;
|
|
9
10
|
|
|
10
11
|
export type CozeClientFactoryParams = {
|
|
11
12
|
config: CozeConfig;
|
|
13
|
+
customHeaders?: Record<string, string>;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export async function loadCozeSdk(): Promise<CozeSdkModule> {
|
|
@@ -24,7 +26,7 @@ async function createClient<TClient>(
|
|
|
24
26
|
): Promise<TClient> {
|
|
25
27
|
const sdk = await loadCozeSdk();
|
|
26
28
|
const Client = resolveCtor(sdk);
|
|
27
|
-
return new Client(new sdk.Config(params.config));
|
|
29
|
+
return new Client(new sdk.Config(params.config), params.customHeaders);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export async function createSearchClient(
|
package/src/shared/image-gen.ts
CHANGED
|
@@ -7,6 +7,11 @@ export type ImageGenerationInput = {
|
|
|
7
7
|
size?: string;
|
|
8
8
|
sequential?: boolean;
|
|
9
9
|
maxSequential?: number;
|
|
10
|
+
image?: string[];
|
|
11
|
+
responseFormat?: "url";
|
|
12
|
+
watermark?: boolean;
|
|
13
|
+
optimizePromptMode?: string;
|
|
14
|
+
headers?: Record<string, string>;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
17
|
export type ImageGenerationResult = {
|
|
@@ -18,7 +23,10 @@ export async function generateImages(
|
|
|
18
23
|
input: ImageGenerationInput,
|
|
19
24
|
clientConfig: CozeConfig,
|
|
20
25
|
): Promise<ImageGenerationResult[]> {
|
|
21
|
-
const client = await createImageGenerationClient({
|
|
26
|
+
const client = await createImageGenerationClient({
|
|
27
|
+
config: clientConfig,
|
|
28
|
+
customHeaders: input.headers,
|
|
29
|
+
});
|
|
22
30
|
const count = input.count ?? 1;
|
|
23
31
|
const results: ImageGenerationResult[] = [];
|
|
24
32
|
|
|
@@ -27,10 +35,17 @@ export async function generateImages(
|
|
|
27
35
|
prompt: input.prompt,
|
|
28
36
|
size: input.size ?? "2K",
|
|
29
37
|
sequentialImageGeneration: input.sequential ? "auto" : "disabled",
|
|
30
|
-
sequentialImageGenerationMaxImages: input.maxSequential ??
|
|
38
|
+
sequentialImageGenerationMaxImages: input.maxSequential ?? 15,
|
|
39
|
+
image: input.image && input.image.length > 0 ? input.image : undefined,
|
|
40
|
+
responseFormat: input.responseFormat,
|
|
41
|
+
watermark: input.watermark,
|
|
42
|
+
optimizePromptMode: input.optimizePromptMode,
|
|
31
43
|
};
|
|
32
44
|
const response = await client.generate(request);
|
|
33
45
|
const helper = client.getResponseHelper(response);
|
|
46
|
+
if (!helper.success) {
|
|
47
|
+
throw new Error(helper.errorMessages.join("; ") || "Image generation failed");
|
|
48
|
+
}
|
|
34
49
|
results.push({
|
|
35
50
|
prompt: input.prompt,
|
|
36
51
|
urls: helper.imageUrls,
|
package/src/skill-cli.ts
CHANGED
|
@@ -62,6 +62,21 @@ function readTrailingValues(args: string[], name: string): string[] {
|
|
|
62
62
|
return values;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
function readRepeatedArgs(args: string[], names: string[]): string[] {
|
|
66
|
+
const values: string[] = [];
|
|
67
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
68
|
+
if (!names.includes(args[index] ?? "")) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const value = args[index + 1];
|
|
72
|
+
if (!value || value.startsWith("-")) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
values.push(value);
|
|
76
|
+
}
|
|
77
|
+
return values;
|
|
78
|
+
}
|
|
79
|
+
|
|
65
80
|
function readNumberArg(args: string[], name: string): number | undefined {
|
|
66
81
|
const value = readArg(args, name);
|
|
67
82
|
if (!value) {
|
|
@@ -85,6 +100,47 @@ function findMissingValueFlag(args: string[], names: string[]): string | undefin
|
|
|
85
100
|
return undefined;
|
|
86
101
|
}
|
|
87
102
|
|
|
103
|
+
function parseBooleanArg(value: string | undefined, name: string): boolean | undefined {
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
if (value === "true") {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (value === "false") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`${name} must be true or false`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findHeaderSeparator(value: string): ":" | "=" | undefined {
|
|
117
|
+
if (value.includes(":")) {
|
|
118
|
+
return ":";
|
|
119
|
+
}
|
|
120
|
+
if (value.includes("=")) {
|
|
121
|
+
return "=";
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseHeaders(values: string[]): Record<string, string> {
|
|
127
|
+
const headers: Record<string, string> = {};
|
|
128
|
+
for (const value of values) {
|
|
129
|
+
const separator = findHeaderSeparator(value);
|
|
130
|
+
if (!separator) {
|
|
131
|
+
throw new Error(`Invalid header format: ${value}`);
|
|
132
|
+
}
|
|
133
|
+
const index = value.indexOf(separator);
|
|
134
|
+
const key = value.slice(0, index).trim();
|
|
135
|
+
const headerValue = value.slice(index + 1).trim();
|
|
136
|
+
if (!key || !headerValue) {
|
|
137
|
+
throw new Error(`Invalid header format: ${value}`);
|
|
138
|
+
}
|
|
139
|
+
headers[key] = headerValue;
|
|
140
|
+
}
|
|
141
|
+
return headers;
|
|
142
|
+
}
|
|
143
|
+
|
|
88
144
|
export async function runImageCli(
|
|
89
145
|
args: string[],
|
|
90
146
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -95,6 +151,13 @@ export async function runImageCli(
|
|
|
95
151
|
"--count",
|
|
96
152
|
"--size",
|
|
97
153
|
"--max-sequential",
|
|
154
|
+
"--image",
|
|
155
|
+
"--response-format",
|
|
156
|
+
"--watermark",
|
|
157
|
+
"--optimize-prompt-mode",
|
|
158
|
+
"--output",
|
|
159
|
+
"--header",
|
|
160
|
+
"-H",
|
|
98
161
|
]);
|
|
99
162
|
if (missingValueFlag) {
|
|
100
163
|
io.error(`Error: ${missingValueFlag} requires a value`);
|
|
@@ -105,6 +168,15 @@ export async function runImageCli(
|
|
|
105
168
|
io.error("Error: --prompt is required");
|
|
106
169
|
return 1;
|
|
107
170
|
}
|
|
171
|
+
const responseFormat = readArg(args, "--response-format");
|
|
172
|
+
if (responseFormat && responseFormat !== "url") {
|
|
173
|
+
io.error("Error: --response-format only supports url");
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
if (args.includes("--output")) {
|
|
177
|
+
io.error("Error: --output is not supported; this skill only prints image URLs");
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
108
180
|
try {
|
|
109
181
|
const config = await requireConfig(env);
|
|
110
182
|
const results = await generateImages(
|
|
@@ -114,6 +186,11 @@ export async function runImageCli(
|
|
|
114
186
|
size: readArg(args, "--size"),
|
|
115
187
|
sequential: args.includes("--sequential"),
|
|
116
188
|
maxSequential: readNumberArg(args, "--max-sequential"),
|
|
189
|
+
image: readRepeatedArgs(args, ["--image"]),
|
|
190
|
+
responseFormat: responseFormat as "url" | undefined,
|
|
191
|
+
watermark: parseBooleanArg(readArg(args, "--watermark"), "--watermark"),
|
|
192
|
+
optimizePromptMode: readArg(args, "--optimize-prompt-mode"),
|
|
193
|
+
headers: parseHeaders(readRepeatedArgs(args, ["--header", "-H"])),
|
|
117
194
|
},
|
|
118
195
|
config,
|
|
119
196
|
);
|
package/index.test.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import plugin from "./index.js";
|
|
5
|
-
|
|
6
|
-
describe("coze-openclaw-plugin plugin", () => {
|
|
7
|
-
it("registers coze web tools", () => {
|
|
8
|
-
const registerTool = vi.fn();
|
|
9
|
-
|
|
10
|
-
plugin.register?.({
|
|
11
|
-
id: "coze-openclaw-plugin",
|
|
12
|
-
name: "Coze OpenClaw Plugin",
|
|
13
|
-
description: "Coze OpenClaw",
|
|
14
|
-
source: "test",
|
|
15
|
-
config: {},
|
|
16
|
-
pluginConfig: {},
|
|
17
|
-
runtime: {} as never,
|
|
18
|
-
logger: {
|
|
19
|
-
debug() {},
|
|
20
|
-
info() {},
|
|
21
|
-
warn() {},
|
|
22
|
-
error() {},
|
|
23
|
-
},
|
|
24
|
-
registerTool,
|
|
25
|
-
registerHook() {},
|
|
26
|
-
registerHttpRoute() {},
|
|
27
|
-
registerChannel() {},
|
|
28
|
-
registerGatewayMethod() {},
|
|
29
|
-
registerCli() {},
|
|
30
|
-
registerService() {},
|
|
31
|
-
registerProvider() {},
|
|
32
|
-
registerCommand() {},
|
|
33
|
-
registerContextEngine() {},
|
|
34
|
-
resolvePath(input: string) {
|
|
35
|
-
return input;
|
|
36
|
-
},
|
|
37
|
-
on() {},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
expect(registerTool).toHaveBeenCalledTimes(2);
|
|
41
|
-
const toolNames = registerTool.mock.calls
|
|
42
|
-
.map((call) => {
|
|
43
|
-
const tool = call[0];
|
|
44
|
-
return typeof tool === "function" ? undefined : tool.name;
|
|
45
|
-
})
|
|
46
|
-
.filter(Boolean);
|
|
47
|
-
expect(toolNames).toEqual(["coze_web_search", "coze_web_fetch"]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("declares bundled skills in the plugin manifest", () => {
|
|
51
|
-
const manifestPath = path.join(
|
|
52
|
-
process.cwd(),
|
|
53
|
-
"extensions/coze-openclaw-plugin/openclaw.plugin.json",
|
|
54
|
-
);
|
|
55
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as {
|
|
56
|
-
id: string;
|
|
57
|
-
skills?: string[];
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
expect(manifest.id).toBe("coze-openclaw-plugin");
|
|
61
|
-
expect(manifest.skills).toEqual(["./skills"]);
|
|
62
|
-
});
|
|
63
|
-
});
|
package/src/config.test.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
6
|
-
loadCozePluginConfigFromOpenClawConfig,
|
|
7
|
-
resolveCozeClientConfig,
|
|
8
|
-
resolveOpenClawConfigPath,
|
|
9
|
-
} from "./config.js";
|
|
10
|
-
|
|
11
|
-
const tempDirs: string[] = [];
|
|
12
|
-
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
await Promise.all(tempDirs.splice(0, tempDirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("resolveCozeClientConfig", () => {
|
|
18
|
-
it("reads values from plugin config", () => {
|
|
19
|
-
const result = resolveCozeClientConfig(
|
|
20
|
-
{
|
|
21
|
-
apiKey: "plugin-key",
|
|
22
|
-
baseUrl: "https://plugin.example.com",
|
|
23
|
-
modelBaseUrl: "https://plugin-model.example.com",
|
|
24
|
-
timeout: 3000,
|
|
25
|
-
},
|
|
26
|
-
{} as NodeJS.ProcessEnv,
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
expect(result).toMatchObject({
|
|
30
|
-
apiKey: "plugin-key",
|
|
31
|
-
baseUrl: "https://plugin.example.com",
|
|
32
|
-
modelBaseUrl: "https://plugin-model.example.com",
|
|
33
|
-
timeout: 3000,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("does not fall back to environment variables", () => {
|
|
38
|
-
const result = resolveCozeClientConfig({}, {
|
|
39
|
-
COZE_API_KEY: "env-key",
|
|
40
|
-
COZE_BASE_URL: "https://env.example.com",
|
|
41
|
-
COZE_MODEL_BASE_URL: "https://env-model.example.com",
|
|
42
|
-
COZE_RETRY_TIMES: "3",
|
|
43
|
-
COZE_RETRY_DELAY: "100",
|
|
44
|
-
COZE_TIMEOUT: "3000",
|
|
45
|
-
} as NodeJS.ProcessEnv);
|
|
46
|
-
|
|
47
|
-
expect(result).toEqual({
|
|
48
|
-
apiKey: undefined,
|
|
49
|
-
baseUrl: undefined,
|
|
50
|
-
modelBaseUrl: undefined,
|
|
51
|
-
retryTimes: undefined,
|
|
52
|
-
retryDelay: undefined,
|
|
53
|
-
timeout: undefined,
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("resolves the OpenClaw config path from OPENCLAW_CONFIG_PATH", () => {
|
|
58
|
-
expect(
|
|
59
|
-
resolveOpenClawConfigPath({
|
|
60
|
-
OPENCLAW_CONFIG_PATH: "~/custom/openclaw.json",
|
|
61
|
-
HOME: "/tmp/example-home",
|
|
62
|
-
} as NodeJS.ProcessEnv),
|
|
63
|
-
).toBe(path.join("/tmp/example-home", "custom", "openclaw.json"));
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("loads plugin config from the OpenClaw config file", async () => {
|
|
67
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "coze-openclaw-plugin-config-"));
|
|
68
|
-
tempDirs.push(tempDir);
|
|
69
|
-
const configPath = path.join(tempDir, "openclaw.json");
|
|
70
|
-
|
|
71
|
-
await fs.writeFile(
|
|
72
|
-
configPath,
|
|
73
|
-
`{
|
|
74
|
-
plugins: {
|
|
75
|
-
entries: {
|
|
76
|
-
"coze-openclaw-plugin": {
|
|
77
|
-
config: {
|
|
78
|
-
apiKey: "plugin-key",
|
|
79
|
-
baseUrl: "https://plugin.example.com",
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
}`,
|
|
85
|
-
"utf-8",
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const result = await loadCozePluginConfigFromOpenClawConfig({
|
|
89
|
-
OPENCLAW_CONFIG_PATH: configPath,
|
|
90
|
-
} as NodeJS.ProcessEnv);
|
|
91
|
-
|
|
92
|
-
expect(result).toEqual({
|
|
93
|
-
apiKey: "plugin-key",
|
|
94
|
-
baseUrl: "https://plugin.example.com",
|
|
95
|
-
modelBaseUrl: undefined,
|
|
96
|
-
retryTimes: undefined,
|
|
97
|
-
retryDelay: undefined,
|
|
98
|
-
timeout: undefined,
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("resolves env templates from the OpenClaw config file", async () => {
|
|
103
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "coze-openclaw-plugin-config-"));
|
|
104
|
-
tempDirs.push(tempDir);
|
|
105
|
-
const configPath = path.join(tempDir, "openclaw.json");
|
|
106
|
-
|
|
107
|
-
await fs.writeFile(
|
|
108
|
-
configPath,
|
|
109
|
-
`{
|
|
110
|
-
plugins: {
|
|
111
|
-
entries: {
|
|
112
|
-
"coze-openclaw-plugin": {
|
|
113
|
-
config: {
|
|
114
|
-
apiKey: "\${COZE_WORKLOAD_IDENTITY_API_KEY}",
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
}`,
|
|
120
|
-
"utf-8",
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const result = await loadCozePluginConfigFromOpenClawConfig({
|
|
124
|
-
OPENCLAW_CONFIG_PATH: configPath,
|
|
125
|
-
COZE_WORKLOAD_IDENTITY_API_KEY: "workload-key",
|
|
126
|
-
} as NodeJS.ProcessEnv);
|
|
127
|
-
|
|
128
|
-
expect(result?.apiKey).toBe("workload-key");
|
|
129
|
-
});
|
|
130
|
-
});
|
package/src/plugin.test.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import plugin from "../index.js";
|
|
3
|
-
|
|
4
|
-
function createApi(pluginConfig: Record<string, unknown>) {
|
|
5
|
-
return {
|
|
6
|
-
id: "coze-openclaw-plugin",
|
|
7
|
-
name: "Coze OpenClaw Plugin",
|
|
8
|
-
description: "Coze OpenClaw",
|
|
9
|
-
source: "test",
|
|
10
|
-
config: {},
|
|
11
|
-
pluginConfig,
|
|
12
|
-
runtime: {} as never,
|
|
13
|
-
logger: {
|
|
14
|
-
debug() {},
|
|
15
|
-
info: vi.fn(),
|
|
16
|
-
warn() {},
|
|
17
|
-
error() {},
|
|
18
|
-
},
|
|
19
|
-
registerTool: vi.fn(),
|
|
20
|
-
registerHook() {},
|
|
21
|
-
registerHttpRoute() {},
|
|
22
|
-
registerChannel() {},
|
|
23
|
-
registerGatewayMethod() {},
|
|
24
|
-
registerCli() {},
|
|
25
|
-
registerService() {},
|
|
26
|
-
registerProvider() {},
|
|
27
|
-
registerCommand() {},
|
|
28
|
-
registerContextEngine() {},
|
|
29
|
-
resolvePath(input: string) {
|
|
30
|
-
return input;
|
|
31
|
-
},
|
|
32
|
-
on() {},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe("plugin registration", () => {
|
|
37
|
-
it("skips tool registration when apiKey is missing", () => {
|
|
38
|
-
const api = createApi({});
|
|
39
|
-
|
|
40
|
-
plugin.register?.(api);
|
|
41
|
-
|
|
42
|
-
expect(api.registerTool).not.toHaveBeenCalled();
|
|
43
|
-
expect(api.logger.info).toHaveBeenCalledWith(
|
|
44
|
-
"Skipping Coze tool registration because plugins.entries.coze-openclaw-plugin.config.apiKey is missing.",
|
|
45
|
-
);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("registers coze web tools when apiKey exists", () => {
|
|
49
|
-
const api = createApi({ apiKey: "test-key" });
|
|
50
|
-
|
|
51
|
-
plugin.register?.(api);
|
|
52
|
-
|
|
53
|
-
expect(api.registerTool).toHaveBeenCalledTimes(2);
|
|
54
|
-
const toolNames = api.registerTool.mock.calls
|
|
55
|
-
.map((call) => {
|
|
56
|
-
const tool = call[0];
|
|
57
|
-
return typeof tool === "function" ? undefined : tool.name;
|
|
58
|
-
})
|
|
59
|
-
.filter(Boolean);
|
|
60
|
-
expect(toolNames).toEqual(["coze_web_search", "coze_web_fetch"]);
|
|
61
|
-
});
|
|
62
|
-
});
|
package/src/skill-cli.test.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const hoisted = vi.hoisted(() => ({
|
|
4
|
-
loadCozePluginConfigFromOpenClawConfig: vi.fn(),
|
|
5
|
-
resolveCozeClientConfig: vi.fn(),
|
|
6
|
-
synthesizeSpeech: vi.fn(),
|
|
7
|
-
transcribeSpeech: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
vi.mock("./config.js", async () => {
|
|
11
|
-
const actual = await vi.importActual<typeof import("./config.js")>("./config.js");
|
|
12
|
-
return {
|
|
13
|
-
...actual,
|
|
14
|
-
loadCozePluginConfigFromOpenClawConfig: (...args: unknown[]) =>
|
|
15
|
-
hoisted.loadCozePluginConfigFromOpenClawConfig(...args),
|
|
16
|
-
resolveCozeClientConfig: (...args: unknown[]) => hoisted.resolveCozeClientConfig(...args),
|
|
17
|
-
};
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
vi.mock("./shared/tts.js", () => ({
|
|
21
|
-
synthesizeSpeech: (...args: unknown[]) => hoisted.synthesizeSpeech(...args),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
vi.mock("./shared/asr.js", () => ({
|
|
25
|
-
transcribeSpeech: (...args: unknown[]) => hoisted.transcribeSpeech(...args),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
const { runAsrCli, runTtsCli } = await import("./skill-cli.js");
|
|
29
|
-
|
|
30
|
-
function createIo() {
|
|
31
|
-
return {
|
|
32
|
-
logs: [] as string[],
|
|
33
|
-
errors: [] as string[],
|
|
34
|
-
log(message: string) {
|
|
35
|
-
this.logs.push(message);
|
|
36
|
-
},
|
|
37
|
-
error(message: string) {
|
|
38
|
-
this.errors.push(message);
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe("skill cli", () => {
|
|
44
|
-
it("rejects missing values for single-value flags", async () => {
|
|
45
|
-
const io = createIo();
|
|
46
|
-
|
|
47
|
-
const code = await runAsrCli(["--file", "--url", "https://example.com/audio.mp3"], {}, io);
|
|
48
|
-
|
|
49
|
-
expect(code).toBe(1);
|
|
50
|
-
expect(io.errors).toContain("Error: --file requires a value");
|
|
51
|
-
expect(hoisted.transcribeSpeech).not.toHaveBeenCalled();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("rejects missing values in tts options before calling the sdk", async () => {
|
|
55
|
-
hoisted.loadCozePluginConfigFromOpenClawConfig.mockResolvedValue({});
|
|
56
|
-
hoisted.resolveCozeClientConfig.mockReturnValue({ apiKey: "test-key" });
|
|
57
|
-
const io = createIo();
|
|
58
|
-
|
|
59
|
-
const code = await runTtsCli(["--text", "Hello", "--speaker", "--format", "mp3"], {}, io);
|
|
60
|
-
|
|
61
|
-
expect(code).toBe(1);
|
|
62
|
-
expect(io.errors).toContain("Error: --speaker requires a value");
|
|
63
|
-
expect(hoisted.synthesizeSpeech).not.toHaveBeenCalled();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const hoisted = vi.hoisted(() => ({
|
|
4
|
-
fetchContent: vi.fn(),
|
|
5
|
-
}));
|
|
6
|
-
|
|
7
|
-
vi.mock("../shared/fetch.js", () => ({
|
|
8
|
-
fetchContent: (...args: unknown[]) => hoisted.fetchContent(...args),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
const { createCozeWebFetchTool } = await import("./web-fetch.js");
|
|
12
|
-
|
|
13
|
-
function getTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
14
|
-
const first = result.content[0];
|
|
15
|
-
return first?.type === "text" ? first.text ?? "" : "";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("createCozeWebFetchTool", () => {
|
|
19
|
-
it("fetches multiple urls and returns normalized details", async () => {
|
|
20
|
-
hoisted.fetchContent.mockResolvedValueOnce([
|
|
21
|
-
{
|
|
22
|
-
url: "https://example.com/one",
|
|
23
|
-
title: "One",
|
|
24
|
-
text: "First page",
|
|
25
|
-
links: [],
|
|
26
|
-
images: [],
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
url: "https://example.com/two",
|
|
30
|
-
title: "Two",
|
|
31
|
-
text: "Second page",
|
|
32
|
-
links: [{ title: "Ref", url: "https://example.com/ref" }],
|
|
33
|
-
images: [],
|
|
34
|
-
},
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
const tool = createCozeWebFetchTool({
|
|
38
|
-
pluginConfig: { apiKey: "test-key" },
|
|
39
|
-
logger: {
|
|
40
|
-
debug() {},
|
|
41
|
-
info() {},
|
|
42
|
-
warn() {},
|
|
43
|
-
error() {},
|
|
44
|
-
},
|
|
45
|
-
env: {},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const result = await tool.execute("call-1", {
|
|
49
|
-
urls: ["https://example.com/one", "https://example.com/two"],
|
|
50
|
-
textOnly: true,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
expect(hoisted.fetchContent).toHaveBeenCalledWith(
|
|
54
|
-
expect.objectContaining({
|
|
55
|
-
urls: ["https://example.com/one", "https://example.com/two"],
|
|
56
|
-
textOnly: true,
|
|
57
|
-
}),
|
|
58
|
-
expect.objectContaining({ apiKey: "test-key" }),
|
|
59
|
-
);
|
|
60
|
-
expect(result.details).toMatchObject({
|
|
61
|
-
count: 2,
|
|
62
|
-
urls: ["https://example.com/one", "https://example.com/two"],
|
|
63
|
-
});
|
|
64
|
-
const text = getTextContent(result);
|
|
65
|
-
expect(text).toContain("First page");
|
|
66
|
-
expect(text).toContain("Second page");
|
|
67
|
-
});
|
|
68
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
const hoisted = vi.hoisted(() => ({
|
|
4
|
-
searchWeb: vi.fn(),
|
|
5
|
-
}));
|
|
6
|
-
|
|
7
|
-
vi.mock("../shared/search.js", () => ({
|
|
8
|
-
searchWeb: (...args: unknown[]) => hoisted.searchWeb(...args),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
const { createCozeWebSearchTool } = await import("./web-search.js");
|
|
12
|
-
|
|
13
|
-
function getTextContent(result: { content: Array<{ type: string; text?: string }> }): string {
|
|
14
|
-
const first = result.content[0];
|
|
15
|
-
return first?.type === "text" ? first.text ?? "" : "";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("createCozeWebSearchTool", () => {
|
|
19
|
-
it("executes web search and returns structured details", async () => {
|
|
20
|
-
hoisted.searchWeb.mockResolvedValueOnce({
|
|
21
|
-
query: "OpenClaw",
|
|
22
|
-
type: "web",
|
|
23
|
-
summary: "summary",
|
|
24
|
-
items: [
|
|
25
|
-
{
|
|
26
|
-
title: "OpenClaw",
|
|
27
|
-
url: "https://openclaw.ai",
|
|
28
|
-
siteName: "OpenClaw",
|
|
29
|
-
snippet: "AI agent",
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const tool = createCozeWebSearchTool({
|
|
35
|
-
pluginConfig: { apiKey: "test-key" },
|
|
36
|
-
logger: {
|
|
37
|
-
debug() {},
|
|
38
|
-
info() {},
|
|
39
|
-
warn() {},
|
|
40
|
-
error() {},
|
|
41
|
-
},
|
|
42
|
-
env: {},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const result = await tool.execute("call-1", {
|
|
46
|
-
query: "OpenClaw",
|
|
47
|
-
type: "web",
|
|
48
|
-
count: 5,
|
|
49
|
-
needSummary: true,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
expect(hoisted.searchWeb).toHaveBeenCalledWith(
|
|
53
|
-
expect.objectContaining({
|
|
54
|
-
query: "OpenClaw",
|
|
55
|
-
type: "web",
|
|
56
|
-
count: 5,
|
|
57
|
-
needSummary: true,
|
|
58
|
-
}),
|
|
59
|
-
expect.objectContaining({ apiKey: "test-key" }),
|
|
60
|
-
);
|
|
61
|
-
expect(result.details).toMatchObject({
|
|
62
|
-
query: "OpenClaw",
|
|
63
|
-
type: "web",
|
|
64
|
-
summary: "summary",
|
|
65
|
-
count: 1,
|
|
66
|
-
});
|
|
67
|
-
expect(getTextContent(result)).toContain("OpenClaw");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("supports image search mode", async () => {
|
|
71
|
-
hoisted.searchWeb.mockResolvedValueOnce({
|
|
72
|
-
query: "lobster",
|
|
73
|
-
type: "image",
|
|
74
|
-
items: [
|
|
75
|
-
{
|
|
76
|
-
title: "Lobster",
|
|
77
|
-
url: "https://example.com/page",
|
|
78
|
-
imageUrl: "https://example.com/image.png",
|
|
79
|
-
siteName: "Example",
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
const tool = createCozeWebSearchTool({
|
|
85
|
-
pluginConfig: { apiKey: "test-key" },
|
|
86
|
-
logger: {
|
|
87
|
-
debug() {},
|
|
88
|
-
info() {},
|
|
89
|
-
warn() {},
|
|
90
|
-
error() {},
|
|
91
|
-
},
|
|
92
|
-
env: {},
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const result = await tool.execute("call-2", {
|
|
96
|
-
query: "lobster",
|
|
97
|
-
type: "image",
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
expect(result.details).toMatchObject({
|
|
101
|
-
type: "image",
|
|
102
|
-
count: 1,
|
|
103
|
-
});
|
|
104
|
-
expect(getTextContent(result)).toContain("https://example.com/image.png");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("renders extracted content when needContent is enabled", async () => {
|
|
108
|
-
hoisted.searchWeb.mockResolvedValueOnce({
|
|
109
|
-
query: "OpenClaw",
|
|
110
|
-
type: "web",
|
|
111
|
-
items: [
|
|
112
|
-
{
|
|
113
|
-
title: "OpenClaw",
|
|
114
|
-
url: "https://openclaw.ai",
|
|
115
|
-
snippet: "Short summary",
|
|
116
|
-
content: "Long-form extracted page content.",
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const tool = createCozeWebSearchTool({
|
|
122
|
-
pluginConfig: { apiKey: "test-key" },
|
|
123
|
-
logger: {
|
|
124
|
-
debug() {},
|
|
125
|
-
info() {},
|
|
126
|
-
warn() {},
|
|
127
|
-
error() {},
|
|
128
|
-
},
|
|
129
|
-
env: {},
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const result = await tool.execute("call-3", {
|
|
133
|
-
query: "OpenClaw",
|
|
134
|
-
type: "web",
|
|
135
|
-
needContent: true,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
expect(getTextContent(result)).toContain("Long-form extracted page content.");
|
|
139
|
-
});
|
|
140
|
-
});
|