@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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Static, Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import { formatCozeError } from "../client.js";
|
|
4
|
+
import { getMissingCozeConfigMessage, resolveCozeClientConfig } from "../config.js";
|
|
5
|
+
import { fetchContent } from "../shared/fetch.js";
|
|
6
|
+
|
|
7
|
+
type ToolLogger = {
|
|
8
|
+
debug?: (message: string) => void;
|
|
9
|
+
info?: (message: string) => void;
|
|
10
|
+
warn: (message: string) => void;
|
|
11
|
+
error?: (message: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const FetchToolSchema = Type.Object(
|
|
15
|
+
{
|
|
16
|
+
urls: Type.Array(Type.String({ description: "URL to fetch." }), {
|
|
17
|
+
description: "One or more URLs to fetch.",
|
|
18
|
+
minItems: 1,
|
|
19
|
+
}),
|
|
20
|
+
format: Type.Optional(
|
|
21
|
+
Type.Unsafe<"text" | "markdown" | "json">({
|
|
22
|
+
type: "string",
|
|
23
|
+
enum: ["text", "markdown", "json"],
|
|
24
|
+
description: "Formatting preference for the textual output.",
|
|
25
|
+
}),
|
|
26
|
+
),
|
|
27
|
+
textOnly: Type.Optional(
|
|
28
|
+
Type.Boolean({ description: "Only return extracted text, without images or links." }),
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
{ additionalProperties: false },
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
type FetchToolParams = Static<typeof FetchToolSchema>;
|
|
35
|
+
|
|
36
|
+
function renderFetchedText(
|
|
37
|
+
items: Awaited<ReturnType<typeof fetchContent>>,
|
|
38
|
+
format: "text" | "markdown" | "json",
|
|
39
|
+
): string {
|
|
40
|
+
if (format === "json") {
|
|
41
|
+
return JSON.stringify(items, null, 2);
|
|
42
|
+
}
|
|
43
|
+
if (format === "markdown") {
|
|
44
|
+
return items
|
|
45
|
+
.map((item) => {
|
|
46
|
+
const parts = [`# ${item.title ?? item.url}`, "", `**URL**: ${item.url}`, "", item.text];
|
|
47
|
+
if (item.links.length > 0) {
|
|
48
|
+
parts.push("", "## Links");
|
|
49
|
+
for (const link of item.links) {
|
|
50
|
+
parts.push(`- [${link.title ?? link.url ?? "Link"}](${link.url ?? ""})`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return parts.join("\n");
|
|
54
|
+
})
|
|
55
|
+
.join("\n\n---\n\n");
|
|
56
|
+
}
|
|
57
|
+
return items
|
|
58
|
+
.map((item, index) => {
|
|
59
|
+
const lines = [`[${index + 1}] ${item.title ?? item.url}`, `URL: ${item.url}`, "", item.text];
|
|
60
|
+
if (item.links.length > 0) {
|
|
61
|
+
lines.push("", "Links:");
|
|
62
|
+
for (const link of item.links) {
|
|
63
|
+
lines.push(`- ${link.title ?? link.url ?? "Link"} -> ${link.url ?? ""}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (item.images.length > 0) {
|
|
67
|
+
lines.push("", "Images:");
|
|
68
|
+
for (const image of item.images) {
|
|
69
|
+
lines.push(`- ${image.url ?? ""}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
})
|
|
74
|
+
.join("\n\n============================================================\n\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildErrorResult(message: string) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
80
|
+
details: { error: true, message },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createCozeWebFetchTool(params: {
|
|
85
|
+
pluginConfig?: Record<string, unknown>;
|
|
86
|
+
logger: ToolLogger;
|
|
87
|
+
env?: NodeJS.ProcessEnv;
|
|
88
|
+
}): AnyAgentTool {
|
|
89
|
+
return {
|
|
90
|
+
name: "coze_web_fetch",
|
|
91
|
+
label: "Coze Web Fetch",
|
|
92
|
+
description:
|
|
93
|
+
"Fetch and extract structured content from web pages or documents through Coze.",
|
|
94
|
+
parameters: FetchToolSchema,
|
|
95
|
+
execute: async (_toolCallId, rawParams) => {
|
|
96
|
+
const toolParams = rawParams as FetchToolParams;
|
|
97
|
+
const clientConfig = resolveCozeClientConfig(params.pluginConfig, params.env);
|
|
98
|
+
if (!clientConfig.apiKey) {
|
|
99
|
+
return buildErrorResult(getMissingCozeConfigMessage());
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const items = await fetchContent(
|
|
103
|
+
{
|
|
104
|
+
urls: toolParams.urls,
|
|
105
|
+
textOnly: toolParams.textOnly,
|
|
106
|
+
},
|
|
107
|
+
clientConfig,
|
|
108
|
+
);
|
|
109
|
+
const format = toolParams.format ?? "text";
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: renderFetchedText(items, format) }],
|
|
112
|
+
details: {
|
|
113
|
+
count: items.length,
|
|
114
|
+
urls: items.map((item) => item.url),
|
|
115
|
+
items,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const message = formatCozeError(error);
|
|
120
|
+
params.logger.warn(`coze-web-fetch failed: ${message}`);
|
|
121
|
+
return buildErrorResult(message);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Static, Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AnyAgentTool } from "openclaw/plugin-sdk/core";
|
|
3
|
+
import { formatCozeError } from "../client.js";
|
|
4
|
+
import { getMissingCozeConfigMessage, resolveCozeClientConfig } from "../config.js";
|
|
5
|
+
import { searchWeb } from "../shared/search.js";
|
|
6
|
+
|
|
7
|
+
type ToolLogger = {
|
|
8
|
+
debug?: (message: string) => void;
|
|
9
|
+
info?: (message: string) => void;
|
|
10
|
+
warn: (message: string) => void;
|
|
11
|
+
error?: (message: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const SearchToolSchema = Type.Object(
|
|
15
|
+
{
|
|
16
|
+
query: Type.String({ description: "Search query." }),
|
|
17
|
+
type: Type.Optional(
|
|
18
|
+
Type.Unsafe<"web" | "image">({
|
|
19
|
+
type: "string",
|
|
20
|
+
enum: ["web", "image"],
|
|
21
|
+
description: "Search type. Defaults to web.",
|
|
22
|
+
}),
|
|
23
|
+
),
|
|
24
|
+
count: Type.Optional(
|
|
25
|
+
Type.Number({
|
|
26
|
+
description: "Number of results to return.",
|
|
27
|
+
minimum: 1,
|
|
28
|
+
maximum: 20,
|
|
29
|
+
}),
|
|
30
|
+
),
|
|
31
|
+
timeRange: Type.Optional(
|
|
32
|
+
Type.Unsafe<"1d" | "1w" | "1m">({
|
|
33
|
+
type: "string",
|
|
34
|
+
enum: ["1d", "1w", "1m"],
|
|
35
|
+
description: "Recency filter for web search.",
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
sites: Type.Optional(
|
|
39
|
+
Type.String({ description: "Comma-separated domains to include." }),
|
|
40
|
+
),
|
|
41
|
+
blockHosts: Type.Optional(
|
|
42
|
+
Type.String({ description: "Comma-separated domains to exclude." }),
|
|
43
|
+
),
|
|
44
|
+
needSummary: Type.Optional(
|
|
45
|
+
Type.Boolean({ description: "Whether to include Coze summary output." }),
|
|
46
|
+
),
|
|
47
|
+
needContent: Type.Optional(
|
|
48
|
+
Type.Boolean({ description: "Whether to include extracted page content." }),
|
|
49
|
+
),
|
|
50
|
+
},
|
|
51
|
+
{ additionalProperties: false },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
type SearchToolParams = Static<typeof SearchToolSchema>;
|
|
55
|
+
|
|
56
|
+
function buildSearchText(params: Awaited<ReturnType<typeof searchWeb>>): string {
|
|
57
|
+
const lines = [`Coze web search: ${params.query}`];
|
|
58
|
+
if (params.summary) {
|
|
59
|
+
lines.push("", `Summary: ${params.summary}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push("", `Results (${params.items.length})`);
|
|
62
|
+
for (const [index, item] of params.items.entries()) {
|
|
63
|
+
lines.push(`${index + 1}. ${item.title}`);
|
|
64
|
+
if (item.url) {
|
|
65
|
+
lines.push(` URL: ${item.url}`);
|
|
66
|
+
}
|
|
67
|
+
if (item.imageUrl) {
|
|
68
|
+
lines.push(` Image: ${item.imageUrl}`);
|
|
69
|
+
}
|
|
70
|
+
if (item.siteName) {
|
|
71
|
+
lines.push(` Source: ${item.siteName}`);
|
|
72
|
+
}
|
|
73
|
+
if (item.publishTime) {
|
|
74
|
+
lines.push(` Published: ${item.publishTime}`);
|
|
75
|
+
}
|
|
76
|
+
if (item.snippet) {
|
|
77
|
+
lines.push(` ${item.snippet}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildErrorResult(message: string) {
|
|
84
|
+
return {
|
|
85
|
+
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
86
|
+
details: { error: true, message },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createCozeWebSearchTool(params: {
|
|
91
|
+
pluginConfig?: Record<string, unknown>;
|
|
92
|
+
logger: ToolLogger;
|
|
93
|
+
env?: NodeJS.ProcessEnv;
|
|
94
|
+
}): AnyAgentTool {
|
|
95
|
+
return {
|
|
96
|
+
name: "coze_web_search",
|
|
97
|
+
label: "Coze Web Search",
|
|
98
|
+
description:
|
|
99
|
+
"Search the web or images through Coze. Supports summaries, recency filters, and site restrictions.",
|
|
100
|
+
parameters: SearchToolSchema,
|
|
101
|
+
execute: async (_toolCallId, rawParams) => {
|
|
102
|
+
const toolParams = rawParams as SearchToolParams;
|
|
103
|
+
const clientConfig = resolveCozeClientConfig(params.pluginConfig, params.env);
|
|
104
|
+
if (!clientConfig.apiKey) {
|
|
105
|
+
return buildErrorResult(getMissingCozeConfigMessage());
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const result = await searchWeb(toolParams, clientConfig);
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: buildSearchText(result) }],
|
|
111
|
+
details: {
|
|
112
|
+
query: result.query,
|
|
113
|
+
type: result.type,
|
|
114
|
+
summary: result.summary,
|
|
115
|
+
count: result.items.length,
|
|
116
|
+
items: result.items,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const message = formatCozeError(error);
|
|
121
|
+
params.logger.warn(`coze-web-search failed: ${message}`);
|
|
122
|
+
return buildErrorResult(message);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|