-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(webauth): sync models.providers to config after webauth, remove s… #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| import type { StreamFn } from "@mariozechner/pi-agent-core"; | ||
| import { | ||
| createAssistantMessageEventStream, | ||
| type AssistantMessage, | ||
| type TextContent, | ||
| type ToolCall, | ||
| type ToolResultMessage, | ||
| } from "@mariozechner/pi-ai"; | ||
| import { | ||
| PerplexityWebClientBrowser, | ||
| type PerplexityWebClientOptions, | ||
| } from "../providers/perplexity-web-client-browser.js"; | ||
|
|
||
| // Helper to strip messages for web providers | ||
| function stripForWebProvider(prompt: string): string { | ||
| return prompt; | ||
| } | ||
|
|
||
| export function createPerplexityWebStreamFn(cookieOrJson: string): StreamFn { | ||
| let options: PerplexityWebClientOptions; | ||
| try { | ||
| const parsed = JSON.parse(cookieOrJson); | ||
| options = typeof parsed === "string" ? { cookie: parsed, userAgent: "Mozilla/5.0" } : parsed; | ||
| } catch { | ||
| options = { cookie: cookieOrJson, userAgent: "Mozilla/5.0" }; | ||
| } | ||
| const client = new PerplexityWebClientBrowser(options); | ||
|
|
||
| return (model, context, streamOptions) => { | ||
| const stream = createAssistantMessageEventStream(); | ||
|
|
||
| const run = async () => { | ||
| try { | ||
| await client.init(); | ||
|
|
||
| const messages = context.messages || []; | ||
| const systemPrompt = (context as unknown as { systemPrompt?: string }).systemPrompt || ""; | ||
|
|
||
| const historyParts: string[] = []; | ||
| if (systemPrompt && !messages.some((m) => (m.role as string) === "system")) { | ||
| historyParts.push(`System: ${systemPrompt}`); | ||
| } | ||
| for (const m of messages) { | ||
| const role = m.role === "user" || m.role === "toolResult" ? "User" : "Assistant"; | ||
| let content = ""; | ||
| if (m.role === "toolResult") { | ||
| const tr = m as unknown as ToolResultMessage; | ||
| let resultText = ""; | ||
| if (Array.isArray(tr.content)) { | ||
| for (const part of tr.content) { | ||
| if (part.type === "text") resultText += part.text; | ||
| } | ||
| } | ||
| content = `\n[Tool Result: ${tr.toolName}]\n${resultText}\n`; | ||
| } else if (Array.isArray(m.content)) { | ||
| for (const part of m.content) { | ||
| if (part.type === "text") content += (part as TextContent).text; | ||
| } | ||
| } else { | ||
| content = String(m.content); | ||
| } | ||
| if (m.role === "user" && content) { | ||
| content = stripForWebProvider(content) || content; | ||
| } | ||
| historyParts.push(`${role}: ${content}`); | ||
| } | ||
|
|
||
| const prompt = historyParts.join("\n\n"); | ||
| if (!prompt) throw new Error("No message found to send to Perplexity API"); | ||
|
|
||
| console.log(`[PerplexityWebStream] Starting run`); | ||
|
|
||
| const responseStream = await client.chatCompletions({ | ||
| message: prompt, | ||
| model: model.id, | ||
| signal: streamOptions?.signal, | ||
| }); | ||
|
|
||
| if (!responseStream) throw new Error("Perplexity API returned empty response body"); | ||
|
|
||
| const reader = responseStream.getReader(); | ||
| const decoder = new TextDecoder(); | ||
| let buffer = ""; | ||
|
|
||
| const contentParts: (TextContent | ToolCall)[] = []; | ||
|
|
||
| const createPartial = (): AssistantMessage => ({ | ||
| role: "assistant", | ||
| content: [...contentParts], | ||
| api: model.api, | ||
| provider: model.provider, | ||
| model: model.id, | ||
| usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, | ||
| stopReason: "stop", | ||
| timestamp: Date.now(), | ||
| }); | ||
|
|
||
| const processLine = (line: string) => { | ||
| if (!line || !line.startsWith("data:")) return; | ||
| const dataStr = line.slice(5).trim(); | ||
| if (dataStr === "[DONE]" || !dataStr) return; | ||
| try { | ||
| const data = JSON.parse(dataStr); | ||
| const delta = data.text || data.content || data.delta; | ||
| if (typeof delta === "string" && delta) { | ||
| if (contentParts.length === 0) { | ||
| contentParts[0] = { type: "text", text: "" }; | ||
| stream.push({ type: "text_start", contentIndex: 0, partial: createPartial() }); | ||
| } | ||
| (contentParts[0] as TextContent).text += delta; | ||
| stream.push({ type: "text_delta", contentIndex: 0, delta, partial: createPartial() }); | ||
| } | ||
| } catch { | ||
| // ignore | ||
| } | ||
| }; | ||
|
|
||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| if (buffer.trim()) processLine(buffer.trim()); | ||
| break; | ||
| } | ||
| const chunk = decoder.decode(value, { stream: true }); | ||
| const combined = buffer + chunk; | ||
| const parts = combined.split("\n"); | ||
| buffer = parts.pop() || ""; | ||
| for (const part of parts) { | ||
| processLine(part.trim()); | ||
| } | ||
| } | ||
|
|
||
| const assistantMessage: AssistantMessage = { | ||
| role: "assistant", | ||
| content: contentParts.length > 0 ? contentParts : [{ type: "text", text: "" }], | ||
| stopReason: "stop", | ||
| api: model.api, | ||
| provider: model.provider, | ||
| model: model.id, | ||
| usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, | ||
| timestamp: Date.now(), | ||
| }; | ||
|
|
||
| stream.push({ type: "done", reason: "stop", message: assistantMessage }); | ||
| } catch (err) { | ||
| stream.push({ | ||
| type: "error", | ||
| reason: "error", | ||
| error: { | ||
| role: "assistant", | ||
| content: [], | ||
| stopReason: "error", | ||
| errorMessage: err instanceof Error ? err.message : String(err), | ||
| api: model.api, | ||
| provider: model.provider, | ||
| model: model.id, | ||
| usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, | ||
| timestamp: Date.now(), | ||
| }, | ||
| } as any); | ||
| } finally { | ||
| stream.end(); | ||
| } | ||
| }; | ||
|
|
||
| queueMicrotask(() => void run()); | ||
| return stream; | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,31 +5,31 @@ | |||||||||||||||||||||||||||||||
| * 支持同时授权多个 Web 模型 | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import type { WizardStep } from "../wizard/types.js"; | ||||||||||||||||||||||||||||||||
| import { loadConfig, writeConfigFile } from "../config/io.js"; | ||||||||||||||||||||||||||||||||
| import type { OpenClawConfig } from "../config/config.js"; | ||||||||||||||||||||||||||||||||
| import { resolveBrowserConfig, resolveProfile } from "../browser/config.js"; | ||||||||||||||||||||||||||||||||
| import fs from "node:fs/promises"; | ||||||||||||||||||||||||||||||||
| import path from "node:path"; | ||||||||||||||||||||||||||||||||
| import { resolveOpenClawAgentDir } from "../agents/agent-paths.js"; | ||||||||||||||||||||||||||||||||
| import { ensureAuthProfileStore, saveAuthProfileStore } from "../agents/auth-profiles.js"; | ||||||||||||||||||||||||||||||||
| import type { AuthProfileStore } from "../agents/auth-profiles/types.js"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| import { ensureOpenClawModelsJson } from "../agents/models-config.js"; | ||||||||||||||||||||||||||||||||
| import type { OpenClawConfig } from "../config/config.js"; | ||||||||||||||||||||||||||||||||
| import { loadConfig, writeConfigFile } from "../config/io.js"; | ||||||||||||||||||||||||||||||||
| import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; | ||||||||||||||||||||||||||||||||
| import { loginChatGPTWeb } from "../providers/chatgpt-web-auth.js"; | ||||||||||||||||||||||||||||||||
| // 导入各个 web 模型的登录函数 | ||||||||||||||||||||||||||||||||
| import { loginClaudeWeb } from "../providers/claude-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginChatGPTWeb } from "../providers/chatgpt-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginDeepseekWeb } from "../providers/deepseek-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginDoubaoWeb } from "../providers/doubao-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginGeminiWeb } from "../providers/gemini-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginZWeb } from "../providers/glm-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginGlmIntlWeb } from "../providers/glm-intl-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginZWeb } from "../providers/glm-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginGrokWeb } from "../providers/grok-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginKimiWeb } from "../providers/kimi-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginQwenWeb } from "../providers/qwen-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginQwenCNWeb } from "../providers/qwen-cn-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import { loginQwenWeb } from "../providers/qwen-web-auth.js"; | ||||||||||||||||||||||||||||||||
| import type { WizardStep } from "../wizard/types.js"; | ||||||||||||||||||||||||||||||||
| import { applyAgentDefaultModelPrimary } from "./onboard-auth.config-shared.js"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Web 模型凭证保存助手函数 | ||||||||||||||||||||||||||||||||
| async function saveWebModelCredentials( | ||||||||||||||||||||||||||||||||
| providerId: string, | ||||||||||||||||||||||||||||||||
| credentials: unknown | ||||||||||||||||||||||||||||||||
| ): Promise<void> { | ||||||||||||||||||||||||||||||||
| async function saveWebModelCredentials(providerId: string, credentials: unknown): Promise<void> { | ||||||||||||||||||||||||||||||||
| const store = ensureAuthProfileStore(); | ||||||||||||||||||||||||||||||||
| const profileId = `${providerId}:default`; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -105,6 +105,65 @@ async function addModelToWhitelist(providerId: string, modelIds: string[]): Prom | |||||||||||||||||||||||||||||||
| console.log(` > 已更新模型白名单到 openclaw.json`); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * 将 agent models.json 中的 providers 同步到 openclaw.json。 | ||||||||||||||||||||||||||||||||
| * 解决首次运行时报错的问题:openclaw.json 初始 models.providers 为空, | ||||||||||||||||||||||||||||||||
| * 导致 resolveConfiguredModelRef 默认回退到 anthropic,且 model catalog 无可用 provider。 | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| async function syncModelsProvidersToConfig(): Promise<void> { | ||||||||||||||||||||||||||||||||
| const config = loadConfig(); | ||||||||||||||||||||||||||||||||
| await ensureOpenClawModelsJson(config); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const agentDir = resolveOpenClawAgentDir(); | ||||||||||||||||||||||||||||||||
| const modelsPath = path.join(agentDir, "models.json"); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let providers: Record<string, unknown> = {}; | ||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||
| const raw = await fs.readFile(modelsPath, "utf8"); | ||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(raw) as { providers?: Record<string, unknown> }; | ||||||||||||||||||||||||||||||||
| if (parsed?.providers && typeof parsed.providers === "object") { | ||||||||||||||||||||||||||||||||
| providers = parsed.providers; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+127
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 不要静默吞掉同步失败异常。
🔧 建议修复- } catch {
- return;
+ } catch (error) {
+ const code = (error as NodeJS.ErrnoException)?.code;
+ if (code === "ENOENT") return;
+ const message = error instanceof Error ? error.message : String(error);
+ console.warn(` > 同步 models.providers 失败: ${modelsPath} (${message})`);
+ return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (Object.keys(providers).length === 0) { | ||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let nextConfig: OpenClawConfig = { | ||||||||||||||||||||||||||||||||
| ...config, | ||||||||||||||||||||||||||||||||
| models: { | ||||||||||||||||||||||||||||||||
| ...config.models, | ||||||||||||||||||||||||||||||||
| mode: config.models?.mode ?? "merge", | ||||||||||||||||||||||||||||||||
| providers: { ...(config.models?.providers ?? {}), ...providers }, | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 若尚未设置主模型,使用首个 web provider 的首个模型,避免回退到 anthropic | ||||||||||||||||||||||||||||||||
| if (!resolveAgentModelPrimaryValue(config.agents?.defaults?.model)) { | ||||||||||||||||||||||||||||||||
| const firstEntry = Object.entries(providers).find( | ||||||||||||||||||||||||||||||||
| ([, p]) => | ||||||||||||||||||||||||||||||||
| p && | ||||||||||||||||||||||||||||||||
| typeof p === "object" && | ||||||||||||||||||||||||||||||||
| Array.isArray((p as { models?: unknown[] }).models) && | ||||||||||||||||||||||||||||||||
| (p as { models: { id?: string }[] }).models.length > 0, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+146
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 默认主模型选择应与注释一致,仅限 web provider。 注释写的是“首个 web provider”,但当前 🔧 建议修复- const firstEntry = Object.entries(providers).find(
- ([, p]) =>
+ const firstEntry = Object.entries(providers).find(
+ ([providerId, p]) =>
+ providerId.endsWith("-web") &&
p &&
typeof p === "object" &&
Array.isArray((p as { models?: unknown[] }).models) &&
(p as { models: { id?: string }[] }).models.length > 0,
);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| if (firstEntry) { | ||||||||||||||||||||||||||||||||
| const [providerId, provider] = firstEntry; | ||||||||||||||||||||||||||||||||
| const firstModel = (provider as { models: { id: string }[] }).models[0]; | ||||||||||||||||||||||||||||||||
| if (firstModel?.id) { | ||||||||||||||||||||||||||||||||
| nextConfig = applyAgentDefaultModelPrimary(nextConfig, `${providerId}/${firstModel.id}`); | ||||||||||||||||||||||||||||||||
| console.log(` > 已设置默认模型: ${providerId}/${firstModel.id}`); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| await writeConfigFile(nextConfig); | ||||||||||||||||||||||||||||||||
| console.log(` > 已同步 models.providers 到 openclaw.json`); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Web 模型定义 | ||||||||||||||||||||||||||||||||
| interface WebModelProvider { | ||||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||||
|
|
@@ -134,8 +193,8 @@ export async function runOnboardWebAuth(): Promise<void> { | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 显示已授权的模型 | ||||||||||||||||||||||||||||||||
| const store = ensureAuthProfileStore(); | ||||||||||||||||||||||||||||||||
| const authorizedModels = Object.keys(store.profiles).filter((key) => | ||||||||||||||||||||||||||||||||
| key.endsWith("-web") || key.includes("-web:") | ||||||||||||||||||||||||||||||||
| const authorizedModels = Object.keys(store.profiles).filter( | ||||||||||||||||||||||||||||||||
| (key) => key.endsWith("-web") || key.includes("-web:"), | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (authorizedModels.length > 0) { | ||||||||||||||||||||||||||||||||
|
|
@@ -242,6 +301,11 @@ export async function runOnboardWebAuth(): Promise<void> { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // 将 agent models.json 的 providers 同步到 openclaw.json,避免首次运行时报错 | ||||||||||||||||||||||||||||||||
| if (selectedProviders.length > 0) { | ||||||||||||||||||||||||||||||||
| await syncModelsProvidersToConfig(); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| console.log("\n授权完成!"); | ||||||||||||||||||||||||||||||||
| console.log("你可以在 Web UI 中使用这些模型了。"); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 50385
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 1404
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 1084
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 1391
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 1012
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 1079
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 2874
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 140
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 612
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 54
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 13296
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 768
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 4464
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 208
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 669
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 54
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 2084
🏁 Script executed:
Repository: linuxhsj/openclaw-zero-token
Length of output: 731
删除
as any类型断言,使用类型化的错误消息构造函数第 160 行的
as any违反了编码规范。应该导入并使用buildStreamErrorAssistantMessage辅助函数(在src/agents/stream-message-shared.ts中定义),它已经提供了正确的类型推断,无需类型转换。建议的修复方案
该模式已在其他代理文件(如
ollama-stream.ts和openai-ws-stream.ts)中成功应用,无需as any转换。🤖 Prompt for AI Agents