@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 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 and does not fall back to `COZE_*` environment variables
111
+ - The plugin requires `apiKey` in plugin config
120
112
  - Skill visibility depends on `plugins.entries.coze-openclaw-plugin.config.apiKey`
121
- - `coze_web_fetch` fetches URLs sequentially
122
- - Run `npm test` to execute the maintained unit tests under `./src`
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.2",
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/config.test.ts src/plugin.test.ts src/skill-cli.test.ts src/tools/web-search.test.ts src/tools/web-fetch.test.ts"
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"
@@ -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 {baseDir}/scripts/asr.mjs --url "https://example.com/audio.mp3"
16
- node {baseDir}/scripts/asr.mjs --file ./recording.mp3
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 {baseDir}/scripts/gen.mjs --prompt "A futuristic city at sunset"
16
- node {baseDir}/scripts/gen.mjs --prompt "A serene mountain landscape" --count 2 --size 4K
17
- node {baseDir}/scripts/gen.mjs --prompt "A hero's journey through magical lands" --sequential --max-sequential 5
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 generations, default `1`
24
- - `--size <size>` image size, default `2K`
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 `5`
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
- - URLs are printed directly and are suitable for immediate use.
40
+ - `{skillDir}` means the directory containing this `SKILL.md`.
41
+ - Successful runs print generated URLs directly.
42
+ - This skill does not expose base64 output.
@@ -12,22 +12,74 @@ Generate speech audio URLs from text using Coze TTS.
12
12
  ## Quick start
13
13
 
14
14
  ```bash
15
- node {baseDir}/scripts/tts.mjs --text "Hello, welcome to our service"
16
- node {baseDir}/scripts/tts.mjs --texts "Chapter 1" "Chapter 2" --speaker zh_male_m191_uranus_bigtts
17
- node {baseDir}/scripts/tts.mjs --text "Fast announcement" --speech-rate 30 --format mp3 --sample-rate 48000
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(
@@ -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({ config: clientConfig });
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 ?? 5,
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
- });
@@ -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
- });
@@ -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
- });
@@ -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
- });