chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,557 @@
import { contextBridge, ipcRenderer } from "electron";
import type { SandboxPolicy } from "@shared/types/sandbox";
// Expose protected methods to the renderer process
contextBridge.exposeInMainWorld("electronAPI", {
// Process info (available in preload but not in renderer)
versions: {
node: process.versions.node,
electron: process.versions.electron,
chrome: process.versions.chrome,
},
// Settings
settings: {
get: (key: string) => ipcRenderer.invoke("settings:get", key),
set: (key: string, value: unknown) =>
ipcRenderer.invoke("settings:set", key, value),
},
// Window controls
window: {
minimize: () => ipcRenderer.invoke("window:minimize"),
maximize: () => ipcRenderer.invoke("window:maximize"),
close: () => ipcRenderer.invoke("window:close"),
},
// MCP Proxy management (qiming-mcp-stdio-proxy 聚合代理)
mcp: {
start: () => ipcRenderer.invoke("mcp:start"),
stop: () => ipcRenderer.invoke("mcp:stop"),
restart: () => ipcRenderer.invoke("mcp:restart"),
status: () => ipcRenderer.invoke("mcp:status"),
getConfig: () => ipcRenderer.invoke("mcp:getConfig"),
setConfig: (config: {
mcpServers: Record<
string,
{ command: string; args: string[]; env?: Record<string, string> }
>;
}) => ipcRenderer.invoke("mcp:setConfig", config),
discoverTools: (serverId: string) =>
ipcRenderer.invoke("mcp:discoverTools", serverId),
exportConfig: () => ipcRenderer.invoke("mcp:exportConfig"),
getPort: () => Promise.resolve(0),
setPort: (_port: number) => Promise.resolve({ success: true }),
},
// Lanproxy management (via IPC to main process)
lanproxy: {
start: (config: {
serverIp: string;
serverPort: number;
clientKey: string;
ssl?: boolean;
}) => ipcRenderer.invoke("lanproxy:start", config),
stop: () => ipcRenderer.invoke("lanproxy:stop"),
status: () => ipcRenderer.invoke("lanproxy:status"),
isAvailable: () =>
ipcRenderer.invoke("lanproxy:isAvailable") as Promise<{
available: boolean;
}>,
},
// Agent Runner management (via IPC to main process)
agentRunner: {
start: (config: {
binPath: string;
backendPort: number;
proxyPort: number;
apiKey: string;
apiBaseUrl: string;
defaultModel: string;
}) => ipcRenderer.invoke("agentRunner:start", config),
stop: () => ipcRenderer.invoke("agentRunner:stop"),
status: () => ipcRenderer.invoke("agentRunner:status"),
},
// Sandbox policy and diagnostics
sandbox: {
status: () => ipcRenderer.invoke("sandbox:status"),
getPolicy: () => ipcRenderer.invoke("sandbox:policy:get"),
setPolicy: (patch: Partial<SandboxPolicy>) =>
ipcRenderer.invoke("sandbox:policy:set", patch),
capabilities: () => ipcRenderer.invoke("sandbox:capabilities"),
setup: () => ipcRenderer.invoke("sandbox:setup"),
},
// Agent - unified ACP service (claude-code/qimingcode)
agent: {
// Unified Agent ACP
init: (config: any) => ipcRenderer.invoke("agent:init", config),
destroy: () => ipcRenderer.invoke("agent:destroy"),
getEngineType: () => ipcRenderer.invoke("agent:getEngineType"),
isReady: () => ipcRenderer.invoke("agent:isReady"),
serviceStatus: () => ipcRenderer.invoke("agent:serviceStatus"),
// Session management (ACP)
listSessions: () => ipcRenderer.invoke("agent:listSessions"),
createSession: (opts?: { parentID?: string; title?: string }) =>
ipcRenderer.invoke("agent:createSession", opts),
getSession: (id: string) => ipcRenderer.invoke("agent:getSession", id),
deleteSession: (id: string) =>
ipcRenderer.invoke("agent:deleteSession", id),
updateSession: (id: string, title?: string) =>
ipcRenderer.invoke("agent:updateSession", id, title),
getSessionStatus: () => ipcRenderer.invoke("agent:getSessionStatus"),
forkSession: (id: string, messageId?: string) =>
ipcRenderer.invoke("agent:forkSession", id, messageId),
// Messages
getMessages: (sessionId: string, limit?: number) =>
ipcRenderer.invoke("agent:getMessages", sessionId, limit),
getMessage: (sessionId: string, messageId: string) =>
ipcRenderer.invoke("agent:getMessage", sessionId, messageId),
// Prompt / Command / Shell
prompt: (sessionId: string, parts: any[], opts?: any) =>
ipcRenderer.invoke("agent:prompt", sessionId, parts, opts),
promptAsync: (sessionId: string, parts: any[], opts?: any) =>
ipcRenderer.invoke("agent:promptAsync", sessionId, parts, opts),
command: (sessionId: string, cmd: string, args?: string, opts?: any) =>
ipcRenderer.invoke("agent:command", sessionId, cmd, args, opts),
shell: (sessionId: string, cmd: string, agent?: string, model?: any) =>
ipcRenderer.invoke("agent:shell", sessionId, cmd, agent, model),
// Abort
abort: (sessionId: string) => ipcRenderer.invoke("agent:abort", sessionId),
// Permission
respondPermission: (
sessionId: string,
permissionId: string,
response: "once" | "always" | "reject",
) =>
ipcRenderer.invoke(
"agent:respondPermission",
sessionId,
permissionId,
response,
),
// Session operations
getSessionDiff: (sessionId: string, messageId?: string) =>
ipcRenderer.invoke("agent:getSessionDiff", sessionId, messageId),
revert: (sessionId: string, messageId: string, partId?: string) =>
ipcRenderer.invoke("agent:revert", sessionId, messageId, partId),
unrevert: (sessionId: string) =>
ipcRenderer.invoke("agent:unrevert", sessionId),
shareSession: (sessionId: string) =>
ipcRenderer.invoke("agent:shareSession", sessionId),
// Tools
listTools: (provider?: string, model?: string) =>
ipcRenderer.invoke("agent:listTools", provider, model),
// Providers
listProviders: () => ipcRenderer.invoke("agent:listProviders"),
// Config
getConfig: () => ipcRenderer.invoke("agent:getConfig"),
// File operations
findText: (pattern: string) =>
ipcRenderer.invoke("agent:findText", pattern),
findFiles: (query: string, dirs?: boolean) =>
ipcRenderer.invoke("agent:findFiles", query, dirs),
listFiles: (dirPath: string) =>
ipcRenderer.invoke("agent:listFiles", dirPath),
readFile: (filePath: string) =>
ipcRenderer.invoke("agent:readFile", filePath),
// MCP via ACP
mcpStatus: () => ipcRenderer.invoke("agent:mcpStatus"),
// Agents & Commands
listAgents: () => ipcRenderer.invoke("agent:listAgents"),
listCommands: () => ipcRenderer.invoke("agent:listCommands"),
// Claude Code specific
claudePrompt: (message: string) =>
ipcRenderer.invoke("agent:claudePrompt", message),
// Sessions tab (detailed view)
listSessionsDetailed: () =>
ipcRenderer.invoke("agent:listSessionsDetailed"),
stopSession: (sessionId: string) =>
ipcRenderer.invoke("agent:stopSession", sessionId),
// SSE Event listening
onEvent: (
callback: (event: any, data: { type: string; data: any }) => void,
) => {
ipcRenderer.on("agent:event", callback);
},
offEvent: (
callback: (event: any, data: { type: string; data: any }) => void,
) => {
ipcRenderer.removeListener("agent:event", callback);
},
},
// File Server management
fileServer: {
start: (port?: number) => ipcRenderer.invoke("fileServer:start", port),
stop: () => ipcRenderer.invoke("fileServer:stop"),
status: () => ipcRenderer.invoke("fileServer:status"),
},
// Computer Server lifecycle (Agent HTTP 接口服务)
computerServer: {
start: (port?: number) => ipcRenderer.invoke("computerServer:start", port),
stop: () => ipcRenderer.invoke("computerServer:stop"),
status: () => ipcRenderer.invoke("computerServer:status"),
},
// GUI Agent Server lifecycle (桌面自动化视觉操作服务)
guiServer: {
start: () => ipcRenderer.invoke("guiServer:start"),
stop: () => ipcRenderer.invoke("guiServer:stop"),
status: () => ipcRenderer.invoke("guiServer:status"),
isEnabled: () => ipcRenderer.invoke("guiServer:isEnabled"),
setEnabled: (enabled: boolean) =>
ipcRenderer.invoke("guiServer:setEnabled", enabled),
},
// Admin Server lifecycle (管理接口服务)
adminServer: {
start: (port?: number) => ipcRenderer.invoke("adminServer:start", port),
stop: () => ipcRenderer.invoke("adminServer:stop"),
status: () => ipcRenderer.invoke("adminServer:status"),
},
// Computer API (对齐 rcoder /computer/* API)
computer: {
chat: (request: any) => ipcRenderer.invoke("computer:chat", request),
agentStatus: (request: any) =>
ipcRenderer.invoke("computer:agentStatus", request),
agentStop: (request: any) =>
ipcRenderer.invoke("computer:agentStop", request),
cancelSession: (request: any) =>
ipcRenderer.invoke("computer:cancelSession", request),
health: () => ipcRenderer.invoke("computer:health"),
onProgress: (callback: any) => {
ipcRenderer.on("computer:progress", callback);
},
offProgress: (callback: any) => {
ipcRenderer.removeListener("computer:progress", callback);
},
},
// Services (对齐 Tauri services_restart_all)
services: {
restartAll: () => ipcRenderer.invoke("services:restartAll"),
stopAll: () => ipcRenderer.invoke("services:stopAll"),
},
// Tray status sync
tray: {
updateStatus: (status: "running" | "stopped" | "error" | "starting") =>
ipcRenderer.invoke("tray:updateStatus", status),
updateServicesStatus: (running: boolean) =>
ipcRenderer.invoke("tray:updateServicesStatus", running),
},
// Dependency management
dependencies: {
checkAll: (options?: { checkLatest?: boolean }) =>
ipcRenderer.invoke("dependencies:checkAll", options),
checkNode: () => ipcRenderer.invoke("dependencies:checkNode"),
checkUv: () => ipcRenderer.invoke("dependencies:checkUv"),
checkMcpProxyBundled: () =>
ipcRenderer.invoke("dependencies:checkMcpProxyBundled"),
checkQimingCodeBundled: () =>
ipcRenderer.invoke("dependencies:checkQimingCodeBundled"),
checkClaudeCodeAcpBundled: () =>
ipcRenderer.invoke("dependencies:checkClaudeCodeAcpBundled"),
checkQimingFileServerBundled: () =>
ipcRenderer.invoke("dependencies:checkQimingFileServerBundled"),
detectPackage: (packageName: string, binName?: string) =>
ipcRenderer.invoke("dependencies:detectPackage", packageName, binName),
installPackage: (
packageName: string,
options?: { registry?: string; version?: string },
) =>
ipcRenderer.invoke("dependencies:installPackage", packageName, options),
installMissing: () => ipcRenderer.invoke("dependencies:installMissing"),
getAppDataDir: () => ipcRenderer.invoke("dependencies:getAppDataDir"),
getRequiredList: () => ipcRenderer.invoke("dependencies:getRequiredList"),
},
// Engine Manager (claude-code / qimingcode)
engine: {
checkLocal: (engine: string) =>
ipcRenderer.invoke("engine:checkLocal", engine),
checkGlobal: (engine: string) =>
ipcRenderer.invoke("engine:checkGlobal", engine),
getVersion: (engine: string) =>
ipcRenderer.invoke("engine:getVersion", engine),
findBinary: (engine: string) =>
ipcRenderer.invoke("engine:findBinary", engine),
install: (engine: string, options?: { registry?: string }) =>
ipcRenderer.invoke("engine:install", engine, options),
start: (config: {
engine: string;
apiKey?: string;
baseUrl?: string;
model?: string;
workspaceDir?: string;
}) => ipcRenderer.invoke("engine:start", config),
stop: (engineId: string) => ipcRenderer.invoke("engine:stop", engineId),
status: (engineId?: string) =>
ipcRenderer.invoke("engine:status", engineId),
send: (engineId: string, message: string) =>
ipcRenderer.invoke("engine:send", engineId, message),
stopAll: () => ipcRenderer.invoke("engine:stopAll"),
},
// Shell utilities
shell: {
openExternal: (url: string) =>
ipcRenderer.invoke("shell:openExternal", url),
// 打开本地目录/文件到系统文件管理器,供设置页“打开工作空间目录”等功能复用。
openPath: (targetPath: string) =>
ipcRenderer.invoke("shell:openPath", targetPath),
},
// Session / Cookie management (for embedded webview)
session: {
setCookie: (params: {
url: string;
name: string;
value: string;
domain?: string;
httpOnly?: boolean;
secure?: boolean;
}) => ipcRenderer.invoke("session:setCookie", params),
getCookie: (params: { url: string; name: string }) =>
ipcRenderer.invoke("session:getCookie", params),
removeCookie: (params: { url: string; name: string }) =>
ipcRenderer.invoke("session:removeCookie", params),
flushStore: () => ipcRenderer.invoke("session:flushStore"),
},
// WebView Window - 独立窗口打开会话浏览器
webview: {
openWindow: (params: { url: string; title?: string }) =>
ipcRenderer.invoke("webview:openWindow", params) as Promise<{
success: boolean;
reused?: boolean;
error?: string;
}>,
closeWindow: () =>
ipcRenderer.invoke("webview:closeWindow") as Promise<{
success: boolean;
error?: string;
}>,
isWindowOpen: () =>
ipcRenderer.invoke("webview:isWindowOpen") as Promise<boolean>,
},
// Mirror / Registry
mirror: {
get: () => ipcRenderer.invoke("mirror:get"),
set: (config: { npmRegistry?: string; uvIndexUrl?: string }) =>
ipcRenderer.invoke("mirror:set", config),
},
// i18n - 语言同步
i18n: {
getLang: () => ipcRenderer.invoke("i18n:getLang"),
setLang: (lang: string) => {
console.debug(`[preload] i18n.setLang("${lang}")`);
return ipcRenderer.invoke("i18n:setLang", lang);
},
},
// Dialog utilities
dialog: {
openDirectory: (title?: string) =>
ipcRenderer.invoke("dialog:openDirectory", title),
},
// Autolaunch
autolaunch: {
get: () => ipcRenderer.invoke("autolaunch:get"),
set: (enabled: boolean) => ipcRenderer.invoke("autolaunch:set", enabled),
},
// Long-term Memory
memory: {
// Lifecycle
init: (workspaceDir: string, config?: any) =>
ipcRenderer.invoke("memory:init", workspaceDir, config),
destroy: () => ipcRenderer.invoke("memory:destroy"),
status: () => ipcRenderer.invoke("memory:status"),
ensureReady: () =>
ipcRenderer.invoke("memory:ensureReady") as Promise<{
ready: boolean;
synced: boolean;
}>,
// Configuration
getConfig: () => ipcRenderer.invoke("memory:getConfig"),
updateConfig: (config: any) =>
ipcRenderer.invoke("memory:updateConfig", config),
// Extraction
extract: (
sessionId: string,
messageId: string,
messages: any[],
modelConfig: any,
) =>
ipcRenderer.invoke(
"memory:extract",
sessionId,
messageId,
messages,
modelConfig,
),
append: (content: string, title?: string) =>
ipcRenderer.invoke("memory:append", content, title),
handleMessage: (
message: { role: "user" | "assistant"; content: string },
sessionId: string,
modelConfig: any,
) =>
ipcRenderer.invoke(
"memory:handleMessage",
message,
sessionId,
modelConfig,
) as Promise<{ success: boolean; error?: string }>,
onSessionEnd: (sessionId: string, modelConfig: any) =>
ipcRenderer.invoke(
"memory:onSessionEnd",
sessionId,
modelConfig,
) as Promise<{ success: boolean; taskId?: string; error?: string }>,
getExtractionProgress: (sessionId: string) =>
ipcRenderer.invoke("memory:getExtractionProgress", sessionId) as Promise<{
success: boolean;
progress?: any[];
error?: string;
}>,
// Retrieval
search: (query: string, options?: any) =>
ipcRenderer.invoke("memory:search", query, options),
getContext: (query: string, options?: any) =>
ipcRenderer.invoke("memory:getContext", query, options),
// File operations
sync: () => ipcRenderer.invoke("memory:sync"),
rebuildIndex: () => ipcRenderer.invoke("memory:rebuildIndex"),
getFiles: () => ipcRenderer.invoke("memory:getFiles"),
// Management
add: (entry: any) => ipcRenderer.invoke("memory:add", entry),
update: (id: string, updates: any) =>
ipcRenderer.invoke("memory:update", id, updates),
delete: (id: string) => ipcRenderer.invoke("memory:delete", id),
list: (options?: any) => ipcRenderer.invoke("memory:list", options),
// Scheduled tasks
runConsolidation: (modelConfig?: {
provider: string;
model: string;
apiKey: string;
baseUrl?: string;
}) => ipcRenderer.invoke("memory:runConsolidation", modelConfig),
runCleanup: () => ipcRenderer.invoke("memory:runCleanup"),
// Vector
checkVectorSupport: () => ipcRenderer.invoke("memory:checkVectorSupport"),
setEmbeddingConfig: (config: any) =>
ipcRenderer.invoke("memory:setEmbeddingConfig", config),
// Queue status
getQueueStatus: () => ipcRenderer.invoke("memory:getQueueStatus"),
getSchedulerStatus: () => ipcRenderer.invoke("memory:getSchedulerStatus"),
},
// Log
log: {
getDir: () => ipcRenderer.invoke("log:getDir"),
openDir: () => ipcRenderer.invoke("log:openDir"),
list: (count?: number, offset?: number) =>
ipcRenderer.invoke("log:list", count, offset),
write: (
level: "info" | "warn" | "error",
message: string,
...args: unknown[]
) => ipcRenderer.invoke("log:write", level, message, ...args),
},
// App
app: {
checkUpdate: () => ipcRenderer.invoke("app:checkUpdate"),
getVersion: () => ipcRenderer.invoke("app:getVersion"),
downloadUpdate: () => ipcRenderer.invoke("app:downloadUpdate"),
installUpdate: () => ipcRenderer.invoke("app:installUpdate"),
getUpdateState: () => ipcRenderer.invoke("app:getUpdateState"),
openReleasesPage: () => ipcRenderer.invoke("app:openReleasesPage"),
getUpdateDebugInfo: () => ipcRenderer.invoke("app:getUpdateDebugInfo"),
getDeviceId: () => ipcRenderer.invoke("app:getDeviceId"),
},
// Permissions (macOS)
permissions: {
check: () => ipcRenderer.invoke("permissions:check"),
openSettings: (permissionKey: string) =>
ipcRenderer.invoke("permissions:openSettings", permissionKey),
},
// Quick Init — 读取快捷初始化配置
quickInit: {
getConfig: () => ipcRenderer.invoke("quickInit:getConfig"),
},
// Event listeners
// 保存 callback → wrapper 映射,使 off() 能正确移除 on() 注册的 listener
on: (channel: string, callback: (...args: unknown[]) => void) => {
const validChannels = [
"menu:new-session",
"menu:settings",
"menu:mcp-settings",
"menu:dependencies",
"cowork:message",
"cowork:permission",
"agent:event",
"computer:progress",
"update:status",
"deps:syncCompleted",
"autolaunch:changed",
"memory:sync",
"memory:consolidation",
"memory:cleanup",
"admin:servicesRestarting",
"admin:servicesRestarted",
];
if (validChannels.includes(channel)) {
const wrapper = (_: unknown, ...args: unknown[]) => callback(...args);
(callback as any).__ipcWrapper = wrapper;
ipcRenderer.on(channel, wrapper as any);
}
},
off: (channel: string, callback: (...args: unknown[]) => void) => {
const wrapper = (callback as any).__ipcWrapper || callback;
ipcRenderer.removeListener(channel, wrapper);
},
// PERF 专用日志fire-and-forget写入主进程 perf.YYYY-MM-DD.log
perf: {
log: (msg: string) => ipcRenderer.send("perf:log", msg),
},
});

