@coze-arch/cli 0.0.18 → 0.0.19-beta.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/lib/__templates__/expo/.coze +1 -0
- package/lib/__templates__/expo/.cozeproj/scripts/validate.sh +8 -0
- package/lib/__templates__/expo/package.json +2 -1
- package/lib/__templates__/nextjs/.coze +1 -0
- package/lib/__templates__/nextjs/package.json +3 -1
- package/lib/__templates__/nextjs/scripts/validate.sh +10 -0
- package/lib/__templates__/nuxt-vue/.coze +1 -0
- package/lib/__templates__/nuxt-vue/eslint.config.mjs +25 -0
- package/lib/__templates__/nuxt-vue/package.json +9 -2
- package/lib/__templates__/nuxt-vue/pnpm-lock.yaml +790 -10
- package/lib/__templates__/nuxt-vue/scripts/validate.sh +10 -0
- package/lib/__templates__/pi-agent/.coze +10 -0
- package/lib/__templates__/pi-agent/AGENTS.md +144 -0
- package/lib/__templates__/pi-agent/README.md +216 -0
- package/lib/__templates__/pi-agent/_gitignore +3 -0
- package/lib/__templates__/pi-agent/_npmrc +23 -0
- package/lib/__templates__/pi-agent/bin/pi-bot.ts +8 -0
- package/lib/__templates__/pi-agent/docs/project-overview.md +374 -0
- package/lib/__templates__/pi-agent/docs/user/getting-started.md +47 -0
- package/lib/__templates__/pi-agent/package.json +63 -0
- package/lib/__templates__/pi-agent/pi-resources/SYSTEM.md +15 -0
- package/lib/__templates__/pi-agent/pi-resources/extensions/preference-memory/index.ts +355 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/SKILL.md +36 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-asr/scripts/asr.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/SKILL.md +41 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-image-gen/scripts/gen.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/SKILL.md +85 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-tts/scripts/tts.mjs +9 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/SKILL.md +53 -0
- package/lib/__templates__/pi-agent/pi-resources/skills/coze-video-gen/scripts/gen.mjs +9 -0
- package/lib/__templates__/pi-agent/pnpm-lock.yaml +8282 -0
- package/lib/__templates__/pi-agent/scripts/dev.sh +14 -0
- package/lib/__templates__/pi-agent/scripts/prepare.sh +35 -0
- package/lib/__templates__/pi-agent/src/agent.ts +363 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/index.ts +760 -0
- package/lib/__templates__/pi-agent/src/channels/feishu/streaming-card.ts +297 -0
- package/lib/__templates__/pi-agent/src/channels/wechat/index.ts +171 -0
- package/lib/__templates__/pi-agent/src/cli.ts +117 -0
- package/lib/__templates__/pi-agent/src/config.ts +708 -0
- package/lib/__templates__/pi-agent/src/core.ts +218 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/channels.ts +104 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/docs.ts +204 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/models.ts +98 -0
- package/lib/__templates__/pi-agent/src/dashboard/api/overview.ts +33 -0
- package/lib/__templates__/pi-agent/src/dashboard/config-store.ts +64 -0
- package/lib/__templates__/pi-agent/src/dashboard/index.ts +39 -0
- package/lib/__templates__/pi-agent/src/dashboard/server.ts +622 -0
- package/lib/__templates__/pi-agent/src/dashboard/types.ts +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/index.html +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/postcss.config.cjs +7 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/app-layout.tsx +186 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/page-title.tsx +17 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/alert.tsx +22 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/badge.tsx +25 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/button.tsx +40 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/card.tsx +29 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/input.tsx +18 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/label.tsx +8 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/select.tsx +80 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/components/ui/separator.tsx +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-fetch.ts +32 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/hooks/use-local-storage-state.ts +23 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/main.tsx +30 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/channels-page.tsx +188 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/chat-page.tsx +451 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/docs-page.tsx +65 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/models-page.tsx +122 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/pages/overview-page.tsx +134 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/services/chat-ws-service.ts +167 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/styles.css +294 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/src/utils/index.ts +11 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/tsconfig.json +13 -0
- package/lib/__templates__/pi-agent/src/dashboard/web/vite.config.ts +17 -0
- package/lib/__templates__/pi-agent/src/index.ts +123 -0
- package/lib/__templates__/pi-agent/src/pi-resources.ts +125 -0
- package/lib/__templates__/pi-agent/src/session-store.ts +223 -0
- package/lib/__templates__/pi-agent/src/tools/common/format-coze-error.ts +12 -0
- package/lib/__templates__/pi-agent/src/tools/index.ts +2 -0
- package/lib/__templates__/pi-agent/src/tools/web-fetch/index.ts +195 -0
- package/lib/__templates__/pi-agent/src/tools/web-search/index.ts +206 -0
- package/lib/__templates__/pi-agent/template.config.js +45 -0
- package/lib/__templates__/pi-agent/tests/cli.test.ts +136 -0
- package/lib/__templates__/pi-agent/tests/config.test.ts +315 -0
- package/lib/__templates__/pi-agent/tests/dashboard-docs-api.test.ts +125 -0
- package/lib/__templates__/pi-agent/tests/dashboard-models-api.test.ts +171 -0
- package/lib/__templates__/pi-agent/tests/feishu-channel.test.ts +149 -0
- package/lib/__templates__/pi-agent/tests/feishu-streaming-card.test.ts +15 -0
- package/lib/__templates__/pi-agent/tests/pi-resources.test.ts +73 -0
- package/lib/__templates__/pi-agent/tests/preference-memory.test.ts +43 -0
- package/lib/__templates__/pi-agent/tests/session-store.test.ts +61 -0
- package/lib/__templates__/pi-agent/tests/smoke/run-smoke.ts +275 -0
- package/lib/__templates__/pi-agent/tests/web-fetch.test.ts +157 -0
- package/lib/__templates__/pi-agent/tests/web-search.test.ts +208 -0
- package/lib/__templates__/pi-agent/tsconfig.json +21 -0
- package/lib/__templates__/pi-agent/types/larksuiteoapi-node-sdk.d.ts +113 -0
- package/lib/__templates__/taro/.coze +1 -0
- package/lib/__templates__/taro/.cozeproj/scripts/validate.sh +8 -0
- package/lib/__templates__/taro/package.json +1 -1
- package/lib/__templates__/templates.json +24 -0
- package/lib/__templates__/vite/.coze +1 -0
- package/lib/__templates__/vite/package.json +3 -1
- package/lib/__templates__/vite/scripts/validate.sh +10 -0
- package/lib/cli.js +13 -2
- package/package.json +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { createBotApp } from "../../src/index.js";
|
|
4
|
+
import { createMemoryConfigStore } from "../../src/dashboard/config-store.js";
|
|
5
|
+
|
|
6
|
+
async function readJson(url: string, init?: RequestInit): Promise<unknown> {
|
|
7
|
+
const res = await fetch(url, init);
|
|
8
|
+
assert.equal(res.ok, true, `${init?.method ?? "GET"} ${url} failed with ${res.status}`);
|
|
9
|
+
return (await res.json()) as unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function allocateFreePort(): Promise<number> {
|
|
13
|
+
return await new Promise<number>((resolve, reject) => {
|
|
14
|
+
const server = createServer();
|
|
15
|
+
server.once("error", reject);
|
|
16
|
+
server.listen(0, "127.0.0.1", () => {
|
|
17
|
+
const address = server.address();
|
|
18
|
+
if (!address || typeof address === "string") {
|
|
19
|
+
server.close(() => reject(new Error("Failed to allocate free port.")));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const { port } = address;
|
|
23
|
+
server.close((error) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
reject(error);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
resolve(port);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function run(): Promise<void> {
|
|
35
|
+
const previousNodeEnv = process.env.NODE_ENV;
|
|
36
|
+
const previousDashboardPort = process.env.PI_BOT_DASHBOARD_PORT;
|
|
37
|
+
process.env.NODE_ENV = "production";
|
|
38
|
+
process.env.PI_BOT_DASHBOARD_PORT = String(await allocateFreePort());
|
|
39
|
+
|
|
40
|
+
const configStore = createMemoryConfigStore({
|
|
41
|
+
agents: {
|
|
42
|
+
defaults: {
|
|
43
|
+
model: {
|
|
44
|
+
primary: "coze/auto"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
models: {
|
|
49
|
+
providers: {
|
|
50
|
+
coze: {
|
|
51
|
+
models: [
|
|
52
|
+
{ id: "auto", name: "Auto" },
|
|
53
|
+
{ id: "glm-4.7", name: "GLM-4.7" }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
channels: {
|
|
59
|
+
feishu: {
|
|
60
|
+
enabled: true,
|
|
61
|
+
requireMention: true,
|
|
62
|
+
appId: "app-id",
|
|
63
|
+
thinkingReaction: {
|
|
64
|
+
enabled: true,
|
|
65
|
+
emojiType: "SMILE"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
wechat: {
|
|
69
|
+
enabled: true,
|
|
70
|
+
requireMention: false,
|
|
71
|
+
implementation: "mock"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let app: Awaited<ReturnType<typeof createBotApp>> | null = null;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
app = await createBotApp(
|
|
80
|
+
{
|
|
81
|
+
appName: "smoke-bot",
|
|
82
|
+
agent: {
|
|
83
|
+
mode: "mock"
|
|
84
|
+
},
|
|
85
|
+
routing: {
|
|
86
|
+
feishuGroupRequireMention: true,
|
|
87
|
+
wechatGroupRequireMention: false
|
|
88
|
+
},
|
|
89
|
+
channels: {
|
|
90
|
+
feishu: { enabled: true },
|
|
91
|
+
wechat: { enabled: true }
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
dashboardConfigStore: configStore
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await app.start();
|
|
100
|
+
|
|
101
|
+
const dashboardUrl = app.dashboard.getUrl();
|
|
102
|
+
const initialModels = (await readJson(`${dashboardUrl}/api/models`)) as {
|
|
103
|
+
defaultModel: string;
|
|
104
|
+
options: Array<{ value: string; label: string }>;
|
|
105
|
+
};
|
|
106
|
+
assert.equal(initialModels.defaultModel, "coze/auto");
|
|
107
|
+
assert.deepEqual(initialModels.options, [
|
|
108
|
+
{ value: "coze/auto", label: "coze / Auto" },
|
|
109
|
+
{ value: "coze/glm-4.7", label: "coze / GLM-4.7" }
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
await readJson(`${dashboardUrl}/api/models`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/json"
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
models: {
|
|
119
|
+
defaultModel: "coze/glm-4.7"
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
});
|
|
123
|
+
const savedModels = (await readJson(`${dashboardUrl}/api/models`)) as { defaultModel: string };
|
|
124
|
+
assert.equal(savedModels.defaultModel, "coze/glm-4.7");
|
|
125
|
+
assert.equal(
|
|
126
|
+
(((configStore.snapshot().agents as { defaults?: { model?: { primary?: string } } }).defaults?.model?.primary) ?? ""),
|
|
127
|
+
"coze/glm-4.7"
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const initialChannels = (await readJson(`${dashboardUrl}/api/channels`)) as {
|
|
131
|
+
feishu: { enabled: boolean; thinkingReaction?: { enabled?: boolean; emojiType?: string } };
|
|
132
|
+
wechat: { enabled: boolean; implementation?: string };
|
|
133
|
+
routing: { feishuGroupRequireMention: boolean; wechatGroupRequireMention: boolean };
|
|
134
|
+
};
|
|
135
|
+
assert.equal(initialChannels.feishu.enabled, true);
|
|
136
|
+
assert.equal(initialChannels.feishu.thinkingReaction?.emojiType, "SMILE");
|
|
137
|
+
assert.equal(initialChannels.wechat.implementation, "mock");
|
|
138
|
+
|
|
139
|
+
await readJson(`${dashboardUrl}/api/channels`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
"Content-Type": "application/json"
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
channels: {
|
|
146
|
+
routing: {
|
|
147
|
+
feishuGroupRequireMention: false,
|
|
148
|
+
wechatGroupRequireMention: true
|
|
149
|
+
},
|
|
150
|
+
feishu: {
|
|
151
|
+
enabled: false,
|
|
152
|
+
requireMention: false,
|
|
153
|
+
appId: "updated-app-id",
|
|
154
|
+
domain: "",
|
|
155
|
+
encryptKey: "",
|
|
156
|
+
verificationToken: "",
|
|
157
|
+
appSecret: "",
|
|
158
|
+
thinkingReaction: {
|
|
159
|
+
enabled: false,
|
|
160
|
+
emojiType: "THINK"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
wechat: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
requireMention: true,
|
|
166
|
+
implementation: "mock"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
const savedChannels = (await readJson(`${dashboardUrl}/api/channels`)) as {
|
|
172
|
+
feishu: { enabled: boolean; appId?: string; thinkingReaction?: { enabled?: boolean; emojiType?: string } };
|
|
173
|
+
wechat: { requireMention?: boolean };
|
|
174
|
+
};
|
|
175
|
+
assert.equal(savedChannels.feishu.enabled, false);
|
|
176
|
+
assert.equal(savedChannels.feishu.appId, "updated-app-id");
|
|
177
|
+
assert.equal(savedChannels.feishu.thinkingReaction?.enabled, false);
|
|
178
|
+
assert.equal(savedChannels.wechat.requireMention, true);
|
|
179
|
+
|
|
180
|
+
const feishuResult = await app.channels.feishu?.simulateIncomingText({
|
|
181
|
+
text: "hello from feishu",
|
|
182
|
+
senderId: "feishu-user-1",
|
|
183
|
+
conversationId: "feishu-dm-1",
|
|
184
|
+
isDirectMessage: true
|
|
185
|
+
});
|
|
186
|
+
assert.ok(feishuResult);
|
|
187
|
+
assert.equal(feishuResult.handled, true);
|
|
188
|
+
assert.equal(feishuResult.reply?.text, "mock:feishu:dm:feishu-user-1: hello from feishu");
|
|
189
|
+
|
|
190
|
+
const duplicateFeishuEvent = {
|
|
191
|
+
message: {
|
|
192
|
+
message_id: "feishu-duplicate-1",
|
|
193
|
+
chat_id: "feishu-dm-duplicate",
|
|
194
|
+
chat_type: "p2p",
|
|
195
|
+
content: JSON.stringify({ text: "duplicate hello" })
|
|
196
|
+
},
|
|
197
|
+
sender: {
|
|
198
|
+
sender_id: {
|
|
199
|
+
open_id: "feishu-user-duplicate"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const firstDuplicateResult = await app.channels.feishu?.handleEvent(duplicateFeishuEvent);
|
|
205
|
+
assert.ok(firstDuplicateResult);
|
|
206
|
+
assert.equal(firstDuplicateResult.handled, true);
|
|
207
|
+
|
|
208
|
+
const secondDuplicateResult = await app.channels.feishu?.handleEvent(duplicateFeishuEvent);
|
|
209
|
+
assert.ok(secondDuplicateResult);
|
|
210
|
+
assert.equal(secondDuplicateResult.handled, false);
|
|
211
|
+
assert.equal(secondDuplicateResult.reason, "ignored");
|
|
212
|
+
|
|
213
|
+
const filteredGroupMessage = await app.channels.feishu?.simulateIncomingText({
|
|
214
|
+
text: "group message without mention",
|
|
215
|
+
senderId: "feishu-user-2",
|
|
216
|
+
conversationId: "feishu-group-1",
|
|
217
|
+
isDirectMessage: false,
|
|
218
|
+
mentions: []
|
|
219
|
+
});
|
|
220
|
+
assert.ok(filteredGroupMessage);
|
|
221
|
+
assert.equal(filteredGroupMessage.handled, false);
|
|
222
|
+
|
|
223
|
+
const handledGroupMessage = await app.channels.feishu?.simulateIncomingText({
|
|
224
|
+
text: "group message with mention",
|
|
225
|
+
senderId: "feishu-user-2",
|
|
226
|
+
conversationId: "feishu-group-1",
|
|
227
|
+
isDirectMessage: false,
|
|
228
|
+
mentions: [{ id: "bot" }]
|
|
229
|
+
});
|
|
230
|
+
assert.ok(handledGroupMessage);
|
|
231
|
+
assert.equal(handledGroupMessage.handled, true);
|
|
232
|
+
|
|
233
|
+
const wechatResult = await app.channels.wechat?.simulateIncomingText({
|
|
234
|
+
text: "hello from wechat",
|
|
235
|
+
senderId: "wechat-user-1",
|
|
236
|
+
conversationId: "wechat-dm-1",
|
|
237
|
+
isDirectMessage: true
|
|
238
|
+
});
|
|
239
|
+
assert.ok(wechatResult);
|
|
240
|
+
assert.equal(wechatResult.handled, true);
|
|
241
|
+
assert.equal(wechatResult.reply?.text, "mock:wechat:group:wechat-dm-1: hello from wechat");
|
|
242
|
+
|
|
243
|
+
const secondWechatResult = await app.channels.wechat?.simulateIncomingText({
|
|
244
|
+
text: "hello again from wechat",
|
|
245
|
+
senderId: "wechat-user-1",
|
|
246
|
+
conversationId: "wechat-dm-1",
|
|
247
|
+
isDirectMessage: true
|
|
248
|
+
});
|
|
249
|
+
assert.ok(secondWechatResult);
|
|
250
|
+
assert.equal(secondWechatResult.handled, true);
|
|
251
|
+
assert.equal(
|
|
252
|
+
secondWechatResult.reply?.text,
|
|
253
|
+
"mock:wechat:group:wechat-dm-1: hello again from wechat"
|
|
254
|
+
);
|
|
255
|
+
} finally {
|
|
256
|
+
await app?.stop();
|
|
257
|
+
if (previousNodeEnv === undefined) {
|
|
258
|
+
delete process.env.NODE_ENV;
|
|
259
|
+
} else {
|
|
260
|
+
process.env.NODE_ENV = previousNodeEnv;
|
|
261
|
+
}
|
|
262
|
+
if (previousDashboardPort === undefined) {
|
|
263
|
+
delete process.env.PI_BOT_DASHBOARD_PORT;
|
|
264
|
+
} else {
|
|
265
|
+
process.env.PI_BOT_DASHBOARD_PORT = previousDashboardPort;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log("smoke test passed");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
run().catch((error: unknown) => {
|
|
273
|
+
console.error(error);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { FetchClient, type FetchResponse } from "coze-coding-dev-sdk";
|
|
4
|
+
import { cozeWebFetchTool } from "../src/tools/web-fetch/index.js";
|
|
5
|
+
|
|
6
|
+
function resultText(result: { content: { type: string; text?: string }[] }): string {
|
|
7
|
+
return result.content[0].text ?? "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function makeFetchResponse(overrides: Partial<FetchResponse> = {}): FetchResponse {
|
|
11
|
+
return {
|
|
12
|
+
url: "https://example.com",
|
|
13
|
+
title: "Example Page",
|
|
14
|
+
content: [
|
|
15
|
+
{ type: "text", text: "Hello world" },
|
|
16
|
+
{ type: "link", text: "Link A", url: "https://a.com" },
|
|
17
|
+
{ type: "image", image: { display_url: "https://img.com/1.png", width: 640, height: 480 } },
|
|
18
|
+
],
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("web-fetch: text format renders numbered items with links and images", async (t) => {
|
|
24
|
+
const resp = makeFetchResponse();
|
|
25
|
+
t.mock.method(FetchClient.prototype, "fetch", async () => resp);
|
|
26
|
+
|
|
27
|
+
const result = await cozeWebFetchTool.execute("call-1", { urls: "https://example.com" }, undefined as any, undefined as any, undefined as any);
|
|
28
|
+
const text = resultText(result);
|
|
29
|
+
|
|
30
|
+
assert.ok(text.includes("1. Example Page"));
|
|
31
|
+
assert.ok(text.includes("URL: https://example.com"));
|
|
32
|
+
assert.ok(text.includes("Hello world"));
|
|
33
|
+
assert.ok(text.includes("Link A: https://a.com"));
|
|
34
|
+
assert.ok(text.includes("https://img.com/1.png (640x480)"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("web-fetch: markdown format renders headings and markdown links", async (t) => {
|
|
38
|
+
const resp = makeFetchResponse();
|
|
39
|
+
t.mock.method(FetchClient.prototype, "fetch", async () => resp);
|
|
40
|
+
|
|
41
|
+
const result = await cozeWebFetchTool.execute("call-2", { urls: ["https://example.com"], format: "markdown" }, undefined as any, undefined as any, undefined as any);
|
|
42
|
+
const text = resultText(result);
|
|
43
|
+
|
|
44
|
+
assert.ok(text.includes("# Example Page"));
|
|
45
|
+
assert.ok(text.includes("- [Link A](https://a.com)"));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("web-fetch: json format returns valid JSON", async (t) => {
|
|
49
|
+
const resp = makeFetchResponse();
|
|
50
|
+
t.mock.method(FetchClient.prototype, "fetch", async () => resp);
|
|
51
|
+
|
|
52
|
+
const result = await cozeWebFetchTool.execute("call-3", { urls: "https://example.com", format: "json" }, undefined as any, undefined as any, undefined as any);
|
|
53
|
+
const parsed = JSON.parse(resultText(result));
|
|
54
|
+
|
|
55
|
+
assert.equal(Array.isArray(parsed), true);
|
|
56
|
+
assert.equal(parsed[0].url, "https://example.com");
|
|
57
|
+
assert.equal(parsed[0].title, "Example Page");
|
|
58
|
+
assert.equal(parsed[0].links.length, 1);
|
|
59
|
+
assert.equal(parsed[0].images.length, 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("web-fetch: textOnly omits links and images", async (t) => {
|
|
63
|
+
const resp = makeFetchResponse();
|
|
64
|
+
t.mock.method(FetchClient.prototype, "fetch", async () => resp);
|
|
65
|
+
|
|
66
|
+
const result = await cozeWebFetchTool.execute("call-4", { urls: "https://example.com", format: "json", textOnly: true }, undefined as any, undefined as any, undefined as any);
|
|
67
|
+
const parsed = JSON.parse(resultText(result));
|
|
68
|
+
|
|
69
|
+
assert.equal(parsed[0].links.length, 0);
|
|
70
|
+
assert.equal(parsed[0].images.length, 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("web-fetch: filters out links with missing url", async (t) => {
|
|
74
|
+
const resp = makeFetchResponse({
|
|
75
|
+
content: [
|
|
76
|
+
{ type: "text", text: "body" },
|
|
77
|
+
{ type: "link", text: "Valid", url: "https://valid.com" },
|
|
78
|
+
{ type: "link", text: "Missing URL" },
|
|
79
|
+
{ type: "link", text: undefined, url: undefined },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
t.mock.method(FetchClient.prototype, "fetch", async () => resp);
|
|
83
|
+
|
|
84
|
+
const textResult = await cozeWebFetchTool.execute("call-5a", { urls: "https://example.com" }, undefined as any, undefined as any, undefined as any);
|
|
85
|
+
assert.ok(resultText(textResult).includes("Valid: https://valid.com"));
|
|
86
|
+
assert.ok(!resultText(textResult).includes("Missing URL"));
|
|
87
|
+
|
|
88
|
+
const mdResult = await cozeWebFetchTool.execute("call-5b", { urls: "https://example.com", format: "markdown" }, undefined as any, undefined as any, undefined as any);
|
|
89
|
+
assert.ok(resultText(mdResult).includes("[Valid](https://valid.com)"));
|
|
90
|
+
assert.ok(!resultText(mdResult).includes("Missing URL"));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("web-fetch: image dimensions only shown when both width and height are numbers", async (t) => {
|
|
94
|
+
const resp = makeFetchResponse({
|
|
95
|
+
content: [
|
|
96
|
+
{ type: "text", text: "body" },
|
|
97
|
+
{ type: "image", image: { display_url: "https://img.com/a.png", width: 100, height: 200 } },
|
|
98
|
+
{ type: "image", image: { display_url: "https://img.com/b.png", width: 100 } },
|
|
99
|
+
{ type: "image", image: { display_url: "https://img.com/c.png" } },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
t.mock.method(FetchClient.prototype, "fetch", async () => resp);
|
|
103
|
+
|
|
104
|
+
const result = await cozeWebFetchTool.execute("call-6", { urls: "https://example.com" }, undefined as any, undefined as any, undefined as any);
|
|
105
|
+
const text = resultText(result);
|
|
106
|
+
|
|
107
|
+
assert.ok(text.includes("https://img.com/a.png (100x200)"));
|
|
108
|
+
assert.ok(text.includes("https://img.com/b.png"));
|
|
109
|
+
assert.ok(!text.includes("b.png (100x"));
|
|
110
|
+
assert.ok(text.includes("https://img.com/c.png"));
|
|
111
|
+
assert.ok(!text.includes("c.png ("));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("web-fetch: concurrent fetching preserves order", async (t) => {
|
|
115
|
+
const callOrder: number[] = [];
|
|
116
|
+
let callIndex = 0;
|
|
117
|
+
|
|
118
|
+
t.mock.method(FetchClient.prototype, "fetch", async (url: string) => {
|
|
119
|
+
const idx = callIndex++;
|
|
120
|
+
const delay = url.includes("slow") ? 50 : 10;
|
|
121
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
122
|
+
callOrder.push(idx);
|
|
123
|
+
return makeFetchResponse({ url, title: `Page ${idx}` });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const urls = ["https://slow.com", "https://fast1.com", "https://fast2.com"];
|
|
127
|
+
const result = await cozeWebFetchTool.execute("call-7", { urls, format: "json" }, undefined as any, undefined as any, undefined as any);
|
|
128
|
+
const parsed = JSON.parse(resultText(result));
|
|
129
|
+
|
|
130
|
+
assert.equal(parsed.length, 3);
|
|
131
|
+
assert.equal(parsed[0].url, "https://slow.com");
|
|
132
|
+
assert.equal(parsed[1].url, "https://fast1.com");
|
|
133
|
+
assert.equal(parsed[2].url, "https://fast2.com");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("web-fetch: returns error content on fetch failure", async (t) => {
|
|
137
|
+
t.mock.method(FetchClient.prototype, "fetch", async () => {
|
|
138
|
+
throw new Error("Connection refused");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = await cozeWebFetchTool.execute("call-8", { urls: "https://example.com" }, undefined as any, undefined as any, undefined as any);
|
|
142
|
+
|
|
143
|
+
assert.ok(resultText(result).includes("Error: Connection refused"));
|
|
144
|
+
assert.equal((result as any).details.error, true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("web-fetch: single url string is normalized to array", async (t) => {
|
|
148
|
+
const calls: string[] = [];
|
|
149
|
+
t.mock.method(FetchClient.prototype, "fetch", async (url: string) => {
|
|
150
|
+
calls.push(url);
|
|
151
|
+
return makeFetchResponse({ url });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await cozeWebFetchTool.execute("call-9", { urls: "https://single.com" }, undefined as any, undefined as any, undefined as any);
|
|
155
|
+
|
|
156
|
+
assert.deepEqual(calls, ["https://single.com"]);
|
|
157
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { SearchClient, type SearchResponse } from "coze-coding-dev-sdk";
|
|
4
|
+
import { cozeWebSearchTool } from "../src/tools/web-search/index.js";
|
|
5
|
+
|
|
6
|
+
function resultText(result: { content: { type: string; text?: string }[] }): string {
|
|
7
|
+
return result.content[0].text ?? "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function makeWebSearchResponse(overrides: Partial<SearchResponse> = {}): SearchResponse {
|
|
11
|
+
return {
|
|
12
|
+
web_items: [
|
|
13
|
+
{
|
|
14
|
+
id: "1",
|
|
15
|
+
sort_id: 1,
|
|
16
|
+
title: "Result A",
|
|
17
|
+
site_name: "example.com",
|
|
18
|
+
url: "https://example.com/a",
|
|
19
|
+
snippet: "Snippet A",
|
|
20
|
+
auth_info_des: "",
|
|
21
|
+
auth_info_level: 0,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "2",
|
|
25
|
+
sort_id: 2,
|
|
26
|
+
title: "Result B",
|
|
27
|
+
url: "https://example.com/b",
|
|
28
|
+
snippet: "Snippet B",
|
|
29
|
+
publish_time: "2025-01-01",
|
|
30
|
+
auth_info_des: "",
|
|
31
|
+
auth_info_level: 0,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
image_items: [],
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeImageSearchResponse(): SearchResponse {
|
|
40
|
+
return {
|
|
41
|
+
web_items: [],
|
|
42
|
+
image_items: [
|
|
43
|
+
{
|
|
44
|
+
id: "img-1",
|
|
45
|
+
sort_id: 1,
|
|
46
|
+
title: "Cat Photo",
|
|
47
|
+
url: "https://example.com/cat",
|
|
48
|
+
site_name: "photos.com",
|
|
49
|
+
image: { url: "https://cdn.com/cat.jpg", width: 800, height: 600, shape: "rect" },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "img-2",
|
|
53
|
+
sort_id: 2,
|
|
54
|
+
url: "https://example.com/dog",
|
|
55
|
+
image: { url: "https://cdn.com/dog.jpg", width: 1024, height: 768, shape: "rect" },
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test("web-search: basic web search returns formatted text", async (t) => {
|
|
62
|
+
const resp = makeWebSearchResponse();
|
|
63
|
+
t.mock.method(SearchClient.prototype, "webSearch", async () => resp);
|
|
64
|
+
|
|
65
|
+
const result = await cozeWebSearchTool.execute("call-1", { query: "test" }, undefined as any, undefined as any, undefined as any);
|
|
66
|
+
const text = resultText(result);
|
|
67
|
+
|
|
68
|
+
assert.ok(text.includes("Coze web search: test"));
|
|
69
|
+
assert.ok(text.includes("Results (2)"));
|
|
70
|
+
assert.ok(text.includes("1. Result A"));
|
|
71
|
+
assert.ok(text.includes("URL: https://example.com/a"));
|
|
72
|
+
assert.ok(text.includes("Source: example.com"));
|
|
73
|
+
assert.ok(text.includes("Snippet A"));
|
|
74
|
+
assert.ok(text.includes("2. Result B"));
|
|
75
|
+
assert.ok(text.includes("Published: 2025-01-01"));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("web-search: includes summary when present", async (t) => {
|
|
79
|
+
const resp = makeWebSearchResponse({ summary: "This is the summary" });
|
|
80
|
+
t.mock.method(SearchClient.prototype, "webSearch", async () => resp);
|
|
81
|
+
|
|
82
|
+
const result = await cozeWebSearchTool.execute("call-2", { query: "test", needSummary: true }, undefined as any, undefined as any, undefined as any);
|
|
83
|
+
const text = resultText(result);
|
|
84
|
+
|
|
85
|
+
assert.ok(text.includes("Summary: This is the summary"));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("web-search: includes content when needContent is true", async (t) => {
|
|
89
|
+
const resp = makeWebSearchResponse();
|
|
90
|
+
resp.web_items[0].content = "Full page content here";
|
|
91
|
+
t.mock.method(SearchClient.prototype, "advancedSearch", async () => resp);
|
|
92
|
+
|
|
93
|
+
const result = await cozeWebSearchTool.execute("call-3", { query: "test", needContent: true }, undefined as any, undefined as any, undefined as any);
|
|
94
|
+
const text = resultText(result);
|
|
95
|
+
|
|
96
|
+
assert.ok(text.includes("Content:"));
|
|
97
|
+
assert.ok(text.includes("Full page content here"));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("web-search: image search maps image items", async (t) => {
|
|
101
|
+
const resp = makeImageSearchResponse();
|
|
102
|
+
t.mock.method(SearchClient.prototype, "imageSearch", async () => resp);
|
|
103
|
+
|
|
104
|
+
const result = await cozeWebSearchTool.execute("call-4", { query: "cats", type: "image" }, undefined as any, undefined as any, undefined as any);
|
|
105
|
+
const text = resultText(result);
|
|
106
|
+
|
|
107
|
+
assert.ok(text.includes("1. Cat Photo"));
|
|
108
|
+
assert.ok(text.includes("Image: https://cdn.com/cat.jpg"));
|
|
109
|
+
assert.ok(text.includes("2. Untitled"));
|
|
110
|
+
assert.ok(text.includes("Image: https://cdn.com/dog.jpg"));
|
|
111
|
+
assert.equal((result as any).details.type, "image");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("web-search: uses advancedSearch when filters are present", async (t) => {
|
|
115
|
+
const resp = makeWebSearchResponse();
|
|
116
|
+
const calls: string[] = [];
|
|
117
|
+
t.mock.method(SearchClient.prototype, "advancedSearch", async () => {
|
|
118
|
+
calls.push("advancedSearch");
|
|
119
|
+
return resp;
|
|
120
|
+
});
|
|
121
|
+
t.mock.method(SearchClient.prototype, "webSearch", async () => {
|
|
122
|
+
calls.push("webSearch");
|
|
123
|
+
return resp;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await cozeWebSearchTool.execute("call-5", { query: "test", timeRange: "1d" }, undefined as any, undefined as any, undefined as any);
|
|
127
|
+
assert.deepEqual(calls, ["advancedSearch"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("web-search: uses webSearch when no filters present", async (t) => {
|
|
131
|
+
const resp = makeWebSearchResponse();
|
|
132
|
+
const calls: string[] = [];
|
|
133
|
+
t.mock.method(SearchClient.prototype, "advancedSearch", async () => {
|
|
134
|
+
calls.push("advancedSearch");
|
|
135
|
+
return resp;
|
|
136
|
+
});
|
|
137
|
+
t.mock.method(SearchClient.prototype, "webSearch", async () => {
|
|
138
|
+
calls.push("webSearch");
|
|
139
|
+
return resp;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await cozeWebSearchTool.execute("call-6", { query: "test" }, undefined as any, undefined as any, undefined as any);
|
|
143
|
+
assert.deepEqual(calls, ["webSearch"]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("web-search: uses advancedSearch when sites filter is set", async (t) => {
|
|
147
|
+
const resp = makeWebSearchResponse();
|
|
148
|
+
const calls: string[] = [];
|
|
149
|
+
t.mock.method(SearchClient.prototype, "advancedSearch", async () => {
|
|
150
|
+
calls.push("advancedSearch");
|
|
151
|
+
return resp;
|
|
152
|
+
});
|
|
153
|
+
t.mock.method(SearchClient.prototype, "webSearch", async () => {
|
|
154
|
+
calls.push("webSearch");
|
|
155
|
+
return resp;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await cozeWebSearchTool.execute("call-7", { query: "test", sites: "example.com" }, undefined as any, undefined as any, undefined as any);
|
|
159
|
+
assert.deepEqual(calls, ["advancedSearch"]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("web-search: returns error content on search failure", async (t) => {
|
|
163
|
+
t.mock.method(SearchClient.prototype, "webSearch", async () => {
|
|
164
|
+
throw new Error("API timeout");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = await cozeWebSearchTool.execute("call-8", { query: "test" }, undefined as any, undefined as any, undefined as any);
|
|
168
|
+
|
|
169
|
+
assert.ok(resultText(result).includes("Error: API timeout"));
|
|
170
|
+
assert.equal((result as any).details.error, true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("web-search: defaults count to 10 and type to web", async (t) => {
|
|
174
|
+
const captured: any[] = [];
|
|
175
|
+
t.mock.method(SearchClient.prototype, "webSearch", async (query: string, count: number, needSummary?: boolean) => {
|
|
176
|
+
captured.push({ query, count, needSummary });
|
|
177
|
+
return makeWebSearchResponse();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await cozeWebSearchTool.execute("call-9", { query: "hello" }, undefined as any, undefined as any, undefined as any);
|
|
181
|
+
|
|
182
|
+
assert.equal(captured[0].query, "hello");
|
|
183
|
+
assert.equal(captured[0].count, 10);
|
|
184
|
+
assert.equal((captured[0] as any).needSummary, undefined);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("web-search: details contain correct metadata", async (t) => {
|
|
188
|
+
const resp = makeWebSearchResponse({ summary: "Sum" });
|
|
189
|
+
t.mock.method(SearchClient.prototype, "webSearch", async () => resp);
|
|
190
|
+
|
|
191
|
+
const result = await cozeWebSearchTool.execute("call-10", { query: "meta test" }, undefined as any, undefined as any, undefined as any);
|
|
192
|
+
|
|
193
|
+
assert.equal((result as any).details.query, "meta test");
|
|
194
|
+
assert.equal((result as any).details.type, "web");
|
|
195
|
+
assert.equal((result as any).details.summary, "Sum");
|
|
196
|
+
assert.equal((result as any).details.count, 2);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("web-search: empty results produces correct output", async (t) => {
|
|
200
|
+
const resp: SearchResponse = { web_items: [], image_items: [] };
|
|
201
|
+
t.mock.method(SearchClient.prototype, "webSearch", async () => resp);
|
|
202
|
+
|
|
203
|
+
const result = await cozeWebSearchTool.execute("call-11", { query: "nothing" }, undefined as any, undefined as any, undefined as any);
|
|
204
|
+
const text = resultText(result);
|
|
205
|
+
|
|
206
|
+
assert.ok(text.includes("Results (0)"));
|
|
207
|
+
assert.equal((result as any).details.count, 0);
|
|
208
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"baseUrl": "."
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"src/**/*.ts",
|
|
17
|
+
"pi-resources/extensions/**/*.ts",
|
|
18
|
+
"tests/**/*.ts",
|
|
19
|
+
"types/**/*.d.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|