@cozeclaw/coze-openclaw-plugin 0.1.0
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/LICENSE +21 -0
- package/README.md +125 -0
- package/index.test.ts +63 -0
- package/index.ts +25 -0
- package/openclaw.plugin.json +57 -0
- package/package.json +25 -0
- package/skills/coze-asr/SKILL.md +27 -0
- package/skills/coze-asr/scripts/asr.mjs +9 -0
- package/skills/coze-image-gen/SKILL.md +31 -0
- package/skills/coze-image-gen/scripts/gen.mjs +9 -0
- package/skills/coze-tts/SKILL.md +33 -0
- package/skills/coze-tts/scripts/tts.mjs +9 -0
- package/src/client.ts +71 -0
- package/src/config.test.ts +130 -0
- package/src/config.ts +158 -0
- package/src/shared/asr.ts +38 -0
- package/src/shared/fetch.ts +79 -0
- package/src/shared/image-gen.ts +41 -0
- package/src/shared/search.ts +96 -0
- package/src/shared/tts.ts +45 -0
- package/src/skill-cli.ts +178 -0
- package/src/tools/web-fetch.test.ts +68 -0
- package/src/tools/web-fetch.ts +125 -0
- package/src/tools/web-search.test.ts +106 -0
- package/src/tools/web-search.ts +126 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 coze-dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @cozeclaw/coze-openclaw-plugin
|
|
2
|
+
|
|
3
|
+
OpenClaw plugin that adds Coze-powered web tools and bundled media skills.
|
|
4
|
+
|
|
5
|
+
- Tools: `coze_web_search`, `coze_web_fetch`
|
|
6
|
+
- Skills: `coze-tts`, `coze-asr`, `coze-image-gen`
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
openclaw plugins install @cozeclaw/coze-openclaw-plugin
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Configure
|
|
15
|
+
|
|
16
|
+
This plugin requires:
|
|
17
|
+
|
|
18
|
+
- `plugins.entries.coze-openclaw-plugin.config.apiKey`
|
|
19
|
+
|
|
20
|
+
Minimal configuration:
|
|
21
|
+
|
|
22
|
+
```json5
|
|
23
|
+
{
|
|
24
|
+
plugins: {
|
|
25
|
+
entries: {
|
|
26
|
+
"coze-openclaw-plugin": {
|
|
27
|
+
enabled: true,
|
|
28
|
+
config: {
|
|
29
|
+
apiKey: "YOUR_COZE_API_KEY"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Optional config fields:
|
|
38
|
+
|
|
39
|
+
- `baseUrl`
|
|
40
|
+
- `modelBaseUrl`
|
|
41
|
+
- `retryTimes`
|
|
42
|
+
- `retryDelay`
|
|
43
|
+
- `timeout`
|
|
44
|
+
|
|
45
|
+
## Replace Built-in Web Tools
|
|
46
|
+
|
|
47
|
+
If you want to use this plugin instead of OpenClaw built-in web search and fetch:
|
|
48
|
+
|
|
49
|
+
```json5
|
|
50
|
+
{
|
|
51
|
+
plugins: {
|
|
52
|
+
entries: {
|
|
53
|
+
"coze-openclaw-plugin": {
|
|
54
|
+
enabled: true,
|
|
55
|
+
config: {
|
|
56
|
+
apiKey: "YOUR_COZE_API_KEY"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
tools: {
|
|
62
|
+
web: {
|
|
63
|
+
search: {
|
|
64
|
+
enabled: false
|
|
65
|
+
},
|
|
66
|
+
fetch: {
|
|
67
|
+
enabled: false
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tools
|
|
75
|
+
|
|
76
|
+
### `coze_web_search`
|
|
77
|
+
|
|
78
|
+
Search the web or images through Coze.
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
|
|
82
|
+
- `query`
|
|
83
|
+
- `type`: `web` or `image`
|
|
84
|
+
- `count`
|
|
85
|
+
- `timeRange`
|
|
86
|
+
- `sites`
|
|
87
|
+
- `blockHosts`
|
|
88
|
+
- `needSummary`
|
|
89
|
+
- `needContent`
|
|
90
|
+
|
|
91
|
+
### `coze_web_fetch`
|
|
92
|
+
|
|
93
|
+
Fetch and normalize page or document content through Coze.
|
|
94
|
+
|
|
95
|
+
Parameters:
|
|
96
|
+
|
|
97
|
+
- `urls`
|
|
98
|
+
- `format`: `text`, `markdown`, or `json`
|
|
99
|
+
- `textOnly`
|
|
100
|
+
|
|
101
|
+
## Skills
|
|
102
|
+
|
|
103
|
+
Bundled skills:
|
|
104
|
+
|
|
105
|
+
- `coze-tts`: text to speech
|
|
106
|
+
- `coze-asr`: speech to text
|
|
107
|
+
- `coze-image-gen`: image generation
|
|
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
|
+
## Notes
|
|
118
|
+
|
|
119
|
+
- The plugin requires `apiKey` in plugin config and does not fall back to `COZE_*` environment variables
|
|
120
|
+
- Skill visibility depends on `plugins.entries.coze-openclaw-plugin.config.apiKey`
|
|
121
|
+
- `coze_web_fetch` fetches URLs sequentially
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
package/index.test.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { createCozeWebFetchTool } from "./src/tools/web-fetch.js";
|
|
3
|
+
import { createCozeWebSearchTool } from "./src/tools/web-search.js";
|
|
4
|
+
|
|
5
|
+
const plugin = {
|
|
6
|
+
id: "coze-openclaw-plugin",
|
|
7
|
+
name: "Coze OpenClaw Plugin",
|
|
8
|
+
description: "Coze web tools and bundled generation skills for OpenClaw.",
|
|
9
|
+
register(api: OpenClawPluginApi) {
|
|
10
|
+
api.registerTool(
|
|
11
|
+
createCozeWebSearchTool({
|
|
12
|
+
pluginConfig: api.pluginConfig,
|
|
13
|
+
logger: api.logger,
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
api.registerTool(
|
|
17
|
+
createCozeWebFetchTool({
|
|
18
|
+
pluginConfig: api.pluginConfig,
|
|
19
|
+
logger: api.logger,
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default plugin;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "coze-openclaw-plugin",
|
|
3
|
+
"name": "Coze OpenClaw Plugin",
|
|
4
|
+
"description": "Coze web tools and bundled generation skills for OpenClaw.",
|
|
5
|
+
"skills": ["./skills"],
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": ["apiKey"],
|
|
10
|
+
"properties": {
|
|
11
|
+
"apiKey": {
|
|
12
|
+
"type": "string"
|
|
13
|
+
},
|
|
14
|
+
"baseUrl": {
|
|
15
|
+
"type": "string"
|
|
16
|
+
},
|
|
17
|
+
"modelBaseUrl": {
|
|
18
|
+
"type": "string"
|
|
19
|
+
},
|
|
20
|
+
"retryTimes": {
|
|
21
|
+
"type": "number"
|
|
22
|
+
},
|
|
23
|
+
"retryDelay": {
|
|
24
|
+
"type": "number"
|
|
25
|
+
},
|
|
26
|
+
"timeout": {
|
|
27
|
+
"type": "number"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"uiHints": {
|
|
32
|
+
"apiKey": {
|
|
33
|
+
"label": "Coze API Key",
|
|
34
|
+
"sensitive": true
|
|
35
|
+
},
|
|
36
|
+
"baseUrl": {
|
|
37
|
+
"label": "Coze Base URL",
|
|
38
|
+
"advanced": true
|
|
39
|
+
},
|
|
40
|
+
"modelBaseUrl": {
|
|
41
|
+
"label": "Coze Model Base URL",
|
|
42
|
+
"advanced": true
|
|
43
|
+
},
|
|
44
|
+
"retryTimes": {
|
|
45
|
+
"label": "Retry Times",
|
|
46
|
+
"advanced": true
|
|
47
|
+
},
|
|
48
|
+
"retryDelay": {
|
|
49
|
+
"label": "Retry Delay (ms)",
|
|
50
|
+
"advanced": true
|
|
51
|
+
},
|
|
52
|
+
"timeout": {
|
|
53
|
+
"label": "Request Timeout (ms)",
|
|
54
|
+
"advanced": true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cozeclaw/coze-openclaw-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw Coze tools and bundled skills",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"openclaw": "v2026.2.6"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@sinclair/typebox": "0.34.48",
|
|
11
|
+
"coze-coding-dev-sdk": "0.7.17",
|
|
12
|
+
"json5": "2.2.3",
|
|
13
|
+
"jiti": "2.6.1"
|
|
14
|
+
},
|
|
15
|
+
"openclaw": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"./index.ts"
|
|
18
|
+
],
|
|
19
|
+
"install": {
|
|
20
|
+
"npmSpec": "@cozeclaw/coze-openclaw-plugin",
|
|
21
|
+
"localPath": "extensions/coze-openclaw-plugin",
|
|
22
|
+
"defaultChoice": "npm"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coze-asr
|
|
3
|
+
description: Convert speech audio to text using Coze ASR. Use when you need to transcribe spoken content from a remote audio URL or a local audio file into plain text.
|
|
4
|
+
homepage: https://www.coze.com
|
|
5
|
+
metadata: { "openclaw": { "emoji": "🎙️", "requires": { "bins": ["node"], "config": ["plugins.entries.coze-openclaw-plugin.config.apiKey"] } } }
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Coze ASR
|
|
9
|
+
|
|
10
|
+
Transcribe audio from a URL or local file using Coze ASR.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
node {baseDir}/scripts/asr.mjs --url "https://example.com/audio.mp3"
|
|
16
|
+
node {baseDir}/scripts/asr.mjs --file ./recording.mp3
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Options
|
|
20
|
+
|
|
21
|
+
- `--url <url>` remote audio URL
|
|
22
|
+
- `--file <path>` local audio file
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
|
|
26
|
+
- The skill runtime requires `plugins.entries.coze-openclaw-plugin.config.apiKey`.
|
|
27
|
+
- Local files are read and uploaded as base64 audio content.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import createJiti from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url);
|
|
6
|
+
const { runAsrCli } = await jiti.import("../../../src/skill-cli.ts");
|
|
7
|
+
|
|
8
|
+
const code = await runAsrCli(process.argv.slice(2), process.env);
|
|
9
|
+
process.exit(code);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coze-image-gen
|
|
3
|
+
description: Generate one or more images from text prompts using Coze image generation. Use when you need to create images from a natural-language prompt, produce multiple image variants, or generate sequential frames from the same prompt.
|
|
4
|
+
homepage: https://www.coze.com
|
|
5
|
+
metadata: { "openclaw": { "emoji": "🎨", "requires": { "bins": ["node"], "config": ["plugins.entries.coze-openclaw-plugin.config.apiKey"] } } }
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Coze Image Generation
|
|
9
|
+
|
|
10
|
+
Generate one or more images from a prompt using Coze.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```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
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Options
|
|
21
|
+
|
|
22
|
+
- `--prompt <text>` required prompt text
|
|
23
|
+
- `--count <n>` number of generations, default `1`
|
|
24
|
+
- `--size <size>` image size, default `2K`
|
|
25
|
+
- `--sequential` enable sequential image generation
|
|
26
|
+
- `--max-sequential <n>` max sequential frames, default `5`
|
|
27
|
+
|
|
28
|
+
## Notes
|
|
29
|
+
|
|
30
|
+
- The skill runtime requires `plugins.entries.coze-openclaw-plugin.config.apiKey`.
|
|
31
|
+
- URLs are printed directly and are suitable for immediate use.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import createJiti from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url);
|
|
6
|
+
const { runImageCli } = await jiti.import("../../../src/skill-cli.ts");
|
|
7
|
+
|
|
8
|
+
const code = await runImageCli(process.argv.slice(2), process.env);
|
|
9
|
+
process.exit(code);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coze-tts
|
|
3
|
+
description: Convert text to speech using Coze TTS. Use when you need to synthesize spoken audio from one text input or multiple text segments, optionally with a specific speaker, format, sample rate, or speech settings.
|
|
4
|
+
homepage: https://www.coze.com
|
|
5
|
+
metadata: { "openclaw": { "emoji": "🔊", "requires": { "bins": ["node"], "config": ["plugins.entries.coze-openclaw-plugin.config.apiKey"] } } }
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Coze TTS
|
|
9
|
+
|
|
10
|
+
Generate speech audio URLs from text using Coze TTS.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
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
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Options
|
|
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
|
|
29
|
+
|
|
30
|
+
## Notes
|
|
31
|
+
|
|
32
|
+
- The skill runtime requires `plugins.entries.coze-openclaw-plugin.config.apiKey`.
|
|
33
|
+
- The script prints one audio URL per generated segment.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import createJiti from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url);
|
|
6
|
+
const { runTtsCli } = await jiti.import("../../../src/skill-cli.ts");
|
|
7
|
+
|
|
8
|
+
const code = await runTtsCli(process.argv.slice(2), process.env);
|
|
9
|
+
process.exit(code);
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CozeConfig } from "coze-coding-dev-sdk";
|
|
2
|
+
|
|
3
|
+
type CozeSdkModule = typeof import("coze-coding-dev-sdk");
|
|
4
|
+
type ConfigurableClientConstructor<TClient> = new (
|
|
5
|
+
config: InstanceType<CozeSdkModule["Config"]>,
|
|
6
|
+
) => TClient;
|
|
7
|
+
|
|
8
|
+
let sdkPromise: Promise<CozeSdkModule> | null = null;
|
|
9
|
+
|
|
10
|
+
export type CozeClientFactoryParams = {
|
|
11
|
+
config: CozeConfig;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function loadCozeSdk(): Promise<CozeSdkModule> {
|
|
15
|
+
if (!sdkPromise) {
|
|
16
|
+
sdkPromise = import("coze-coding-dev-sdk");
|
|
17
|
+
}
|
|
18
|
+
return sdkPromise;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function createClient<TClient>(
|
|
22
|
+
params: CozeClientFactoryParams,
|
|
23
|
+
resolveCtor: (sdk: CozeSdkModule) => ConfigurableClientConstructor<TClient>,
|
|
24
|
+
): Promise<TClient> {
|
|
25
|
+
const sdk = await loadCozeSdk();
|
|
26
|
+
const Client = resolveCtor(sdk);
|
|
27
|
+
return new Client(new sdk.Config(params.config));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function createSearchClient(
|
|
31
|
+
params: CozeClientFactoryParams,
|
|
32
|
+
): Promise<InstanceType<CozeSdkModule["SearchClient"]>> {
|
|
33
|
+
return createClient(params, (sdk) => sdk.SearchClient);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function createFetchClient(
|
|
37
|
+
params: CozeClientFactoryParams,
|
|
38
|
+
): Promise<InstanceType<CozeSdkModule["FetchClient"]>> {
|
|
39
|
+
return createClient(params, (sdk) => sdk.FetchClient);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createImageGenerationClient(
|
|
43
|
+
params: CozeClientFactoryParams,
|
|
44
|
+
): Promise<InstanceType<CozeSdkModule["ImageGenerationClient"]>> {
|
|
45
|
+
return createClient(params, (sdk) => sdk.ImageGenerationClient);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function createTtsClient(
|
|
49
|
+
params: CozeClientFactoryParams,
|
|
50
|
+
): Promise<InstanceType<CozeSdkModule["TTSClient"]>> {
|
|
51
|
+
return createClient(params, (sdk) => sdk.TTSClient);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function createAsrClient(
|
|
55
|
+
params: CozeClientFactoryParams,
|
|
56
|
+
): Promise<InstanceType<CozeSdkModule["ASRClient"]>> {
|
|
57
|
+
return createClient(params, (sdk) => sdk.ASRClient);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatCozeError(error: unknown): string {
|
|
61
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
62
|
+
const message = String(error.message);
|
|
63
|
+
const status =
|
|
64
|
+
"statusCode" in error && typeof error.statusCode === "number" ? ` (${error.statusCode})` : "";
|
|
65
|
+
return `${message}${status}`;
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
return error.message;
|
|
69
|
+
}
|
|
70
|
+
return String(error);
|
|
71
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
});
|