@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/src/config.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { CozeConfig } from "coze-coding-dev-sdk";
|
|
5
|
+
import JSON5 from "json5";
|
|
6
|
+
|
|
7
|
+
type CozePluginConfig = {
|
|
8
|
+
apiKey?: unknown;
|
|
9
|
+
baseUrl?: unknown;
|
|
10
|
+
modelBaseUrl?: unknown;
|
|
11
|
+
retryTimes?: unknown;
|
|
12
|
+
retryDelay?: unknown;
|
|
13
|
+
timeout?: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type OpenClawConfigFile = {
|
|
17
|
+
plugins?: {
|
|
18
|
+
entries?: Record<
|
|
19
|
+
string,
|
|
20
|
+
{
|
|
21
|
+
config?: CozePluginConfig;
|
|
22
|
+
}
|
|
23
|
+
>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function readString(value: unknown): string | undefined {
|
|
28
|
+
if (typeof value !== "string") {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return trimmed ? trimmed : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readNumber(value: unknown): number | undefined {
|
|
36
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (typeof value !== "string") {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const parsed = Number(trimmed);
|
|
47
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveHomeDir(env: NodeJS.ProcessEnv): string {
|
|
51
|
+
const home = env.HOME?.trim() || env.USERPROFILE?.trim() || os.homedir();
|
|
52
|
+
return home || os.homedir();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function expandHomeDir(rawPath: string, env: NodeJS.ProcessEnv): string {
|
|
56
|
+
if (rawPath === "~") {
|
|
57
|
+
return resolveHomeDir(env);
|
|
58
|
+
}
|
|
59
|
+
if (rawPath.startsWith("~/") || rawPath.startsWith("~\\")) {
|
|
60
|
+
return path.join(resolveHomeDir(env), rawPath.slice(2));
|
|
61
|
+
}
|
|
62
|
+
return rawPath;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveOpenClawConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
66
|
+
const explicitPath = env.OPENCLAW_CONFIG_PATH?.trim() || env.CLAWDBOT_CONFIG_PATH?.trim();
|
|
67
|
+
if (explicitPath) {
|
|
68
|
+
return path.resolve(expandHomeDir(explicitPath, env));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const stateDir = env.OPENCLAW_STATE_DIR?.trim();
|
|
72
|
+
if (stateDir) {
|
|
73
|
+
return path.resolve(expandHomeDir(path.join(stateDir, "openclaw.json"), env));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return path.join(resolveHomeDir(env), ".openclaw", "openclaw.json");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveEnvTemplate(value: string, env: NodeJS.ProcessEnv): string | undefined {
|
|
80
|
+
const pattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
81
|
+
let missing = false;
|
|
82
|
+
const resolved = value.replace(pattern, (_match, envName: string) => {
|
|
83
|
+
const envValue = env[envName]?.trim();
|
|
84
|
+
if (!envValue) {
|
|
85
|
+
missing = true;
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
return envValue;
|
|
89
|
+
});
|
|
90
|
+
if (missing && !resolved.trim()) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return resolved.trim() ? resolved : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizePluginConfigFromFile(
|
|
97
|
+
value: CozePluginConfig | undefined,
|
|
98
|
+
env: NodeJS.ProcessEnv,
|
|
99
|
+
): CozePluginConfig | undefined {
|
|
100
|
+
if (!value || typeof value !== "object") {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const maybeResolveString = (input: unknown): string | undefined => {
|
|
105
|
+
if (typeof input !== "string") {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
return resolveEnvTemplate(input, env) ?? readString(input);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
apiKey: maybeResolveString(value.apiKey),
|
|
113
|
+
baseUrl: maybeResolveString(value.baseUrl),
|
|
114
|
+
modelBaseUrl: maybeResolveString(value.modelBaseUrl),
|
|
115
|
+
retryTimes:
|
|
116
|
+
typeof value.retryTimes === "string"
|
|
117
|
+
? maybeResolveString(value.retryTimes)
|
|
118
|
+
: value.retryTimes,
|
|
119
|
+
retryDelay:
|
|
120
|
+
typeof value.retryDelay === "string"
|
|
121
|
+
? maybeResolveString(value.retryDelay)
|
|
122
|
+
: value.retryDelay,
|
|
123
|
+
timeout: typeof value.timeout === "string" ? maybeResolveString(value.timeout) : value.timeout,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function loadCozePluginConfigFromOpenClawConfig(
|
|
128
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
129
|
+
): Promise<CozePluginConfig | undefined> {
|
|
130
|
+
const configPath = resolveOpenClawConfigPath(env);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
134
|
+
const parsed = JSON5.parse(content) as OpenClawConfigFile;
|
|
135
|
+
const pluginConfig = parsed.plugins?.entries?.["coze-openclaw-plugin"]?.config;
|
|
136
|
+
return normalizePluginConfigFromFile(pluginConfig, env);
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function resolveCozeClientConfig(
|
|
143
|
+
pluginConfig?: CozePluginConfig,
|
|
144
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
145
|
+
): CozeConfig {
|
|
146
|
+
return {
|
|
147
|
+
apiKey: readString(pluginConfig?.apiKey),
|
|
148
|
+
baseUrl: readString(pluginConfig?.baseUrl),
|
|
149
|
+
modelBaseUrl: readString(pluginConfig?.modelBaseUrl),
|
|
150
|
+
retryTimes: readNumber(pluginConfig?.retryTimes),
|
|
151
|
+
retryDelay: readNumber(pluginConfig?.retryDelay),
|
|
152
|
+
timeout: readNumber(pluginConfig?.timeout),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getMissingCozeConfigMessage(): string {
|
|
157
|
+
return "Coze API key missing. Set plugins.entries.coze-openclaw-plugin.config.apiKey.";
|
|
158
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import type { CozeConfig } from "coze-coding-dev-sdk";
|
|
4
|
+
import { createAsrClient } from "../client.js";
|
|
5
|
+
|
|
6
|
+
export type AsrInput = {
|
|
7
|
+
url?: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AsrResult = {
|
|
12
|
+
text: string;
|
|
13
|
+
duration?: number;
|
|
14
|
+
segments?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function transcribeSpeech(
|
|
18
|
+
input: AsrInput,
|
|
19
|
+
clientConfig: CozeConfig,
|
|
20
|
+
): Promise<AsrResult> {
|
|
21
|
+
const client = await createAsrClient({ config: clientConfig });
|
|
22
|
+
const request =
|
|
23
|
+
input.file !== undefined
|
|
24
|
+
? {
|
|
25
|
+
uid: `coze-openclaw-plugin-asr-${randomUUID()}`,
|
|
26
|
+
base64Data: fs.readFileSync(input.file).toString("base64"),
|
|
27
|
+
}
|
|
28
|
+
: {
|
|
29
|
+
uid: `coze-openclaw-plugin-asr-${randomUUID()}`,
|
|
30
|
+
url: input.url,
|
|
31
|
+
};
|
|
32
|
+
const result = await client.recognize(request);
|
|
33
|
+
return {
|
|
34
|
+
text: result.text,
|
|
35
|
+
duration: result.duration,
|
|
36
|
+
segments: result.utterances?.length,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CozeConfig, FetchContentItem, FetchResponse } from "coze-coding-dev-sdk";
|
|
2
|
+
import { createFetchClient } from "../client.js";
|
|
3
|
+
|
|
4
|
+
export type FetchInput = {
|
|
5
|
+
urls: string[];
|
|
6
|
+
textOnly?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type FetchResultLink = {
|
|
10
|
+
title?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type FetchResultImage = {
|
|
15
|
+
url?: string;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type FetchResultItem = {
|
|
21
|
+
url: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
text: string;
|
|
24
|
+
links: FetchResultLink[];
|
|
25
|
+
images: FetchResultImage[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function joinText(items: FetchContentItem[]): string {
|
|
29
|
+
return items
|
|
30
|
+
.filter((item) => item.type === "text" && item.text)
|
|
31
|
+
.map((item) => item.text?.trim() ?? "")
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join("\n\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function collectLinks(items: FetchContentItem[]): FetchResultLink[] {
|
|
37
|
+
return items
|
|
38
|
+
.filter((item) => item.type === "link")
|
|
39
|
+
.map((item) => ({
|
|
40
|
+
title: item.text,
|
|
41
|
+
url: item.url,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function collectImages(items: FetchContentItem[]): FetchResultImage[] {
|
|
46
|
+
return items
|
|
47
|
+
.filter((item) => item.type === "image")
|
|
48
|
+
.map((item) => ({
|
|
49
|
+
url: item.image?.display_url ?? item.image?.image_url,
|
|
50
|
+
width: item.image?.width,
|
|
51
|
+
height: item.image?.height,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeFetchedItem(response: FetchResponse, textOnly: boolean): FetchResultItem {
|
|
56
|
+
const content = response.content ?? [];
|
|
57
|
+
return {
|
|
58
|
+
url: response.url ?? "",
|
|
59
|
+
title: response.title,
|
|
60
|
+
text: joinText(content),
|
|
61
|
+
links: textOnly ? [] : collectLinks(content),
|
|
62
|
+
images: textOnly ? [] : collectImages(content),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function fetchContent(
|
|
67
|
+
input: FetchInput,
|
|
68
|
+
clientConfig: CozeConfig,
|
|
69
|
+
): Promise<FetchResultItem[]> {
|
|
70
|
+
const client = await createFetchClient({ config: clientConfig });
|
|
71
|
+
const results: FetchResultItem[] = [];
|
|
72
|
+
|
|
73
|
+
for (const url of input.urls) {
|
|
74
|
+
const response = await client.fetch(url);
|
|
75
|
+
results.push(normalizeFetchedItem(response, input.textOnly === true));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CozeConfig, ImageGenerationRequest } from "coze-coding-dev-sdk";
|
|
2
|
+
import { createImageGenerationClient } from "../client.js";
|
|
3
|
+
|
|
4
|
+
export type ImageGenerationInput = {
|
|
5
|
+
prompt: string;
|
|
6
|
+
count?: number;
|
|
7
|
+
size?: string;
|
|
8
|
+
sequential?: boolean;
|
|
9
|
+
maxSequential?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ImageGenerationResult = {
|
|
13
|
+
prompt: string;
|
|
14
|
+
urls: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function generateImages(
|
|
18
|
+
input: ImageGenerationInput,
|
|
19
|
+
clientConfig: CozeConfig,
|
|
20
|
+
): Promise<ImageGenerationResult[]> {
|
|
21
|
+
const client = await createImageGenerationClient({ config: clientConfig });
|
|
22
|
+
const count = input.count ?? 1;
|
|
23
|
+
const results: ImageGenerationResult[] = [];
|
|
24
|
+
|
|
25
|
+
for (let index = 0; index < count; index += 1) {
|
|
26
|
+
const request: ImageGenerationRequest = {
|
|
27
|
+
prompt: input.prompt,
|
|
28
|
+
size: input.size ?? "2K",
|
|
29
|
+
sequentialImageGeneration: input.sequential ? "auto" : "disabled",
|
|
30
|
+
sequentialImageGenerationMaxImages: input.maxSequential ?? 5,
|
|
31
|
+
};
|
|
32
|
+
const response = await client.generate(request);
|
|
33
|
+
const helper = client.getResponseHelper(response);
|
|
34
|
+
results.push({
|
|
35
|
+
prompt: input.prompt,
|
|
36
|
+
urls: helper.imageUrls,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { CozeConfig, ImageItem, SearchResponse, WebItem } from "coze-coding-dev-sdk";
|
|
2
|
+
import { createSearchClient } from "../client.js";
|
|
3
|
+
|
|
4
|
+
export type SearchInput = {
|
|
5
|
+
query: string;
|
|
6
|
+
type?: "web" | "image";
|
|
7
|
+
count?: number;
|
|
8
|
+
timeRange?: string;
|
|
9
|
+
sites?: string;
|
|
10
|
+
blockHosts?: string;
|
|
11
|
+
needSummary?: boolean;
|
|
12
|
+
needContent?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SearchResultItem = {
|
|
16
|
+
title: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
siteName?: string;
|
|
19
|
+
snippet?: string;
|
|
20
|
+
content?: string;
|
|
21
|
+
publishTime?: string;
|
|
22
|
+
imageUrl?: string;
|
|
23
|
+
imageWidth?: number;
|
|
24
|
+
imageHeight?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type SearchResult = {
|
|
28
|
+
query: string;
|
|
29
|
+
type: "web" | "image";
|
|
30
|
+
summary?: string;
|
|
31
|
+
items: SearchResultItem[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function mapWebItem(item: WebItem): SearchResultItem {
|
|
35
|
+
return {
|
|
36
|
+
title: item.title,
|
|
37
|
+
url: item.url,
|
|
38
|
+
siteName: item.site_name,
|
|
39
|
+
snippet: item.snippet,
|
|
40
|
+
content: item.content,
|
|
41
|
+
publishTime: item.publish_time,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mapImageItem(item: ImageItem): SearchResultItem {
|
|
46
|
+
return {
|
|
47
|
+
title: item.title ?? "Untitled",
|
|
48
|
+
url: item.url,
|
|
49
|
+
siteName: item.site_name,
|
|
50
|
+
publishTime: item.publish_time,
|
|
51
|
+
imageUrl: item.image?.url,
|
|
52
|
+
imageWidth: item.image?.width,
|
|
53
|
+
imageHeight: item.image?.height,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasAdvancedOptions(input: SearchInput): boolean {
|
|
58
|
+
return Boolean(input.timeRange || input.sites || input.blockHosts || input.needContent);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function searchWeb(input: SearchInput, clientConfig: CozeConfig): Promise<SearchResult> {
|
|
62
|
+
const client = await createSearchClient({ config: clientConfig });
|
|
63
|
+
const type = input.type ?? "web";
|
|
64
|
+
const count = input.count ?? 10;
|
|
65
|
+
let response: SearchResponse;
|
|
66
|
+
|
|
67
|
+
if (type === "image") {
|
|
68
|
+
response = await client.imageSearch(input.query, count);
|
|
69
|
+
return {
|
|
70
|
+
query: input.query,
|
|
71
|
+
type,
|
|
72
|
+
items: (response.image_items ?? []).map(mapImageItem),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (hasAdvancedOptions(input)) {
|
|
77
|
+
response = await client.advancedSearch(input.query, {
|
|
78
|
+
searchType: "web",
|
|
79
|
+
count,
|
|
80
|
+
timeRange: input.timeRange,
|
|
81
|
+
sites: input.sites,
|
|
82
|
+
blockHosts: input.blockHosts,
|
|
83
|
+
needSummary: input.needSummary,
|
|
84
|
+
needContent: input.needContent,
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
response = await client.webSearch(input.query, count, input.needSummary);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
query: input.query,
|
|
92
|
+
type,
|
|
93
|
+
summary: response.summary,
|
|
94
|
+
items: (response.web_items ?? []).map(mapWebItem),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AudioFormat, CozeConfig, SampleRate } from "coze-coding-dev-sdk";
|
|
3
|
+
import { createTtsClient } from "../client.js";
|
|
4
|
+
|
|
5
|
+
export type TtsInput = {
|
|
6
|
+
texts: string[];
|
|
7
|
+
speaker?: string;
|
|
8
|
+
format?: AudioFormat;
|
|
9
|
+
sampleRate?: SampleRate;
|
|
10
|
+
speechRate?: number;
|
|
11
|
+
loudnessRate?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type TtsResult = {
|
|
15
|
+
text: string;
|
|
16
|
+
audioUri: string;
|
|
17
|
+
audioSize: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function synthesizeSpeech(
|
|
21
|
+
input: TtsInput,
|
|
22
|
+
clientConfig: CozeConfig,
|
|
23
|
+
): Promise<TtsResult[]> {
|
|
24
|
+
const client = await createTtsClient({ config: clientConfig });
|
|
25
|
+
const results: TtsResult[] = [];
|
|
26
|
+
|
|
27
|
+
for (const text of input.texts) {
|
|
28
|
+
const response = await client.synthesize({
|
|
29
|
+
uid: `coze-openclaw-plugin-tts-${randomUUID()}`,
|
|
30
|
+
text,
|
|
31
|
+
speaker: input.speaker,
|
|
32
|
+
audioFormat: input.format,
|
|
33
|
+
sampleRate: input.sampleRate,
|
|
34
|
+
speechRate: input.speechRate,
|
|
35
|
+
loudnessRate: input.loudnessRate,
|
|
36
|
+
});
|
|
37
|
+
results.push({
|
|
38
|
+
text,
|
|
39
|
+
audioUri: response.audioUri,
|
|
40
|
+
audioSize: response.audioSize,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return results;
|
|
45
|
+
}
|
package/src/skill-cli.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { CozeConfig } from "coze-coding-dev-sdk";
|
|
2
|
+
import { formatCozeError } from "./client.js";
|
|
3
|
+
import {
|
|
4
|
+
getMissingCozeConfigMessage,
|
|
5
|
+
loadCozePluginConfigFromOpenClawConfig,
|
|
6
|
+
resolveCozeClientConfig,
|
|
7
|
+
} from "./config.js";
|
|
8
|
+
import { transcribeSpeech } from "./shared/asr.js";
|
|
9
|
+
import { generateImages } from "./shared/image-gen.js";
|
|
10
|
+
import { synthesizeSpeech } from "./shared/tts.js";
|
|
11
|
+
|
|
12
|
+
type SkillIo = {
|
|
13
|
+
log: (message: string) => void;
|
|
14
|
+
error: (message: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function createDefaultIo(): SkillIo {
|
|
18
|
+
return {
|
|
19
|
+
log(message: string) {
|
|
20
|
+
console.log(message);
|
|
21
|
+
},
|
|
22
|
+
error(message: string) {
|
|
23
|
+
console.error(message);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function requireConfig(env: NodeJS.ProcessEnv): Promise<CozeConfig> {
|
|
29
|
+
const pluginConfig = await loadCozePluginConfigFromOpenClawConfig(env);
|
|
30
|
+
const config = resolveCozeClientConfig(pluginConfig, env);
|
|
31
|
+
if (!config.apiKey) {
|
|
32
|
+
throw new Error(getMissingCozeConfigMessage());
|
|
33
|
+
}
|
|
34
|
+
return config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readArg(args: string[], name: string): string | undefined {
|
|
38
|
+
const index = args.indexOf(name);
|
|
39
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readTrailingValues(args: string[], name: string): string[] {
|
|
43
|
+
const index = args.indexOf(name);
|
|
44
|
+
if (index < 0) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const values: string[] = [];
|
|
48
|
+
for (let cursor = index + 1; cursor < args.length; cursor += 1) {
|
|
49
|
+
const value = args[cursor];
|
|
50
|
+
if (value.startsWith("--")) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
values.push(value);
|
|
54
|
+
}
|
|
55
|
+
return values;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readNumberArg(args: string[], name: string): number | undefined {
|
|
59
|
+
const value = readArg(args, name);
|
|
60
|
+
if (!value) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const parsed = Number(value);
|
|
64
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runImageCli(
|
|
68
|
+
args: string[],
|
|
69
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
70
|
+
io: SkillIo = createDefaultIo(),
|
|
71
|
+
): Promise<number> {
|
|
72
|
+
const prompt = readArg(args, "--prompt");
|
|
73
|
+
if (!prompt) {
|
|
74
|
+
io.error("Error: --prompt is required");
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const config = await requireConfig(env);
|
|
79
|
+
const results = await generateImages(
|
|
80
|
+
{
|
|
81
|
+
prompt,
|
|
82
|
+
count: readNumberArg(args, "--count"),
|
|
83
|
+
size: readArg(args, "--size"),
|
|
84
|
+
sequential: args.includes("--sequential"),
|
|
85
|
+
maxSequential: readNumberArg(args, "--max-sequential"),
|
|
86
|
+
},
|
|
87
|
+
config,
|
|
88
|
+
);
|
|
89
|
+
for (const [index, item] of results.entries()) {
|
|
90
|
+
io.log(`[${index + 1}/${results.length}] ${item.prompt}`);
|
|
91
|
+
for (const url of item.urls) {
|
|
92
|
+
io.log(` ${url}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return 0;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
io.error(`Error: ${formatCozeError(error)}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runTtsCli(
|
|
103
|
+
args: string[],
|
|
104
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
105
|
+
io: SkillIo = createDefaultIo(),
|
|
106
|
+
): Promise<number> {
|
|
107
|
+
const text = readArg(args, "--text");
|
|
108
|
+
const texts = readTrailingValues(args, "--texts");
|
|
109
|
+
const mergedTexts = text ? [text] : texts;
|
|
110
|
+
if (mergedTexts.length === 0) {
|
|
111
|
+
io.error("Error: --text or --texts is required");
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const config = await requireConfig(env);
|
|
116
|
+
const results = await synthesizeSpeech(
|
|
117
|
+
{
|
|
118
|
+
texts: mergedTexts,
|
|
119
|
+
speaker: readArg(args, "--speaker"),
|
|
120
|
+
format: readArg(args, "--format") as "mp3" | "pcm" | "ogg_opus" | undefined,
|
|
121
|
+
sampleRate: readNumberArg(args, "--sample-rate") as
|
|
122
|
+
| 8000
|
|
123
|
+
| 16000
|
|
124
|
+
| 22050
|
|
125
|
+
| 24000
|
|
126
|
+
| 32000
|
|
127
|
+
| 44100
|
|
128
|
+
| 48000
|
|
129
|
+
| undefined,
|
|
130
|
+
speechRate: readNumberArg(args, "--speech-rate"),
|
|
131
|
+
loudnessRate: readNumberArg(args, "--loudness-rate"),
|
|
132
|
+
},
|
|
133
|
+
config,
|
|
134
|
+
);
|
|
135
|
+
for (const [index, item] of results.entries()) {
|
|
136
|
+
io.log(
|
|
137
|
+
`[${index + 1}/${results.length}] ${item.text.slice(0, 50)}${item.text.length > 50 ? "..." : ""}`,
|
|
138
|
+
);
|
|
139
|
+
io.log(` ${item.audioUri}`);
|
|
140
|
+
}
|
|
141
|
+
return 0;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
io.error(`Error: ${formatCozeError(error)}`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function runAsrCli(
|
|
149
|
+
args: string[],
|
|
150
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
151
|
+
io: SkillIo = createDefaultIo(),
|
|
152
|
+
): Promise<number> {
|
|
153
|
+
const url = readArg(args, "--url") ?? readArg(args, "-u");
|
|
154
|
+
const file = readArg(args, "--file") ?? readArg(args, "-f");
|
|
155
|
+
if (!url && !file) {
|
|
156
|
+
io.error("Error: --url or --file is required");
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const config = await requireConfig(env);
|
|
161
|
+
const result = await transcribeSpeech({ url, file }, config);
|
|
162
|
+
io.log("============================================================");
|
|
163
|
+
io.log("TRANSCRIPTION");
|
|
164
|
+
io.log("============================================================");
|
|
165
|
+
io.log(result.text);
|
|
166
|
+
io.log("============================================================");
|
|
167
|
+
if (result.duration !== undefined) {
|
|
168
|
+
io.log(`Duration: ${result.duration}`);
|
|
169
|
+
}
|
|
170
|
+
if (result.segments !== undefined) {
|
|
171
|
+
io.log(`Segments: ${result.segments}`);
|
|
172
|
+
}
|
|
173
|
+
return 0;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
io.error(`Error: ${formatCozeError(error)}`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
});
|