@jiangxiaosheng/digital-agent-platform 1.0.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/README.md +37 -0
- package/eslint.config.mjs +20 -0
- package/messages/en.json +184 -0
- package/messages/zh.json +184 -0
- package/middleware.ts +9 -0
- package/next-env.d.ts +5 -0
- package/next.config.ts +16 -0
- package/package.json +59 -0
- package/postcss.config.mjs +5 -0
- package/src/app/[locale]/agents/[id]/chat/page.tsx +833 -0
- package/src/app/[locale]/agents/[id]/edit/page.tsx +58 -0
- package/src/app/[locale]/agents/new/page.tsx +52 -0
- package/src/app/[locale]/agents/page.tsx +122 -0
- package/src/app/[locale]/knowledge/page.tsx +305 -0
- package/src/app/[locale]/layout.tsx +46 -0
- package/src/app/[locale]/page.tsx +6 -0
- package/src/app/[locale]/settings/page.tsx +1204 -0
- package/src/app/api/agents/[id]/route.ts +27 -0
- package/src/app/api/agents/route.ts +27 -0
- package/src/app/api/auth-config/test/route.ts +69 -0
- package/src/app/api/bash-output/route.ts +57 -0
- package/src/app/api/chat/[agentId]/route.ts +164 -0
- package/src/app/api/env-vars/[key]/route.ts +22 -0
- package/src/app/api/env-vars/route.ts +26 -0
- package/src/app/api/env-vars/scan/route.ts +66 -0
- package/src/app/api/file-serve/route.ts +58 -0
- package/src/app/api/fs/browse/route.ts +47 -0
- package/src/app/api/knowledge/[id]/documents/[docId]/route.ts +22 -0
- package/src/app/api/knowledge/[id]/route.ts +21 -0
- package/src/app/api/knowledge/[id]/upload/route.ts +90 -0
- package/src/app/api/knowledge/route.ts +15 -0
- package/src/app/api/knowledge/search/route.ts +26 -0
- package/src/app/api/models/route.ts +160 -0
- package/src/app/api/models-config/route.ts +41 -0
- package/src/app/api/models-config/test/route.ts +84 -0
- package/src/app/api/sessions/[agentId]/[sessionId]/route.ts +12 -0
- package/src/app/api/sessions/[agentId]/route.ts +19 -0
- package/src/app/api/sessions/history/route.ts +166 -0
- package/src/app/api/settings/route.ts +45 -0
- package/src/app/api/skills/[name]/route.ts +40 -0
- package/src/app/api/skills/install/route.ts +35 -0
- package/src/app/api/skills/route.ts +54 -0
- package/src/app/api/skills/search/route.ts +60 -0
- package/src/app/api/tts/route.ts +235 -0
- package/src/app/api/voice-provider/route.ts +98 -0
- package/src/app/api/workspace-files/route.ts +151 -0
- package/src/app/globals.css +176 -0
- package/src/app/layout.tsx +13 -0
- package/src/app/page.tsx +6 -0
- package/src/components/agents/agent-form.tsx +457 -0
- package/src/components/agents/model-selector.tsx +290 -0
- package/src/components/agents/skills-manager.tsx +347 -0
- package/src/components/chat/input-bar.tsx +561 -0
- package/src/components/chat/message-bubble.tsx +395 -0
- package/src/components/layout/sidebar.tsx +304 -0
- package/src/components/providers.tsx +19 -0
- package/src/components/ui/badge.tsx +26 -0
- package/src/components/ui/button.tsx +43 -0
- package/src/components/ui/card.tsx +46 -0
- package/src/components/ui/dialog.tsx +74 -0
- package/src/components/ui/directory-picker.tsx +185 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/label.tsx +16 -0
- package/src/components/ui/scroll-area.tsx +41 -0
- package/src/components/ui/select.tsx +93 -0
- package/src/components/ui/slider.tsx +35 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/tabs.tsx +48 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toaster.tsx +50 -0
- package/src/hooks/use-toast.ts +43 -0
- package/src/i18n/request.ts +13 -0
- package/src/i18n/routing.ts +7 -0
- package/src/lib/agent-factory.ts +200 -0
- package/src/lib/agents/store.ts +95 -0
- package/src/lib/db/client.ts +25 -0
- package/src/lib/db/schema.ts +66 -0
- package/src/lib/env-vars.ts +33 -0
- package/src/lib/knowledge/chunker.ts +34 -0
- package/src/lib/knowledge/embedder.ts +48 -0
- package/src/lib/knowledge/retriever.ts +94 -0
- package/src/lib/knowledge/store.ts +74 -0
- package/src/lib/models-config.ts +55 -0
- package/src/lib/settings.ts +33 -0
- package/src/lib/skills.ts +79 -0
- package/src/lib/system-prompt.ts +52 -0
- package/src/lib/tool-builder.ts +19 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/voice-providers-config.ts +71 -0
- package/src/middleware.ts +9 -0
- package/src/types/agent.ts +124 -0
- package/src/types/knowledge.ts +39 -0
- package/src/types/settings.ts +31 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { kbStore } from "@/lib/knowledge/store";
|
|
3
|
+
import { chunkText } from "@/lib/knowledge/chunker";
|
|
4
|
+
import { embedText } from "@/lib/knowledge/embedder";
|
|
5
|
+
import { storeChunk } from "@/lib/knowledge/retriever";
|
|
6
|
+
import { initSchema } from "@/lib/db/schema";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import os from "os";
|
|
10
|
+
|
|
11
|
+
export const dynamic = "force-dynamic";
|
|
12
|
+
|
|
13
|
+
initSchema();
|
|
14
|
+
|
|
15
|
+
export async function POST(
|
|
16
|
+
req: NextRequest,
|
|
17
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
18
|
+
) {
|
|
19
|
+
const { id: kbId } = await params;
|
|
20
|
+
|
|
21
|
+
const kb = kbStore.getById(kbId);
|
|
22
|
+
if (!kb) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
23
|
+
|
|
24
|
+
const formData = await req.formData();
|
|
25
|
+
const file = formData.get("file") as File | null;
|
|
26
|
+
if (!file) return NextResponse.json({ error: "No file" }, { status: 400 });
|
|
27
|
+
|
|
28
|
+
// Save file to disk
|
|
29
|
+
const uploadDir = path.join(os.homedir(), ".digital-agents", "uploads", kbId);
|
|
30
|
+
if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
|
|
31
|
+
const filePath = path.join(uploadDir, file.name);
|
|
32
|
+
const bytes = await file.arrayBuffer();
|
|
33
|
+
fs.writeFileSync(filePath, Buffer.from(bytes));
|
|
34
|
+
|
|
35
|
+
// Register document
|
|
36
|
+
const docId = kbStore.createDocument(kbId, file.name, filePath, file.type);
|
|
37
|
+
|
|
38
|
+
// Process async (don't await — respond immediately, process in background)
|
|
39
|
+
processDocument(kbId, docId, filePath, file.name, file.type).catch((e) => {
|
|
40
|
+
kbStore.updateDocumentStatus(docId, "error", 0, String(e));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return NextResponse.json({ docId, status: "processing" }, { status: 202 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function processDocument(
|
|
47
|
+
kbId: string,
|
|
48
|
+
docId: string,
|
|
49
|
+
filePath: string,
|
|
50
|
+
filename: string,
|
|
51
|
+
mimeType: string
|
|
52
|
+
) {
|
|
53
|
+
// Parse document
|
|
54
|
+
const text = await parseDocument(filePath, mimeType);
|
|
55
|
+
|
|
56
|
+
// Chunk
|
|
57
|
+
const chunks = chunkText(text, { filename });
|
|
58
|
+
|
|
59
|
+
// Embed and store
|
|
60
|
+
let stored = 0;
|
|
61
|
+
for (const chunk of chunks) {
|
|
62
|
+
const embedding = await embedText(chunk.content);
|
|
63
|
+
storeChunk(kbId, docId, chunk.content, embedding, chunk.metadata);
|
|
64
|
+
stored++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
kbStore.updateDocumentStatus(docId, "ready", stored);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function parseDocument(filePath: string, mimeType: string): Promise<string> {
|
|
71
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
72
|
+
|
|
73
|
+
if (ext === ".pdf") {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
75
|
+
const pdfParse = require("pdf-parse");
|
|
76
|
+
const buffer = fs.readFileSync(filePath);
|
|
77
|
+
const data = await pdfParse(buffer);
|
|
78
|
+
return data.text as string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (ext === ".docx" || mimeType.includes("wordprocessingml")) {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
83
|
+
const mammoth = require("mammoth");
|
|
84
|
+
const result = await mammoth.extractRawText({ path: filePath });
|
|
85
|
+
return result.value as string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// TXT / Markdown
|
|
89
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
90
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { kbStore } from "@/lib/knowledge/store";
|
|
3
|
+
import { initSchema } from "@/lib/db/schema";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return NextResponse.json(kbStore.list());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function POST(req: NextRequest) {
|
|
11
|
+
const { name, description } = (await req.json()) as { name: string; description?: string };
|
|
12
|
+
if (!name?.trim()) return NextResponse.json({ error: "Name required" }, { status: 400 });
|
|
13
|
+
const kb = kbStore.create(name, description);
|
|
14
|
+
return NextResponse.json(kb, { status: 201 });
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { searchChunks } from "@/lib/knowledge/retriever";
|
|
3
|
+
import { embedText } from "@/lib/knowledge/embedder";
|
|
4
|
+
import { initSchema } from "@/lib/db/schema";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export async function POST(req: NextRequest) {
|
|
8
|
+
const { kbIds, query, topK = 5 } = (await req.json()) as {
|
|
9
|
+
kbIds: string[];
|
|
10
|
+
query: string;
|
|
11
|
+
topK?: number;
|
|
12
|
+
};
|
|
13
|
+
if (!query || !kbIds?.length) return NextResponse.json([]);
|
|
14
|
+
|
|
15
|
+
const queryVec = await embedText(query);
|
|
16
|
+
const results = await searchChunks(kbIds, queryVec, topK);
|
|
17
|
+
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
results.map((r) => ({
|
|
20
|
+
content: r.chunk.content,
|
|
21
|
+
score: r.score,
|
|
22
|
+
filename: r.chunk.metadata.filename,
|
|
23
|
+
kbId: r.chunk.kbId,
|
|
24
|
+
}))
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
export type ModelInfo = {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
providerId: string;
|
|
11
|
+
providerName: string;
|
|
12
|
+
api: string;
|
|
13
|
+
contextWindow: number;
|
|
14
|
+
maxTokens: number;
|
|
15
|
+
supportsThinking: boolean;
|
|
16
|
+
supportsTemperature: boolean;
|
|
17
|
+
thinkingLevels: string[];
|
|
18
|
+
inputTypes: string[];
|
|
19
|
+
costInput?: number;
|
|
20
|
+
costOutput?: number;
|
|
21
|
+
isCustom?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ProviderGroup = {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
models: ModelInfo[];
|
|
28
|
+
isCustom?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
interface SdkModel {
|
|
32
|
+
id: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
api?: string;
|
|
35
|
+
provider: string;
|
|
36
|
+
contextWindow?: number;
|
|
37
|
+
maxTokens?: number;
|
|
38
|
+
reasoning?: boolean;
|
|
39
|
+
input?: string[];
|
|
40
|
+
cost?: { input?: number; output?: number };
|
|
41
|
+
thinkingLevelMap?: Record<string, string | null>;
|
|
42
|
+
compat?: { supportsTemperature?: boolean; [k: string]: unknown };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getAgentDir() {
|
|
46
|
+
return process.env.PI_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const PRIORITY = [
|
|
50
|
+
"anthropic", "openai", "google", "mistral", "groq",
|
|
51
|
+
"deepseek", "xai", "amazon-bedrock", "google-vertex",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export async function GET() {
|
|
55
|
+
try {
|
|
56
|
+
const { ModelRuntime } = await import("@earendil-works/pi-coding-agent");
|
|
57
|
+
|
|
58
|
+
const agentDir = getAgentDir();
|
|
59
|
+
const modelsStorePath = path.join(agentDir, "models.json");
|
|
60
|
+
const authPath = path.join(agentDir, "auth.json");
|
|
61
|
+
|
|
62
|
+
const runtime = await ModelRuntime.create({ authPath, modelsStorePath });
|
|
63
|
+
|
|
64
|
+
const sdkProviders = runtime.getProviders();
|
|
65
|
+
const providerNameMap = new Map<string, string>(sdkProviders.map((p) => [p.id, p.name]));
|
|
66
|
+
|
|
67
|
+
const allModels = runtime.getModels() as unknown as SdkModel[];
|
|
68
|
+
const groupMap = new Map<string, ProviderGroup>();
|
|
69
|
+
|
|
70
|
+
// Read custom provider names from models.json for isCustom flag
|
|
71
|
+
const { readModelsConfig } = await import("@/lib/models-config");
|
|
72
|
+
const modelsConfig = readModelsConfig();
|
|
73
|
+
const customProviderIds = new Set(Object.keys(modelsConfig.providers));
|
|
74
|
+
|
|
75
|
+
// Read configured built-in providers from auth.json
|
|
76
|
+
const { readAuthConfig } = await import("@/lib/models-config");
|
|
77
|
+
const authConfig = readAuthConfig();
|
|
78
|
+
const configuredBuiltinIds = new Set(Object.keys(authConfig));
|
|
79
|
+
|
|
80
|
+
// A provider is "available" if it has a key in auth.json OR is a custom provider in models.json
|
|
81
|
+
const isProviderConfigured = (id: string) =>
|
|
82
|
+
customProviderIds.has(id) || configuredBuiltinIds.has(id);
|
|
83
|
+
|
|
84
|
+
for (const m of allModels) {
|
|
85
|
+
const providerId = m.provider ?? "unknown";
|
|
86
|
+
|
|
87
|
+
// Skip unconfigured built-in providers (show only configured or custom)
|
|
88
|
+
if (!isProviderConfigured(providerId)) continue;
|
|
89
|
+
|
|
90
|
+
const providerName = providerNameMap.get(providerId) ?? providerId;
|
|
91
|
+
const isCustom = customProviderIds.has(providerId);
|
|
92
|
+
|
|
93
|
+
const rawMap = m.thinkingLevelMap ?? {};
|
|
94
|
+
const thinkingLevels = Object.entries(rawMap)
|
|
95
|
+
.filter(([, v]) => v !== null)
|
|
96
|
+
.map(([k]) => k);
|
|
97
|
+
|
|
98
|
+
const supportsThinking = m.reasoning === true || thinkingLevels.length > 0;
|
|
99
|
+
const supportsTemperature = m.compat?.supportsTemperature !== false;
|
|
100
|
+
|
|
101
|
+
const info: ModelInfo = {
|
|
102
|
+
id: m.id,
|
|
103
|
+
name: m.name ?? m.id,
|
|
104
|
+
providerId,
|
|
105
|
+
providerName,
|
|
106
|
+
api: m.api ?? "",
|
|
107
|
+
contextWindow: m.contextWindow ?? 200000,
|
|
108
|
+
maxTokens: m.maxTokens ?? 8192,
|
|
109
|
+
supportsThinking,
|
|
110
|
+
supportsTemperature,
|
|
111
|
+
thinkingLevels,
|
|
112
|
+
inputTypes: m.input ?? ["text"],
|
|
113
|
+
costInput: m.cost?.input,
|
|
114
|
+
costOutput: m.cost?.output,
|
|
115
|
+
isCustom,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (!groupMap.has(providerId)) {
|
|
119
|
+
groupMap.set(providerId, { id: providerId, name: providerName, models: [], isCustom });
|
|
120
|
+
}
|
|
121
|
+
groupMap.get(providerId)!.models.push(info);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const groups = [...groupMap.values()]
|
|
125
|
+
.filter((g) => g.models.length > 0)
|
|
126
|
+
.sort((a, b) => {
|
|
127
|
+
if (a.isCustom && !b.isCustom) return -1;
|
|
128
|
+
if (!a.isCustom && b.isCustom) return 1;
|
|
129
|
+
const ai = PRIORITY.indexOf(a.id), bi = PRIORITY.indexOf(b.id);
|
|
130
|
+
if (ai !== -1 && bi !== -1) return ai - bi;
|
|
131
|
+
if (ai !== -1) return -1;
|
|
132
|
+
if (bi !== -1) return 1;
|
|
133
|
+
return a.name.localeCompare(b.name);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return NextResponse.json(groups);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error("ModelRuntime error:", e);
|
|
139
|
+
return NextResponse.json(getFallback());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getFallback(): ProviderGroup[] {
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
id: "anthropic", name: "Anthropic", models: [
|
|
147
|
+
{ id: "claude-opus-4-8", name: "Claude Opus 4.8", providerId: "anthropic", providerName: "Anthropic", api: "anthropic-messages", contextWindow: 1000000, maxTokens: 128000, supportsThinking: true, supportsTemperature: false, thinkingLevels: ["xhigh", "max"], inputTypes: ["text", "image"], costInput: 15, costOutput: 75 },
|
|
148
|
+
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", providerId: "anthropic", providerName: "Anthropic", api: "anthropic-messages", contextWindow: 1000000, maxTokens: 128000, supportsThinking: true, supportsTemperature: false, thinkingLevels: ["xhigh", "max"], inputTypes: ["text", "image"], costInput: 3, costOutput: 15 },
|
|
149
|
+
{ id: "claude-opus-4-5", name: "Claude Opus 4.5", providerId: "anthropic", providerName: "Anthropic", api: "anthropic-messages", contextWindow: 200000, maxTokens: 64000, supportsThinking: false, supportsTemperature: true, thinkingLevels: [], inputTypes: ["text", "image"], costInput: 5, costOutput: 25 },
|
|
150
|
+
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", providerId: "anthropic", providerName: "Anthropic", api: "anthropic-messages", contextWindow: 200000, maxTokens: 64000, supportsThinking: false, supportsTemperature: true, thinkingLevels: [], inputTypes: ["text", "image"], costInput: 1, costOutput: 5 },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "openai", name: "OpenAI", models: [
|
|
155
|
+
{ id: "gpt-4o", name: "GPT-4o", providerId: "openai", providerName: "OpenAI", api: "openai-responses", contextWindow: 128000, maxTokens: 16384, supportsThinking: false, supportsTemperature: true, thinkingLevels: [], inputTypes: ["text", "image"], costInput: 2.5, costOutput: 10 },
|
|
156
|
+
{ id: "o3", name: "o3", providerId: "openai", providerName: "OpenAI", api: "openai-responses", contextWindow: 200000, maxTokens: 100000, supportsThinking: true, supportsTemperature: false, thinkingLevels: ["low", "medium", "high"], inputTypes: ["text", "image"], costInput: 10, costOutput: 40 },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
readModelsConfig, writeModelsConfig,
|
|
4
|
+
readAuthConfig, writeAuthConfig,
|
|
5
|
+
} from "@/lib/models-config";
|
|
6
|
+
import type { ModelsConfig } from "@/lib/models-config";
|
|
7
|
+
|
|
8
|
+
export const dynamic = "force-dynamic";
|
|
9
|
+
|
|
10
|
+
// GET — returns { modelsConfig, auth (masked) }
|
|
11
|
+
export async function GET() {
|
|
12
|
+
const modelsConfig = readModelsConfig();
|
|
13
|
+
const authRaw = readAuthConfig();
|
|
14
|
+
const auth: Record<string, string> = {};
|
|
15
|
+
for (const [k, v] of Object.entries(authRaw)) {
|
|
16
|
+
auth[k] = v.key ? "***" + v.key.slice(-4) : "";
|
|
17
|
+
}
|
|
18
|
+
return NextResponse.json({ modelsConfig, auth });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// PUT — body: { modelsConfig?, auth? }
|
|
22
|
+
export async function PUT(req: NextRequest) {
|
|
23
|
+
try {
|
|
24
|
+
const body = await req.json() as {
|
|
25
|
+
modelsConfig?: ModelsConfig;
|
|
26
|
+
auth?: Record<string, string>;
|
|
27
|
+
};
|
|
28
|
+
if (body.modelsConfig) writeModelsConfig(body.modelsConfig);
|
|
29
|
+
if (body.auth) {
|
|
30
|
+
const current = readAuthConfig();
|
|
31
|
+
for (const [provider, key] of Object.entries(body.auth)) {
|
|
32
|
+
if (!key) delete current[provider];
|
|
33
|
+
else if (!key.startsWith("***")) current[provider] = { type: "apiKey", key };
|
|
34
|
+
}
|
|
35
|
+
writeAuthConfig(current);
|
|
36
|
+
}
|
|
37
|
+
return NextResponse.json({ success: true });
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return NextResponse.json({ error: String(e) }, { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { readAuthConfig } from "@/lib/models-config";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest) {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
try {
|
|
9
|
+
const { providerName, provider, model } = await req.json() as {
|
|
10
|
+
providerName: string;
|
|
11
|
+
provider: { baseUrl?: string; api?: string; apiKey?: string };
|
|
12
|
+
model: { id: string };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (!model.id?.trim()) {
|
|
16
|
+
return NextResponse.json({ ok: false, error: "Model ID required" }, { status: 400 });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const baseUrl = provider.baseUrl?.replace(/\/$/, "") || "https://api.openai.com/v1";
|
|
20
|
+
let apiKey = provider.apiKey ?? "";
|
|
21
|
+
|
|
22
|
+
// Resolve API key: env var or shell command
|
|
23
|
+
if (apiKey.startsWith("!")) {
|
|
24
|
+
const { execSync } = await import("child_process");
|
|
25
|
+
apiKey = execSync(apiKey.slice(1)).toString().trim();
|
|
26
|
+
} else if (apiKey && !apiKey.startsWith("sk-") && !apiKey.includes(" ") && apiKey === apiKey.toUpperCase()) {
|
|
27
|
+
apiKey = process.env[apiKey] ?? apiKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Also try from auth.json if no key provided
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
const auth = readAuthConfig();
|
|
33
|
+
apiKey = auth[providerName]?.key ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const api = provider.api ?? "openai-responses";
|
|
37
|
+
let status = 0;
|
|
38
|
+
let responseText = "";
|
|
39
|
+
|
|
40
|
+
if (api === "anthropic-messages") {
|
|
41
|
+
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
"x-api-key": apiKey,
|
|
46
|
+
"anthropic-version": "2023-06-01",
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
model: model.id,
|
|
50
|
+
max_tokens: 10,
|
|
51
|
+
messages: [{ role: "user", content: "ping" }],
|
|
52
|
+
}),
|
|
53
|
+
signal: AbortSignal.timeout(15000),
|
|
54
|
+
});
|
|
55
|
+
status = res.status;
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
if (!res.ok) return NextResponse.json({ ok: false, error: data.error?.message ?? JSON.stringify(data), latencyMs: Date.now() - start, status });
|
|
58
|
+
responseText = data.content?.[0]?.text ?? "";
|
|
59
|
+
} else {
|
|
60
|
+
// OpenAI-compatible
|
|
61
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
Authorization: `Bearer ${apiKey}`,
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
model: model.id,
|
|
69
|
+
max_tokens: 10,
|
|
70
|
+
messages: [{ role: "user", content: "ping" }],
|
|
71
|
+
}),
|
|
72
|
+
signal: AbortSignal.timeout(15000),
|
|
73
|
+
});
|
|
74
|
+
status = res.status;
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
if (!res.ok) return NextResponse.json({ ok: false, error: data.error?.message ?? JSON.stringify(data), latencyMs: Date.now() - start, status });
|
|
77
|
+
responseText = data.choices?.[0]?.message?.content ?? "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return NextResponse.json({ ok: true, latencyMs: Date.now() - start, status, responseText });
|
|
81
|
+
} catch (e) {
|
|
82
|
+
return NextResponse.json({ ok: false, error: String(e), latencyMs: Date.now() - start }, { status: 500 });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { sessionStore } from "@/lib/agents/store";
|
|
3
|
+
|
|
4
|
+
type Params = { params: Promise<{ agentId: string; sessionId: string }> };
|
|
5
|
+
|
|
6
|
+
// DELETE — remove a single session record (DB only, JSONL file untouched)
|
|
7
|
+
export async function DELETE(_req: Request, { params }: Params) {
|
|
8
|
+
const { sessionId } = await params;
|
|
9
|
+
const ok = sessionStore.delete(sessionId);
|
|
10
|
+
if (!ok) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
11
|
+
return NextResponse.json({ success: true });
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { sessionStore } from "@/lib/agents/store";
|
|
3
|
+
import { initSchema } from "@/lib/db/schema";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type Params = { params: Promise<{ agentId: string }> };
|
|
7
|
+
|
|
8
|
+
// GET — list all sessions for an agent
|
|
9
|
+
export async function GET(_req: NextRequest, { params }: Params) {
|
|
10
|
+
const { agentId } = await params;
|
|
11
|
+
return NextResponse.json(sessionStore.list(agentId));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// DELETE — clear ALL sessions for an agent
|
|
15
|
+
export async function DELETE(_req: NextRequest, { params }: Params) {
|
|
16
|
+
const { agentId } = await params;
|
|
17
|
+
const count = sessionStore.deleteByAgent(agentId);
|
|
18
|
+
return NextResponse.json({ success: true, deleted: count });
|
|
19
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
interface ContentBlock {
|
|
7
|
+
type: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
thinking?: string;
|
|
10
|
+
name?: string; // tool_use: tool name
|
|
11
|
+
id?: string; // tool_use: call id
|
|
12
|
+
input?: unknown; // tool_use: args
|
|
13
|
+
content?: unknown; // tool_result: result
|
|
14
|
+
is_error?: boolean; // tool_result: error flag
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface JournalEntry {
|
|
18
|
+
type: string;
|
|
19
|
+
id: string;
|
|
20
|
+
parentId: string | null;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
message?: {
|
|
23
|
+
role: "user" | "assistant";
|
|
24
|
+
content: string | ContentBlock[];
|
|
25
|
+
stopReason?: string;
|
|
26
|
+
errorMessage?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface HistoryToolCall {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
input?: string;
|
|
34
|
+
output?: string;
|
|
35
|
+
isError?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface HistoryMessage {
|
|
39
|
+
id: string;
|
|
40
|
+
role: "user" | "assistant";
|
|
41
|
+
content: string;
|
|
42
|
+
thinking?: string;
|
|
43
|
+
toolCalls?: HistoryToolCall[];
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Extract text, thinking, and tool calls from a message content block array */
|
|
48
|
+
function extractContent(content: string | ContentBlock[]): {
|
|
49
|
+
text: string;
|
|
50
|
+
thinking?: string;
|
|
51
|
+
toolUses: Map<string, HistoryToolCall>;
|
|
52
|
+
} {
|
|
53
|
+
if (typeof content === "string") return { text: content, toolUses: new Map() };
|
|
54
|
+
|
|
55
|
+
const textParts: string[] = [];
|
|
56
|
+
const thinkingParts: string[] = [];
|
|
57
|
+
const toolUses = new Map<string, HistoryToolCall>();
|
|
58
|
+
|
|
59
|
+
for (const block of content) {
|
|
60
|
+
if (block.type === "text" && block.text) textParts.push(block.text);
|
|
61
|
+
else if (block.type === "thinking" && block.thinking) thinkingParts.push(block.thinking);
|
|
62
|
+
else if (block.type === "tool_use" && block.id && block.name) {
|
|
63
|
+
toolUses.set(block.id, {
|
|
64
|
+
id: block.id,
|
|
65
|
+
name: block.name,
|
|
66
|
+
input: block.input !== undefined ? JSON.stringify(block.input, null, 2) : undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
text: textParts.join(""),
|
|
73
|
+
thinking: thinkingParts.join("") || undefined,
|
|
74
|
+
toolUses,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readSessionMessages(sessionFile: string): HistoryMessage[] {
|
|
79
|
+
if (!fs.existsSync(sessionFile)) return [];
|
|
80
|
+
|
|
81
|
+
const lines = fs.readFileSync(sessionFile, "utf8").split("\n").filter(Boolean);
|
|
82
|
+
const entries: JournalEntry[] = [];
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
try { entries.push(JSON.parse(line) as JournalEntry); } catch { /* skip */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build id → entry map and hasChild set
|
|
88
|
+
const idMap = new Map(entries.map((e) => [e.id, e]));
|
|
89
|
+
const hasChild = new Set<string>();
|
|
90
|
+
for (const e of entries) { if (e.parentId) hasChild.add(e.parentId); }
|
|
91
|
+
|
|
92
|
+
// Find latest leaf
|
|
93
|
+
const leaves = entries.filter((e) => !hasChild.has(e.id));
|
|
94
|
+
if (!leaves.length) return [];
|
|
95
|
+
const leaf = leaves[leaves.length - 1];
|
|
96
|
+
|
|
97
|
+
// Walk leaf → root
|
|
98
|
+
const path: JournalEntry[] = [];
|
|
99
|
+
let cur: JournalEntry | undefined = leaf;
|
|
100
|
+
while (cur) {
|
|
101
|
+
path.unshift(cur);
|
|
102
|
+
cur = cur.parentId ? idMap.get(cur.parentId) : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Collect tool_result outputs keyed by tool_call_id from user messages
|
|
106
|
+
const toolResults = new Map<string, { output: string; isError: boolean }>();
|
|
107
|
+
for (const entry of path) {
|
|
108
|
+
if (entry.type !== "message" || !entry.message || entry.message.role !== "user") continue;
|
|
109
|
+
const blocks = Array.isArray(entry.message.content) ? entry.message.content : [];
|
|
110
|
+
for (const block of blocks) {
|
|
111
|
+
if (block.type === "tool_result" && block.id) {
|
|
112
|
+
const out = typeof block.content === "string"
|
|
113
|
+
? block.content
|
|
114
|
+
: Array.isArray(block.content)
|
|
115
|
+
? (block.content as ContentBlock[]).filter(b => b.type === "text").map(b => b.text ?? "").join("")
|
|
116
|
+
: "";
|
|
117
|
+
toolResults.set(block.id, { output: out, isError: !!block.is_error });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const messages: HistoryMessage[] = [];
|
|
123
|
+
for (const entry of path) {
|
|
124
|
+
if (entry.type !== "message" || !entry.message) continue;
|
|
125
|
+
const { role, content, stopReason, errorMessage } = entry.message;
|
|
126
|
+
if (role === "assistant" && (stopReason === "error" || errorMessage)) continue;
|
|
127
|
+
|
|
128
|
+
const { text, thinking, toolUses } = extractContent(content);
|
|
129
|
+
const visibleText = text.trim();
|
|
130
|
+
const hasToolUses = toolUses.size > 0;
|
|
131
|
+
|
|
132
|
+
if (!visibleText && !hasToolUses) continue;
|
|
133
|
+
// Skip user messages that are pure tool_result blocks
|
|
134
|
+
if (role === "user" && !visibleText && Array.isArray(content) &&
|
|
135
|
+
content.every(b => b.type === "tool_result")) continue;
|
|
136
|
+
|
|
137
|
+
// Merge tool outputs into tool calls
|
|
138
|
+
const toolCalls: HistoryToolCall[] = [];
|
|
139
|
+
for (const [id, tc] of toolUses.entries()) {
|
|
140
|
+
const result = toolResults.get(id);
|
|
141
|
+
toolCalls.push({ ...tc, output: result?.output, isError: result?.isError });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
messages.push({
|
|
145
|
+
id: entry.id,
|
|
146
|
+
role,
|
|
147
|
+
content: visibleText,
|
|
148
|
+
thinking: thinking || undefined,
|
|
149
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
150
|
+
createdAt: entry.timestamp,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return messages;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function POST(req: NextRequest) {
|
|
158
|
+
try {
|
|
159
|
+
const { sessionFile } = await req.json() as { sessionFile: string };
|
|
160
|
+
if (!sessionFile) return NextResponse.json({ error: "sessionFile required" }, { status: 400 });
|
|
161
|
+
const messages = readSessionMessages(sessionFile);
|
|
162
|
+
return NextResponse.json({ messages });
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return NextResponse.json({ error: String(e) }, { status: 500 });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSettings, saveSettings } from "@/lib/settings";
|
|
3
|
+
import { initSchema } from "@/lib/db/schema";
|
|
4
|
+
import type { AppSettings } from "@/types/settings";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
const settings = getSettings();
|
|
9
|
+
// Mask API keys in response
|
|
10
|
+
const masked = {
|
|
11
|
+
...settings,
|
|
12
|
+
apiKeys: Object.fromEntries(
|
|
13
|
+
Object.entries(settings.apiKeys).map(([k, v]) => [
|
|
14
|
+
k,
|
|
15
|
+
v ? "***" + v.slice(-4) : "",
|
|
16
|
+
])
|
|
17
|
+
),
|
|
18
|
+
};
|
|
19
|
+
return NextResponse.json(masked);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function PUT(req: NextRequest) {
|
|
23
|
+
try {
|
|
24
|
+
const body = (await req.json()) as Partial<AppSettings>;
|
|
25
|
+
const current = getSettings();
|
|
26
|
+
// Merge — if API key value starts with ***, keep existing
|
|
27
|
+
const apiKeys = { ...current.apiKeys };
|
|
28
|
+
if (body.apiKeys) {
|
|
29
|
+
for (const [provider, val] of Object.entries(body.apiKeys)) {
|
|
30
|
+
if (val && !val.startsWith("***")) {
|
|
31
|
+
(apiKeys as Record<string, string>)[provider] = val;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const merged: AppSettings = {
|
|
36
|
+
...current,
|
|
37
|
+
...body,
|
|
38
|
+
apiKeys,
|
|
39
|
+
};
|
|
40
|
+
saveSettings(merged);
|
|
41
|
+
return NextResponse.json({ success: true });
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return NextResponse.json({ error: String(e) }, { status: 500 });
|
|
44
|
+
}
|
|
45
|
+
}
|