@coze-arch/cli 0.0.17 → 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 +18 -31
- 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,223 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export type SessionRecord = {
|
|
7
|
+
sessionKey: string;
|
|
8
|
+
keyHash: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
sessionFile: string;
|
|
11
|
+
updatedAt: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type IndexFileShapeV1 = {
|
|
15
|
+
version: 1;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
sessions: Record<
|
|
18
|
+
string,
|
|
19
|
+
{
|
|
20
|
+
keyHash: string;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
sessionFile: string;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
}
|
|
25
|
+
>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function sha256Hex(input: string): string {
|
|
29
|
+
return createHash("sha256").update(input).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function safeRename(from: string, to: string): void {
|
|
33
|
+
try {
|
|
34
|
+
fs.renameSync(from, to);
|
|
35
|
+
} catch {
|
|
36
|
+
// best-effort
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureDir(dirPath: string): void {
|
|
41
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readJsonFile(filePath: string): unknown {
|
|
45
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
46
|
+
return JSON.parse(raw) as unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeFileAtomic(filePath: string, content: string, mode: number): void {
|
|
50
|
+
const dir = path.dirname(filePath);
|
|
51
|
+
ensureDir(dir);
|
|
52
|
+
const tmpPath = `${filePath}.tmp`;
|
|
53
|
+
fs.writeFileSync(tmpPath, content, { encoding: "utf-8", mode });
|
|
54
|
+
fs.renameSync(tmpPath, filePath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureTranscriptHeader(params: { sessionFile: string; sessionId: string }): void {
|
|
58
|
+
if (fs.existsSync(params.sessionFile)) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
ensureDir(path.dirname(params.sessionFile));
|
|
62
|
+
const header = {
|
|
63
|
+
type: "session",
|
|
64
|
+
version: CURRENT_SESSION_VERSION,
|
|
65
|
+
id: params.sessionId,
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
};
|
|
69
|
+
fs.writeFileSync(params.sessionFile, `${JSON.stringify(header)}\n`, {
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
mode: 0o600,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function decodeIndexFileShapeV1(value: unknown): IndexFileShapeV1 | null {
|
|
76
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
77
|
+
const rec = value as Record<string, unknown>;
|
|
78
|
+
if (rec.version !== 1) return null;
|
|
79
|
+
if (!("sessions" in rec) || !rec.sessions || typeof rec.sessions !== "object" || Array.isArray(rec.sessions)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const updatedAt = typeof rec.updatedAt === "number" ? rec.updatedAt : Date.now();
|
|
83
|
+
const sessions = rec.sessions as Record<string, unknown>;
|
|
84
|
+
const out: IndexFileShapeV1["sessions"] = {};
|
|
85
|
+
for (const [sessionKey, entry] of Object.entries(sessions)) {
|
|
86
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
87
|
+
const e = entry as Record<string, unknown>;
|
|
88
|
+
const keyHash = typeof e.keyHash === "string" ? e.keyHash : "";
|
|
89
|
+
const sessionId = typeof e.sessionId === "string" ? e.sessionId : "";
|
|
90
|
+
const sessionFile = typeof e.sessionFile === "string" ? e.sessionFile : "";
|
|
91
|
+
const entryUpdatedAt = typeof e.updatedAt === "number" ? e.updatedAt : updatedAt;
|
|
92
|
+
if (!sessionKey || !keyHash || !sessionId || !sessionFile) continue;
|
|
93
|
+
out[sessionKey] = { keyHash, sessionId, sessionFile, updatedAt: entryUpdatedAt };
|
|
94
|
+
}
|
|
95
|
+
return { version: 1, updatedAt, sessions: out };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class SessionStore {
|
|
99
|
+
private readonly sessionsDir: string;
|
|
100
|
+
private readonly indexPath: string;
|
|
101
|
+
private readonly transcriptsDir: string;
|
|
102
|
+
private readonly archiveDir: string;
|
|
103
|
+
private readonly cache = new Map<string, SessionRecord>();
|
|
104
|
+
|
|
105
|
+
constructor(private readonly agentDir: string) {
|
|
106
|
+
this.sessionsDir = path.join(agentDir, ".pi-bot", "sessions");
|
|
107
|
+
this.indexPath = path.join(this.sessionsDir, "index.json");
|
|
108
|
+
this.transcriptsDir = path.join(this.sessionsDir, "transcripts");
|
|
109
|
+
this.archiveDir = path.join(this.sessionsDir, "archive");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
load(): void {
|
|
113
|
+
this.cache.clear();
|
|
114
|
+
|
|
115
|
+
if (!fs.existsSync(this.indexPath)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const decoded = decodeIndexFileShapeV1(readJsonFile(this.indexPath));
|
|
121
|
+
if (!decoded) {
|
|
122
|
+
throw new Error("invalid index.json schema");
|
|
123
|
+
}
|
|
124
|
+
for (const [sessionKey, entry] of Object.entries(decoded.sessions)) {
|
|
125
|
+
this.cache.set(sessionKey, {
|
|
126
|
+
sessionKey,
|
|
127
|
+
keyHash: entry.keyHash,
|
|
128
|
+
sessionId: entry.sessionId,
|
|
129
|
+
sessionFile: entry.sessionFile,
|
|
130
|
+
updatedAt: entry.updatedAt,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// Keep a corrupt copy for debugging instead of crashing startup.
|
|
135
|
+
safeRename(this.indexPath, `${this.indexPath}.corrupt-${Date.now()}`);
|
|
136
|
+
this.cache.clear();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
listSessionKeys(): string[] {
|
|
141
|
+
return Array.from(this.cache.keys());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get(sessionKey: string): SessionRecord | undefined {
|
|
145
|
+
return this.cache.get(sessionKey);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
ensureSession(sessionKey: string): SessionRecord {
|
|
149
|
+
const existing = this.cache.get(sessionKey);
|
|
150
|
+
if (existing) {
|
|
151
|
+
// If the transcript was deleted externally, recreate the header so the
|
|
152
|
+
// session remains recoverable.
|
|
153
|
+
ensureTranscriptHeader({ sessionFile: existing.sessionFile, sessionId: existing.sessionId });
|
|
154
|
+
return existing;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const keyHash = sha256Hex(sessionKey);
|
|
159
|
+
const sessionId = randomUUID();
|
|
160
|
+
const sessionFile = path.join(this.transcriptsDir, `${keyHash}-${sessionId}.jsonl`);
|
|
161
|
+
const record: SessionRecord = {
|
|
162
|
+
sessionKey,
|
|
163
|
+
keyHash,
|
|
164
|
+
sessionId,
|
|
165
|
+
sessionFile,
|
|
166
|
+
updatedAt: now,
|
|
167
|
+
};
|
|
168
|
+
this.cache.set(sessionKey, record);
|
|
169
|
+
ensureTranscriptHeader({ sessionFile, sessionId });
|
|
170
|
+
this.saveIndex();
|
|
171
|
+
return record;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
resetSession(sessionKey: string): SessionRecord {
|
|
175
|
+
const prior = this.cache.get(sessionKey);
|
|
176
|
+
if (prior && fs.existsSync(prior.sessionFile)) {
|
|
177
|
+
ensureDir(this.archiveDir);
|
|
178
|
+
const baseName = path.basename(prior.sessionFile);
|
|
179
|
+
const archivedBase = baseName.endsWith(".jsonl") ? baseName : `${baseName}.jsonl`;
|
|
180
|
+
const archivedPathCandidate = path.join(this.archiveDir, archivedBase);
|
|
181
|
+
const archivedPath = fs.existsSync(archivedPathCandidate)
|
|
182
|
+
? path.join(this.archiveDir, `${prior.keyHash}-${prior.sessionId}-${Date.now()}.jsonl`)
|
|
183
|
+
: archivedPathCandidate;
|
|
184
|
+
safeRename(prior.sessionFile, archivedPath);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Keep keyHash stable for this sessionKey, rotate sessionId + sessionFile.
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
const keyHash = prior?.keyHash ?? sha256Hex(sessionKey);
|
|
190
|
+
const sessionId = randomUUID();
|
|
191
|
+
const sessionFile = path.join(this.transcriptsDir, `${keyHash}-${sessionId}.jsonl`);
|
|
192
|
+
const next: SessionRecord = {
|
|
193
|
+
sessionKey,
|
|
194
|
+
keyHash,
|
|
195
|
+
sessionId,
|
|
196
|
+
sessionFile,
|
|
197
|
+
updatedAt: now,
|
|
198
|
+
};
|
|
199
|
+
this.cache.set(sessionKey, next);
|
|
200
|
+
ensureTranscriptHeader({ sessionFile, sessionId });
|
|
201
|
+
this.saveIndex();
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private saveIndex(): void {
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
const sessions: IndexFileShapeV1["sessions"] = {};
|
|
208
|
+
for (const [sessionKey, entry] of this.cache.entries()) {
|
|
209
|
+
sessions[sessionKey] = {
|
|
210
|
+
keyHash: entry.keyHash,
|
|
211
|
+
sessionId: entry.sessionId,
|
|
212
|
+
sessionFile: entry.sessionFile,
|
|
213
|
+
updatedAt: entry.updatedAt,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const payload: IndexFileShapeV1 = {
|
|
217
|
+
version: 1,
|
|
218
|
+
updatedAt: now,
|
|
219
|
+
sessions,
|
|
220
|
+
};
|
|
221
|
+
writeFileAtomic(this.indexPath, JSON.stringify(payload, null, 2), 0o600);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function formatCozeError(error: unknown): string {
|
|
2
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
3
|
+
const message = String(error.message);
|
|
4
|
+
const status =
|
|
5
|
+
"statusCode" in error && typeof error.statusCode === "number" ? ` (${error.statusCode})` : "";
|
|
6
|
+
return `${message}${status}`;
|
|
7
|
+
}
|
|
8
|
+
if (error instanceof Error) {
|
|
9
|
+
return error.message;
|
|
10
|
+
}
|
|
11
|
+
return String(error);
|
|
12
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
2
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { FetchClient, type FetchContentItem, type FetchResponse } from "coze-coding-dev-sdk";
|
|
4
|
+
import { formatCozeError } from "../common/format-coze-error.js";
|
|
5
|
+
|
|
6
|
+
type FetchResultLink = {
|
|
7
|
+
title?: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type FetchResultImage = {
|
|
12
|
+
url?: string;
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type FetchResultItem = {
|
|
18
|
+
url: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
text: string;
|
|
21
|
+
links: FetchResultLink[];
|
|
22
|
+
images: FetchResultImage[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function joinText(items: FetchContentItem[]): string {
|
|
26
|
+
return items
|
|
27
|
+
.filter((item) => item.type === "text" && item.text)
|
|
28
|
+
.map((item) => item.text?.trim() ?? "")
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join("\n\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectLinks(items: FetchContentItem[]): FetchResultLink[] {
|
|
34
|
+
return items
|
|
35
|
+
.filter((item) => item.type === "link")
|
|
36
|
+
.map((item) => ({
|
|
37
|
+
title: item.text,
|
|
38
|
+
url: item.url,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function collectImages(items: FetchContentItem[]): FetchResultImage[] {
|
|
43
|
+
return items
|
|
44
|
+
.filter((item) => item.type === "image")
|
|
45
|
+
.map((item) => ({
|
|
46
|
+
url: item.image?.display_url ?? item.image?.image_url,
|
|
47
|
+
width: item.image?.width,
|
|
48
|
+
height: item.image?.height,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeFetchedItem(response: FetchResponse, textOnly: boolean): FetchResultItem {
|
|
53
|
+
const content = response.content ?? [];
|
|
54
|
+
return {
|
|
55
|
+
url: response.url ?? "",
|
|
56
|
+
title: response.title,
|
|
57
|
+
text: joinText(content),
|
|
58
|
+
links: textOnly ? [] : collectLinks(content),
|
|
59
|
+
images: textOnly ? [] : collectImages(content),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MAX_CONCURRENCY = 5;
|
|
64
|
+
|
|
65
|
+
async function fetchContent(
|
|
66
|
+
urls: string[],
|
|
67
|
+
textOnly: boolean,
|
|
68
|
+
): Promise<FetchResultItem[]> {
|
|
69
|
+
const client = new FetchClient();
|
|
70
|
+
const results: FetchResultItem[] = new Array(urls.length);
|
|
71
|
+
let next = 0;
|
|
72
|
+
|
|
73
|
+
async function worker() {
|
|
74
|
+
while (next < urls.length) {
|
|
75
|
+
const idx = next++;
|
|
76
|
+
const response = await client.fetch(urls[idx]);
|
|
77
|
+
results[idx] = normalizeFetchedItem(response, textOnly);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENCY, urls.length) }, () => worker()));
|
|
82
|
+
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const FetchToolSchema = Type.Object(
|
|
87
|
+
{
|
|
88
|
+
urls: Type.Union(
|
|
89
|
+
[
|
|
90
|
+
Type.String({ description: "Single URL to fetch." }),
|
|
91
|
+
Type.Array(Type.String({ description: "URL to fetch." }), {
|
|
92
|
+
description: "One or more URLs to fetch.",
|
|
93
|
+
minItems: 1,
|
|
94
|
+
}),
|
|
95
|
+
],
|
|
96
|
+
{
|
|
97
|
+
description:
|
|
98
|
+
"URL input. Accepts either a single URL string or an array of URL strings.",
|
|
99
|
+
},
|
|
100
|
+
),
|
|
101
|
+
format: Type.Optional(
|
|
102
|
+
Type.Unsafe<"text" | "markdown" | "json">({
|
|
103
|
+
type: "string",
|
|
104
|
+
enum: ["text", "markdown", "json"],
|
|
105
|
+
description: "Formatting preference for the textual output.",
|
|
106
|
+
}),
|
|
107
|
+
),
|
|
108
|
+
textOnly: Type.Optional(
|
|
109
|
+
Type.Boolean({ description: "Only return extracted text, without images or links." }),
|
|
110
|
+
),
|
|
111
|
+
},
|
|
112
|
+
{ additionalProperties: false },
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
function renderFetchedText(items: FetchResultItem[], format: "text" | "markdown" | "json"): string {
|
|
116
|
+
if (format === "json") {
|
|
117
|
+
return JSON.stringify(items, null, 2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (format === "markdown") {
|
|
121
|
+
return items
|
|
122
|
+
.map((item) => {
|
|
123
|
+
const lines: string[] = [];
|
|
124
|
+
lines.push(`# ${item.title ?? "Untitled"}`);
|
|
125
|
+
lines.push(`URL: ${item.url}`);
|
|
126
|
+
lines.push("", item.text);
|
|
127
|
+
if (item.links.length > 0) {
|
|
128
|
+
const validLinks = item.links.filter((link) => link.url);
|
|
129
|
+
if (validLinks.length > 0) {
|
|
130
|
+
lines.push("", "## Links");
|
|
131
|
+
for (const link of validLinks) {
|
|
132
|
+
lines.push(`- [${link.title ?? link.url}](${link.url})`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
})
|
|
138
|
+
.join("\n\n---\n\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return items
|
|
142
|
+
.map((item, index) => {
|
|
143
|
+
const lines: string[] = [];
|
|
144
|
+
lines.push(`${index + 1}. ${item.title ?? "Untitled"}`);
|
|
145
|
+
lines.push(` URL: ${item.url}`);
|
|
146
|
+
lines.push(` ${item.text}`);
|
|
147
|
+
if (item.links.length > 0) {
|
|
148
|
+
const validLinks = item.links.filter((link) => link.url);
|
|
149
|
+
if (validLinks.length > 0) {
|
|
150
|
+
lines.push(" Links:");
|
|
151
|
+
for (const link of validLinks) {
|
|
152
|
+
lines.push(` - ${link.title ?? link.url}: ${link.url}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (item.images.length > 0) {
|
|
157
|
+
lines.push(" Images:");
|
|
158
|
+
for (const img of item.images) {
|
|
159
|
+
lines.push(` - ${img.url}${typeof img.width === "number" && typeof img.height === "number" ? ` (${img.width}x${img.height})` : ""}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
})
|
|
164
|
+
.join("\n\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const cozeWebFetchTool: ToolDefinition = {
|
|
168
|
+
name: "coze_web_fetch",
|
|
169
|
+
label: "Coze Web Fetch",
|
|
170
|
+
description:
|
|
171
|
+
"Fetch and extract structured content from web pages or documents through Coze.",
|
|
172
|
+
parameters: FetchToolSchema,
|
|
173
|
+
async execute(_toolCallId, rawParams) {
|
|
174
|
+
const params = rawParams as Static<typeof FetchToolSchema>;
|
|
175
|
+
const urls = typeof params.urls === "string" ? [params.urls] : params.urls;
|
|
176
|
+
const textOnly = params.textOnly === true;
|
|
177
|
+
console.log("[coze_web_fetch] urls=%s textOnly=%s format=%s", urls.join(", "), textOnly, params.format ?? "text");
|
|
178
|
+
try {
|
|
179
|
+
const items = await fetchContent(urls, textOnly);
|
|
180
|
+
console.log("[coze_web_fetch] done, %d pages fetched", items.length);
|
|
181
|
+
const format = params.format ?? "text";
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: renderFetchedText(items, format) }],
|
|
184
|
+
details: { urls, format, count: items.length },
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const message = formatCozeError(error);
|
|
188
|
+
console.error("[coze_web_fetch] error:", message);
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
191
|
+
details: { error: true, message },
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
2
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { SearchClient, type ImageItem, type SearchResponse, type WebItem } from "coze-coding-dev-sdk";
|
|
4
|
+
import { formatCozeError } from "../common/format-coze-error.js";
|
|
5
|
+
|
|
6
|
+
type SearchResultItem = {
|
|
7
|
+
title: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
siteName?: string;
|
|
10
|
+
snippet?: string;
|
|
11
|
+
content?: string;
|
|
12
|
+
publishTime?: string;
|
|
13
|
+
imageUrl?: string;
|
|
14
|
+
imageWidth?: number;
|
|
15
|
+
imageHeight?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type SearchResult = {
|
|
19
|
+
query: string;
|
|
20
|
+
type: "web" | "image";
|
|
21
|
+
summary?: string;
|
|
22
|
+
items: SearchResultItem[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function mapWebItem(item: WebItem): SearchResultItem {
|
|
26
|
+
return {
|
|
27
|
+
title: item.title,
|
|
28
|
+
url: item.url,
|
|
29
|
+
siteName: item.site_name,
|
|
30
|
+
snippet: item.snippet,
|
|
31
|
+
content: item.content,
|
|
32
|
+
publishTime: item.publish_time,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mapImageItem(item: ImageItem): SearchResultItem {
|
|
37
|
+
return {
|
|
38
|
+
title: item.title ?? "Untitled",
|
|
39
|
+
url: item.url,
|
|
40
|
+
siteName: item.site_name,
|
|
41
|
+
publishTime: item.publish_time,
|
|
42
|
+
imageUrl: item.image?.url,
|
|
43
|
+
imageWidth: item.image?.width,
|
|
44
|
+
imageHeight: item.image?.height,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type SearchInput = {
|
|
49
|
+
query: string;
|
|
50
|
+
type?: "web" | "image";
|
|
51
|
+
count?: number;
|
|
52
|
+
timeRange?: string;
|
|
53
|
+
sites?: string;
|
|
54
|
+
blockHosts?: string;
|
|
55
|
+
needSummary?: boolean;
|
|
56
|
+
needContent?: boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
async function searchWeb(input: SearchInput): Promise<SearchResult> {
|
|
60
|
+
const client = new SearchClient();
|
|
61
|
+
const type = input.type ?? "web";
|
|
62
|
+
const count = input.count ?? 10;
|
|
63
|
+
let response: SearchResponse;
|
|
64
|
+
|
|
65
|
+
if (type === "image") {
|
|
66
|
+
response = await client.imageSearch(input.query, count);
|
|
67
|
+
return {
|
|
68
|
+
query: input.query,
|
|
69
|
+
type,
|
|
70
|
+
items: (response.image_items ?? []).map(mapImageItem),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const hasAdvanced = Boolean(input.timeRange || input.sites || input.blockHosts || input.needContent);
|
|
75
|
+
if (hasAdvanced) {
|
|
76
|
+
response = await client.advancedSearch(input.query, {
|
|
77
|
+
searchType: "web",
|
|
78
|
+
count,
|
|
79
|
+
timeRange: input.timeRange,
|
|
80
|
+
sites: input.sites,
|
|
81
|
+
blockHosts: input.blockHosts,
|
|
82
|
+
needSummary: input.needSummary,
|
|
83
|
+
needContent: input.needContent,
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
response = await client.webSearch(input.query, count, input.needSummary);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
query: input.query,
|
|
91
|
+
type,
|
|
92
|
+
summary: response.summary,
|
|
93
|
+
items: (response.web_items ?? []).map(mapWebItem),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const SearchToolSchema = Type.Object(
|
|
98
|
+
{
|
|
99
|
+
query: Type.String({ description: "Search query." }),
|
|
100
|
+
type: Type.Optional(
|
|
101
|
+
Type.Unsafe<"web" | "image">({
|
|
102
|
+
type: "string",
|
|
103
|
+
enum: ["web", "image"],
|
|
104
|
+
description: "Search type. Defaults to web.",
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
count: Type.Optional(
|
|
108
|
+
Type.Integer({
|
|
109
|
+
description: "Number of results to return.",
|
|
110
|
+
minimum: 1,
|
|
111
|
+
maximum: 20,
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
timeRange: Type.Optional(
|
|
115
|
+
Type.Unsafe<"1d" | "1w" | "1m">({
|
|
116
|
+
type: "string",
|
|
117
|
+
enum: ["1d", "1w", "1m"],
|
|
118
|
+
description: "Recency filter for web search.",
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
sites: Type.Optional(
|
|
122
|
+
Type.String({ description: "Comma-separated domains to include." }),
|
|
123
|
+
),
|
|
124
|
+
blockHosts: Type.Optional(
|
|
125
|
+
Type.String({ description: "Comma-separated domains to exclude." }),
|
|
126
|
+
),
|
|
127
|
+
needSummary: Type.Optional(
|
|
128
|
+
Type.Boolean({ description: "Whether to include Coze summary output." }),
|
|
129
|
+
),
|
|
130
|
+
needContent: Type.Optional(
|
|
131
|
+
Type.Boolean({ description: "Whether to include extracted page content." }),
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
{ additionalProperties: false },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
function buildSearchText(
|
|
138
|
+
result: SearchResult,
|
|
139
|
+
options: { includeContent: boolean },
|
|
140
|
+
): string {
|
|
141
|
+
const lines = [`Coze web search: ${result.query}`];
|
|
142
|
+
if (result.summary) {
|
|
143
|
+
lines.push("", `Summary: ${result.summary}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push("", `Results (${result.items.length})`);
|
|
146
|
+
for (const [index, item] of result.items.entries()) {
|
|
147
|
+
lines.push(`${index + 1}. ${item.title}`);
|
|
148
|
+
if (item.url) {
|
|
149
|
+
lines.push(` URL: ${item.url}`);
|
|
150
|
+
}
|
|
151
|
+
if (item.imageUrl) {
|
|
152
|
+
lines.push(` Image: ${item.imageUrl}`);
|
|
153
|
+
}
|
|
154
|
+
if (item.siteName) {
|
|
155
|
+
lines.push(` Source: ${item.siteName}`);
|
|
156
|
+
}
|
|
157
|
+
if (item.publishTime) {
|
|
158
|
+
lines.push(` Published: ${item.publishTime}`);
|
|
159
|
+
}
|
|
160
|
+
if (item.snippet) {
|
|
161
|
+
lines.push(` ${item.snippet}`);
|
|
162
|
+
}
|
|
163
|
+
if (options.includeContent && item.content) {
|
|
164
|
+
lines.push(" Content:");
|
|
165
|
+
lines.push(` ${item.content}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const cozeWebSearchTool: ToolDefinition = {
|
|
172
|
+
name: "coze_web_search",
|
|
173
|
+
label: "Coze Web Search",
|
|
174
|
+
description:
|
|
175
|
+
"Search the web or images through Coze. Supports summaries, recency filters, and site restrictions.",
|
|
176
|
+
parameters: SearchToolSchema,
|
|
177
|
+
async execute(_toolCallId, rawParams) {
|
|
178
|
+
const params = rawParams as Static<typeof SearchToolSchema>;
|
|
179
|
+
console.log("[coze_web_search] query=%s type=%s count=%s", params.query, params.type ?? "web", params.count ?? 10);
|
|
180
|
+
try {
|
|
181
|
+
const result = await searchWeb(params);
|
|
182
|
+
console.log("[coze_web_search] done, %d results", result.items.length);
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: buildSearchText(result, { includeContent: params.needContent === true }),
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
details: {
|
|
191
|
+
query: result.query,
|
|
192
|
+
type: result.type,
|
|
193
|
+
summary: result.summary,
|
|
194
|
+
count: result.items.length,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const message = formatCozeError(error);
|
|
199
|
+
console.error("[coze_web_search] error:", message);
|
|
200
|
+
return {
|
|
201
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
202
|
+
details: { error: true, message },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export const paramsSchema = {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
port: {
|
|
14
|
+
type: 'number',
|
|
15
|
+
default: 5000,
|
|
16
|
+
minimum: 1024,
|
|
17
|
+
maximum: 65535,
|
|
18
|
+
description: 'Dashboard server port',
|
|
19
|
+
},
|
|
20
|
+
workspaceDir: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
default: '/workspace/workspace',
|
|
23
|
+
description: 'Workspace directory path',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
required: [],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const config = {
|
|
31
|
+
description:
|
|
32
|
+
'Pi Agent:`coze init ${COZE_WORKSPACE_PATH} --template pi-agent`\n' +
|
|
33
|
+
'- 适用:基于 pi-agent-core 的 AI Agent 应用\n' +
|
|
34
|
+
'- 支持飞书、微信等多渠道接入\n' +
|
|
35
|
+
'- 内置 Dashboard 管理面板\n' +
|
|
36
|
+
'- 使用 TypeScript + Express + Vite',
|
|
37
|
+
paramsSchema,
|
|
38
|
+
|
|
39
|
+
defaultParams: {
|
|
40
|
+
port: 5000,
|
|
41
|
+
workspaceDir: '/workspace/workspace',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default config;
|