@cozeclaw/coze-openclaw-plugin 0.1.0 → 0.1.1
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 +1 -0
- package/package.json +6 -2
- package/src/skill-cli.test.ts +65 -0
- package/src/skill-cli.ts +50 -1
- package/src/tools/web-search.test.ts +34 -0
- package/src/tools/web-search.ts +14 -2
package/README.md
CHANGED
|
@@ -119,6 +119,7 @@ node {baseDir}/scripts/gen.mjs --prompt "A futuristic city at sunset"
|
|
|
119
119
|
- The plugin requires `apiKey` in plugin config and does not fall back to `COZE_*` environment variables
|
|
120
120
|
- Skill visibility depends on `plugins.entries.coze-openclaw-plugin.config.apiKey`
|
|
121
121
|
- `coze_web_fetch` fetches URLs sequentially
|
|
122
|
+
- Run `npm test` to execute the maintained unit tests under `./src`
|
|
122
123
|
|
|
123
124
|
## License
|
|
124
125
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cozeclaw/coze-openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenClaw Coze tools and bundled skills",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run src/config.test.ts src/skill-cli.test.ts src/tools/web-search.test.ts src/tools/web-fetch.test.ts"
|
|
8
|
+
},
|
|
6
9
|
"devDependencies": {
|
|
7
|
-
"openclaw": "v2026.2.6"
|
|
10
|
+
"openclaw": "v2026.2.6",
|
|
11
|
+
"vitest": "^3.2.4"
|
|
8
12
|
},
|
|
9
13
|
"dependencies": {
|
|
10
14
|
"@sinclair/typebox": "0.34.48",
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
});
|
package/src/skill-cli.ts
CHANGED
|
@@ -36,7 +36,14 @@ async function requireConfig(env: NodeJS.ProcessEnv): Promise<CozeConfig> {
|
|
|
36
36
|
|
|
37
37
|
function readArg(args: string[], name: string): string | undefined {
|
|
38
38
|
const index = args.indexOf(name);
|
|
39
|
-
|
|
39
|
+
if (index < 0) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const value = args[index + 1];
|
|
43
|
+
if (!value || value.startsWith("-")) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
function readTrailingValues(args: string[], name: string): string[] {
|
|
@@ -64,11 +71,35 @@ function readNumberArg(args: string[], name: string): number | undefined {
|
|
|
64
71
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
function findMissingValueFlag(args: string[], names: string[]): string | undefined {
|
|
75
|
+
for (const name of names) {
|
|
76
|
+
const index = args.indexOf(name);
|
|
77
|
+
if (index < 0) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const value = args[index + 1];
|
|
81
|
+
if (!value || value.startsWith("-")) {
|
|
82
|
+
return name;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
export async function runImageCli(
|
|
68
89
|
args: string[],
|
|
69
90
|
env: NodeJS.ProcessEnv = process.env,
|
|
70
91
|
io: SkillIo = createDefaultIo(),
|
|
71
92
|
): Promise<number> {
|
|
93
|
+
const missingValueFlag = findMissingValueFlag(args, [
|
|
94
|
+
"--prompt",
|
|
95
|
+
"--count",
|
|
96
|
+
"--size",
|
|
97
|
+
"--max-sequential",
|
|
98
|
+
]);
|
|
99
|
+
if (missingValueFlag) {
|
|
100
|
+
io.error(`Error: ${missingValueFlag} requires a value`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
72
103
|
const prompt = readArg(args, "--prompt");
|
|
73
104
|
if (!prompt) {
|
|
74
105
|
io.error("Error: --prompt is required");
|
|
@@ -104,6 +135,19 @@ export async function runTtsCli(
|
|
|
104
135
|
env: NodeJS.ProcessEnv = process.env,
|
|
105
136
|
io: SkillIo = createDefaultIo(),
|
|
106
137
|
): Promise<number> {
|
|
138
|
+
const missingValueFlag = findMissingValueFlag(args, [
|
|
139
|
+
"--text",
|
|
140
|
+
"--texts",
|
|
141
|
+
"--speaker",
|
|
142
|
+
"--format",
|
|
143
|
+
"--sample-rate",
|
|
144
|
+
"--speech-rate",
|
|
145
|
+
"--loudness-rate",
|
|
146
|
+
]);
|
|
147
|
+
if (missingValueFlag) {
|
|
148
|
+
io.error(`Error: ${missingValueFlag} requires a value`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
107
151
|
const text = readArg(args, "--text");
|
|
108
152
|
const texts = readTrailingValues(args, "--texts");
|
|
109
153
|
const mergedTexts = text ? [text] : texts;
|
|
@@ -150,6 +194,11 @@ export async function runAsrCli(
|
|
|
150
194
|
env: NodeJS.ProcessEnv = process.env,
|
|
151
195
|
io: SkillIo = createDefaultIo(),
|
|
152
196
|
): Promise<number> {
|
|
197
|
+
const missingValueFlag = findMissingValueFlag(args, ["--url", "-u", "--file", "-f"]);
|
|
198
|
+
if (missingValueFlag) {
|
|
199
|
+
io.error(`Error: ${missingValueFlag} requires a value`);
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
153
202
|
const url = readArg(args, "--url") ?? readArg(args, "-u");
|
|
154
203
|
const file = readArg(args, "--file") ?? readArg(args, "-f");
|
|
155
204
|
if (!url && !file) {
|
|
@@ -103,4 +103,38 @@ describe("createCozeWebSearchTool", () => {
|
|
|
103
103
|
});
|
|
104
104
|
expect(getTextContent(result)).toContain("https://example.com/image.png");
|
|
105
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
|
+
});
|
|
106
140
|
});
|
package/src/tools/web-search.ts
CHANGED
|
@@ -53,7 +53,10 @@ const SearchToolSchema = Type.Object(
|
|
|
53
53
|
|
|
54
54
|
type SearchToolParams = Static<typeof SearchToolSchema>;
|
|
55
55
|
|
|
56
|
-
function buildSearchText(
|
|
56
|
+
function buildSearchText(
|
|
57
|
+
params: Awaited<ReturnType<typeof searchWeb>>,
|
|
58
|
+
options: { includeContent: boolean },
|
|
59
|
+
): string {
|
|
57
60
|
const lines = [`Coze web search: ${params.query}`];
|
|
58
61
|
if (params.summary) {
|
|
59
62
|
lines.push("", `Summary: ${params.summary}`);
|
|
@@ -76,6 +79,10 @@ function buildSearchText(params: Awaited<ReturnType<typeof searchWeb>>): string
|
|
|
76
79
|
if (item.snippet) {
|
|
77
80
|
lines.push(` ${item.snippet}`);
|
|
78
81
|
}
|
|
82
|
+
if (options.includeContent && item.content) {
|
|
83
|
+
lines.push(" Content:");
|
|
84
|
+
lines.push(` ${item.content}`);
|
|
85
|
+
}
|
|
79
86
|
}
|
|
80
87
|
return lines.join("\n");
|
|
81
88
|
}
|
|
@@ -107,7 +114,12 @@ export function createCozeWebSearchTool(params: {
|
|
|
107
114
|
try {
|
|
108
115
|
const result = await searchWeb(toolParams, clientConfig);
|
|
109
116
|
return {
|
|
110
|
-
content: [
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: buildSearchText(result, { includeContent: toolParams.needContent === true }),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
111
123
|
details: {
|
|
112
124
|
query: result.query,
|
|
113
125
|
type: result.type,
|