diff --git a/resources/providers/atlascloud/manifest.json b/resources/providers/atlascloud/manifest.json new file mode 100644 index 0000000..f33bcd7 --- /dev/null +++ b/resources/providers/atlascloud/manifest.json @@ -0,0 +1,36 @@ +{ + "apiVersion": 1, + "id": "atlascloud", + "name": "Atlas Cloud", + "version": "1.0.0", + "entry": "provider.bundle.js", + "moduleType": "module", + "capabilities": ["chat"], + "configSchema": { + "type": "object", + "properties": { + "apiKey": { + "type": "password", + "title": "接口密钥", + "description": "Atlas Cloud API Key,建议通过 ATLASCLOUD_API_KEY 管理。" + }, + "model": { + "type": "select", + "title": "模型名称", + "default": "qwen/qwen3.5-flash", + "enum": [ + "qwen/qwen3.5-flash", + "qwen/qwen3-vl-235b-a22b-thinking", + "google/gemini-3.5-flash", + "xai/grok-4.3" + ] + }, + "systemPrompt": { + "type": "string", + "title": "系统提示词", + "default": "" + } + }, + "required": ["apiKey"] + } +} diff --git a/resources/providers/atlascloud/provider.bundle.js b/resources/providers/atlascloud/provider.bundle.js new file mode 100644 index 0000000..4c722a1 --- /dev/null +++ b/resources/providers/atlascloud/provider.bundle.js @@ -0,0 +1,139 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +const DEFAULT_MODEL = 'qwen/qwen3.5-flash' +const DEFAULT_BASE_URL = 'https://api.atlascloud.ai/v1' +const DEFAULT_PROMPT = `你是一个桌面聊天自动回复助手。你会收到一张聊天窗口截图。 + +## 你的任务 +分析截图中的聊天内容,生成合适的回复。 + +## 规则 +1. 只输出回复文字,不要解释、不要添加多余内容 +2. 防自我循环:仔细观察截图。如果最后一条消息明显是"我"发送的,必须输出 [SKIP] +3. 如果最新消息是系统消息、群公告、红包、转账等非对话消息,输出 [SKIP] +4. 如果无法判断是否需要回复,输出 [SKIP] +5. 回复要自然、口语化,像真人对话` + +export const manifest = { + id: 'atlascloud', + apiVersion: 1 +} + +export function createProvider(context) { + const providerConfig = context && context.providerConfig ? context.providerConfig : {} + + return { + async *run(input) { + if (!input || !input.screenshot) { + yield { type: 'skip' } + return + } + + const apiKey = providerConfig.apiKey + if (!apiKey) { + yield { type: 'error', error: 'Atlas Cloud 缺少接口密钥' } + return + } + + const memorySection = buildMemorySection(input.memoryCards) + yield { + type: 'thinking', + content: memorySection + ? `正在通过 Atlas Cloud 分析聊天内容(已加载 ${input.memoryCards.length} 条团队经验)...` + : '正在通过 Atlas Cloud 分析聊天内容...' + } + + try { + const reply = await requestReply({ + screenshot: input.screenshot, + apiKey, + model: providerConfig.model || DEFAULT_MODEL, + systemPrompt: (providerConfig.systemPrompt || DEFAULT_PROMPT) + memorySection + }) + + if (!reply || reply.trim() === '[SKIP]') { + yield { type: 'skip' } + return + } + + yield { type: 'reply_text', content: reply.trim() } + } catch (error) { + const message = error && error.message ? error.message : String(error) + if (context && context.host && typeof context.host.log === 'function') { + context.host.log(`provider error: ${message}`) + } + yield { type: 'error', error: message || 'Atlas Cloud 调用失败' } + } + } + } +} + +async function requestReply({ screenshot, apiKey, model, systemPrompt }) { + const body = { + model, + messages: [ + { role: 'system', content: systemPrompt }, + { + role: 'user', + content: [ + { type: 'image_url', image_url: { url: normalizeImageUrl(screenshot) } }, + { type: 'text', text: '请根据截图中聊天窗口的最新消息进行回复。' } + ] + } + ], + stream: false + } + + const response = await fetch(`${DEFAULT_BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + + if (!response.ok) { + const details = await readErrorDetails(response) + throw new Error( + `Atlas Cloud API request failed: ${response.status} ${response.statusText}${details}` + ) + } + + const json = await response.json() + return json && json.choices && json.choices[0] && json.choices[0].message + ? json.choices[0].message.content || '' + : '' +} + +async function readErrorDetails(response) { + try { + const text = await response.text() + return text ? ` - ${text.slice(0, 300)}` : '' + } catch { + return '' + } +} + +function buildMemorySection(memoryCards) { + if (!Array.isArray(memoryCards) || memoryCards.length === 0) { + return '' + } + const lines = memoryCards.map((card, index) => { + const rationale = card.rationale ? `(原因:${card.rationale})` : '' + return `${index + 1}. 【${card.scenario}】${card.guidance}${rationale}` + }) + return `\n\n## 团队经验(来自工作记忆,优先遵循)\n${lines.join('\n')}` +} + +function normalizeImageUrl(screenshot) { + const rawBase64 = stripBase64Prefix(screenshot) + if (rawBase64.startsWith('http')) { + return rawBase64 + } + return `data:image/png;base64,${rawBase64}` +} + +function stripBase64Prefix(base64) { + const idx = String(base64).indexOf('base64,') + return idx !== -1 ? String(base64).slice(idx + 'base64,'.length) : String(base64) +} diff --git a/src/main/provider-bundle.ts b/src/main/provider-bundle.ts index efd22b1..ee14408 100644 --- a/src/main/provider-bundle.ts +++ b/src/main/provider-bundle.ts @@ -6,6 +6,11 @@ import { fileURLToPath, pathToFileURL } from 'node:url' import { ProviderAdapter, ProviderEvent, ProviderInput } from '../core/session-types' export const BUILTIN_DOUBAO_PROVIDER_ID = 'volcengine-ark' +export const BUILTIN_ATLASCLOUD_PROVIDER_ID = 'atlascloud' +const BUILTIN_PROVIDER_IDS = new Set([ + BUILTIN_DOUBAO_PROVIDER_ID, + BUILTIN_ATLASCLOUD_PROVIDER_ID +]) /** * 内置 doubao(火山方舟)provider 的资源目录。 @@ -132,6 +137,10 @@ export async function installProviderFromUrl(manifestUrl: string): Promise { + const providerId = normalizeBuiltinProviderId(manifestUrl.slice('builtin://'.length)) + if (!BUILTIN_PROVIDER_IDS.has(providerId)) { + throw new Error(`未知内置聊天服务: ${providerId}`) + } + + const manifest = await getBuiltinProviderManifestRaw(providerId) + const installed = await getBuiltinProviderInstalledInfo(providerId) + if (!manifest || !installed) { + throw new Error(`内置聊天服务资源缺失: ${providerId}`) + } + + return { installed, manifest } +} + +function normalizeBuiltinProviderId(id: string): string { + return id === 'doubao' ? BUILTIN_DOUBAO_PROVIDER_ID : id +} + export async function getInstalledProviderManifest( installed: InstalledProviderInfo | null | undefined ): Promise { @@ -170,9 +198,9 @@ export async function getInstalledProviderManifest( } } -/** 读取内置 doubao 的原始 manifest(保留 apiKey 字段,供调试 / 校验) */ -export async function getBuiltinDoubaoManifestRaw(): Promise { - const dir = getBuiltinProviderDir(BUILTIN_DOUBAO_PROVIDER_ID) +/** 读取内置 provider 的原始 manifest(保留 apiKey 字段,供调试 / 校验) */ +export async function getBuiltinProviderManifestRaw(id: string): Promise { + const dir = getBuiltinProviderDir(id) const manifestFile = path.join(dir, 'manifest.json') try { const content = await readFile(manifestFile, 'utf8') @@ -182,6 +210,11 @@ export async function getBuiltinDoubaoManifestRaw(): Promise { + return getBuiltinProviderManifestRaw(BUILTIN_DOUBAO_PROVIDER_ID) +} + /** * 拿到对外暴露的内置 doubao manifest: * - 移除 `apiKey` 字段(与视觉密钥共享,不需要用户重复填写) @@ -209,10 +242,10 @@ export async function getBuiltinDoubaoManifestForUi(): Promise { - const raw = await getBuiltinDoubaoManifestRaw() +export async function getBuiltinProviderInstalledInfo(id: string): Promise { + const raw = await getBuiltinProviderManifestRaw(id) if (!raw) return null - const dir = getBuiltinProviderDir(BUILTIN_DOUBAO_PROVIDER_ID) + const dir = getBuiltinProviderDir(id) return { id: raw.id, name: raw.name, @@ -222,6 +255,11 @@ export async function getBuiltinDoubaoInstalledInfo(): Promise { + return getBuiltinProviderInstalledInfo(BUILTIN_DOUBAO_PROVIDER_ID) +} + /** 直接加载内置 doubao provider;调用方负责传入合并好的 config(含 apiKey) */ export async function loadBuiltinDoubaoProvider( providerConfig: Record diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 50b99cc..ebd83e5 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -175,6 +175,45 @@ const BUILTIN_PROVIDER_CATALOG: ProviderCatalogItem[] = [ } ] } + }, + { + id: 'atlascloud', + name: 'Atlas Cloud', + description: '内置 OpenAI-compatible 聊天 Provider,支持截图分析和自动回复。', + version: '1.0.0', + manifestUrl: 'builtin://atlascloud', + capabilities: ['chat'], + configSchema: { + fields: [ + { + key: 'apiKey', + label: 'API Key', + type: 'password', + required: true, + placeholder: '输入 Atlas Cloud API Key' + }, + { + key: 'model', + label: '模型', + type: 'select', + required: true, + defaultValue: 'qwen/qwen3.5-flash', + options: [ + { label: 'Qwen3.5 Flash', value: 'qwen/qwen3.5-flash' }, + { label: 'Qwen3 VL 235B', value: 'qwen/qwen3-vl-235b-a22b-thinking' }, + { label: 'Gemini 3.5 Flash', value: 'google/gemini-3.5-flash' }, + { label: 'Grok 4.3', value: 'xai/grok-4.3' } + ], + hint: '这些模型在 Atlas Cloud 模型目录中支持 text + image 输入。' + }, + { + key: 'systemPrompt', + label: '系统提示词', + type: 'textarea', + placeholder: '你是一个桌面聊天自动回复助手。根据截图中的聊天内容,生成合适的回复...' + } + ] + } } ] @@ -559,11 +598,12 @@ function BottomBar({ }) { const handleStart = useCallback(async () => { const settings = (await window.electron?.invoke('settings:getAll')) as AppSettings | undefined - if (!settings?.vision?.apiKey) { - showToast(t('control.start.novisionkey'), 'error') + if (!settings) { + showToast(t('toast.startFailed'), 'error') return } - // 没装自定义 provider → 走内置 doubao(getInstalled 会返回 isBuiltinDefault: true) + // 没装自定义 provider → 走内置 doubao(getInstalled 会返回 isBuiltinDefault: true); + // 其他内置/自定义 provider 使用自身 configSchema 校验。 const providerInfo = (await window.electron?.invoke('provider:getInstalled')) as { manifest: ProviderManifest | null isBuiltinDefault?: boolean