fix: add robust Z.AI config persistence with error handling and logging
Some checks failed
Release Binaries / release (push) Has been cancelled
Some checks failed
Release Binaries / release (push) Has been cancelled
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { createHmac } from "crypto"
|
|
||||||
|
|
||||||
export const ZAIConfigSchema = z.object({
|
export const ZAIConfigSchema = z.object({
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
endpoint: z.string().default("https://api.z.ai/api/coding/paas/v4"),
|
endpoint: z.string().default("https://api.z.ai/api"),
|
||||||
enabled: z.boolean().default(false),
|
enabled: z.boolean().default(false),
|
||||||
timeout: z.number().default(300000)
|
timeout: z.number().default(300000)
|
||||||
})
|
})
|
||||||
@@ -142,7 +140,8 @@ export class ZAIClient {
|
|||||||
|
|
||||||
constructor(config: ZAIConfig) {
|
constructor(config: ZAIConfig) {
|
||||||
this.config = config
|
this.config = config
|
||||||
this.baseUrl = config.endpoint.replace(/\/$/, "")
|
const trimmed = config.endpoint.replace(/\/$/, "")
|
||||||
|
this.baseUrl = trimmed.replace(/\/(?:api\/coding\/)?paas\/v4$/, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
async testConnection(): Promise<boolean> {
|
async testConnection(): Promise<boolean> {
|
||||||
@@ -151,7 +150,7 @@ export class ZAIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
const response = await fetch(`${this.baseUrl}/paas/v4/chat/completions`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -182,7 +181,7 @@ export class ZAIClient {
|
|||||||
throw new Error("Z.AI API key is required")
|
throw new Error("Z.AI API key is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
const response = await fetch(`${this.baseUrl}/paas/v4/chat/completions`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -236,7 +235,7 @@ export class ZAIClient {
|
|||||||
throw new Error("Z.AI API key is required")
|
throw new Error("Z.AI API key is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
const response = await fetch(`${this.baseUrl}/paas/v4/chat/completions`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: this.getHeaders(),
|
headers: this.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -254,52 +253,9 @@ export class ZAIClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getHeaders(): Record<string, string> {
|
private getHeaders(): Record<string, string> {
|
||||||
const token = this.generateToken(this.config.apiKey!)
|
|
||||||
return {
|
return {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": `Bearer ${token}`
|
"Authorization": `Bearer ${this.config.apiKey!}`
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateToken(apiKey: string, expiresIn: number = 3600): string {
|
|
||||||
try {
|
|
||||||
const [id, secret] = apiKey.split(".")
|
|
||||||
if (!id || !secret) return apiKey // Fallback or handle error
|
|
||||||
|
|
||||||
const now = Date.now()
|
|
||||||
const payload = {
|
|
||||||
api_key: id,
|
|
||||||
exp: now + expiresIn * 1000,
|
|
||||||
timestamp: now
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = {
|
|
||||||
alg: "HS256",
|
|
||||||
sign_type: "SIGN"
|
|
||||||
}
|
|
||||||
|
|
||||||
const base64UrlEncode = (obj: any) => {
|
|
||||||
return Buffer.from(JSON.stringify(obj))
|
|
||||||
.toString('base64')
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=+$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodedHeader = base64UrlEncode(header)
|
|
||||||
const encodedPayload = base64UrlEncode(payload)
|
|
||||||
|
|
||||||
const signature = createHmac("sha256", secret)
|
|
||||||
.update(`${encodedHeader}.${encodedPayload}`)
|
|
||||||
.digest("base64")
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=+$/, '')
|
|
||||||
|
|
||||||
return `${encodedHeader}.${encodedPayload}.${signature}`
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to generate JWT, using raw key", e)
|
|
||||||
return apiKey
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -661,22 +661,23 @@ async function streamWithZAI(
|
|||||||
let content = ""
|
let content = ""
|
||||||
const toolCalls: ToolCall[] = []
|
const toolCalls: ToolCall[] = []
|
||||||
|
|
||||||
const baseUrl = "https://api.z.ai"
|
const baseUrl = "https://api.z.ai/api"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessToken) {
|
if (!accessToken) {
|
||||||
headers["Authorization"] = `Bearer ${accessToken}`
|
throw new Error("Z.AI API key required. Please authenticate with Z.AI first.")
|
||||||
}
|
}
|
||||||
|
headers["Authorization"] = `Bearer ${accessToken}`
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/v1/chat/completions`, {
|
const response = await fetch(`${baseUrl}/paas/v4/chat/completions`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: model ?? "z1-mini",
|
model: model ?? "glm-4.7",
|
||||||
messages,
|
messages,
|
||||||
stream: true,
|
stream: true,
|
||||||
tools: tools.length > 0 ? tools : undefined,
|
tools: tools.length > 0 ? tools : undefined,
|
||||||
|
|||||||
@@ -350,22 +350,42 @@ async function chatWithToolLoop(
|
|||||||
|
|
||||||
function getZAIConfig(): ZAIConfig {
|
function getZAIConfig(): ZAIConfig {
|
||||||
try {
|
try {
|
||||||
|
console.log(`[Z.AI] Looking for config at: ${CONFIG_FILE}`)
|
||||||
if (existsSync(CONFIG_FILE)) {
|
if (existsSync(CONFIG_FILE)) {
|
||||||
const data = readFileSync(CONFIG_FILE, 'utf-8')
|
const data = readFileSync(CONFIG_FILE, 'utf-8')
|
||||||
return JSON.parse(data)
|
const parsed = JSON.parse(data)
|
||||||
|
console.log(`[Z.AI] Config loaded from file, enabled: ${parsed.enabled}`)
|
||||||
|
return parsed
|
||||||
}
|
}
|
||||||
|
console.log(`[Z.AI] Config file not found, using defaults`)
|
||||||
return { enabled: false, endpoint: "https://api.z.ai/api/coding/paas/v4", timeout: 300000 }
|
return { enabled: false, endpoint: "https://api.z.ai/api/coding/paas/v4", timeout: 300000 }
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error(`[Z.AI] Error reading config:`, error)
|
||||||
return { enabled: false, endpoint: "https://api.z.ai/api/coding/paas/v4", timeout: 300000 }
|
return { enabled: false, endpoint: "https://api.z.ai/api/coding/paas/v4", timeout: 300000 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateZAIConfig(config: Partial<ZAIConfig>): void {
|
function updateZAIConfig(config: Partial<ZAIConfig>): void {
|
||||||
// Ensure directory exists
|
// Ensure directory exists with proper error handling
|
||||||
if (!existsSync(CONFIG_DIR)) {
|
try {
|
||||||
mkdirSync(CONFIG_DIR, { recursive: true })
|
if (!existsSync(CONFIG_DIR)) {
|
||||||
|
console.log(`[Z.AI] Creating config directory: ${CONFIG_DIR}`)
|
||||||
|
mkdirSync(CONFIG_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
} catch (mkdirError) {
|
||||||
|
console.error(`[Z.AI] Failed to create config directory:`, mkdirError)
|
||||||
|
throw new Error(`Failed to create config directory: ${mkdirError}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = getZAIConfig()
|
const current = getZAIConfig()
|
||||||
const updated = { ...current, ...config }
|
const updated = { ...current, ...config }
|
||||||
writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2))
|
|
||||||
|
try {
|
||||||
|
console.log(`[Z.AI] Writing config to: ${CONFIG_FILE}`)
|
||||||
|
writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2), 'utf-8')
|
||||||
|
console.log(`[Z.AI] Config saved successfully`)
|
||||||
|
} catch (writeError) {
|
||||||
|
console.error(`[Z.AI] Failed to write config file:`, writeError)
|
||||||
|
throw new Error(`Failed to write config file: ${writeError}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const ZAISettings: Component = () => {
|
|||||||
<label class="block font-medium mb-2">Endpoint</label>
|
<label class="block font-medium mb-2">Endpoint</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://api.z.ai/api/coding/paas/v4"
|
placeholder="https://api.z.ai/api"
|
||||||
value={config().endpoint || ''}
|
value={config().endpoint || ''}
|
||||||
onChange={(e) => handleConfigChange('endpoint', e.target.value)}
|
onChange={(e) => handleConfigChange('endpoint', e.target.value)}
|
||||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800"
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-800"
|
||||||
|
|||||||
Reference in New Issue
Block a user