View File

@@ -0,0 +1,94 @@
import { contextBridge, ipcRenderer } from "electron";
type PerfPayload = Record<string, unknown>;
const CHAT_ROUTE_RE = /^\/home\/chat\/\d+\/\d+$/;
const CHAT_ROOT_SELECTOR = '[data-qimingclaw-perf-scope="chat-root"]';
function resolveRoutePath(): string {
const pathname = window.location.pathname || "";
if (CHAT_ROUTE_RE.test(pathname)) {
return pathname;
}
// 兼容 hash 路由写法:/#/home/chat/:id/:agentId
const hash = window.location.hash || "";
const hashPath = hash.startsWith("#") ? hash.slice(1) : hash;
return hashPath || pathname;
}
function isChatScopeEnabled(): boolean {
const routePath = resolveRoutePath();
if (!CHAT_ROUTE_RE.test(routePath)) {
return false;
}
return Boolean(document.querySelector(CHAT_ROOT_SELECTOR));
}
function safeStringify(data: unknown): string {
try {
return JSON.stringify(data ?? {});
} catch {
return "{}";
}
}
/**
* onceKeySet 用于 markOnce 去重,确保同一 key 只触发一次日志。
* 清理策略:
* - 当集合大小超过 ONCE_KEY_MAX_SIZE 时,直接清空(简单粗暴但有效)。
* - 对于单个消息的生命周期,在 stream_end 时调用 cleanupMessageOnceKeys
* 清理该消息相关的 key允许同一条消息在重新加载后再次触发 markOnce。
*
* 设计说明:
* markOnce 的 key 格式为 `${mid}:first_chunk` 和 `${mid}:stream_end`。
* 当消息流结束时,我们清理这些 key这样如果用户刷新页面或重新进入同一会话
* 该消息的性能日志可以重新记录一次(而不是被 markOnce 永久跳过)。
*/
const onceKeySet = new Set<string>();
const ONCE_KEY_MAX_SIZE = 5000;
function maybeCompactOnceKeys(): void {
if (onceKeySet.size <= ONCE_KEY_MAX_SIZE) return;
// 简单上限保护:超过阈值时清空,避免长期运行导致集合无限增长。
onceKeySet.clear();
}
/**
* 清理指定消息的 once keys允许该消息在下次加载时重新记录性能日志。
* 仅在 stream_end 阶段调用(见 markOnce 内部)。
*/
function cleanupMessageOnceKeys(payload: PerfPayload): void {
const mid = payload.mid;
if (typeof mid !== "string" || !mid) return;
onceKeySet.delete(`${mid}:first_chunk`);
onceKeySet.delete(`${mid}:stream_end`);
}
const perf = {
enabled(): boolean {
return isChatScopeEnabled();
},
mark(stage: string, payload: PerfPayload = {}): void {
if (!this.enabled()) return;
const nowTs = Date.now();
const routePath = resolveRoutePath();
const msg = `[PERF][FE] stage=${stage} route=${routePath} ts=${nowTs} extra=${safeStringify(payload)}`;
ipcRenderer.send("perf:log", msg);
},
markOnce(key: string, stage: string, payload: PerfPayload = {}): void {
if (onceKeySet.has(key)) return;
onceKeySet.add(key);
maybeCompactOnceKeys();
this.mark(stage, payload);
if (stage === "stream_end") {
cleanupMessageOnceKeys(payload);
}
},
};
contextBridge.exposeInMainWorld("QimingClawBridge", {
perf,
});