diff --git a/.gitignore b/.gitignore index d196c22..9527636 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ out .DS_Store .eslintcache *.log* +resources/connectors/wechat/ .omc .codex/ docs/design/ diff --git a/electron-builder.yml b/electron-builder.yml index 04b427c..094c3fb 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -16,6 +16,7 @@ extraMetadata: asarUnpack: - resources/** extraResources: + # Includes the optional WeChat connector binary for the target platform. - from: resources to: resources filter: diff --git a/package.json b/package.json index 59e6ff5..dd0002c 100644 --- a/package.json +++ b/package.json @@ -25,16 +25,20 @@ "test:skill-install": "node scripts/test-skill-install-instruction.cjs", "cp:env": "node scripts/ensure-env.cjs", "prepare:env": "node scripts/ensure-env.cjs", - "predev": "node scripts/ensure-env.cjs", + "predev": "node scripts/ensure-env.cjs && node scripts/build-wechat-connector.cjs", "start": "electron-vite preview", "dev": "electron-vite dev", - "build": "npm run typecheck && electron-vite build", + "build:wechat-connector": "node scripts/build-wechat-connector.cjs", + "build:wechat-connector:win": "node scripts/build-wechat-connector.cjs --platform win32 --arch x64,arm64", + "build:wechat-connector:mac": "node scripts/build-wechat-connector.cjs --platform darwin --arch x64,arm64", + "build:native-services": "npm run build:wechat-connector", + "build": "npm run typecheck && npm run build:native-services && electron-vite build", "postinstall": "electron-builder install-app-deps && node scripts/prepare-electron-runtime.cjs", "build:unpack": "npm run build && electron-builder --config electron-builder.yml --dir", - "build:win": "npm run build && electron-builder --config electron-builder.yml --win", - "build:mac:x64": "electron-vite build && electron-builder --config electron-builder.yml --mac --x64", - "build:mac:arm64": "electron-vite build && electron-builder --config electron-builder.yml --mac --arm64", - "release:mac": "electron-vite build && electron-builder --config electron-builder.yml --mac --x64 --arm64 --publish always", + "build:win": "npm run typecheck && npm run build:wechat-connector:win && electron-vite build && electron-builder --config electron-builder.yml --win", + "build:mac:x64": "npm run typecheck && node scripts/build-wechat-connector.cjs --platform darwin --arch x64 && electron-vite build && electron-builder --config electron-builder.yml --mac --x64", + "build:mac:arm64": "npm run typecheck && node scripts/build-wechat-connector.cjs --platform darwin --arch arm64 && electron-vite build && electron-builder --config electron-builder.yml --mac --arm64", + "release:mac": "npm run typecheck && npm run build:wechat-connector:mac && electron-vite build && electron-builder --config electron-builder.yml --mac --x64 --arm64 --publish always", "build:linux": "electron-vite build && electron-builder --config electron-builder.yml --linux" }, "dependencies": { diff --git a/scripts/build-wechat-connector.cjs b/scripts/build-wechat-connector.cjs new file mode 100644 index 0000000..41f0f3a --- /dev/null +++ b/scripts/build-wechat-connector.cjs @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-require-imports, @typescript-eslint/explicit-function-return-type */ +const { execFileSync } = require('node:child_process') +const fs = require('node:fs') +const path = require('node:path') + +const projectRoot = path.resolve(__dirname, '..') +const sourceDir = process.env.WECHAT_CONNECTOR_SOURCE + ? path.resolve(process.env.WECHAT_CONNECTOR_SOURCE) + : path.join(projectRoot, 'services', 'wechat-connector') +const outputRoot = path.join(projectRoot, 'resources', 'connectors', 'wechat') + +function normalizePlatform(value) { + if (value === 'win32' || value === 'windows') return 'windows' + if (value === 'darwin' || value === 'macos') return 'darwin' + if (value === 'linux') return 'linux' + throw new Error(`Unsupported connector platform: ${value}`) +} + +function normalizeArch(value) { + if (value === 'x64' || value === 'amd64') return 'amd64' + if (value === 'arm64') return 'arm64' + throw new Error(`Unsupported connector architecture: ${value}`) +} + +function parseTargets() { + const platformArg = process.argv.indexOf('--platform') + const archArg = process.argv.indexOf('--arch') + const platforms = platformArg >= 0 ? process.argv[platformArg + 1].split(',') : [process.platform] + const arches = archArg >= 0 ? process.argv[archArg + 1].split(',') : [process.arch] + return platforms.flatMap((platform) => + arches.map((arch) => ({ goos: normalizePlatform(platform), goarch: normalizeArch(arch) })) + ) +} + +if (!fs.existsSync(path.join(sourceDir, 'go.mod'))) { + const existingTargets = parseTargets().every((target) => { + const directoryName = `${target.goos === 'windows' ? 'win32' : target.goos}-${target.goarch === 'amd64' ? 'x64' : target.goarch}` + const executable = target.goos === 'windows' ? 'wechat-connector.exe' : 'wechat-connector' + return fs.existsSync(path.join(outputRoot, directoryName, executable)) + }) + if (existingTargets) { + console.log('[build-wechat-connector] source not configured; reusing existing binary') + process.exit(0) + } + throw new Error( + 'Wechat connector source is not included in this repository. Set WECHAT_CONNECTOR_SOURCE to a compatible connector checkout.' + ) +} + +fs.rmSync(outputRoot, { recursive: true, force: true }) + +for (const target of parseTargets()) { + const directoryName = `${target.goos === 'windows' ? 'win32' : target.goos}-${target.goarch === 'amd64' ? 'x64' : target.goarch}` + const outputDir = path.join(outputRoot, directoryName) + const outputPath = path.join( + outputDir, + target.goos === 'windows' ? 'wechat-connector.exe' : 'wechat-connector' + ) + fs.mkdirSync(outputDir, { recursive: true }) + execFileSync('go', ['build', '-trimpath', '-o', outputPath, '.'], { + cwd: sourceDir, + env: { ...process.env, GOOS: target.goos, GOARCH: target.goarch, CGO_ENABLED: '0' }, + stdio: 'inherit' + }) + if (target.goos !== 'windows') fs.chmodSync(outputPath, 0o755) + console.log(`[build-wechat-connector] built ${directoryName}: ${outputPath}`) +} diff --git a/src/main/http-server.ts b/src/main/http-server.ts index 4d10c62..f7ea559 100644 --- a/src/main/http-server.ts +++ b/src/main/http-server.ts @@ -9,6 +9,8 @@ import { } from './services/chat-service' import { exportGroupReport } from './group-report-service' import { GroupReportExportRequest } from '../shared/group-report' +import { generateAgentGroupReport } from './services/agent-group-report-service' +import { agentHubService } from './services/agent-hub-service' import { safeError, safeLog, safeWarn } from './safe-log' export const DEFAULT_HTTP_HOST = '127.0.0.1' @@ -157,7 +159,9 @@ const routes: Record = { if (keyword) { const lower = keyword.toLowerCase() groups = groups.filter( - (c) => c.m_nsNickName.toLowerCase().includes(lower) || c.m_nsUsrName.toLowerCase().includes(lower) + (c) => + c.m_nsNickName.toLowerCase().includes(lower) || + c.m_nsUsrName.toLowerCase().includes(lower) ) } sendJson(res, 200, { count: groups.length, chatrooms: groups }) @@ -236,13 +240,62 @@ const routes: Record = { try { request = JSON.parse(body) as GroupReportExportRequest } catch (error) { - return sendError(res, 400, '请求体 JSON 解析失败', error instanceof Error ? error.message : String(error)) + return sendError( + res, + 400, + '请求体 JSON 解析失败', + error instanceof Error ? error.message : String(error) + ) } if (!request?.report || !request?.metadata) { return sendError(res, 400, '请求体需包含 report 和 metadata 字段') } const result = await exportGroupReport(request) sendJson(res, result.success ? 200 : 500, result) + }, + + '/api/v1/agent/group-report': async ({ req, res, body }) => { + if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求') + if (!isReady()) return sendError(res, 503, 'WechatExplorer 数据库未初始化') + let request: { group?: string; range?: 'today' | 'yesterday' | '7days' } + try { + request = JSON.parse(typeof body === 'string' ? body : '{}') + } catch { + return sendError(res, 400, '请求体 JSON 解析失败') + } + const result = await generateAgentGroupReport({ + group: request.group || '', + range: request.range + }) + sendJson(res, result.success ? 200 : 400, result) + }, + + '/api/v1/agent/status': ({ res }) => { + const status = agentHubService.getStatus() + sendJson(res, 200, { + ok: status.hub === 'online' && status.connector === 'online', + hub: status.hub, + connector: status.connector, + dataApi: status.dataApi, + databaseReady: status.databaseReady, + accountId: status.accountId + }) + }, + + '/api/v1/agent/send': async ({ req, res, body }) => { + if (req.method !== 'POST') return sendError(res, 405, '需要 POST 请求') + let request: { to?: string; text?: string; media_url?: string } + try { + request = JSON.parse(typeof body === 'string' ? body : '{}') + } catch { + return sendError(res, 400, '请求体 JSON 解析失败') + } + const result = await agentHubService.testSend({ + to: request.to, + text: request.text, + mediaUrl: request.media_url + }) + sendJson(res, result.success ? 200 : result.status === 'token_expired' ? 401 : 503, result) } } @@ -311,7 +364,11 @@ export interface ApiServerState { } let singleton: HttpServerHandle | null = null -let singletonState: ApiServerState = { running: false, host: DEFAULT_HTTP_HOST, port: DEFAULT_HTTP_PORT } +let singletonState: ApiServerState = { + running: false, + host: DEFAULT_HTTP_HOST, + port: DEFAULT_HTTP_PORT +} function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) @@ -326,7 +383,10 @@ export const apiServer = { return { ...singletonState } }, - async start(host: string = DEFAULT_HTTP_HOST, port: number = DEFAULT_HTTP_PORT): Promise { + async start( + host: string = DEFAULT_HTTP_HOST, + port: number = DEFAULT_HTTP_PORT + ): Promise { if (singleton) { return this.getState() } diff --git a/src/main/index.ts b/src/main/index.ts index a39a1b9..c6f65e8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -68,6 +68,7 @@ import { saveCachedMessages } from './services/bootstrap-cache' import { installSafeConsole } from './safe-log' +import { agentHubService } from './services/agent-hub-service' // electron-vite can close the child's stdout/stderr after spawning Electron. // Plain console.error then throws EPIPE on a closed pipe and crashes the IPC @@ -698,6 +699,25 @@ app.whenReady().then(async () => { return { success: false, error: error instanceof Error ? error.message : String(error) } } }) + ipcMain.handle('agent-hub:getStatus', () => agentHubService.getStatus()) + ipcMain.handle('agent-hub:getLogs', () => agentHubService.getLogs()) + ipcMain.handle('agent-hub:clearLogs', () => agentHubService.clearLogs()) + ipcMain.handle('agent-hub:startLogin', () => agentHubService.startLogin()) + ipcMain.handle('agent-hub:cancelLogin', () => agentHubService.cancelLogin()) + ipcMain.handle('agent-hub:reconnect', () => agentHubService.reconnect()) + ipcMain.handle('agent-hub:disconnect', () => agentHubService.disconnect()) + ipcMain.handle('agent-hub:selectTestImage', async (event) => { + const window = BrowserWindow.fromWebContents(event.sender) + const result = await dialog.showOpenDialog(window!, { + title: '选择要测试发送的图片', + properties: ['openFile'], + filters: [ + { name: '图片', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp'] }, + { name: '所有文件', extensions: ['*'] } + ] + }) + return result.canceled ? { canceled: true } : { canceled: false, path: result.filePaths[0] } + }) createWindow() @@ -707,6 +727,8 @@ app.whenReady().then(async () => { await apiServer.start(settings.apiHost, settings.apiPort) } + await agentHubService.start(settings) + if (TRAY_MODE) { app.dock?.hide() setupTray() @@ -730,6 +752,7 @@ app.on('window-all-closed', () => { }) app.on('before-quit', async () => { + agentHubService.stop() chat.setChatDb(null) await apiServer.stop().catch(() => undefined) if (tray) { diff --git a/src/main/services/agent-group-report-service.ts b/src/main/services/agent-group-report-service.ts new file mode 100644 index 0000000..2b42852 --- /dev/null +++ b/src/main/services/agent-group-report-service.ts @@ -0,0 +1,89 @@ +import type { Contact, Message } from '../../shared/types' +import { exportGroupReport } from '../group-report-service' +import { getGroupSnapshot, listMessages, resolveMd5 } from './chat-service' +import { AIProviderService } from './ai-provider-service' +import { + buildGroupReportInput, + getSummaryDateRange, + GROUP_REPORT_SYSTEM_PROMPT, + isInternalName, + parseGroupDailyReport, + type SummaryDateRange +} from '../../renderer/src/utils/group-report' + +const aiProvider = new AIProviderService() + +export interface AgentGroupReportRequest { + group: string + range?: SummaryDateRange +} + +export interface AgentGroupReportResult { + success: boolean + groupName?: string + pngPath?: string + messageCount?: number + error?: string +} + +export async function generateAgentGroupReport( + request: AgentGroupReportRequest +): Promise { + const query = String(request.group || '') + .trim() + .replace(/群聊?$/, '') + .trim() + if (!query) return { success: false, error: '缺少群聊名称' } + const contact = resolveMd5(query) + if (!contact) return { success: false, error: `没有找到群聊“${query}”` } + if (contact.type !== 'group' && !contact.m_nsUsrName.endsWith('@chatroom')) { + return { success: false, error: `“${query}”不是群聊` } + } + + const range = request.range === 'yesterday' || request.range === '7days' ? request.range : 'today' + const { startTime, endTime } = getSummaryDateRange(range) + let messages = listMessages(contact.md5, startTime, endTime) as Message[] + if (!messages.length) return { success: false, error: '所选时间范围没有可总结的消息' } + + const snapshot = getGroupSnapshot(contact.md5) + if (snapshot) { + const members = new Map( + snapshot.members.map((member) => [ + member.wxid, + { name: member.nickname, avatar: member.avatar } + ]) + ) + messages = messages.map((message) => { + if (!isInternalName(message.name)) return message + const member = members.get(String(message.senderId || message.name || '')) + return member?.name + ? { ...message, name: member.name, img: message.img || member.avatar } + : message + }) + } + + const input = await buildGroupReportInput(messages, contact as Contact, true, 'full') + const ai = await aiProvider.chat([ + { role: 'system', content: GROUP_REPORT_SYSTEM_PROMPT }, + { role: 'user', content: input.prompt } + ]) + if (!ai.success || !ai.data) return { success: false, error: ai.error || 'AI 总结失败' } + const report = parseGroupDailyReport( + ai.data, + input.topSpeakers, + input.activeTimeline, + input.voiceLeaderboard, + input.metadata, + input.media + ) + const exported = await exportGroupReport({ report, metadata: input.metadata }) + if (!exported.success || !exported.pngPath) { + return { success: false, error: exported.error || '总结图片生成失败' } + } + return { + success: true, + groupName: input.metadata.groupName, + pngPath: exported.pngPath, + messageCount: messages.length + } +} diff --git a/src/main/services/agent-hub-service.ts b/src/main/services/agent-hub-service.ts new file mode 100644 index 0000000..fcce51d --- /dev/null +++ b/src/main/services/agent-hub-service.ts @@ -0,0 +1,672 @@ +import { app, BrowserWindow } from 'electron' +import { ChildProcess, execFile, spawn } from 'child_process' +import { randomBytes, timingSafeEqual } from 'crypto' +import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'fs' +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'http' +import { dirname, join } from 'path' +import { promisify } from 'util' +import type { + AgentHubActionResult, + AgentHubLogEntry, + AgentHubLogLevel, + AgentHubLogSource, + AgentHubStatus +} from '../../shared/agent-hub' +import type { AppSettings } from './settings-store' +import { generateAgentGroupReport } from './agent-group-report-service' +import { isReady, listRecentChat } from './chat-service' + +const execFileAsync = promisify(execFile) +const HEALTH_INTERVAL_MS = 5_000 +const HUB_ADDR = '127.0.0.1:5300' +const HUB_HOST = '127.0.0.1' +const HUB_PORT = 5300 +const CONNECTOR_ADDR = '127.0.0.1:18011' +const MAX_LOG_ENTRIES = 800 + +interface InboundMessage { + account_id?: string + from_user_id?: string + message_id?: string | number + items?: Array<{ type?: number; text?: string }> +} + +interface GroupReportIntent { + group: string + range: 'today' | 'yesterday' | '7days' +} + +function resolveBundledBinary( + resourceSegments: string[], + executable: string, + packaged = app.isPackaged, + platform = process.platform, + arch = process.arch +): string { + const relativeSegments = [...resourceSegments, `${platform}-${arch}`, executable] + const packagedPath = join(process.resourcesPath, 'resources', ...relativeSegments) + const developmentPath = join(app.getAppPath(), 'resources', ...relativeSegments) + const candidates = packaged ? [packagedPath, developmentPath] : [developmentPath, packagedPath] + return candidates.find((candidate) => existsSync(candidate)) || candidates[0] +} + +export function resolveWechatConnectorBinaryPath( + packaged = app.isPackaged, + platform = process.platform, + arch = process.arch +): string { + return resolveBundledBinary( + ['connectors', 'wechat'], + platform === 'win32' ? 'wechat-connector.exe' : 'wechat-connector', + packaged, + platform, + arch + ) +} + +class AgentHubService { + private hubServer: Server | null = null + private connectorChild: ChildProcess | null = null + private loginChild: ChildProcess | null = null + private stopping = false + private healthTimer: NodeJS.Timeout | null = null + private logs: AgentHubLogEntry[] = [] + private nextLogId = 1 + private readonly processedMessages = new Map() + private readonly inboundToken = + process.env['AGENT_HUB_INBOUND_TOKEN'] || randomBytes(32).toString('hex') + private status: AgentHubStatus = { + hub: 'offline', + connector: 'checking', + dataApi: 'checking', + updatedAt: Date.now() + } + + async start(settings: AppSettings): Promise { + void settings + this.stopping = false + const hubStarted = await this.startHub() + await this.initializeConnector() + return hubStarted + } + + getStatus(): AgentHubStatus { + return { ...this.status } + } + + getLogs(): AgentHubLogEntry[] { + return [...this.logs] + } + + clearLogs(): void { + this.logs = [] + try { + writeFileSync(this.logFilePath(), '', 'utf8') + } catch { + // The live log remains usable when the persistent file cannot be cleared. + } + this.addLog('system', 'info', '运行日志已清空') + } + + async testSend(input: { to?: string; text?: string; mediaUrl?: string }): Promise<{ + success: boolean + status: 'sent' | 'token_expired' | 'connector_offline' | 'invalid_request' | 'send_failed' + message: string + }> { + const to = String(input.to || this.status.wechatUserId || '').trim() + const text = String(input.text || '').trim() + const mediaUrl = String(input.mediaUrl || '').trim() + if (!to || (!text && !mediaUrl)) { + return { + success: false, + status: 'invalid_request', + message: '请填写接收者以及文字或图片路径' + } + } + try { + const response = await fetch(`http://${CONNECTOR_ADDR}/api/send`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + account_id: this.status.accountId, + to, + text: text || undefined, + media_url: mediaUrl || undefined + }), + signal: AbortSignal.timeout(30_000) + }) + const body = await response.text() + if (response.ok) { + this.addLog('system', 'info', 'API 页面发送测试成功') + return { success: true, status: 'sent', message: '发送成功' } + } + const expired = /token|session|expired|unauthorized/i.test(body) + return { + success: false, + status: expired ? 'token_expired' : 'send_failed', + message: expired + ? '微信登录凭证已失效,请重新扫码登录' + : `发送失败:${body || response.status}` + } + } catch (error) { + return { + success: false, + status: 'connector_offline', + message: `微信连接器不可用:${error instanceof Error ? error.message : String(error)}` + } + } + } + + async startLogin(): Promise { + if (this.loginChild && this.loginChild.exitCode === null) { + return { success: true, status: this.getStatus() } + } + const executable = resolveWechatConnectorBinaryPath() + if (!existsSync(executable)) { + return this.fail(`微信连接器不存在:${executable}`) + } + + this.stopConnector() + this.patchStatus({ connector: 'starting', qrCodeDataUrl: undefined, error: undefined }) + const child = spawn(executable, ['login', '--json'], { + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true + }) + this.loginChild = child + this.addLog('wechat-connector', 'info', '已启动扫码登录流程') + let stdoutBuffer = '' + let stderr = '' + + child.stdout?.on('data', (data: Buffer) => { + stdoutBuffer += data.toString() + const lines = stdoutBuffer.split(/\r?\n/) + stdoutBuffer = lines.pop() || '' + for (const line of lines) this.handleLoginEvent(line) + }) + child.stderr?.on('data', (data: Buffer) => { + stderr += data.toString() + this.addProcessOutput('wechat-connector', 'warn', data.toString()) + }) + child.once('error', (error) => { + this.addLog('wechat-connector', 'error', `登录进程错误:${error.message}`) + this.patchStatus({ connector: 'error', error: error.message }) + }) + child.once('exit', (code) => { + if (this.loginChild === child) this.loginChild = null + if ( + code !== 0 && + this.status.connector !== 'online' && + this.status.connector !== 'disconnected' && + !this.stopping + ) { + this.patchStatus({ connector: 'error', error: stderr.trim() || `登录进程退出:${code}` }) + } + }) + return { success: true, status: this.getStatus() } + } + + cancelLogin(): AgentHubActionResult { + if (this.loginChild && this.loginChild.exitCode === null) this.loginChild.kill() + this.loginChild = null + this.patchStatus({ connector: 'disconnected', qrCodeDataUrl: undefined, error: undefined }) + return { success: true, status: this.getStatus() } + } + + async reconnect(): Promise { + const accounts = await this.loadAccounts() + if (accounts.length === 0) return this.startLogin() + this.startConnector(accounts.at(-1)!) + return { success: true, status: this.getStatus() } + } + + disconnect(): AgentHubActionResult { + this.stopConnector() + this.patchStatus({ connector: 'disconnected', error: undefined }) + return { success: true, status: this.getStatus() } + } + + stop(): void { + this.stopping = true + this.clearHealthCheck() + if (this.loginChild && this.loginChild.exitCode === null) this.loginChild.kill() + this.loginChild = null + this.stopConnector() + const hubServer = this.hubServer + this.hubServer = null + hubServer?.close() + this.patchStatus({ hub: 'offline' }) + } + + private async startHub(): Promise { + if (this.hubServer) return true + this.patchStatus({ hub: 'starting' }) + const server = createServer((request, response) => { + void this.handleHubRequest(request, response).catch((error) => { + this.addLog('agent-hub', 'error', `请求处理失败:${this.errorMessage(error)}`) + this.sendHubJson(response, 500, { error: 'internal error' }) + }) + }) + this.hubServer = server + return new Promise((resolve) => { + const fail = (error: Error): void => { + if (this.hubServer === server) this.hubServer = null + this.patchStatus({ hub: 'error', error: error.message }) + this.addLog('agent-hub', 'error', `TypeScript 服务启动失败:${error.message}`) + resolve(false) + } + server.once('error', fail) + server.listen(HUB_PORT, HUB_HOST, () => { + server.off('error', fail) + server.on('error', (error) => { + this.patchStatus({ hub: 'error', error: error.message }) + this.addLog('agent-hub', 'error', error.message) + }) + this.patchStatus({ hub: 'online', error: undefined }) + this.addLog('system', 'info', `Agent Hub TypeScript 服务已启动(${HUB_ADDR})`) + this.scheduleHealthCheck() + resolve(true) + }) + }) + } + + private async handleHubRequest( + request: IncomingMessage, + response: ServerResponse + ): Promise { + const url = new URL(request.url || '/', `http://${HUB_ADDR}`) + if (request.method === 'GET' && url.pathname === '/health') { + return this.sendHubJson(response, 200, { + status: 'ok', + service: 'agent-hub', + runtime: 'typescript' + }) + } + if (request.method !== 'POST' || url.pathname !== '/v1/connectors/wechat/inbound') { + return this.sendHubJson(response, 404, { error: 'not found' }) + } + if (!this.authorized(request.headers.authorization)) { + return this.sendHubJson(response, 401, { error: 'unauthorized' }) + } + + let inbound: InboundMessage + try { + inbound = JSON.parse(await this.readHubBody(request)) as InboundMessage + } catch { + return this.sendHubJson(response, 400, { error: 'invalid request' }) + } + const from = String(inbound.from_user_id || '').trim() + if (!from) return this.sendHubJson(response, 400, { error: 'from_user_id is required' }) + + const messageId = String(inbound.message_id || '') + this.cleanProcessedMessages() + if (messageId && this.processedMessages.has(messageId)) { + return this.sendHubJson(response, 200, { status: 'duplicate' }) + } + const text = (inbound.items || []) + .filter((item) => item.type === 1 && item.text?.trim()) + .map((item) => item.text!.trim()) + .join(' ') + this.addLog('agent-hub', 'info', `收到微信消息 message_id=${messageId || 'unknown'}`) + + const reportIntent = this.matchGroupReportIntent(text) + if (reportIntent) { + if (messageId) this.processedMessages.set(messageId, Date.now()) + this.addLog( + 'agent-hub', + 'info', + `匹配群聊总结:${reportIntent.group}(${reportIntent.range})` + ) + await this.sendConnector(inbound, '收到!正在生成群聊总结,请等待…').catch((error) => { + this.addLog('agent-hub', 'warn', `等待提示发送失败:${this.errorMessage(error)}`) + }) + void this.generateAndSendReport(inbound, reportIntent) + return this.sendHubJson(response, 202, { status: 'generating' }) + } + + const limit = this.matchRecentChatIntent(text) + if (limit === null) { + this.addLog('agent-hub', 'info', '消息已忽略:没有匹配到支持的意图') + return this.sendHubJson(response, 202, { status: 'ignored', reason: 'no matching intent' }) + } + if (!isReady()) return this.sendHubJson(response, 502, { error: 'upstream query failed' }) + const items = listRecentChat(limit) + const lines = items.map((item, index) => { + const name = item.m_nsNickName.trim() || item.m_nsUsrName.trim() + return `${index + 1}. ${name}(${item.type === 'group' ? '群聊' : '联系人'})` + }) + const reply = lines.length + ? `最近 ${items.length} 个会话:\n${lines.join('\n')}` + : '暂时没有找到最近会话。' + try { + await this.sendConnector(inbound, reply) + } catch (error) { + this.addLog('agent-hub', 'error', `回复发送失败:${this.errorMessage(error)}`) + return this.sendHubJson(response, 502, { error: 'reply delivery failed' }) + } + if (messageId) this.processedMessages.set(messageId, Date.now()) + this.addLog('agent-hub', 'info', `最近会话回复已发送(${items.length} 条)`) + this.sendHubJson(response, 200, { status: 'ok' }) + } + + private async generateAndSendReport( + inbound: InboundMessage, + intent: GroupReportIntent + ): Promise { + try { + const result = await generateAgentGroupReport({ group: intent.group, range: intent.range }) + if (!result.success || !result.pngPath) throw new Error(result.error || '群聊总结生成失败') + await this.sendConnector( + inbound, + `已生成${result.groupName || intent.group}的群聊总结(${result.messageCount || 0} 条消息),正在发送图片。` + ) + await this.sendConnector(inbound, undefined, result.pngPath) + this.addLog('agent-hub', 'info', `群聊总结图片已发送:${result.groupName || intent.group}`) + } catch (error) { + const message = this.errorMessage(error) + this.addLog('agent-hub', 'error', `群聊总结生成失败:${message}`) + await this.sendConnector(inbound, `群聊总结生成失败:${message}`).catch(() => undefined) + } + } + + private async sendConnector( + inbound: InboundMessage, + text?: string, + mediaUrl?: string + ): Promise { + const response = await fetch(`http://${CONNECTOR_ADDR}/api/send`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + account_id: inbound.account_id, + to: inbound.from_user_id, + text, + media_url: mediaUrl + }), + signal: AbortSignal.timeout(mediaUrl ? 60_000 : 30_000) + }) + if (!response.ok) throw new Error((await response.text()) || `HTTP ${response.status}`) + } + + private matchRecentChatIntent(text: string): number | null { + const normalized = text.replace(/\s+/g, '') + if (!normalized.includes('最近') || !/(消息|会话|聊天)/.test(normalized)) return null + const limit = Number(normalized.match(/\d{1,2}/)?.[0] || 5) + return Math.max(1, Math.min(20, limit)) + } + + private matchGroupReportIntent(text: string): GroupReportIntent | null { + const normalized = text.trim() + if (!normalized.includes('群') || !/(总结|日报|报告)/.test(normalized)) return null + const range = /(7天|七天|一周)/.test(normalized) + ? '7days' + : /(昨天|昨日)/.test(normalized) + ? 'yesterday' + : 'today' + const group = normalized + .replace( + /请|帮我|生成|做一份|做个|今天的|今日的|今天|今日|昨天的|昨日的|昨天|昨日|最近7天的|最近七天的|最近7天|最近七天|近7天的|近七天的|近7天|近七天|消息|聊天记录|聊天|群聊总结|群总结|群日报|群报告|总结|日报|报告|图片|长图/g, + '' + ) + .replace(/[,。!??::]/g, '') + .trim() + .replace(/成$/, '') + .replace(/群$/, '') + .trim() + return group ? { group, range } : null + } + + private authorized(header: string | undefined): boolean { + if (!header?.startsWith('Bearer ')) return false + const expected = Buffer.from(this.inboundToken) + const provided = Buffer.from(header.slice(7)) + return expected.length === provided.length && timingSafeEqual(expected, provided) + } + + private readHubBody(request: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + let size = 0 + request.on('data', (chunk: Buffer) => { + size += chunk.length + if (size > 1024 * 1024) { + reject(new Error('request too large')) + request.destroy() + return + } + chunks.push(chunk) + }) + request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + request.on('error', reject) + }) + } + + private sendHubJson(response: ServerResponse, status: number, payload: unknown): void { + if (response.writableEnded) return + response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }) + response.end(JSON.stringify(payload)) + } + + private cleanProcessedMessages(): void { + const cutoff = Date.now() - 10 * 60_000 + for (const [id, timestamp] of this.processedMessages) { + if (timestamp < cutoff) this.processedMessages.delete(id) + } + } + + private errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) + } + + private async initializeConnector(): Promise { + this.patchStatus({ connector: 'checking' }) + try { + const accounts = await this.loadAccounts() + if (accounts.length === 0) { + this.patchStatus({ connector: 'disconnected' }) + return + } + this.startConnector(accounts.at(-1)!) + } catch (error) { + this.patchStatus({ + connector: 'error', + error: error instanceof Error ? error.message : String(error) + }) + } + } + + private async loadAccounts(): Promise<{ accountId: string; wechatUserId: string }[]> { + const executable = resolveWechatConnectorBinaryPath() + if (!existsSync(executable)) throw new Error(`微信连接器不存在:${executable}`) + const { stdout } = await execFileAsync(executable, ['accounts', '--json'], { + windowsHide: true, + timeout: 10_000 + }) + const parsed = JSON.parse(stdout) as { + accounts?: { account_id: string; wechat_user_id: string }[] + } + return (parsed.accounts || []).map((account) => ({ + accountId: account.account_id, + wechatUserId: account.wechat_user_id + })) + } + + private startConnector(account: { accountId: string; wechatUserId: string }): void { + if (this.connectorChild && this.connectorChild.exitCode === null) return + const executable = resolveWechatConnectorBinaryPath() + this.patchStatus({ + connector: 'starting', + accountId: account.accountId, + wechatUserId: account.wechatUserId, + qrCodeDataUrl: undefined, + error: undefined + }) + const child = spawn( + executable, + ['start', '--foreground', '--api-addr', CONNECTOR_ADDR, '--account-id', account.accountId], + { + env: { + ...process.env, + WECHAT_CONNECTOR_INBOUND_WEBHOOK_URL: `http://${HUB_ADDR}/v1/connectors/wechat/inbound`, + WECHAT_CONNECTOR_INBOUND_WEBHOOK_TOKEN: this.inboundToken, + WECHAT_CONNECTOR_INBOUND_WEBHOOK_ONLY: 'true' + }, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true + } + ) + this.connectorChild = child + this.addLog('system', 'info', `正在启动微信连接器(账号 ${account.accountId})`) + child.stdout?.on('data', (data: Buffer) => this.handleConnectorOutput('info', data.toString())) + child.stderr?.on('data', (data: Buffer) => this.handleConnectorOutput('warn', data.toString())) + child.once('spawn', () => { + this.addLog('system', 'info', `微信连接器已启动(PID ${child.pid})`) + this.patchStatus({ connector: 'online' }) + }) + child.once('error', (error) => { + this.addLog('wechat-connector', 'error', error.message) + this.patchStatus({ connector: 'error', error: error.message }) + }) + child.once('exit', (code) => { + if (this.connectorChild === child) this.connectorChild = null + this.addLog('system', code === 0 ? 'info' : 'error', `微信连接器已退出(code=${code})`) + if (!this.stopping && this.status.connector !== 'disconnected') { + this.patchStatus({ connector: 'error', error: `微信连接器退出:${code}` }) + } + }) + } + + private stopConnector(): void { + const child = this.connectorChild + this.connectorChild = null + if (child && child.exitCode === null) child.kill() + } + + private handleLoginEvent(line: string): void { + if (!line.trim()) return + try { + const event = JSON.parse(line) as { + status: string + qr_code_data_url?: string + account_id?: string + wechat_user_id?: string + } + switch (event.status) { + case 'qrcode': + case 'wait': + this.patchStatus({ + connector: 'waiting_scan', + qrCodeDataUrl: event.qr_code_data_url || this.status.qrCodeDataUrl + }) + break + case 'scaned': + this.patchStatus({ connector: 'scanned' }) + break + case 'confirmed': + this.patchStatus({ connector: 'starting' }) + break + case 'expired': + this.patchStatus({ connector: 'error', error: '二维码已过期,请重新获取' }) + break + case 'active': { + const account = { + accountId: event.account_id || '', + wechatUserId: event.wechat_user_id || '' + } + this.patchStatus({ ...account, connector: 'starting', qrCodeDataUrl: undefined }) + this.startConnector(account) + break + } + } + } catch (error) { + console.warn('[AgentHub] invalid login event:', line, error) + } + } + + private patchStatus(patch: Partial): void { + this.status = { ...this.status, ...patch, updatedAt: Date.now() } + for (const window of BrowserWindow.getAllWindows()) { + window.webContents.send('agent-hub:status', this.getStatus()) + } + } + + private addProcessOutput( + source: AgentHubLogSource, + level: AgentHubLogLevel, + output: string + ): void { + for (const line of output.split(/\r?\n/)) { + if (line.trim()) this.addLog(source, level, line) + } + } + + private handleConnectorOutput(level: AgentHubLogLevel, output: string): void { + this.addProcessOutput('wechat-connector', level, output) + if (/session expired/i.test(output)) { + this.addLog('system', 'error', '当前微信机器人登录已失效,需要重新扫码登录') + this.patchStatus({ connector: 'error', error: '当前登录已失效,请重新扫码登录' }) + this.stopConnector() + } + } + + private addLog(source: AgentHubLogSource, level: AgentHubLogLevel, rawMessage: string): void { + const message = this.redactLog(rawMessage).trim() + if (!message) return + const entry: AgentHubLogEntry = { + id: this.nextLogId++, + timestamp: Date.now(), + source, + level, + message + } + this.logs.push(entry) + if (this.logs.length > MAX_LOG_ENTRIES) this.logs.splice(0, this.logs.length - MAX_LOG_ENTRIES) + try { + const path = this.logFilePath() + mkdirSync(dirname(path), { recursive: true }) + appendFileSync( + path, + `${new Date(entry.timestamp).toISOString()} [${source}] [${level}] ${message}\n`, + 'utf8' + ) + } catch { + // Do not interrupt message handling because log persistence failed. + } + for (const window of BrowserWindow.getAllWindows()) { + window.webContents.send('agent-hub:log', entry) + } + } + + private redactLog(message: string): string { + return message + .replace(/Bearer\s+[A-Za-z0-9._~-]+/gi, 'Bearer [已隐藏]') + .replace(/data:image\/[^;]+;base64,[A-Za-z0-9+/=]+/gi, 'data:image/[二维码已隐藏]') + .replace(/(token[=:\s]+)[^\s,}]+/gi, '$1[已隐藏]') + } + + private logFilePath(): string { + return join(app.getPath('logs'), 'agent-hub.log') + } + + private fail(error: string): AgentHubActionResult { + this.patchStatus({ connector: 'error', error }) + return { success: false, status: this.getStatus(), error } + } + + private scheduleHealthCheck(): void { + this.clearHealthCheck() + this.healthTimer = setInterval(() => this.checkDataApi(), HEALTH_INTERVAL_MS) + this.checkDataApi() + } + + private checkDataApi(): void { + const ready = isReady() + this.patchStatus({ dataApi: 'online', databaseReady: ready }) + } + + private clearHealthCheck(): void { + if (this.healthTimer) clearInterval(this.healthTimer) + this.healthTimer = null + } +} + +export const agentHubService = new AgentHubService() diff --git a/src/main/services/local-api-test-service.ts b/src/main/services/local-api-test-service.ts index b4dad71..11f4c8f 100644 --- a/src/main/services/local-api-test-service.ts +++ b/src/main/services/local-api-test-service.ts @@ -8,6 +8,7 @@ import { } from '../../shared/local-api-test' const REQUEST_TIMEOUT_MS = 10_000 +const GROUP_REPORT_TIMEOUT_MS = 180_000 const MAX_BODY_SIZE = 512 * 1024 function isEndpointId(value: unknown): value is LocalApiEndpointId { @@ -122,7 +123,9 @@ export async function testLocalApiRequest(payload: unknown): Promise { + const timeoutMs = + endpointId === 'agent-group-report' ? GROUP_REPORT_TIMEOUT_MS : REQUEST_TIMEOUT_MS + request.setTimeout(timeoutMs, () => { request.destroy(new Error('请求超时')) finish({ ok: false, @@ -132,7 +135,7 @@ export async function testLocalApiRequest(payload: unknown): Promise { diff --git a/src/main/services/settings-store.ts b/src/main/services/settings-store.ts index b6a14d5..5029d20 100644 --- a/src/main/services/settings-store.ts +++ b/src/main/services/settings-store.ts @@ -12,6 +12,8 @@ export interface AppSettings { imageXorKey: string imageAesKey: string imageKeyFallbackDisabled: boolean + autoLogin: boolean + autoLoginPreferenceSet: boolean } function getDefaultDbRoot(): string { @@ -118,7 +120,13 @@ const DEFAULT_SETTINGS: AppSettings = { imageKeyRoot: defaultDbRoot, imageXorKey: '', imageAesKey: '', - imageKeyFallbackDisabled: false + imageKeyFallbackDisabled: false, + autoLogin: ['1', 'true', 'yes', 'on'].includes( + String(import.meta.env.VITE_AUTO_LOGIN || '') + .trim() + .toLowerCase() + ), + autoLoginPreferenceSet: false } const SETTINGS_FILE = path.join( @@ -138,6 +146,12 @@ export function loadSettings(): AppSettings { if (fs.existsSync(SETTINGS_FILE)) { const raw = fs.readJsonSync(SETTINGS_FILE) as Partial cache = { ...DEFAULT_SETTINGS, ...raw } + if (raw.autoLogin === undefined) { + const hasSavedDatabaseKey = fs.existsSync( + path.join(app.getPath('userData'), 'wechat-db-key.bin') + ) + if (hasSavedDatabaseKey) cache.autoLogin = true + } if (process.platform === 'win32' && !isUsableDbRoot(cache.dbRoot)) { cache.dbRoot = getDefaultDbRoot() } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index a914428..3519c27 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -37,6 +37,7 @@ import type { ImageCandidateQuery, ImageInsight } from '../shared/image-insight' +import type { AgentHubActionResult, AgentHubLogEntry, AgentHubStatus } from '../shared/agent-hub' export type ParsedContent = | { type: 'text'; content: string } @@ -186,6 +187,8 @@ declare global { apiHost: string apiPort: number imageKeyRoot: string + autoLogin: boolean + autoLoginPreferenceSet: boolean imageXorKey: string imageAesKey: string } @@ -210,6 +213,8 @@ declare global { apiHost: string apiPort: number imageKeyRoot: string + autoLogin: boolean + autoLoginPreferenceSet: boolean imageXorKey: string imageAesKey: string } @@ -222,6 +227,8 @@ declare global { apiHost: string apiPort: number imageKeyRoot: string + autoLogin: boolean + autoLoginPreferenceSet: boolean imageXorKey: string imageAesKey: string }> @@ -232,6 +239,8 @@ declare global { apiHost: string apiPort: number imageKeyRoot: string + autoLogin: boolean + autoLoginPreferenceSet: boolean imageXorKey: string imageAesKey: string } @@ -298,6 +307,16 @@ declare global { sessionId: string, limit?: number ) => Promise<{ success: boolean; insights: ImageInsight[] }> + getAgentHubStatus: () => Promise + getAgentHubLogs: () => Promise + clearAgentHubLogs: () => Promise + startAgentHubLogin: () => Promise + cancelAgentHubLogin: () => Promise + reconnectAgentHub: () => Promise + disconnectAgentHub: () => Promise + selectAgentHubTestImage: () => Promise<{ canceled: boolean; path?: string }> + onAgentHubStatus: (callback: (status: AgentHubStatus) => void) => () => void + onAgentHubLog: (callback: (entry: AgentHubLogEntry) => void) => () => void } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index b7e9f96..9a7a823 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -15,6 +15,7 @@ import type { ImageCandidateQuery, ImageInsight } from '../shared/image-insight' +import type { AgentHubLogEntry, AgentHubStatus } from '../shared/agent-hub' // 渲染器的自定义 API const api = { @@ -132,7 +133,27 @@ const api = { sessionId: string, limit?: number ): Promise<{ success: boolean; insights: ImageInsight[] }> => - ipcRenderer.invoke('image:listInsights', sessionId, limit) + ipcRenderer.invoke('image:listInsights', sessionId, limit), + getAgentHubStatus: () => ipcRenderer.invoke('agent-hub:getStatus'), + getAgentHubLogs: () => ipcRenderer.invoke('agent-hub:getLogs'), + clearAgentHubLogs: () => ipcRenderer.invoke('agent-hub:clearLogs'), + startAgentHubLogin: () => ipcRenderer.invoke('agent-hub:startLogin'), + cancelAgentHubLogin: () => ipcRenderer.invoke('agent-hub:cancelLogin'), + reconnectAgentHub: () => ipcRenderer.invoke('agent-hub:reconnect'), + disconnectAgentHub: () => ipcRenderer.invoke('agent-hub:disconnect'), + selectAgentHubTestImage: () => ipcRenderer.invoke('agent-hub:selectTestImage'), + onAgentHubStatus: (callback: (status: AgentHubStatus) => void) => { + const listener = (_event: Electron.IpcRendererEvent, status: AgentHubStatus): void => + callback(status) + ipcRenderer.on('agent-hub:status', listener) + return () => ipcRenderer.removeListener('agent-hub:status', listener) + }, + onAgentHubLog: (callback: (entry: AgentHubLogEntry) => void) => { + const listener = (_event: Electron.IpcRendererEvent, entry: AgentHubLogEntry): void => + callback(entry) + ipcRenderer.on('agent-hub:log', listener) + return () => ipcRenderer.removeListener('agent-hub:log', listener) + } } if (process.contextIsolated) { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 77a5640..2b099af 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -4,6 +4,7 @@ import ChatWindow from './components/ChatWindow' import { AppShell } from './components/layout/AppShell' import { ApiWorkspace } from './features/api-center/ApiWorkspace' import { SettingsWorkspace } from './features/settings/SettingsWorkspace' +import { AgentHubWorkspace } from './features/agent-hub/AgentHubWorkspace' import type { SettingsCategoryId } from './features/settings/model/types' import type { AIRuntimeModelConfig } from '../../shared/ai-provider' import { AppPage } from './components/layout/navigation' @@ -57,12 +58,6 @@ interface SelfInfo { const MAC_KEY_FAQ_URL = 'https://github.com/hicccc77/WeFlow/blob/main/docs/MAC-KEY-FAQ.md' const MESSAGE_MONITOR_DEBOUNCE_MS = 8000 const VIEW_MESSAGE_LIMIT = 600 -const AUTO_LOGIN_ENABLED = ['1', 'true', 'yes', 'on'].includes( - String(import.meta.env.VITE_AUTO_LOGIN || '') - .trim() - .toLowerCase() -) - const getMessageIdentity = (message: Message): string => { if (message.localId) return `local:${message.localId}` if (message.id) return `id:${message.id}` @@ -369,13 +364,12 @@ function App(): React.ReactElement { React.useEffect(() => { let active = true const attemptAutoConnect = async (): Promise => { + const settingsResult = await window.api.getSettings() // 预填已保存的微信聊天文件路径 - try { - const settings = await window.api.getSettings() - if (active && settings?.settings?.dbRoot) setDbRootInput(settings.settings.dbRoot) - } catch { - // 忽略读取设置失败,继续走密钥流程 + if (active && settingsResult.settings.dbRoot) { + setDbRootInput(settingsResult.settings.dbRoot) } + const autoLoginEnabled = settingsResult.settings.autoLogin // 优先级 1: 构建期环境变量 VITE_DB_KEY(本地开发用) const envKey = String(import.meta.env.VITE_DB_KEY || '').trim() // 优先级 2: 上一次保存到 safeStorage 的密钥 @@ -393,7 +387,7 @@ function App(): React.ReactElement { setDbKey(key) setAutoConnectSource(envKey ? 'env' : 'saved') setDbKeyStatus( - AUTO_LOGIN_ENABLED + autoLoginEnabled ? envKey ? '检测到环境变量中的密钥,正在自动连接...' : '已加载安全保存的密钥,正在自动连接...' @@ -402,14 +396,17 @@ function App(): React.ReactElement { : '已加载安全保存的密钥,请手动点击 Connect' ) setDbKeyStatusKind('normal') - setBootState(AUTO_LOGIN_ENABLED ? 'connecting' : 'login') + setBootState(autoLoginEnabled ? 'connecting' : 'login') } - if (!AUTO_LOGIN_ENABLED) return + if (!autoLoginEnabled) return try { const result = await window.api.initDb(key) if (!active) return const success = typeof result === 'boolean' ? result : result.success if (success) { + if (!settingsResult.settings.autoLoginPreferenceSet) { + void window.api.setSettings({ autoLogin: true }) + } setIsNativeMonitorActive(typeof result !== 'boolean' && result.monitoring === true) setIsAuthenticated(true) setIsDatabaseConnected(true) @@ -489,6 +486,11 @@ function App(): React.ReactElement { const hasBootstrapCache = await loadBootstrapCache() // 持久化手动输入的密钥,供下次启动继续使用 void window.api.saveDbKey(keyToUse).catch(() => undefined) + void window.api.getSettings().then((current) => { + if (!current.settings.autoLoginPreferenceSet) { + void window.api.setSettings({ autoLogin: true }) + } + }) setStartupProgress({ title: '正在加载账号信息...', subtitle: '即将进入 WechatExplorer', @@ -1036,9 +1038,9 @@ function App(): React.ReactElement { } const renderPlaceholderPage = ( - page: Exclude + page: Exclude ): React.ReactElement => { - const labels: Record, string> = { + const labels: Record, string> = { search: '检索', export: '导出', api: 'API', @@ -1168,6 +1170,8 @@ function App(): React.ReactElement { return renderArchiveWorkspace() case 'report': return renderReportWorkspace() + case 'agent-hub': + return case 'api': return ( p { + margin-top: 8px; +} +.agent-hub-example { + margin: 24px 0 20px; + padding: 16px; + border-radius: 12px; + background: var(--wxex-brand-soft); +} +.agent-hub-example span { + display: block; + color: var(--wxex-text-muted); + font: 600 11px/16px var(--wxex-font); +} +.agent-hub-example strong { + display: block; + margin-top: 4px; + color: var(--wxex-brand); + font: 700 14px/21px var(--wxex-font); +} +.agent-hub-capability-card ul { + margin: 0; + padding: 0; + list-style: none; +} +.agent-hub-capability-card li { + display: flex; + align-items: center; + gap: 9px; + padding: 9px 0; + border-bottom: 1px solid var(--wxex-border); + color: var(--wxex-text-secondary); + font: 13px/19px var(--wxex-font); +} +.agent-hub-capability-card li:last-child { + border-bottom: 0; +} +.agent-hub-capability-card li i { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--wxex-brand); +} +.agent-hub-example strong + strong { + margin-top: 9px; + padding-top: 9px; + border-top: 1px solid rgba(36, 122, 99, 0.15); +} +.agent-hub-capability-card li i.offline { + background: var(--wxex-danger); +} +.agent-hub-log-card { + max-width: 1080px; + margin: 20px auto 0; + padding: 22px; +} +.agent-hub-log-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 14px; +} +.agent-hub-log-heading h2 { + margin-top: 3px; +} +.agent-hub-log-actions { + display: flex; + align-items: center; + gap: 8px; +} +.agent-hub-log-actions select, +.agent-hub-log-actions button { + height: 32px; + border: 1px solid var(--wxex-border-strong); + border-radius: 8px; + background: #fff; + color: var(--wxex-text-secondary); + font: 600 12px/18px var(--wxex-font); +} +.agent-hub-log-actions select { + padding: 0 28px 0 10px; +} +.agent-hub-log-actions button { + padding: 0 11px; + cursor: pointer; +} +.agent-hub-log-actions button:disabled { + opacity: 0.45; + cursor: default; +} +.agent-hub-log-body { + height: 250px; + overflow: auto; + padding: 12px 14px; + border: 1px solid #26312a; + border-radius: 11px; + background: #18201b; + color: #dfe9e2; +} +.agent-hub-log-line { + display: grid; + grid-template-columns: 78px 94px minmax(0, 1fr); + gap: 9px; + align-items: baseline; + min-height: 24px; + font: + 12px/19px ui-monospace, + SFMono-Regular, + Menlo, + monospace; +} +.agent-hub-log-line time { + color: #84938a; +} +.agent-hub-log-line .source { + color: #76d49a; +} +.agent-hub-log-line .source.wechat-connector { + color: #7dbcf1; +} +.agent-hub-log-line .source.system { + color: #d8bd6b; +} +.agent-hub-log-line code { + min-width: 0; + color: inherit; + white-space: pre-wrap; + overflow-wrap: anywhere; +} +.agent-hub-log-line.warn code { + color: #f1cf77; +} +.agent-hub-log-line.error code { + color: #ff9690; +} +.agent-hub-log-empty { + height: 100%; + display: grid; + place-items: center; + color: #84938a; + font: 12px/20px var(--wxex-font); +} +.agent-hub-log-note { + margin-top: 9px !important; + color: var(--wxex-text-muted) !important; + font-size: 11px !important; +} +@media (max-width: 900px) { + .agent-hub-workspace { + padding: 24px; + } + .agent-hub-grid { + grid-template-columns: 1fr; + } + .agent-hub-qr-panel { + flex-direction: column; + text-align: center; + } + .agent-hub-capability-card { + width: 100%; + } +} + .ai-vision-test { display: grid; gap: 16px; @@ -3766,153 +4204,2325 @@ body { } /* API-01 本地 API 中心 */ -.api-center-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; background: var(--wxex-bg-main); } +.api-center-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + overflow: hidden; + background: var(--wxex-bg-main); +} /* SETTINGS-01: formal settings workspace. The legacy SettingsPanel remains only as a fallback source. */ -.settings-workspace { display:flex; width:100%; height:100%; min-width:0; min-height:0; overflow:hidden; background:#fafbfa; } -.settings-sidebar { width:294px; flex:0 0 294px; min-width:0; min-height:0; display:flex; flex-direction:column; background:#eef2f0; border-right:1px solid #dde3e0; } -.settings-sidebar header { padding:22px 20px 16px; border-bottom:1px solid #dde3e0; }.settings-sidebar h1 { margin:0; color:#202724; font-size:20px; }.settings-sidebar header p { margin:5px 0 16px; color:#66706b; font-size:12px; }.settings-search { height:34px; display:flex; align-items:center; gap:8px; padding:0 10px; border:1px solid #dde3e0; border-radius:8px; background:#fff; color:#66706b; }.settings-search input { min-width:0; width:100%; border:0; outline:0; color:#202724; background:transparent; font:inherit; font-size:12px; } -.settings-sidebar-list { flex:1; min-height:0; overflow:auto; padding:12px 8px; }.settings-sidebar-list section { margin:0 0 18px; }.settings-sidebar-list h2 { margin:0 12px 6px; color:#929a96; font-size:12px; font-weight:500; }.settings-sidebar-list button { position:relative; display:block; width:100%; border:0; padding:9px 12px; background:transparent; color:#39433e; text-align:left; font:600 13px inherit; cursor:pointer; border-radius:8px; }.settings-sidebar-list button.active { background:#fff; color:#202724; }.settings-sidebar-list button.active::before { content:''; position:absolute; left:0; top:7px; bottom:7px; width:3px; border-radius:0 3px 3px 0; background:#247a63; }.settings-sidebar-account { flex:0 0 auto; padding:10px; border-top:1px solid #dde3e0; } -.settings-page { position:relative; display:flex; flex:1; min-width:0; min-height:0; flex-direction:column; } -.settings-page-header { flex:0 0 auto; display:flex; justify-content:space-between; align-items:flex-start; gap:18px; padding:24px 34px; border-bottom:1px solid #eef1ef; background:#fafbfa; } -.settings-page-header h1 { margin:0; color:#202724; font-size:21px; } -.settings-page-header p { margin:6px 0 0; color:#66706b; font-size:13px; } -.settings-status-badge { margin-top:5px; flex:0 0 auto; border-radius:16px; padding:7px 12px; background:#edf5f1; color:#2e8b68; font-size:12px; } -.settings-status-badge::before { content:'●'; margin-right:6px; font-size:9px; } -.settings-status-badge.checking { background:#eef4f8; color:#4f7188; } -.settings-status-badge.warning { background:#fff6e8; color:#a56a24; } -.settings-status-badge.error { background:#f9eeee; color:#c85a5a; } -.settings-status-badge.unavailable { background:#f1f3f2; color:#66706b; } -.settings-page-scroll { flex:1; min-height:0; overflow-y:auto; overflow-x:hidden; } -.settings-page-content { box-sizing:border-box; width:min(100%, 860px); padding:34px 34px 64px; } -.settings-privacy-notice { display:flex; gap:14px; align-items:flex-start; padding:17px; border:1px solid #bfded3; border-radius:10px; background:#f0f7f4; color:#36564d; } -.settings-privacy-notice svg { flex:0 0 24px; width:24px; stroke:#247a63; fill:none; stroke-width:1.8; } -.settings-privacy-notice strong { color:#294b41; font-size:14px; } -.settings-privacy-notice p { margin:5px 0 0; color:#66706b; font-size:13px; line-height:1.6; } -.settings-section-heading { margin:30px 0 14px; color:#3d4742; font-size:16px; } -.settings-card { border:1px solid #dde3e0; border-radius:10px; background:#fff; padding:24px 26px; } -.settings-account-overview { display:grid; grid-template-columns:minmax(190px,1fr) minmax(220px,1fr) auto; gap:24px; align-items:center; } -.settings-account-primary { display:flex; min-width:0; align-items:center; gap:15px; } -.settings-avatar { width:66px; height:66px; flex:0 0 auto; overflow:hidden; display:grid; place-items:center; border-radius:9px; background:#dcede6; color:#247a63; font-size:23px; } -.settings-avatar img { width:100%; height:100%; object-fit:cover; } -.settings-account-identity { display:grid; min-width:0; gap:3px; } -.settings-account-identity span,.settings-account-root>span,.settings-account-facts dt { color:#929a96; font-size:12px; } -.settings-account-identity strong,.settings-account-identity small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } -.settings-account-identity strong { color:#35403b; font-size:14px; } -.settings-account-identity small { color:#66706b; font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace; } -.settings-account-facts { display:grid; min-width:0; gap:12px; margin:0; } -.settings-account-facts div { min-width:0; } -.settings-account-facts dd { overflow:hidden; margin:3px 0 0; color:#35403b; font-size:13px; text-overflow:ellipsis; white-space:nowrap; } -.settings-connection-text.success { color:#2e8b68; } -.settings-connection-text.warning { color:#a56a24; } -.settings-connection-text.error { color:#c85a5a; } -.settings-account-actions { display:grid; gap:8px; } -.settings-account-actions button { min-height:34px; white-space:nowrap; } -.settings-account-root { grid-column:1/-1; display:grid; min-width:0; gap:7px; padding-top:19px; border-top:1px solid #edf0ee; } -.settings-account-root>div { display:flex; min-width:0; align-items:center; gap:8px; } -.settings-account-root code { min-width:0; flex:1; overflow:hidden; padding:10px 12px; border:1px solid #dde3e0; border-radius:8px; background:#f2f5f3; color:#46514c; text-overflow:ellipsis; white-space:nowrap; font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace; } -.settings-icon-button { display:grid; width:36px; height:36px; flex:0 0 auto; place-items:center; border:1px solid #dde3e0; border-radius:8px; background:#fff; color:#66706b; cursor:pointer; } -.settings-icon-button:hover:not(:disabled) { border-color:#247a63; color:#247a63; } -.settings-icon-button:disabled { cursor:not-allowed; opacity:.45; } -.settings-icon-button svg { width:17px; fill:none; stroke:currentColor; stroke-linecap:round; stroke-linejoin:round; stroke-width:1.8; } -.settings-health-card { padding:12px; } -.settings-diagnostic-summary { margin:0 0 10px; padding:9px 11px; border-left:3px solid #c68635; border-radius:6px; background:#fff8ec; color:#79501f; font-size:12px; line-height:1.55; } -.settings-diagnostics { display:grid; gap:8px; } -.settings-diagnostic { display:flex; align-items:center; gap:12px; min-width:0; padding:13px; border:1px solid #edf0ee; border-radius:8px; } -.settings-diagnostic-icon { display:grid; width:18px; height:18px; flex:0 0 auto; place-items:center; border:1px solid #929a96; border-radius:50%; color:#929a96; font-size:11px; font-weight:700; } -.settings-diagnostic-icon.success { border-color:#2e8b68; color:#2e8b68; } -.settings-diagnostic-icon.warning { border-color:#c68635; color:#c68635; } -.settings-diagnostic-icon.error { border-color:#c85a5a; color:#c85a5a; } -.settings-diagnostic-icon.checking { border-color:#cbd7d2; border-top-color:#247a63; animation:settings-spin .8s linear infinite; } -.settings-diagnostic span { color:#3d4742; font-size:13px; } -.settings-diagnostic small { margin-left:auto; overflow:hidden; color:#66706b; text-overflow:ellipsis; white-space:nowrap; } -.settings-empty-state { display:grid; flex:1; place-content:center; color:#66706b; text-align:center; } -.settings-empty-state h2 { color:#202724; } +.settings-workspace { + display: flex; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + overflow: hidden; + background: #fafbfa; +} +.settings-sidebar { + width: 294px; + flex: 0 0 294px; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background: #eef2f0; + border-right: 1px solid #dde3e0; +} +.settings-sidebar header { + padding: 22px 20px 16px; + border-bottom: 1px solid #dde3e0; +} +.settings-sidebar h1 { + margin: 0; + color: #202724; + font-size: 20px; +} +.settings-sidebar header p { + margin: 5px 0 16px; + color: #66706b; + font-size: 12px; +} +.settings-search { + height: 34px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 10px; + border: 1px solid #dde3e0; + border-radius: 8px; + background: #fff; + color: #66706b; +} +.settings-search input { + min-width: 0; + width: 100%; + border: 0; + outline: 0; + color: #202724; + background: transparent; + font: inherit; + font-size: 12px; +} +.settings-sidebar-list { + flex: 1; + min-height: 0; + overflow: auto; + padding: 12px 8px; +} +.settings-sidebar-list section { + margin: 0 0 18px; +} +.settings-sidebar-list h2 { + margin: 0 12px 6px; + color: #929a96; + font-size: 12px; + font-weight: 500; +} +.settings-sidebar-list button { + position: relative; + display: block; + width: 100%; + border: 0; + padding: 9px 12px; + background: transparent; + color: #39433e; + text-align: left; + font: 600 13px inherit; + cursor: pointer; + border-radius: 8px; +} +.settings-sidebar-list button.active { + background: #fff; + color: #202724; +} +.settings-sidebar-list button.active::before { + content: ''; + position: absolute; + left: 0; + top: 7px; + bottom: 7px; + width: 3px; + border-radius: 0 3px 3px 0; + background: #247a63; +} +.settings-sidebar-account { + flex: 0 0 auto; + padding: 10px; + border-top: 1px solid #dde3e0; +} +.settings-page { + position: relative; + display: flex; + flex: 1; + min-width: 0; + min-height: 0; + flex-direction: column; +} +.settings-page-header { + flex: 0 0 auto; + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 18px; + padding: 24px 34px; + border-bottom: 1px solid #eef1ef; + background: #fafbfa; +} +.settings-page-header h1 { + margin: 0; + color: #202724; + font-size: 21px; +} +.settings-page-header p { + margin: 6px 0 0; + color: #66706b; + font-size: 13px; +} +.settings-status-badge { + margin-top: 5px; + flex: 0 0 auto; + border-radius: 16px; + padding: 7px 12px; + background: #edf5f1; + color: #2e8b68; + font-size: 12px; +} +.settings-status-badge::before { + content: '●'; + margin-right: 6px; + font-size: 9px; +} +.settings-status-badge.checking { + background: #eef4f8; + color: #4f7188; +} +.settings-status-badge.warning { + background: #fff6e8; + color: #a56a24; +} +.settings-status-badge.error { + background: #f9eeee; + color: #c85a5a; +} +.settings-status-badge.unavailable { + background: #f1f3f2; + color: #66706b; +} +.settings-page-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} +.settings-page-content { + box-sizing: border-box; + width: min(100%, 860px); + padding: 34px 34px 64px; +} +.settings-privacy-notice { + display: flex; + gap: 14px; + align-items: flex-start; + padding: 17px; + border: 1px solid #bfded3; + border-radius: 10px; + background: #f0f7f4; + color: #36564d; +} +.settings-privacy-notice svg { + flex: 0 0 24px; + width: 24px; + stroke: #247a63; + fill: none; + stroke-width: 1.8; +} +.settings-privacy-notice strong { + color: #294b41; + font-size: 14px; +} +.settings-privacy-notice p { + margin: 5px 0 0; + color: #66706b; + font-size: 13px; + line-height: 1.6; +} +.settings-section-heading { + margin: 30px 0 14px; + color: #3d4742; + font-size: 16px; +} +.settings-card { + border: 1px solid #dde3e0; + border-radius: 10px; + background: #fff; + padding: 24px 26px; +} +.settings-account-overview { + display: grid; + grid-template-columns: minmax(190px, 1fr) minmax(220px, 1fr) auto; + gap: 24px; + align-items: center; +} +.settings-account-primary { + display: flex; + min-width: 0; + align-items: center; + gap: 15px; +} +.settings-avatar { + width: 66px; + height: 66px; + flex: 0 0 auto; + overflow: hidden; + display: grid; + place-items: center; + border-radius: 9px; + background: #dcede6; + color: #247a63; + font-size: 23px; +} +.settings-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} +.settings-account-identity { + display: grid; + min-width: 0; + gap: 3px; +} +.settings-account-identity span, +.settings-account-root > span, +.settings-account-facts dt { + color: #929a96; + font-size: 12px; +} +.settings-account-identity strong, +.settings-account-identity small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.settings-account-identity strong { + color: #35403b; + font-size: 14px; +} +.settings-account-identity small { + color: #66706b; + font: + 12px/18px ui-monospace, + SFMono-Regular, + Consolas, + monospace; +} +.settings-account-facts { + display: grid; + min-width: 0; + gap: 12px; + margin: 0; +} +.settings-account-facts div { + min-width: 0; +} +.settings-account-facts dd { + overflow: hidden; + margin: 3px 0 0; + color: #35403b; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} +.settings-connection-text.success { + color: #2e8b68; +} +.settings-connection-text.warning { + color: #a56a24; +} +.settings-connection-text.error { + color: #c85a5a; +} +.settings-account-actions { + display: grid; + gap: 8px; +} +.settings-account-actions button { + min-height: 34px; + white-space: nowrap; +} +.settings-account-root { + grid-column: 1/-1; + display: grid; + min-width: 0; + gap: 7px; + padding-top: 19px; + border-top: 1px solid #edf0ee; +} +.settings-account-root > div { + display: flex; + min-width: 0; + align-items: center; + gap: 8px; +} +.settings-account-root code { + min-width: 0; + flex: 1; + overflow: hidden; + padding: 10px 12px; + border: 1px solid #dde3e0; + border-radius: 8px; + background: #f2f5f3; + color: #46514c; + text-overflow: ellipsis; + white-space: nowrap; + font: + 12px/18px ui-monospace, + SFMono-Regular, + Consolas, + monospace; +} +.settings-icon-button { + display: grid; + width: 36px; + height: 36px; + flex: 0 0 auto; + place-items: center; + border: 1px solid #dde3e0; + border-radius: 8px; + background: #fff; + color: #66706b; + cursor: pointer; +} +.settings-icon-button:hover:not(:disabled) { + border-color: #247a63; + color: #247a63; +} +.settings-icon-button:disabled { + cursor: not-allowed; + opacity: 0.45; +} +.settings-icon-button svg { + width: 17px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.8; +} +.settings-health-card { + padding: 12px; +} +.settings-diagnostic-summary { + margin: 0 0 10px; + padding: 9px 11px; + border-left: 3px solid #c68635; + border-radius: 6px; + background: #fff8ec; + color: #79501f; + font-size: 12px; + line-height: 1.55; +} +.settings-diagnostics { + display: grid; + gap: 8px; +} +.settings-diagnostic { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + padding: 13px; + border: 1px solid #edf0ee; + border-radius: 8px; +} +.settings-diagnostic-icon { + display: grid; + width: 18px; + height: 18px; + flex: 0 0 auto; + place-items: center; + border: 1px solid #929a96; + border-radius: 50%; + color: #929a96; + font-size: 11px; + font-weight: 700; +} +.settings-diagnostic-icon.success { + border-color: #2e8b68; + color: #2e8b68; +} +.settings-diagnostic-icon.warning { + border-color: #c68635; + color: #c68635; +} +.settings-diagnostic-icon.error { + border-color: #c85a5a; + color: #c85a5a; +} +.settings-diagnostic-icon.checking { + border-color: #cbd7d2; + border-top-color: #247a63; + animation: settings-spin 0.8s linear infinite; +} +.settings-diagnostic span { + color: #3d4742; + font-size: 13px; +} +.settings-diagnostic small { + margin-left: auto; + overflow: hidden; + color: #66706b; + text-overflow: ellipsis; + white-space: nowrap; +} +.settings-empty-state { + display: grid; + flex: 1; + place-content: center; + color: #66706b; + text-align: center; +} +.settings-empty-state h2 { + color: #202724; +} /* SETTINGS-02: database key */ -.database-key-content { padding-bottom:48px; } -.database-key-badge.unconfigured { background:#f1f3f2; color:#66706b; } -.database-key-badge.validating { background:#eef4f8; color:#4f7188; } -.database-key-badge.invalid { background:#f9eeee; color:#c85a5a; } -.database-key-status-card dl { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:18px 36px; margin:0 0 20px; } -.database-key-status-card dl div { min-width:0; } -.database-key-status-card dt,.database-key-diagnostics dt { color:#929a96; font-size:12px; } -.database-key-status-card dd,.database-key-diagnostics dd { overflow:hidden; margin:5px 0 0; color:#35403b; font-size:13px; text-overflow:ellipsis; white-space:nowrap; } -.database-key-mono { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; } -.database-key-success { color:#2e8b68!important; }.database-key-muted { color:#66706b!important; } -.database-key-primary,.database-key-secondary { min-height:36px; padding:0 15px; border:1px solid #247a63; border-radius:8px; cursor:pointer; font:600 13px/1 inherit; } -.database-key-primary { background:#247a63; color:#fff; }.database-key-secondary { background:#fff; color:#247a63; } -.database-key-primary:hover:not(:disabled) { background:#1d6754; }.database-key-secondary:hover:not(:disabled) { background:#f0f7f4; } -.database-key-primary:disabled,.database-key-secondary:disabled { cursor:not-allowed; opacity:.45; } -.database-key-editor label { display:block; margin-bottom:9px; color:#46514c; font-size:12px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; } -.database-key-input-row { display:flex; min-width:0; height:42px; border:1px solid #d5ddda; border-radius:8px; background:#f4f7f5; } -.database-key-input-row:focus-within { border-color:#247a63; box-shadow:0 0 0 2px rgba(36,122,99,.09); } -.database-key-input-row input { min-width:0; flex:1; border:0; outline:0; padding:0 14px; background:transparent; color:#202724; font:13px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace; } -.database-key-input-row button { display:grid; width:38px; flex:0 0 38px; place-items:center; border:0; background:transparent; color:#66706b; cursor:pointer; } -.database-key-input-row button:hover:not(:disabled) { color:#247a63; }.database-key-input-row button:disabled { cursor:not-allowed; opacity:.4; } -.database-key-input-row svg { width:17px; fill:none; stroke:currentColor; stroke-linecap:round; stroke-linejoin:round; stroke-width:1.8; } -.database-key-editor>p { margin:9px 0 0; color:#66706b; font-size:12px; line-height:1.6; } -.database-key-actions { display:flex; gap:9px; margin-top:18px; } -.database-key-feedback { display:flex; flex-wrap:wrap; align-items:center; gap:8px 18px; margin-top:12px; padding:12px 15px; border-radius:8px; font-size:12px; line-height:1.5; } -.database-key-feedback.checking { background:#f1f4f3; color:#66706b; }.database-key-feedback.checking i { width:14px; height:14px; border:2px solid #c8d5d0; border-top-color:#247a63; border-radius:50%; animation:settings-spin .8s linear infinite; } -.database-key-feedback.success { border:1px solid #bfded3; background:#eaf5f1; color:#2e765d; }.database-key-feedback.success strong { width:100%; } -.database-key-feedback.error { border:1px solid #efcaca; background:#fff2f2; color:#a84444; } -.database-key-auto-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:24px; }.database-key-auto strong { color:#35403b; font-size:14px; }.database-key-auto p { margin:6px 0 0; color:#66706b; font-size:12px; line-height:1.6; } -.database-key-prerequisites { display:flex; flex-wrap:wrap; gap:8px 20px; margin:18px 0 0; padding:14px 0 0; border-top:1px solid #edf0ee; list-style:none; color:#8b5c25; font-size:12px; }.database-key-prerequisites li::before { content:'○'; margin-right:6px; }.database-key-prerequisites li.ok { color:#2e8b68; }.database-key-prerequisites li.ok::before { content:'✓'; } -.database-key-phases { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:8px; margin:18px 0 0; padding:0; counter-reset:key-phase; list-style:none; }.database-key-phases li { position:relative; padding-top:24px; color:#929a96; text-align:center; font-size:11px; }.database-key-phases li::before { position:absolute; top:0; left:50%; display:grid; width:18px; height:18px; place-items:center; border:1px solid #d5ddda; border-radius:50%; background:#fff; content:counter(key-phase); counter-increment:key-phase; transform:translateX(-50%); }.database-key-phases li.active { color:#247a63; }.database-key-phases li.active::before { border-color:#247a63; background:#247a63; color:#fff; } -.database-key-auto-error { display:grid; gap:4px; margin-top:16px; padding:12px 14px; border-left:3px solid #c68635; background:#fff8ec; color:#79501f; font-size:12px; }.database-key-auto-error span { overflow-wrap:anywhere; }.database-key-auto-error p { color:#79501f; }.database-key-auto-error button { justify-self:start; border:0; padding:4px 0; background:transparent; color:#247a63; cursor:pointer; font:600 12px inherit; } -.database-key-security-info { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1px; overflow:hidden; border:1px solid #e3e8e5; border-radius:9px; background:#e3e8e5; }.database-key-security-info span { display:grid; gap:6px; padding:15px; background:#f5f7f6; color:#66706b; font-size:12px; line-height:1.5; }.database-key-security-info strong { color:#35403b; font-size:13px; } -.database-key-diagnostics { margin-top:22px; border:1px solid #dde3e0; border-radius:9px; background:#fff; }.database-key-diagnostics summary { padding:14px 16px; color:#46514c; cursor:pointer; font-size:13px; font-weight:600; }.database-key-diagnostics dl { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px 28px; margin:0; padding:2px 16px 16px; }.database-key-diagnostics button { margin:0 16px 16px; border:0; padding:0; background:transparent; color:#247a63; cursor:pointer; font:600 12px inherit; } -.database-key-connection-actions { margin-top:30px; }.database-key-connection-actions>h2 { margin:0 0 14px; color:#3d4742; font-size:15px; }.database-key-connection-actions>div { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 20px; border:1px solid #dde3e0; border-radius:9px; background:#fff; }.database-key-connection-actions span { display:grid; gap:4px; }.database-key-connection-actions strong { color:#35403b; font-size:13px; }.database-key-connection-actions small { color:#66706b; font-size:12px; }.database-key-connection-actions button { min-height:34px; flex:0 0 auto; border:1px solid #247a63; border-radius:8px; padding:0 13px; background:#fff; color:#247a63; cursor:pointer; }.database-key-connection-actions button:hover:not(:disabled) { background:#f0f7f4; }.database-key-danger { margin-top:30px; padding-bottom:1px; }.database-key-danger>h2 { margin:0 0 14px; color:#c85a5a; font-size:15px; }.database-key-danger>div { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 20px; border:1px solid #efcdcd; background:#fff7f7; }.database-key-danger>div:first-of-type { border-radius:9px 9px 0 0; }.database-key-danger>div:last-of-type { border-top:0; border-radius:0 0 9px 9px; }.database-key-danger span { display:grid; gap:4px; }.database-key-danger strong { color:#4a3c3c; font-size:13px; }.database-key-danger small { color:#786868; font-size:12px; }.database-key-danger button { min-height:34px; flex:0 0 auto; border:1px solid #e8baba; border-radius:8px; padding:0 13px; background:#fff; color:#c85a5a; cursor:pointer; }.database-key-danger button:disabled { cursor:not-allowed; opacity:.45; } -.database-key-confirm-backdrop { position:fixed; z-index:30; inset:0; display:grid; place-items:center; padding:24px; background:rgba(25,32,29,.34); }.database-key-confirm { width:min(100%,430px); padding:22px; border-radius:12px; background:#fff; box-shadow:0 18px 50px rgba(24,35,30,.2); }.database-key-confirm h2 { margin:0; color:#202724; font-size:17px; }.database-key-confirm p { margin:12px 0 22px; color:#66706b; font-size:13px; line-height:1.7; }.database-key-confirm>div { display:flex; justify-content:flex-end; gap:8px; }.database-key-confirm button { min-height:34px; border:1px solid #dde3e0; border-radius:8px; padding:0 14px; background:#fff; color:#46514c; cursor:pointer; }.database-key-confirm button.danger { border-color:#c85a5a; background:#c85a5a; color:#fff; } +.database-key-content { + padding-bottom: 48px; +} +.database-key-badge.unconfigured { + background: #f1f3f2; + color: #66706b; +} +.database-key-badge.validating { + background: #eef4f8; + color: #4f7188; +} +.database-key-badge.invalid { + background: #f9eeee; + color: #c85a5a; +} +.database-key-status-card dl { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px 36px; + margin: 0 0 20px; +} +.database-key-status-card dl div { + min-width: 0; +} +.database-key-status-card dt, +.database-key-diagnostics dt { + color: #929a96; + font-size: 12px; +} +.database-key-status-card dd, +.database-key-diagnostics dd { + overflow: hidden; + margin: 5px 0 0; + color: #35403b; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} +.database-key-mono { + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; +} +.database-key-success { + color: #2e8b68 !important; +} +.database-key-muted { + color: #66706b !important; +} +.database-key-primary, +.database-key-secondary { + min-height: 36px; + padding: 0 15px; + border: 1px solid #247a63; + border-radius: 8px; + cursor: pointer; + font: 600 13px/1 inherit; +} +.database-key-primary { + background: #247a63; + color: #fff; +} +.database-key-secondary { + background: #fff; + color: #247a63; +} +.database-key-primary:hover:not(:disabled) { + background: #1d6754; +} +.database-key-secondary:hover:not(:disabled) { + background: #f0f7f4; +} +.database-key-primary:disabled, +.database-key-secondary:disabled { + cursor: not-allowed; + opacity: 0.45; +} +.database-key-editor label { + display: block; + margin-bottom: 9px; + color: #46514c; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.database-key-input-row { + display: flex; + min-width: 0; + height: 42px; + border: 1px solid #d5ddda; + border-radius: 8px; + background: #f4f7f5; +} +.database-key-input-row:focus-within { + border-color: #247a63; + box-shadow: 0 0 0 2px rgba(36, 122, 99, 0.09); +} +.database-key-input-row input { + min-width: 0; + flex: 1; + border: 0; + outline: 0; + padding: 0 14px; + background: transparent; + color: #202724; + font: + 13px/1.4 ui-monospace, + SFMono-Regular, + Consolas, + monospace; +} +.database-key-input-row button { + display: grid; + width: 38px; + flex: 0 0 38px; + place-items: center; + border: 0; + background: transparent; + color: #66706b; + cursor: pointer; +} +.database-key-input-row button:hover:not(:disabled) { + color: #247a63; +} +.database-key-input-row button:disabled { + cursor: not-allowed; + opacity: 0.4; +} +.database-key-input-row svg { + width: 17px; + fill: none; + stroke: currentColor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.8; +} +.database-key-editor > p { + margin: 9px 0 0; + color: #66706b; + font-size: 12px; + line-height: 1.6; +} +.database-key-actions { + display: flex; + gap: 9px; + margin-top: 18px; +} +.database-key-feedback { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 18px; + margin-top: 12px; + padding: 12px 15px; + border-radius: 8px; + font-size: 12px; + line-height: 1.5; +} +.database-key-feedback.checking { + background: #f1f4f3; + color: #66706b; +} +.database-key-feedback.checking i { + width: 14px; + height: 14px; + border: 2px solid #c8d5d0; + border-top-color: #247a63; + border-radius: 50%; + animation: settings-spin 0.8s linear infinite; +} +.database-key-feedback.success { + border: 1px solid #bfded3; + background: #eaf5f1; + color: #2e765d; +} +.database-key-feedback.success strong { + width: 100%; +} +.database-key-feedback.error { + border: 1px solid #efcaca; + background: #fff2f2; + color: #a84444; +} +.database-key-auto-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; +} +.database-key-auto strong { + color: #35403b; + font-size: 14px; +} +.database-key-auto p { + margin: 6px 0 0; + color: #66706b; + font-size: 12px; + line-height: 1.6; +} +.database-key-prerequisites { + display: flex; + flex-wrap: wrap; + gap: 8px 20px; + margin: 18px 0 0; + padding: 14px 0 0; + border-top: 1px solid #edf0ee; + list-style: none; + color: #8b5c25; + font-size: 12px; +} +.database-key-prerequisites li::before { + content: '○'; + margin-right: 6px; +} +.database-key-prerequisites li.ok { + color: #2e8b68; +} +.database-key-prerequisites li.ok::before { + content: '✓'; +} +.database-key-phases { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 8px; + margin: 18px 0 0; + padding: 0; + counter-reset: key-phase; + list-style: none; +} +.database-key-phases li { + position: relative; + padding-top: 24px; + color: #929a96; + text-align: center; + font-size: 11px; +} +.database-key-phases li::before { + position: absolute; + top: 0; + left: 50%; + display: grid; + width: 18px; + height: 18px; + place-items: center; + border: 1px solid #d5ddda; + border-radius: 50%; + background: #fff; + content: counter(key-phase); + counter-increment: key-phase; + transform: translateX(-50%); +} +.database-key-phases li.active { + color: #247a63; +} +.database-key-phases li.active::before { + border-color: #247a63; + background: #247a63; + color: #fff; +} +.database-key-auto-error { + display: grid; + gap: 4px; + margin-top: 16px; + padding: 12px 14px; + border-left: 3px solid #c68635; + background: #fff8ec; + color: #79501f; + font-size: 12px; +} +.database-key-auto-error span { + overflow-wrap: anywhere; +} +.database-key-auto-error p { + color: #79501f; +} +.database-key-auto-error button { + justify-self: start; + border: 0; + padding: 4px 0; + background: transparent; + color: #247a63; + cursor: pointer; + font: 600 12px inherit; +} +.database-key-security-info { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1px; + overflow: hidden; + border: 1px solid #e3e8e5; + border-radius: 9px; + background: #e3e8e5; +} +.database-key-security-info span { + display: grid; + gap: 6px; + padding: 15px; + background: #f5f7f6; + color: #66706b; + font-size: 12px; + line-height: 1.5; +} +.database-key-security-info strong { + color: #35403b; + font-size: 13px; +} +.database-key-diagnostics { + margin-top: 22px; + border: 1px solid #dde3e0; + border-radius: 9px; + background: #fff; +} +.database-key-diagnostics summary { + padding: 14px 16px; + color: #46514c; + cursor: pointer; + font-size: 13px; + font-weight: 600; +} +.database-key-diagnostics dl { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px 28px; + margin: 0; + padding: 2px 16px 16px; +} +.database-key-diagnostics button { + margin: 0 16px 16px; + border: 0; + padding: 0; + background: transparent; + color: #247a63; + cursor: pointer; + font: 600 12px inherit; +} +.database-key-connection-actions { + margin-top: 30px; +} +.database-key-connection-actions > h2 { + margin: 0 0 14px; + color: #3d4742; + font-size: 15px; +} +.database-key-connection-actions > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 16px 20px; + border: 1px solid #dde3e0; + border-radius: 9px; + background: #fff; +} +.database-key-connection-actions span { + display: grid; + gap: 4px; +} +.database-key-connection-actions strong { + color: #35403b; + font-size: 13px; +} +.database-key-connection-actions small { + color: #66706b; + font-size: 12px; +} +.database-key-connection-actions button { + min-height: 34px; + flex: 0 0 auto; + border: 1px solid #247a63; + border-radius: 8px; + padding: 0 13px; + background: #fff; + color: #247a63; + cursor: pointer; +} +.database-key-connection-actions button:hover:not(:disabled) { + background: #f0f7f4; +} +.database-key-danger { + margin-top: 30px; + padding-bottom: 1px; +} +.database-key-danger > h2 { + margin: 0 0 14px; + color: #c85a5a; + font-size: 15px; +} +.database-key-danger > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 16px 20px; + border: 1px solid #efcdcd; + background: #fff7f7; +} +.database-key-danger > div:first-of-type { + border-radius: 9px 9px 0 0; +} +.database-key-danger > div:last-of-type { + border-top: 0; + border-radius: 0 0 9px 9px; +} +.database-key-danger span { + display: grid; + gap: 4px; +} +.database-key-danger strong { + color: #4a3c3c; + font-size: 13px; +} +.database-key-danger small { + color: #786868; + font-size: 12px; +} +.database-key-danger button { + min-height: 34px; + flex: 0 0 auto; + border: 1px solid #e8baba; + border-radius: 8px; + padding: 0 13px; + background: #fff; + color: #c85a5a; + cursor: pointer; +} +.database-key-danger button:disabled { + cursor: not-allowed; + opacity: 0.45; +} +.database-key-confirm-backdrop { + position: fixed; + z-index: 30; + inset: 0; + display: grid; + place-items: center; + padding: 24px; + background: rgba(25, 32, 29, 0.34); +} +.database-key-confirm { + width: min(100%, 430px); + padding: 22px; + border-radius: 12px; + background: #fff; + box-shadow: 0 18px 50px rgba(24, 35, 30, 0.2); +} +.database-key-confirm h2 { + margin: 0; + color: #202724; + font-size: 17px; +} +.database-key-confirm p { + margin: 12px 0 22px; + color: #66706b; + font-size: 13px; + line-height: 1.7; +} +.database-key-confirm > div { + display: flex; + justify-content: flex-end; + gap: 8px; +} +.database-key-confirm button { + min-height: 34px; + border: 1px solid #dde3e0; + border-radius: 8px; + padding: 0 14px; + background: #fff; + color: #46514c; + cursor: pointer; +} +.database-key-confirm button.danger { + border-color: #c85a5a; + background: #c85a5a; + color: #fff; +} /* SETTINGS-03: image decryption */ -.image-decryption-content { padding-bottom:48px; } -.image-decrypt-badge.unconfigured { background:#f1f3f2; color:#66706b; }.image-decrypt-badge.partial { background:#fff6e8; color:#a56a24; } -.image-decrypt-status dl { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:18px 30px; margin:0 0 20px; }.image-decrypt-status dl div { min-width:0; }.image-decrypt-status dt { color:#929a96; font-size:12px; }.image-decrypt-status dd { overflow:hidden; margin:5px 0 0; color:#35403b; font-size:13px; text-overflow:ellipsis; white-space:nowrap; }.image-decrypt-status .image-decrypt-wide { grid-column:span 2; }.image-decrypt-mono { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; }.image-state-success { color:#2e8b68!important; }.image-state-muted { color:#66706b!important; }.image-state-error { color:#c85a5a!important; } -.image-resource-checks { padding:10px 18px; }.image-resource-checks>div { display:flex; min-width:0; align-items:center; gap:11px; padding:13px 2px; border-bottom:1px solid #edf0ee; }.image-resource-checks>div:last-child { border-bottom:0; }.image-resource-checks strong { color:#3d4742; font-size:13px; }.image-resource-checks small { margin-left:auto; overflow:hidden; color:#66706b; text-overflow:ellipsis; white-space:nowrap; font-size:12px; }.image-resource-icon { display:grid; width:18px; height:18px; flex:0 0 auto; place-items:center; border-radius:50%; background:#f1f3f2; color:#66706b; font-size:11px; font-weight:700; }.image-resource-icon.available { background:#e6f2ed; color:#2e8b68; }.image-resource-icon.unavailable { background:#f9eaea; color:#c85a5a; } -.image-key-editor { display:grid; gap:17px; }.image-key-editor label { display:grid; min-width:0; gap:8px; color:#46514c; font-size:12px; font-weight:600; }.image-key-editor input,.image-test-section select { box-sizing:border-box; width:100%; height:40px; border:1px solid #d5ddda; border-radius:8px; outline:0; padding:0 12px; background:#f4f7f5; color:#202724; font:12px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace; }.image-key-editor input:focus,.image-test-section select:focus { border-color:#247a63; box-shadow:0 0 0 2px rgba(36,122,99,.09); }.image-key-grid { display:grid; grid-template-columns:180px minmax(0,1fr); gap:14px; }.image-key-editor>p { margin:0; color:#66706b; font-size:12px; line-height:1.6; } -.image-test-section>div:first-child strong,.image-auto-detect strong,.image-auto-unavailable strong { color:#35403b; font-size:14px; }.image-test-section>div:first-child p,.image-auto-detect p,.image-auto-unavailable p { margin:6px 0 0; color:#66706b; font-size:12px; line-height:1.6; }.image-test-section>label { display:block; margin:18px 0 8px; color:#46514c; font-size:12px; font-weight:600; }.image-test-actions { display:flex; gap:9px; margin-top:14px; }.image-test-result { display:flex; flex-wrap:wrap; gap:8px 18px; margin-top:14px; padding:12px 14px; border-radius:8px; color:#2e765d; font-size:12px; }.image-test-result.success { border:1px solid #bfded3; background:#eaf5f1; }.image-test-result.error { border:1px solid #efcaca; background:#fff2f2; color:#a84444; }.image-test-result p { width:100%; margin:2px 0 0; }.image-inline-error { margin:14px 0 0; color:#a84444; font-size:12px; } -.image-auto-heading { display:flex; align-items:flex-start; justify-content:space-between; gap:24px; }.image-auto-detect ul { display:flex; flex-wrap:wrap; gap:10px 22px; margin:17px 0 0; padding:14px 0 0; border-top:1px solid #edf0ee; list-style:none; color:#8b5c25; font-size:12px; }.image-auto-detect li::before { content:'○'; margin-right:6px; }.image-auto-detect li.ok { color:#2e8b68; }.image-auto-detect li.ok::before { content:'✓'; }.image-auto-progress { margin-top:12px!important; padding:9px 12px; border-radius:7px; background:#f1f4f3; }.image-auto-unavailable { padding-top:20px; padding-bottom:20px; }.image-key-danger>div { border-radius:9px!important; }.image-key-danger { margin-top:30px; } -.image-auto-phases { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:8px; margin:18px 0 0; padding:0; counter-reset:image-auto-phase; list-style:none; }.image-auto-phases li { position:relative; padding-top:24px; color:#929a96; text-align:center; font-size:11px; }.image-auto-phases li::before { position:absolute; top:0; left:50%; display:grid; width:18px; height:18px; place-items:center; border:1px solid #d5ddda; border-radius:50%; background:#fff; content:counter(image-auto-phase); counter-increment:image-auto-phase; transform:translateX(-50%); }.image-auto-phases li.active { color:#247a63; }.image-auto-phases li.active::before { border-color:#247a63; background:#247a63; color:#fff; }.image-auto-error { margin:12px 0 0!important; padding:10px 12px; border-left:3px solid #c85a5a; background:#fff2f2; color:#a84444!important; }.image-auto-success { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:7px 20px; align-items:center; margin-top:14px; padding:14px 16px; border:1px solid #bfded3; border-radius:8px; background:#eaf5f1; color:#2e765d; font-size:12px; }.image-auto-success strong { grid-column:1; color:#2e765d; }.image-auto-success span { grid-column:1; }.image-auto-success button { grid-column:2; grid-row:1/4; } +.image-decryption-content { + padding-bottom: 48px; +} +.image-decrypt-badge.unconfigured { + background: #f1f3f2; + color: #66706b; +} +.image-decrypt-badge.partial { + background: #fff6e8; + color: #a56a24; +} +.image-decrypt-status dl { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px 30px; + margin: 0 0 20px; +} +.image-decrypt-status dl div { + min-width: 0; +} +.image-decrypt-status dt { + color: #929a96; + font-size: 12px; +} +.image-decrypt-status dd { + overflow: hidden; + margin: 5px 0 0; + color: #35403b; + font-size: 13px; + text-overflow: ellipsis; + white-space: nowrap; +} +.image-decrypt-status .image-decrypt-wide { + grid-column: span 2; +} +.image-decrypt-mono { + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; +} +.image-state-success { + color: #2e8b68 !important; +} +.image-state-muted { + color: #66706b !important; +} +.image-state-error { + color: #c85a5a !important; +} +.image-resource-checks { + padding: 10px 18px; +} +.image-resource-checks > div { + display: flex; + min-width: 0; + align-items: center; + gap: 11px; + padding: 13px 2px; + border-bottom: 1px solid #edf0ee; +} +.image-resource-checks > div:last-child { + border-bottom: 0; +} +.image-resource-checks strong { + color: #3d4742; + font-size: 13px; +} +.image-resource-checks small { + margin-left: auto; + overflow: hidden; + color: #66706b; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; +} +.image-resource-icon { + display: grid; + width: 18px; + height: 18px; + flex: 0 0 auto; + place-items: center; + border-radius: 50%; + background: #f1f3f2; + color: #66706b; + font-size: 11px; + font-weight: 700; +} +.image-resource-icon.available { + background: #e6f2ed; + color: #2e8b68; +} +.image-resource-icon.unavailable { + background: #f9eaea; + color: #c85a5a; +} +.image-key-editor { + display: grid; + gap: 17px; +} +.image-key-editor label { + display: grid; + min-width: 0; + gap: 8px; + color: #46514c; + font-size: 12px; + font-weight: 600; +} +.image-key-editor input, +.image-test-section select { + box-sizing: border-box; + width: 100%; + height: 40px; + border: 1px solid #d5ddda; + border-radius: 8px; + outline: 0; + padding: 0 12px; + background: #f4f7f5; + color: #202724; + font: + 12px/1.4 ui-monospace, + SFMono-Regular, + Consolas, + monospace; +} +.image-key-editor input:focus, +.image-test-section select:focus { + border-color: #247a63; + box-shadow: 0 0 0 2px rgba(36, 122, 99, 0.09); +} +.image-key-grid { + display: grid; + grid-template-columns: 180px minmax(0, 1fr); + gap: 14px; +} +.image-key-editor > p { + margin: 0; + color: #66706b; + font-size: 12px; + line-height: 1.6; +} +.image-test-section > div:first-child strong, +.image-auto-detect strong, +.image-auto-unavailable strong { + color: #35403b; + font-size: 14px; +} +.image-test-section > div:first-child p, +.image-auto-detect p, +.image-auto-unavailable p { + margin: 6px 0 0; + color: #66706b; + font-size: 12px; + line-height: 1.6; +} +.image-test-section > label { + display: block; + margin: 18px 0 8px; + color: #46514c; + font-size: 12px; + font-weight: 600; +} +.image-test-actions { + display: flex; + gap: 9px; + margin-top: 14px; +} +.image-test-result { + display: flex; + flex-wrap: wrap; + gap: 8px 18px; + margin-top: 14px; + padding: 12px 14px; + border-radius: 8px; + color: #2e765d; + font-size: 12px; +} +.image-test-result.success { + border: 1px solid #bfded3; + background: #eaf5f1; +} +.image-test-result.error { + border: 1px solid #efcaca; + background: #fff2f2; + color: #a84444; +} +.image-test-result p { + width: 100%; + margin: 2px 0 0; +} +.image-inline-error { + margin: 14px 0 0; + color: #a84444; + font-size: 12px; +} +.image-auto-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 24px; +} +.image-auto-detect ul { + display: flex; + flex-wrap: wrap; + gap: 10px 22px; + margin: 17px 0 0; + padding: 14px 0 0; + border-top: 1px solid #edf0ee; + list-style: none; + color: #8b5c25; + font-size: 12px; +} +.image-auto-detect li::before { + content: '○'; + margin-right: 6px; +} +.image-auto-detect li.ok { + color: #2e8b68; +} +.image-auto-detect li.ok::before { + content: '✓'; +} +.image-auto-progress { + margin-top: 12px !important; + padding: 9px 12px; + border-radius: 7px; + background: #f1f4f3; +} +.image-auto-unavailable { + padding-top: 20px; + padding-bottom: 20px; +} +.image-key-danger > div { + border-radius: 9px !important; +} +.image-key-danger { + margin-top: 30px; +} +.image-auto-phases { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + margin: 18px 0 0; + padding: 0; + counter-reset: image-auto-phase; + list-style: none; +} +.image-auto-phases li { + position: relative; + padding-top: 24px; + color: #929a96; + text-align: center; + font-size: 11px; +} +.image-auto-phases li::before { + position: absolute; + top: 0; + left: 50%; + display: grid; + width: 18px; + height: 18px; + place-items: center; + border: 1px solid #d5ddda; + border-radius: 50%; + background: #fff; + content: counter(image-auto-phase); + counter-increment: image-auto-phase; + transform: translateX(-50%); +} +.image-auto-phases li.active { + color: #247a63; +} +.image-auto-phases li.active::before { + border-color: #247a63; + background: #247a63; + color: #fff; +} +.image-auto-error { + margin: 12px 0 0 !important; + padding: 10px 12px; + border-left: 3px solid #c85a5a; + background: #fff2f2; + color: #a84444 !important; +} +.image-auto-success { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 7px 20px; + align-items: center; + margin-top: 14px; + padding: 14px 16px; + border: 1px solid #bfded3; + border-radius: 8px; + background: #eaf5f1; + color: #2e765d; + font-size: 12px; +} +.image-auto-success strong { + grid-column: 1; + color: #2e765d; +} +.image-auto-success span { + grid-column: 1; +} +.image-auto-success button { + grid-column: 2; + grid-row: 1/4; +} /* SETTINGS-04: AI provider center */ -.ai-model-content { padding-bottom:48px; }.ai-model-default { display:flex; align-items:center; justify-content:space-between; gap:20px; }.ai-model-default>div { display:grid; gap:5px; }.ai-model-default>div>span,.ai-model-default small { color:#66706b; font-size:12px; }.ai-model-default strong { color:#202724; font-size:18px; }.ai-model-page-error { padding:11px 14px; border-left:3px solid #c85a5a; background:#fff2f2; color:#a84444; font-size:12px; }.ai-provider-list { display:grid; gap:12px; }.ai-provider-card { border:1px solid #dde3e0; border-radius:10px; background:#fff; padding:19px 20px; }.ai-provider-card header { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; }.ai-provider-card h3 { margin:0 0 5px; color:#202724; font-size:15px; }.ai-provider-card header span { color:#66706b; font-size:12px; }.ai-provider-status { border-radius:12px; padding:4px 8px; background:#f1f3f2; }.ai-provider-status.connected { background:#eaf5f1; color:#2e8b68!important; }.ai-provider-status.error { background:#fff2f2; color:#c85a5a!important; }.ai-provider-card dl { display:grid; grid-template-columns:2fr 1fr 1fr; gap:18px; margin:18px 0; }.ai-provider-card dt { color:#929a96; font-size:11px; }.ai-provider-card dd { overflow:hidden; margin:5px 0 0; color:#3d4742; text-overflow:ellipsis; white-space:nowrap; font-size:12px; }.ai-provider-card footer { display:flex; flex-wrap:wrap; gap:8px; padding-top:14px; border-top:1px solid #edf0ee; }.ai-provider-card button,.ai-provider-editor button { min-height:32px; border:1px solid #d5ddda; border-radius:7px; padding:0 11px; background:#fff; color:#46514c; cursor:pointer; font:600 12px inherit; }.ai-provider-card button:hover,.ai-provider-editor button:hover { border-color:#247a63; color:#247a63; }.ai-provider-card button:disabled,.ai-provider-editor button:disabled { cursor:not-allowed; opacity:.45; }.ai-provider-card button.danger,.ai-provider-editor button.danger { color:#c85a5a; }.ai-provider-error { margin:0 0 12px; color:#a84444; font-size:12px; }.ai-provider-empty { color:#66706b; text-align:center; }.ai-provider-empty p { margin:6px 0 0; font-size:12px; } -.ai-provider-editor { display:grid; gap:18px; margin-top:22px; }.ai-provider-editor>header { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; }.ai-provider-editor h2,.ai-provider-editor h3 { margin:0; color:#35403b; font-size:15px; }.ai-provider-editor header p { margin:5px 0 0; color:#66706b; font-size:12px; }.ai-provider-editor label { display:grid; gap:7px; color:#46514c; font-size:12px; font-weight:600; }.ai-provider-editor input:not([type=checkbox]):not([type=radio]),.ai-provider-editor select,.ai-provider-editor textarea { box-sizing:border-box; width:100%; border:1px solid #d5ddda; border-radius:7px; outline:0; padding:9px 10px; background:#f4f7f5; color:#202724; font:12px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace; }.ai-provider-editor input:focus,.ai-provider-editor select:focus,.ai-provider-editor textarea:focus { border-color:#247a63; box-shadow:0 0 0 2px rgba(36,122,99,.08); }.ai-provider-form-grid,.ai-provider-advanced>div { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; }.ai-provider-form-grid .wide,.ai-provider-advanced .wide { grid-column:1/-1; }.ai-model-table-heading { display:flex; align-items:center; justify-content:space-between; }.ai-model-table { display:grid; gap:9px; }.ai-model-row { display:grid; grid-template-columns:1fr 1fr repeat(3,auto) 110px auto auto; gap:8px; align-items:center; padding:11px; border:1px solid #e3e8e5; border-radius:8px; }.ai-model-row label { display:flex; align-items:center; gap:4px; white-space:nowrap; font-weight:400; }.ai-provider-advanced,.ai-provider-preview { border:1px solid #e3e8e5; border-radius:8px; }.ai-provider-advanced summary,.ai-provider-preview summary { padding:12px 14px; cursor:pointer; color:#46514c; font-size:13px; font-weight:600; }.ai-provider-advanced>div { padding:2px 14px 14px; }.ai-provider-advanced textarea { min-height:90px; resize:vertical; }.ai-provider-preview pre { overflow:auto; max-height:280px; margin:0; padding:0 14px 14px; color:#46514c; font:11px/1.6 ui-monospace,SFMono-Regular,Consolas,monospace; }.ai-provider-editor>footer { display:flex; justify-content:flex-end; gap:8px; } -@keyframes settings-spin { to { transform:rotate(360deg); } } -@media (max-width:900px) { - .settings-sidebar { width:248px; flex-basis:248px; } - .settings-page-content { padding:24px; } - .settings-page-header { padding:20px 24px; } - .settings-account-overview { grid-template-columns:minmax(0,1fr) auto; gap:18px; } - .settings-account-facts { grid-column:1/-1; grid-template-columns:repeat(2,minmax(0,1fr)); } - .settings-account-actions { grid-row:1; grid-column:2; } - .database-key-security-info { grid-template-columns:1fr; } - .image-decrypt-status dl { grid-template-columns:repeat(2,minmax(0,1fr)); }.image-decrypt-status .image-decrypt-wide { grid-column:span 2; } - .ai-model-row { grid-template-columns:1fr 1fr; }.ai-model-row label,.ai-model-row button { justify-self:start; } +.ai-model-content { + padding-bottom: 48px; } -@media (max-width:700px) { - .settings-account-overview { grid-template-columns:minmax(0,1fr); } - .settings-account-actions { grid-row:auto; grid-column:1; grid-template-columns:repeat(2,minmax(0,1fr)); } - .settings-account-root { grid-column:1; } - .database-key-status-card dl,.database-key-diagnostics dl { grid-template-columns:1fr; } - .database-key-auto-heading { flex-direction:column; }.database-key-phases { grid-template-columns:1fr; }.database-key-phases li { padding:0 0 0 28px; text-align:left; }.database-key-phases li::before { top:-2px; left:0; transform:none; } - .image-decrypt-status dl,.image-key-grid { grid-template-columns:1fr; }.image-decrypt-status .image-decrypt-wide { grid-column:1; }.image-auto-heading { flex-direction:column; }.image-auto-phases { grid-template-columns:1fr; }.image-auto-phases li { padding:0 0 0 28px; text-align:left; }.image-auto-phases li::before { top:-2px; left:0; transform:none; }.image-auto-success { grid-template-columns:1fr; }.image-auto-success button { grid-column:1; grid-row:auto; justify-self:start; }.image-resource-checks>div { align-items:flex-start; }.image-resource-checks small { white-space:normal; text-align:right; } - .ai-provider-card dl,.ai-provider-form-grid,.ai-provider-advanced>div { grid-template-columns:1fr; }.ai-provider-form-grid .wide,.ai-provider-advanced .wide { grid-column:1; }.ai-model-row { grid-template-columns:1fr; } +.ai-model-default { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} +.ai-model-default > div { + display: grid; + gap: 5px; +} +.ai-model-default > div > span, +.ai-model-default small { + color: #66706b; + font-size: 12px; +} +.ai-model-default strong { + color: #202724; + font-size: 18px; +} +.ai-model-page-error { + padding: 11px 14px; + border-left: 3px solid #c85a5a; + background: #fff2f2; + color: #a84444; + font-size: 12px; +} +.ai-provider-list { + display: grid; + gap: 12px; +} +.ai-provider-card { + border: 1px solid #dde3e0; + border-radius: 10px; + background: #fff; + padding: 19px 20px; +} +.ai-provider-card header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; +} +.ai-provider-card h3 { + margin: 0 0 5px; + color: #202724; + font-size: 15px; +} +.ai-provider-card header span { + color: #66706b; + font-size: 12px; +} +.ai-provider-status { + border-radius: 12px; + padding: 4px 8px; + background: #f1f3f2; +} +.ai-provider-status.connected { + background: #eaf5f1; + color: #2e8b68 !important; +} +.ai-provider-status.error { + background: #fff2f2; + color: #c85a5a !important; +} +.ai-provider-card dl { + display: grid; + grid-template-columns: 2fr 1fr 1fr; + gap: 18px; + margin: 18px 0; +} +.ai-provider-card dt { + color: #929a96; + font-size: 11px; +} +.ai-provider-card dd { + overflow: hidden; + margin: 5px 0 0; + color: #3d4742; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; +} +.ai-provider-card footer { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding-top: 14px; + border-top: 1px solid #edf0ee; +} +.ai-provider-card button, +.ai-provider-editor button { + min-height: 32px; + border: 1px solid #d5ddda; + border-radius: 7px; + padding: 0 11px; + background: #fff; + color: #46514c; + cursor: pointer; + font: 600 12px inherit; +} +.ai-provider-card button:hover, +.ai-provider-editor button:hover { + border-color: #247a63; + color: #247a63; +} +.ai-provider-card button:disabled, +.ai-provider-editor button:disabled { + cursor: not-allowed; + opacity: 0.45; +} +.ai-provider-card button.danger, +.ai-provider-editor button.danger { + color: #c85a5a; +} +.ai-provider-error { + margin: 0 0 12px; + color: #a84444; + font-size: 12px; +} +.ai-provider-empty { + color: #66706b; + text-align: center; +} +.ai-provider-empty p { + margin: 6px 0 0; + font-size: 12px; +} +.ai-provider-editor { + display: grid; + gap: 18px; + margin-top: 22px; +} +.ai-provider-editor > header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 18px; +} +.ai-provider-editor h2, +.ai-provider-editor h3 { + margin: 0; + color: #35403b; + font-size: 15px; +} +.ai-provider-editor header p { + margin: 5px 0 0; + color: #66706b; + font-size: 12px; +} +.ai-provider-editor label { + display: grid; + gap: 7px; + color: #46514c; + font-size: 12px; + font-weight: 600; +} +.ai-provider-editor input:not([type='checkbox']):not([type='radio']), +.ai-provider-editor select, +.ai-provider-editor textarea { + box-sizing: border-box; + width: 100%; + border: 1px solid #d5ddda; + border-radius: 7px; + outline: 0; + padding: 9px 10px; + background: #f4f7f5; + color: #202724; + font: + 12px/1.4 ui-monospace, + SFMono-Regular, + Consolas, + monospace; +} +.ai-provider-editor input:focus, +.ai-provider-editor select:focus, +.ai-provider-editor textarea:focus { + border-color: #247a63; + box-shadow: 0 0 0 2px rgba(36, 122, 99, 0.08); +} +.ai-provider-form-grid, +.ai-provider-advanced > div { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} +.ai-provider-form-grid .wide, +.ai-provider-advanced .wide { + grid-column: 1/-1; +} +.ai-model-table-heading { + display: flex; + align-items: center; + justify-content: space-between; +} +.ai-model-table { + display: grid; + gap: 9px; +} +.ai-model-row { + display: grid; + grid-template-columns: 1fr 1fr repeat(3, auto) 110px auto auto; + gap: 8px; + align-items: center; + padding: 11px; + border: 1px solid #e3e8e5; + border-radius: 8px; +} +.ai-model-row label { + display: flex; + align-items: center; + gap: 4px; + white-space: nowrap; + font-weight: 400; +} +.ai-provider-advanced, +.ai-provider-preview { + border: 1px solid #e3e8e5; + border-radius: 8px; +} +.ai-provider-advanced summary, +.ai-provider-preview summary { + padding: 12px 14px; + cursor: pointer; + color: #46514c; + font-size: 13px; + font-weight: 600; +} +.ai-provider-advanced > div { + padding: 2px 14px 14px; +} +.ai-provider-advanced textarea { + min-height: 90px; + resize: vertical; +} +.ai-provider-preview pre { + overflow: auto; + max-height: 280px; + margin: 0; + padding: 0 14px 14px; + color: #46514c; + font: + 11px/1.6 ui-monospace, + SFMono-Regular, + Consolas, + monospace; +} +.ai-provider-editor > footer { + display: flex; + justify-content: flex-end; + gap: 8px; +} +@keyframes settings-spin { + to { + transform: rotate(360deg); + } +} +@media (max-width: 900px) { + .settings-sidebar { + width: 248px; + flex-basis: 248px; + } + .settings-page-content { + padding: 24px; + } + .settings-page-header { + padding: 20px 24px; + } + .settings-account-overview { + grid-template-columns: minmax(0, 1fr) auto; + gap: 18px; + } + .settings-account-facts { + grid-column: 1/-1; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .settings-account-actions { + grid-row: 1; + grid-column: 2; + } + .database-key-security-info { + grid-template-columns: 1fr; + } + .image-decrypt-status dl { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .image-decrypt-status .image-decrypt-wide { + grid-column: span 2; + } + .ai-model-row { + grid-template-columns: 1fr 1fr; + } + .ai-model-row label, + .ai-model-row button { + justify-self: start; + } +} +@media (max-width: 700px) { + .settings-account-overview { + grid-template-columns: minmax(0, 1fr); + } + .settings-account-actions { + grid-row: auto; + grid-column: 1; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .settings-account-root { + grid-column: 1; + } + .database-key-status-card dl, + .database-key-diagnostics dl { + grid-template-columns: 1fr; + } + .database-key-auto-heading { + flex-direction: column; + } + .database-key-phases { + grid-template-columns: 1fr; + } + .database-key-phases li { + padding: 0 0 0 28px; + text-align: left; + } + .database-key-phases li::before { + top: -2px; + left: 0; + transform: none; + } + .image-decrypt-status dl, + .image-key-grid { + grid-template-columns: 1fr; + } + .image-decrypt-status .image-decrypt-wide { + grid-column: 1; + } + .image-auto-heading { + flex-direction: column; + } + .image-auto-phases { + grid-template-columns: 1fr; + } + .image-auto-phases li { + padding: 0 0 0 28px; + text-align: left; + } + .image-auto-phases li::before { + top: -2px; + left: 0; + transform: none; + } + .image-auto-success { + grid-template-columns: 1fr; + } + .image-auto-success button { + grid-column: 1; + grid-row: auto; + justify-self: start; + } + .image-resource-checks > div { + align-items: flex-start; + } + .image-resource-checks small { + white-space: normal; + text-align: right; + } + .ai-provider-card dl, + .ai-provider-form-grid, + .ai-provider-advanced > div { + grid-template-columns: 1fr; + } + .ai-provider-form-grid .wide, + .ai-provider-advanced .wide { + grid-column: 1; + } + .ai-model-row { + grid-template-columns: 1fr; + } +} +.api-center-layout > * { + min-width: 0; + min-height: 0; + box-sizing: border-box; +} +.api-section-heading h2, +.api-introduction h2, +.api-integrations h2, +.api-runtime-title h2 { + margin: 0; + color: var(--wxex-text-primary); + font: 700 17px/24px var(--wxex-font); +} +.ready-text { + color: var(--wxex-success); +} +.api-main { + position: relative; + display: flex; + min-width: 0; + min-height: 0; + overflow: hidden; + background: var(--wxex-bg-main); +} +.api-main-scroll { + flex: 1; + min-width: 0; + min-height: 0; + overflow-y: auto; + padding: 22px 28px 36px; +} +.api-workspace-heading { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding-bottom: 16px; + border-bottom: 1px solid var(--wxex-border); +} +.api-title-line { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} +.api-title-line h1 { + margin: 0; + color: var(--wxex-text-primary); + font: 700 22px/30px var(--wxex-font); +} +.api-workspace-heading p { + margin: 4px 0 0; + color: var(--wxex-text-secondary); + font: 13px/19px var(--wxex-font); +} +.api-skill-status, +.api-version { + padding: 2px 7px; + border-radius: 5px; + font: 700 11px/17px var(--wxex-font); +} +.api-skill-status.ready { + color: var(--wxex-brand); + background: var(--wxex-brand-soft); +} +.api-skill-status.error { + color: var(--wxex-danger); + background: #fff1f1; +} +.api-version { + color: var(--wxex-text-muted); + background: var(--wxex-bg-app); +} +.api-header-actions, +.api-tester-actions, +.api-runtime-actions, +.api-skill-file-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.api-header-actions button, +.api-tester-actions button, +.api-runtime-actions button, +.api-endpoint-row button, +.api-response-summary + pre + p + button, +.api-skill-file-actions button { + min-height: 32px; + padding: 0 11px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-sm); + background: var(--wxex-bg-elevated); + color: var(--wxex-text-primary); + cursor: pointer; + font: 600 12px/18px var(--wxex-font); +} +.api-header-actions button:hover, +.api-tester-actions button:hover, +.api-runtime-actions button:hover, +.api-endpoint-row button:hover, +.api-skill-file-actions button:hover { + border-color: var(--wxex-brand); + color: var(--wxex-brand); +} +.api-primary-button { + border-color: var(--wxex-brand) !important; + background: var(--wxex-brand) !important; + color: #fff !important; +} +.api-primary-button:hover:not(:disabled), +.api-primary-button:focus-visible, +.api-primary-button:active { + background: var(--wxex-brand-hover) !important; + color: #fff !important; +} +.api-primary-button:disabled { + cursor: not-allowed; + opacity: 0.55; +} +.api-trust-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-top: 16px; + padding: 10px 12px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-md); + background: #f5f8f6; + color: var(--wxex-text-secondary); + font: 12px/18px var(--wxex-font); +} +.api-trust-bar i { + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--wxex-text-muted); +} +.api-introduction { + margin-top: 24px; + padding: 18px 20px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-lg); + background: var(--wxex-bg-elevated); +} +.api-introduction p { + margin: 10px 0 16px; + color: var(--wxex-text-secondary); + font: 13px/20px var(--wxex-font); +} +.api-flow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 12px; + border: 1px dashed var(--wxex-border); + border-radius: var(--wxex-radius-md); + color: var(--wxex-text-secondary); + font: 12px/18px var(--wxex-font); +} +.api-flow span, +.api-flow strong { + padding: 7px 10px; + border: 1px solid var(--wxex-border); + border-radius: 5px; + background: #fff; + white-space: nowrap; +} +.api-flow strong { + color: var(--wxex-brand); + background: var(--wxex-brand-soft); +} +.api-flow b { + color: var(--wxex-text-muted); + font-size: 17px; +} +.api-endpoint-catalog, +.api-request-tester { + margin-top: 26px; +} +.skill-install-flow { + margin-top: 26px; + padding: 18px 20px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-lg); + background: var(--wxex-bg-elevated); +} +.skill-flow-steps { + display: grid; + gap: 16px; + margin-top: 16px; +} +.skill-flow-steps > section { + position: relative; + display: grid; + grid-template-columns: 26px minmax(0, 1fr); + gap: 10px; + color: var(--wxex-text-muted); +} +.skill-flow-steps > section:not(:last-child)::after { + position: absolute; + top: 28px; + left: 12px; + bottom: -16px; + width: 1px; + background: var(--wxex-border); + content: ''; +} +.skill-flow-steps > section > b { + position: relative; + z-index: 1; + display: grid; + width: 24px; + height: 24px; + place-items: center; + border: 1px solid var(--wxex-border); + border-radius: 50%; + background: var(--wxex-bg-elevated); + font: 700 12px/1 var(--wxex-font); +} +.skill-flow-steps > section.active > b, +.skill-flow-steps > section.done > b { + border-color: var(--wxex-brand); + background: var(--wxex-brand); + color: #fff; +} +.skill-flow-steps h3 { + margin: 1px 0 5px; + color: var(--wxex-text-primary); + font: 700 14px/20px var(--wxex-font); +} +.skill-flow-steps p { + margin: 3px 0; + color: var(--wxex-text-secondary); + font: 12px/18px var(--wxex-font); +} +.skill-flow-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 10px; +} +.skill-flow-actions button, +.api-skill-details button { + min-height: 30px; + padding: 0 10px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-sm); + background: #fff; + color: var(--wxex-text-primary); + cursor: pointer; + font: 600 12px/18px var(--wxex-font); +} +.skill-flow-actions button:disabled, +.api-skill-details button:disabled { + cursor: not-allowed; + opacity: 0.55; +} +.skill-target-selector { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; +} +.skill-target-selector button { + min-height: 30px; + padding: 0 10px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-sm); + background: #fff; + color: var(--wxex-text-secondary); + cursor: pointer; + font: 600 12px/18px var(--wxex-font); +} +.skill-target-selector button.active { + border-color: var(--wxex-brand); + background: var(--wxex-brand-soft); + color: var(--wxex-brand); +} +.api-skill-details { + margin-top: 16px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-md); + background: var(--wxex-bg-elevated); +} +.api-skill-details summary { + padding: 12px 14px; + color: var(--wxex-text-primary); + cursor: pointer; + font: 700 13px/18px var(--wxex-font); +} +.api-skill-details > dl, +.api-skill-details > div { + margin: 0; + padding: 0 14px 14px; +} +.api-skill-details dl div { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + gap: 8px; + padding: 4px 0; + color: var(--wxex-text-secondary); + font: 12px/18px var(--wxex-font); +} +.api-skill-details dd { + margin: 0; + color: var(--wxex-text-primary); +} +.api-skill-more { + position: relative; +} +.api-skill-more summary { + display: grid; + min-height: 32px; + place-items: center; + padding: 0 11px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-sm); + background: var(--wxex-bg-elevated); + color: var(--wxex-text-primary); + cursor: pointer; + font: 600 12px/18px var(--wxex-font); + list-style: none; +} +.api-skill-more[open] > button { + position: absolute; + z-index: 2; + top: 36px; + right: 0; + width: 170px; + padding: 9px 10px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-sm); + background: #fff; + color: var(--wxex-text-primary); + box-shadow: var(--wxex-shadow-popover); + font: 12px/18px var(--wxex-font); + text-align: left; +} +.api-section-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} +.api-section-heading > span { + color: var(--wxex-text-muted); + font: 12px/18px var(--wxex-font); +} +.api-endpoint-table { + margin-top: 12px; + overflow: hidden; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-md); + background: var(--wxex-bg-elevated); +} +.api-endpoint-head, +.api-endpoint-row { + display: grid; + grid-template-columns: 72px minmax(128px, 1.1fr) minmax(150px, 1.2fr) 90px; + align-items: center; + gap: 12px; + padding: 11px 14px; +} +.api-endpoint-head { + background: #f1f4f2; + color: var(--wxex-text-secondary); + font: 700 12px/18px var(--wxex-font); +} +.api-endpoint-row { + min-height: 54px; + border-top: 1px solid var(--wxex-border); + color: var(--wxex-text-secondary); + font: 12px/18px var(--wxex-font); +} +.api-endpoint-row.active { + background: #f7faf8; +} +.api-endpoint-row code { + color: var(--wxex-text-primary); + font: + 12px/18px ui-monospace, + monospace; +} +.api-endpoint-row small { + display: block; + color: var(--wxex-text-muted); +} +.api-endpoint-row > span:last-child { + display: flex; + gap: 4px; +} +.api-endpoint-row button { + min-height: 26px; + padding: 0 7px; + font-size: 11px; +} +.api-method { + display: inline-block; + color: var(--wxex-brand); + font: + 700 11px/18px ui-monospace, + monospace; +} +.api-method.post { + color: var(--wxex-ai); +} +.api-request-meta { + display: flex; + align-items: center; + gap: 10px; + margin: 12px 0; + padding: 10px 12px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-md); + background: #f5f8f6; + overflow: hidden; +} +.api-request-meta code { + overflow: hidden; + color: var(--wxex-text-primary); + font: + 12px/18px ui-monospace, + monospace; + text-overflow: ellipsis; + white-space: nowrap; +} +.api-param-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} +.api-param-grid label, +.api-json-input { + display: grid; + gap: 5px; + color: var(--wxex-text-secondary); + font: 12px/18px var(--wxex-font); +} +.api-param-grid label span b { + margin-left: 5px; + color: var(--wxex-danger); + font-size: 11px; +} +.api-param-grid input, +.api-json-input textarea { + box-sizing: border-box; + width: 100%; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-sm); + outline: none; + background: #fff; + color: var(--wxex-text-primary); + font: 12px/18px var(--wxex-font); +} +.api-param-grid input { + height: 34px; + padding: 0 9px; +} +.api-json-input { + margin-top: 10px; +} +.api-json-input textarea { + min-height: 180px; + padding: 10px; + resize: vertical; + font-family: ui-monospace, monospace; +} +.api-tester-actions { + justify-content: flex-end; + margin-top: 12px; +} +.api-inline-error { + margin: 10px 0 0; + padding: 8px 10px; + border: 1px solid rgba(200, 90, 90, 0.35); + border-radius: var(--wxex-radius-sm); + background: #fff4f4; + color: var(--wxex-danger); + font: 12px/18px var(--wxex-font); +} +.api-runtime-panel { + border-left: 1px solid var(--wxex-border); + background: var(--wxex-bg-main); + overflow: hidden; +} +.api-runtime-scroll { + height: 100%; + overflow-y: auto; +} +.api-runtime-scroll section { + padding: 18px 16px; + border-bottom: 1px solid var(--wxex-border); +} +.api-runtime-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.api-runtime-title > span { + font: 700 12px/18px var(--wxex-font); +} +.api-runtime-title .ready { + color: var(--wxex-success); +} +.api-runtime-title .stopped { + color: var(--wxex-text-muted); +} +.api-runtime-scroll h3 { + margin: 0 0 12px; + color: var(--wxex-text-primary); + font: 700 14px/20px var(--wxex-font); +} +.api-runtime-scroll dl { + margin: 14px 0; +} +.api-runtime-scroll dl div { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 5px 0; + font: 12px/18px var(--wxex-font); +} +.api-runtime-scroll dt { + color: var(--wxex-text-secondary); +} +.api-runtime-scroll dd { + margin: 0; + color: var(--wxex-text-primary); + text-align: right; +} +.warning-text { + color: var(--wxex-warning); +} +.api-runtime-actions { + display: grid; + grid-template-columns: 1fr 1fr; +} +.api-runtime-actions button { + min-height: 30px; + padding: 0 6px; +} +.api-security-warning { + margin: 12px 16px; + padding: 9px 10px; + border-left: 3px solid var(--wxex-warning); + background: #fff8ec; + color: #915d1e; + font: 12px/18px var(--wxex-font); +} +.api-response-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + color: var(--wxex-text-primary); + font: + 12px/18px ui-monospace, + monospace; +} +.api-runtime-scroll pre { + max-height: 180px; + overflow: auto; + margin: 10px 0 8px; + padding: 10px; + border-radius: var(--wxex-radius-sm); + background: #1f2824; + color: #cde7dc; + font: + 11px/16px ui-monospace, + monospace; + white-space: pre-wrap; + word-break: break-word; +} +.api-response-meta, +.api-empty-text { + margin: 0 0 9px; + color: var(--wxex-text-muted); + font: 11px/17px var(--wxex-font); +} +.api-history { + display: grid; + gap: 8px; + margin: 0; + padding: 0; + list-style: none; +} +.api-history li { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 2px 8px; + padding-bottom: 8px; + border-bottom: 1px solid var(--wxex-border); +} +.api-history span { + overflow: hidden; + color: var(--wxex-text-primary); + font: + 11px/17px ui-monospace, + monospace; + text-overflow: ellipsis; + white-space: nowrap; +} +.api-history b { + font: 700 11px/17px var(--wxex-font); +} +.api-history small { + grid-column: 1 / -1; + color: var(--wxex-text-muted); + font: 11px/16px var(--wxex-font); +} +.api-privacy { + background: #f5f8f6; +} +.api-privacy p { + margin: 0; + color: var(--wxex-text-secondary); + font: 12px/18px var(--wxex-font); +} +.api-markdown-overlay { + position: absolute; + inset: 0; + z-index: 4; + display: flex; + align-items: stretch; + justify-content: center; + padding: 20px; + background: rgba(32, 39, 36, 0.24); +} +.api-markdown-overlay > div { + display: flex; + width: min(820px, 100%); + flex-direction: column; + min-height: 0; + padding: 14px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-lg); + background: #fff; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.16); +} +.api-markdown-overlay header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.api-markdown-overlay header > div { + display: flex; + align-items: center; + gap: 8px; +} +.api-markdown-overlay header span { + color: var(--wxex-text-muted); + font: 12px/18px var(--wxex-font); +} +.api-markdown-overlay button { + min-height: 30px; + padding: 0 10px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-sm); + background: #fff; + color: var(--wxex-text-primary); + cursor: pointer; +} +.api-markdown-overlay pre, +.skill-markdown-preview { + min-height: 0; + overflow: auto; + margin: 14px 0 0; + white-space: pre-wrap; + color: var(--wxex-text-primary); + font: 12px/19px var(--wxex-font); +} +.skill-markdown-preview h1 { + font-size: 20px; +} +.skill-markdown-preview h2 { + margin-top: 18px; + font-size: 16px; +} +.skill-markdown-preview p { + margin: 6px 0; +} +.skill-markdown-preview li { + margin-left: 18px; +} +@media (max-width: 1100px) { + .api-center-layout { + grid-template-columns: minmax(0, 1fr) 280px; + } + .api-main-scroll { + padding: 18px; + } + .api-header-actions { + max-width: 260px; + justify-content: flex-end; + } + .api-endpoint-head, + .api-endpoint-row { + grid-template-columns: 56px minmax(110px, 1fr) minmax(100px, 1fr) 78px; + gap: 7px; + padding: 10px; + } + .api-flow { + overflow-x: auto; + justify-content: flex-start; + } } -.api-center-layout > * { min-width: 0; min-height: 0; box-sizing: border-box; } -.api-section-heading h2, .api-introduction h2, .api-integrations h2, .api-runtime-title h2 { margin: 0; color: var(--wxex-text-primary); font: 700 17px/24px var(--wxex-font); } -.ready-text { color: var(--wxex-success); } -.api-main { position: relative; display: flex; min-width: 0; min-height: 0; overflow: hidden; background: var(--wxex-bg-main); }.api-main-scroll { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; padding: 22px 28px 36px; } -.api-workspace-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 16px; border-bottom: 1px solid var(--wxex-border); }.api-title-line { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }.api-title-line h1 { margin: 0; color: var(--wxex-text-primary); font: 700 22px/30px var(--wxex-font); }.api-workspace-heading p { margin: 4px 0 0; color: var(--wxex-text-secondary); font: 13px/19px var(--wxex-font); }.api-skill-status, .api-version { padding: 2px 7px; border-radius: 5px; font: 700 11px/17px var(--wxex-font); }.api-skill-status.ready { color: var(--wxex-brand); background: var(--wxex-brand-soft); }.api-skill-status.error { color: var(--wxex-danger); background: #fff1f1; }.api-version { color: var(--wxex-text-muted); background: var(--wxex-bg-app); } -.api-header-actions, .api-tester-actions, .api-runtime-actions, .api-skill-file-actions { display: flex; flex-wrap: wrap; gap: 8px; }.api-header-actions button, .api-tester-actions button, .api-runtime-actions button, .api-endpoint-row button, .api-response-summary + pre + p + button, .api-skill-file-actions button { min-height: 32px; padding: 0 11px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-sm); background: var(--wxex-bg-elevated); color: var(--wxex-text-primary); cursor: pointer; font: 600 12px/18px var(--wxex-font); }.api-header-actions button:hover, .api-tester-actions button:hover, .api-runtime-actions button:hover, .api-endpoint-row button:hover, .api-skill-file-actions button:hover { border-color: var(--wxex-brand); color: var(--wxex-brand); }.api-primary-button { border-color: var(--wxex-brand) !important; background: var(--wxex-brand) !important; color: #fff !important; }.api-primary-button:hover:not(:disabled), .api-primary-button:focus-visible, .api-primary-button:active { background: var(--wxex-brand-hover) !important; color: #fff !important; }.api-primary-button:disabled { cursor: not-allowed; opacity: .55; } -.api-trust-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 16px; padding: 10px 12px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-md); background: #f5f8f6; color: var(--wxex-text-secondary); font: 12px/18px var(--wxex-font); }.api-trust-bar i { width: 3px; height: 3px; border-radius: 50%; background: var(--wxex-text-muted); }.api-introduction { margin-top: 24px; padding: 18px 20px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-lg); background: var(--wxex-bg-elevated); }.api-introduction p { margin: 10px 0 16px; color: var(--wxex-text-secondary); font: 13px/20px var(--wxex-font); }.api-flow { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 12px; border: 1px dashed var(--wxex-border); border-radius: var(--wxex-radius-md); color: var(--wxex-text-secondary); font: 12px/18px var(--wxex-font); }.api-flow span, .api-flow strong { padding: 7px 10px; border: 1px solid var(--wxex-border); border-radius: 5px; background: #fff; white-space: nowrap; }.api-flow strong { color: var(--wxex-brand); background: var(--wxex-brand-soft); }.api-flow b { color: var(--wxex-text-muted); font-size: 17px; } -.api-endpoint-catalog, .api-request-tester { margin-top: 26px; } -.skill-install-flow { margin-top: 26px; padding: 18px 20px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-lg); background: var(--wxex-bg-elevated); }.skill-flow-steps { display: grid; gap: 16px; margin-top: 16px; }.skill-flow-steps > section { position: relative; display: grid; grid-template-columns: 26px minmax(0,1fr); gap: 10px; color: var(--wxex-text-muted); }.skill-flow-steps > section:not(:last-child)::after { position: absolute; top: 28px; left: 12px; bottom: -16px; width: 1px; background: var(--wxex-border); content: ''; }.skill-flow-steps > section > b { position: relative; z-index: 1; display: grid; width: 24px; height: 24px; place-items: center; border: 1px solid var(--wxex-border); border-radius: 50%; background: var(--wxex-bg-elevated); font: 700 12px/1 var(--wxex-font); }.skill-flow-steps > section.active > b, .skill-flow-steps > section.done > b { border-color: var(--wxex-brand); background: var(--wxex-brand); color: #fff; }.skill-flow-steps h3 { margin: 1px 0 5px; color: var(--wxex-text-primary); font: 700 14px/20px var(--wxex-font); }.skill-flow-steps p { margin: 3px 0; color: var(--wxex-text-secondary); font: 12px/18px var(--wxex-font); }.skill-flow-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }.skill-flow-actions button, .api-skill-details button { min-height: 30px; padding: 0 10px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-sm); background: #fff; color: var(--wxex-text-primary); cursor: pointer; font: 600 12px/18px var(--wxex-font); }.skill-flow-actions button:disabled, .api-skill-details button:disabled { cursor: not-allowed; opacity: .55; }.skill-target-selector { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }.skill-target-selector button { min-height: 30px; padding: 0 10px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-sm); background: #fff; color: var(--wxex-text-secondary); cursor: pointer; font: 600 12px/18px var(--wxex-font); }.skill-target-selector button.active { border-color: var(--wxex-brand); background: var(--wxex-brand-soft); color: var(--wxex-brand); } -.api-skill-details { margin-top: 16px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-md); background: var(--wxex-bg-elevated); }.api-skill-details summary { padding: 12px 14px; color: var(--wxex-text-primary); cursor: pointer; font: 700 13px/18px var(--wxex-font); }.api-skill-details > dl, .api-skill-details > div { margin: 0; padding: 0 14px 14px; }.api-skill-details dl div { display: grid; grid-template-columns: 72px minmax(0,1fr); gap: 8px; padding: 4px 0; color: var(--wxex-text-secondary); font: 12px/18px var(--wxex-font); }.api-skill-details dd { margin: 0; color: var(--wxex-text-primary); } -.api-skill-more { position: relative; }.api-skill-more summary { display: grid; min-height: 32px; place-items: center; padding: 0 11px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-sm); background: var(--wxex-bg-elevated); color: var(--wxex-text-primary); cursor: pointer; font: 600 12px/18px var(--wxex-font); list-style: none; }.api-skill-more[open] > button { position: absolute; z-index: 2; top: 36px; right: 0; width: 170px; padding: 9px 10px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-sm); background: #fff; color: var(--wxex-text-primary); box-shadow: var(--wxex-shadow-popover); font: 12px/18px var(--wxex-font); text-align: left; } -.api-section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }.api-section-heading > span { color: var(--wxex-text-muted); font: 12px/18px var(--wxex-font); }.api-endpoint-table { margin-top: 12px; overflow: hidden; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-md); background: var(--wxex-bg-elevated); }.api-endpoint-head, .api-endpoint-row { display: grid; grid-template-columns: 72px minmax(128px, 1.1fr) minmax(150px, 1.2fr) 90px; align-items: center; gap: 12px; padding: 11px 14px; }.api-endpoint-head { background: #f1f4f2; color: var(--wxex-text-secondary); font: 700 12px/18px var(--wxex-font); }.api-endpoint-row { min-height: 54px; border-top: 1px solid var(--wxex-border); color: var(--wxex-text-secondary); font: 12px/18px var(--wxex-font); }.api-endpoint-row.active { background: #f7faf8; }.api-endpoint-row code { color: var(--wxex-text-primary); font: 12px/18px ui-monospace, monospace; }.api-endpoint-row small { display: block; color: var(--wxex-text-muted); }.api-endpoint-row > span:last-child { display: flex; gap: 4px; }.api-endpoint-row button { min-height: 26px; padding: 0 7px; font-size: 11px; }.api-method { display: inline-block; color: var(--wxex-brand); font: 700 11px/18px ui-monospace, monospace; }.api-method.post { color: var(--wxex-ai); } -.api-request-meta { display: flex; align-items: center; gap: 10px; margin: 12px 0; padding: 10px 12px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-md); background: #f5f8f6; overflow: hidden; }.api-request-meta code { overflow: hidden; color: var(--wxex-text-primary); font: 12px/18px ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; }.api-param-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }.api-param-grid label, .api-json-input { display: grid; gap: 5px; color: var(--wxex-text-secondary); font: 12px/18px var(--wxex-font); }.api-param-grid label span b { margin-left: 5px; color: var(--wxex-danger); font-size: 11px; }.api-param-grid input, .api-json-input textarea { box-sizing: border-box; width: 100%; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-sm); outline: none; background: #fff; color: var(--wxex-text-primary); font: 12px/18px var(--wxex-font); }.api-param-grid input { height: 34px; padding: 0 9px; }.api-json-input { margin-top: 10px; }.api-json-input textarea { min-height: 180px; padding: 10px; resize: vertical; font-family: ui-monospace, monospace; }.api-tester-actions { justify-content: flex-end; margin-top: 12px; }.api-inline-error { margin: 10px 0 0; padding: 8px 10px; border: 1px solid rgba(200,90,90,.35); border-radius: var(--wxex-radius-sm); background: #fff4f4; color: var(--wxex-danger); font: 12px/18px var(--wxex-font); } -.api-runtime-panel { border-left: 1px solid var(--wxex-border); background: var(--wxex-bg-main); overflow: hidden; }.api-runtime-scroll { height: 100%; overflow-y: auto; }.api-runtime-scroll section { padding: 18px 16px; border-bottom: 1px solid var(--wxex-border); }.api-runtime-title { display: flex; align-items: center; justify-content: space-between; gap: 8px; }.api-runtime-title > span { font: 700 12px/18px var(--wxex-font); }.api-runtime-title .ready { color: var(--wxex-success); }.api-runtime-title .stopped { color: var(--wxex-text-muted); }.api-runtime-scroll h3 { margin: 0 0 12px; color: var(--wxex-text-primary); font: 700 14px/20px var(--wxex-font); }.api-runtime-scroll dl { margin: 14px 0; }.api-runtime-scroll dl div { display: flex; justify-content: space-between; gap: 12px; padding: 5px 0; font: 12px/18px var(--wxex-font); }.api-runtime-scroll dt { color: var(--wxex-text-secondary); }.api-runtime-scroll dd { margin: 0; color: var(--wxex-text-primary); text-align: right; }.warning-text { color: var(--wxex-warning); }.api-runtime-actions { display: grid; grid-template-columns: 1fr 1fr; }.api-runtime-actions button { min-height: 30px; padding: 0 6px; }.api-security-warning { margin: 12px 16px; padding: 9px 10px; border-left: 3px solid var(--wxex-warning); background: #fff8ec; color: #915d1e; font: 12px/18px var(--wxex-font); }.api-response-summary { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--wxex-text-primary); font: 12px/18px ui-monospace, monospace; }.api-runtime-scroll pre { max-height: 180px; overflow: auto; margin: 10px 0 8px; padding: 10px; border-radius: var(--wxex-radius-sm); background: #1f2824; color: #cde7dc; font: 11px/16px ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; }.api-response-meta, .api-empty-text { margin: 0 0 9px; color: var(--wxex-text-muted); font: 11px/17px var(--wxex-font); }.api-history { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }.api-history li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 8px; padding-bottom: 8px; border-bottom: 1px solid var(--wxex-border); }.api-history span { overflow: hidden; color: var(--wxex-text-primary); font: 11px/17px ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; }.api-history b { font: 700 11px/17px var(--wxex-font); }.api-history small { grid-column: 1 / -1; color: var(--wxex-text-muted); font: 11px/16px var(--wxex-font); }.api-privacy { background: #f5f8f6; }.api-privacy p { margin: 0; color: var(--wxex-text-secondary); font: 12px/18px var(--wxex-font); } -.api-markdown-overlay { position: absolute; inset: 0; z-index: 4; display: flex; align-items: stretch; justify-content: center; padding: 20px; background: rgba(32,39,36,.24); }.api-markdown-overlay > div { display: flex; width: min(820px, 100%); flex-direction: column; min-height: 0; padding: 14px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-lg); background: #fff; box-shadow: 0 12px 40px rgba(0,0,0,.16); }.api-markdown-overlay header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }.api-markdown-overlay header > div { display: flex; align-items: center; gap: 8px; }.api-markdown-overlay header span { color: var(--wxex-text-muted); font: 12px/18px var(--wxex-font); }.api-markdown-overlay button { min-height: 30px; padding: 0 10px; border: 1px solid var(--wxex-border); border-radius: var(--wxex-radius-sm); background: #fff; color: var(--wxex-text-primary); cursor: pointer; }.api-markdown-overlay pre, .skill-markdown-preview { min-height: 0; overflow: auto; margin: 14px 0 0; white-space: pre-wrap; color: var(--wxex-text-primary); font: 12px/19px var(--wxex-font); }.skill-markdown-preview h1 { font-size: 20px; }.skill-markdown-preview h2 { margin-top: 18px; font-size: 16px; }.skill-markdown-preview p { margin: 6px 0; }.skill-markdown-preview li { margin-left: 18px; } -@media (max-width: 1100px) { .api-center-layout { grid-template-columns: minmax(0,1fr) 280px; }.api-main-scroll { padding: 18px; }.api-header-actions { max-width: 260px; justify-content: flex-end; }.api-endpoint-head, .api-endpoint-row { grid-template-columns: 56px minmax(110px,1fr) minmax(100px,1fr) 78px; gap: 7px; padding: 10px; }.api-flow { overflow-x: auto; justify-content: flex-start; } } .report-history-list-title { padding: 4px 4px 8px; @@ -4609,3 +7219,51 @@ body { font-weight: 700; cursor: pointer; } + +.settings-auto-login-card { + padding: 18px 20px; +} +.settings-auto-login-card label { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + cursor: pointer; +} +.settings-auto-login-card label > span { + display: grid; + gap: 4px; +} +.settings-auto-login-card b { + color: var(--wxex-text-primary); + font: 700 13px/19px var(--wxex-font); +} +.settings-auto-login-card small { + color: var(--wxex-text-secondary); + font: 12px/18px var(--wxex-font); +} +.settings-auto-login-card input { + width: 18px; + height: 18px; + accent-color: var(--wxex-brand); +} +.api-upload-test-row { + display: flex; + align-items: center; + gap: 10px; + margin: 10px 0; +} +.api-upload-test-row button { + min-height: 32px; + padding: 0 12px; + border: 1px solid var(--wxex-border); + border-radius: var(--wxex-radius-sm); + background: var(--wxex-bg-elevated); + color: var(--wxex-text-primary); + cursor: pointer; + font: 600 12px/18px var(--wxex-font); +} +.api-upload-test-row span { + color: var(--wxex-text-muted); + font: 11px/17px var(--wxex-font); +} diff --git a/src/renderer/src/components/layout/PrimaryNavigation.tsx b/src/renderer/src/components/layout/PrimaryNavigation.tsx index 665868d..5a78200 100644 --- a/src/renderer/src/components/layout/PrimaryNavigation.tsx +++ b/src/renderer/src/components/layout/PrimaryNavigation.tsx @@ -45,6 +45,16 @@ function NavIcon({ page }: NavIconProps): React.ReactElement { ) + case 'agent-hub': + return ( + + ) case 'api': return (
+
+
+
WechatExplorer
+

Agent Hub

+

让微信机器人安全调用聊天数据与 AI 能力。

+
+ + Agent Hub {status.hub === 'online' ? '运行中' : '未运行'} + +
+ +
+
+
+
+ 微信机器人 +

连接微信

+
+ + + {STATUS_LABELS[status.connector]} + +
+ + {showQRCode ? ( +
+
+ 微信机器人登录二维码 +
+
+

+ {status.connector === 'scanned' ? '请在手机上确认登录' : '使用微信扫描二维码'} +

+

二维码仅用于机器人账号登录,不会读取你的微信密码。

+ +
+
+ ) : status.connector === 'online' ? ( +
+
+ ✓ +
+
+

微信机器人已连接

+

{status.accountId || status.wechatUserId || '登录凭据已就绪'}

+
+
+ ) : ( +
+
+ +
+

{status.connector === 'error' ? '连接遇到问题' : '尚未连接微信机器人'}

+

{status.error || '扫码登录后,即可从微信向 Agent Hub 提问。'}

+
+ )} + +
+ {status.connector === 'online' ? ( + <> + + + + ) : !isLoginFlow ? ( + + ) : null} +
+
+ + +
+ +
+
+
+ 故障诊断 +

运行日志

+
+
+ + + +
+
+
+ {visibleLogs.length === 0 ? ( +
暂无运行日志。收到消息后,这里会显示处理到哪一步。
+ ) : ( + visibleLogs.map((entry) => ( +
+ + {LOG_SOURCE_LABELS[entry.source]} + {entry.message} +
+ )) + )} +
+

日志会隐藏 Token 和二维码数据,不记录你的微信密码。

+
+
+ ) +} diff --git a/src/renderer/src/features/api-center/components/ApiRequestTester.tsx b/src/renderer/src/features/api-center/components/ApiRequestTester.tsx index e13657e..f39bf14 100644 --- a/src/renderer/src/features/api-center/components/ApiRequestTester.tsx +++ b/src/renderer/src/features/api-center/components/ApiRequestTester.tsx @@ -40,6 +40,17 @@ export function ApiRequestTester({ void onCopyCurl(command) } const update = (key: string, value: string): void => onParams({ ...params, [key]: value }) + const selectTestImage = async (): Promise => { + const result = await window.api.selectAgentHubTestImage() + if (result.canceled || !result.path) return + let payload: Record = {} + try { + payload = JSON.parse(body) as Record + } catch { + // Replace an invalid draft with a valid send-test request. + } + onBody(JSON.stringify({ ...payload, media_url: result.path }, null, 2)) + } return (
@@ -77,6 +88,14 @@ export function ApiRequestTester({ /> )} + {endpoint.id === 'agent-send' && ( +
+ + 选择后只会填入本地路径;点击“发送请求”才会真正发送。 +
+ )}
diff --git a/src/renderer/src/utils/group-report-facts.ts b/src/renderer/src/utils/group-report-facts.ts index ec4d12f..027e481 100644 --- a/src/renderer/src/utils/group-report-facts.ts +++ b/src/renderer/src/utils/group-report-facts.ts @@ -11,6 +11,14 @@ import { ReportVoiceLeaderboardItem } from '../../../shared/group-report' +declare const window: { + api: { + imageListCandidates: (...args: unknown[]) => Promise + imageAnalyze: (...args: unknown[]) => Promise + getImage: (...args: unknown[]) => Promise + } +} + export interface GroupReportTranscriptRow { id: string datetime: string @@ -184,6 +192,7 @@ const buildMediaSection = async ( warnings: string[] }> => { const warnings: string[] = [] + const rendererApi = typeof window === 'undefined' ? null : window.api const rawImageCandidates = messages .map((message, index) => { if (message.contentData?.type !== 'image') return null @@ -214,6 +223,7 @@ const buildMediaSection = async ( // ============================================================ let visionGallery: ReportVisionGalleryItem[] = [] try { + if (!rendererApi) throw new Error('后台模式不读取 Renderer 图片') const sessionId = messages.find((m) => m.sessionId)?.sessionId || (contact?.md5 ?? '') const startTime = messages.length ? parseTimestamp(messages[0]) : 0 const endTime = messages.length ? parseTimestamp(messages[messages.length - 1]) : 0 @@ -233,7 +243,7 @@ const buildMediaSection = async ( } }) - const candidatesResp = await window.api.imageListCandidates({ + const candidatesResp = await rendererApi.imageListCandidates({ sessionId, startTime, endTime, @@ -249,7 +259,7 @@ const buildMediaSection = async ( if (candidate.insight) return candidate.insight // 未命中:解密图片拿 base64 → 调 AI try { - const img = await window.api.getImage( + const img = await rendererApi.getImage( candidate.md5, candidate.datName, candidate.sessionId @@ -260,7 +270,7 @@ const buildMediaSection = async ( ) return null } - const analyzeResp = await window.api.imageAnalyze({ + const analyzeResp = await rendererApi.imageAnalyze({ imageHash: candidate.imageHash, imageDataUrl: img.data, messageId: candidate.messageId, @@ -306,7 +316,7 @@ const buildMediaSection = async ( const orig = rawImageCandidates.find((c) => c.sourceMessageIds[0] === item.messageId) if (!orig) return item try { - const img = await window.api.getImage(orig.md5, orig.datName, orig.sessionId) + const img = await rendererApi.getImage(orig.md5, orig.datName, orig.sessionId) if (img.success && img.data?.startsWith('data:image/')) { return { ...item, imageUrl: img.data } } @@ -323,9 +333,10 @@ const buildMediaSection = async ( visionGallery = [] } - const imageCandidates = await Promise.all( - rawImageCandidates.map(async (item) => { - const result = await window.api.getImage(item.md5, item.datName, item.sessionId) + const imageCandidates = rendererApi + ? await Promise.all( + rawImageCandidates.map(async (item) => { + const result = await rendererApi.getImage(item.md5, item.datName, item.sessionId) if (!result.success || !result.data?.startsWith('data:image/')) return null return { sender: item.sender, @@ -337,9 +348,10 @@ const buildMediaSection = async ( sourceMessageIds: item.sourceMessageIds, replyCount: item.replyCount, score: item.score - } - }) - ) + } + }) + ) + : [] const gallery: ReportMediaGalleryItem[] = imageCandidates .filter((item): item is NonNullable => Boolean(item)) diff --git a/src/shared/agent-hub.ts b/src/shared/agent-hub.ts new file mode 100644 index 0000000..6b8c826 --- /dev/null +++ b/src/shared/agent-hub.ts @@ -0,0 +1,38 @@ +export type AgentHubRuntimeStatus = 'starting' | 'online' | 'offline' | 'error' +export type WechatConnectorStatus = + | 'checking' + | 'disconnected' + | 'starting' + | 'waiting_scan' + | 'scanned' + | 'online' + | 'error' + +export interface AgentHubStatus { + hub: AgentHubRuntimeStatus + connector: WechatConnectorStatus + qrCodeDataUrl?: string + accountId?: string + wechatUserId?: string + error?: string + updatedAt: number + dataApi?: 'checking' | 'online' | 'offline' + databaseReady?: boolean +} + +export interface AgentHubActionResult { + success: boolean + status: AgentHubStatus + error?: string +} + +export type AgentHubLogSource = 'agent-hub' | 'wechat-connector' | 'system' +export type AgentHubLogLevel = 'info' | 'warn' | 'error' + +export interface AgentHubLogEntry { + id: number + timestamp: number + source: AgentHubLogSource + level: AgentHubLogLevel + message: string +} diff --git a/src/shared/local-api-test.ts b/src/shared/local-api-test.ts index 0f2ffe3..b8a9852 100644 --- a/src/shared/local-api-test.ts +++ b/src/shared/local-api-test.ts @@ -11,7 +11,10 @@ export const LOCAL_API_ENDPOINTS = { }, 'group-snapshot': { method: 'GET', path: '/api/v1/group_snapshot', queryKeys: ['md5'] }, resolve: { method: 'GET', path: '/api/v1/resolve', queryKeys: ['q'] }, - report: { method: 'POST', path: '/api/v1/report', queryKeys: [] } + report: { method: 'POST', path: '/api/v1/report', queryKeys: [] }, + 'agent-status': { method: 'GET', path: '/api/v1/agent/status', queryKeys: [] }, + 'agent-group-report': { method: 'POST', path: '/api/v1/agent/group-report', queryKeys: [] }, + 'agent-send': { method: 'POST', path: '/api/v1/agent/send', queryKeys: [] } } as const export type LocalApiEndpointId = keyof typeof LOCAL_API_ENDPOINTS