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,846 @@
import {
LOCALHOST_HOSTNAME,
DEFAULT_FILE_SERVER_PORT,
} from "@shared/constants";
/** 发送 PERF 日志到主进程写入专属日志文件,不可用时降级到 console.log */
function perfLog(msg: string): void {
if (window.electronAPI?.perf) {
window.electronAPI.perf.log(msg);
} else {
console.log(msg);
}
}
export interface FileServerConfig {
baseUrl: string;
apiKey?: string;
}
export interface WorkspaceResult {
success: boolean;
message: string;
workspaceRoot?: string;
}
export interface FileInfo {
name: string;
path: string;
isDirectory: boolean;
size?: number;
modifiedTime?: number;
}
export interface FileListResult {
success: boolean;
files: FileInfo[];
}
// ==================== Chat SSE Types ====================
export interface ChatMessage {
sessionId: string;
messageType: string;
subType: string;
data: any;
timestamp: string;
}
export interface ChatRequest {
user_id: string;
project_id?: string;
prompt: string;
session_id?: string;
attachments?: any[];
data_source_attachments?: string[];
model_provider?: ModelProviderConfig;
request_id?: string;
system_prompt?: string;
user_prompt?: string;
agent_config?: any;
}
export interface ModelProviderConfig {
provider: "anthropic" | "openai" | "google" | "azure";
api_key?: string;
base_url?: string;
model?: string;
max_tokens?: number;
temperature?: number;
}
export interface ChatResponse {
success: boolean;
project_id: string;
session_id: string;
message?: string;
error?: string;
}
export interface AgentStatusRequest {
user_id: string;
project_id?: string;
session_id?: string;
}
export interface AgentStatusResponse {
success: boolean;
status: "Idle" | "Busy";
session_id?: string;
project_id?: string;
}
export interface AgentStopRequest {
user_id: string;
project_id?: string;
session_id?: string;
}
export interface AgentStopResponse {
success: boolean;
message: string;
}
// ==================== File Server Service ====================
class FileServerService {
private config: FileServerConfig = {
baseUrl: `http://${LOCALHOST_HOSTNAME}:${DEFAULT_FILE_SERVER_PORT}`,
};
setConfig(config: Partial<FileServerConfig>) {
this.config = { ...this.config, ...config };
}
getConfig(): FileServerConfig {
return { ...this.config };
}
private getHeaders(): HeadersInit {
return this.config.apiKey
? { Authorization: `Bearer ${this.config.apiKey}` }
: {};
}
async loadConfig(): Promise<void> {
try {
const saved =
await window.electronAPI?.settings.get("file_server_config");
if (saved) {
this.config = { ...this.config, ...(saved as FileServerConfig) };
}
} catch (error) {
console.error("Failed to load file server config:", error);
}
}
async saveConfig(): Promise<void> {
try {
await window.electronAPI?.settings.set("file_server_config", this.config);
} catch (error) {
console.error("Failed to save file server config:", error);
}
}
// ==================== Chat/SSE API (Agent Runner) ====================
// POST /computer/chat - 发送聊天消息
// 注:当前 UI 为 embedded webviewJava 前端 → Java 后端 → Electron
// chat() / streamChat() 不经过此路径,[PERF][Frontend] 日志在实际链路中不触发。
// 代码保留供 Electron renderer 直连场景使用(如未来去除 webview 的方案)。
async chat(request: ChatRequest): Promise<ChatResponse> {
const t0 = Date.now();
// 优先通过 IPCAcpEngine 直接处理,返回 HttpResult<ComputerChatResponse>
if (window.electronAPI?.computer) {
perfLog(
`[PERF][Frontend][chat] request sent: project_id=${request.project_id}, t=${t0}`,
);
const result = await window.electronAPI.computer.chat(request);
const t1 = Date.now();
perfLog(
`[PERF][Frontend][chat] IPC response: session_id=${result.data?.session_id}, duration=${t1 - t0}ms`,
);
// 从 HttpResult 中提取 data映射到 fileServer 本地 ChatResponse 格式
return {
success: result.success,
project_id: result.data?.project_id || "",
session_id: result.data?.session_id || "",
error:
result.data?.error || (result.success ? undefined : result.message),
};
}
// 回退到 HTTPrcoder 返回 HttpResult<ChatResponse> 格式)
perfLog(
`[PERF][Frontend][chat] HTTP request sent: project_id=${request.project_id}, t=${t0}`,
);
const response = await fetch(`${this.config.baseUrl}/computer/chat`, {
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `Chat failed: ${response.statusText}`);
}
// rcoder 返回 HttpResult 格式,提取 data
const httpResult = await response.json();
const t1 = Date.now();
perfLog(
`[PERF][Frontend][chat] HTTP response: session_id=${httpResult.data?.session_id}, duration=${t1 - t0}ms`,
);
return {
success: httpResult.success ?? false,
project_id: httpResult.data?.project_id || "",
session_id: httpResult.data?.session_id || "",
error:
httpResult.data?.error ||
(httpResult.success ? undefined : httpResult.message),
};
}
// GET /computer/progress/{session_id} - SSE 流式进度
// 注:同 chat(),当前链路不经过此路径,[PERF][Frontend][streamChat] 日志在实际链路中不触发。
async *streamChat(sessionId: string): AsyncGenerator<ChatMessage> {
const t0 = Date.now();
perfLog(
`[PERF][Frontend][streamChat] connection initiated: session_id=${sessionId}, t=${t0}`,
);
const response = await fetch(
`${this.config.baseUrl}/computer/progress/${sessionId}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok) {
throw new Error(
`Failed to connect to progress stream: ${response.statusText}`,
);
}
if (!response.body) {
throw new Error("No response body");
}
perfLog(
`[PERF][Frontend][streamChat] SSE connected: session_id=${sessionId}, duration=${Date.now() - t0}ms`,
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let firstChunk = true;
while (true) {
const { done, value } = await reader.read();
if (done) {
perfLog(
`[PERF][Frontend][streamChat] SSE ended: session_id=${sessionId}, total_duration=${Date.now() - t0}ms`,
);
break;
}
if (firstChunk) {
perfLog(
`[PERF][Frontend][streamChat] SSE first chunk: session_id=${sessionId}, duration=${Date.now() - t0}ms`,
);
firstChunk = false;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
// Skip heartbeat events
if (data.includes('"ping"') || data.includes("heartbeat")) {
continue;
}
try {
const message = JSON.parse(data);
yield message;
} catch {
// Not JSON, might be plain text
yield {
data,
messageType: "text",
subType: "message",
timestamp: new Date().toISOString(),
};
}
}
// Handle event types
if (line.startsWith("event: ")) {
const eventType = line.slice(7);
// Event type in 'event:' header
}
}
}
}
// POST /computer/agent/status - 获取 Agent 状态
async getAgentStatus(
request: AgentStatusRequest,
): Promise<AgentStatusResponse> {
// 优先通过 IPC返回 HttpResult<ComputerAgentStatusResponse>
if (window.electronAPI?.computer) {
const result = await window.electronAPI.computer.agentStatus(request);
return {
success: result.success,
status: (result.data?.status === "Busy" ? "Busy" : "Idle") as
| "Idle"
| "Busy",
session_id: result.data?.session_id ?? undefined,
project_id: result.data?.project_id ?? request.project_id,
};
}
// 回退到 HTTPrcoder 返回 HttpResult 格式)
const response = await fetch(
`${this.config.baseUrl}/computer/agent/status`,
{
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify(request),
},
);
if (!response.ok) {
throw new Error(`Failed to get status: ${response.statusText}`);
}
const httpResult = await response.json();
return {
success: httpResult.success ?? false,
status: (httpResult.data?.status === "Busy" ? "Busy" : "Idle") as
| "Idle"
| "Busy",
session_id: httpResult.data?.session_id,
project_id: httpResult.data?.project_id,
};
}
// POST /computer/agent/stop - 停止 Agent
async stopAgent(request: AgentStopRequest): Promise<AgentStopResponse> {
// 优先通过 IPC返回 HttpResult<ComputerAgentStopResponse>
if (window.electronAPI?.computer) {
const result = await window.electronAPI.computer.agentStop(request);
return {
success: result.data?.success ?? result.success,
message: result.data?.message ?? result.message,
};
}
// 回退到 HTTPrcoder 返回 HttpResult 格式)
const response = await fetch(`${this.config.baseUrl}/computer/agent/stop`, {
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`Failed to stop agent: ${response.statusText}`);
}
const httpResult = await response.json();
return {
success: httpResult.data?.success ?? httpResult.success,
message: httpResult.data?.message ?? httpResult.message ?? "Stopped",
};
}
// POST /computer/agent/session/cancel - 取消会话
async cancelSession(request: {
user_id: string;
session_id: string;
}): Promise<{ success: boolean; message: string }> {
// 优先通过 IPC返回 HttpResult<ComputerAgentCancelResponse>
if (window.electronAPI?.computer) {
const result = await window.electronAPI.computer.cancelSession(request);
return {
success: result.data?.success ?? result.success,
message: result.success ? "Cancelled" : result.message,
};
}
// 回退到 HTTPrcoder 返回 HttpResult 格式)
const response = await fetch(
`${this.config.baseUrl}/computer/agent/session/cancel`,
{
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify(request),
},
);
if (!response.ok) {
throw new Error(`Failed to cancel session: ${response.statusText}`);
}
const httpResult = await response.json();
return {
success: httpResult.data?.success ?? httpResult.success,
message: httpResult.success
? "Cancelled"
: (httpResult.message ?? "Failed"),
};
}
// ==================== Computer Routes ====================
// POST /computer/create-workspace - 创建工作空间支持skills同步
async createWorkspace(
userId: string,
cId: string,
zipFile?: File,
): Promise<WorkspaceResult> {
const formData = new FormData();
formData.append("userId", userId);
formData.append("cId", cId);
if (zipFile) {
formData.append("file", zipFile);
}
const response = await fetch(
`${this.config.baseUrl}/computer/create-workspace`,
{
method: "POST",
headers: this.getHeaders(),
body: formData,
},
);
if (!response.ok) {
throw new Error(`Failed to create workspace: ${response.statusText}`);
}
return response.json();
}
// GET /computer/get-file-list - 获取文件列表
async getFileList(
userId: string,
cId: string,
proxyPath?: string,
): Promise<FileListResult> {
const params = new URLSearchParams({ userId, cId });
if (proxyPath) {
params.append("proxyPath", proxyPath);
}
const response = await fetch(
`${this.config.baseUrl}/computer/get-file-list?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok) {
throw new Error(`Failed to get file list: ${response.statusText}`);
}
return response.json();
}
// POST /computer/files-update - 批量更新文件
async updateFiles(
userId: string,
cId: string,
files: { name: string; contents: string }[],
): Promise<{ success: boolean }> {
const response = await fetch(
`${this.config.baseUrl}/computer/files-update`,
{
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, cId, files }),
},
);
if (!response.ok) {
throw new Error(`Failed to update files: ${response.statusText}`);
}
return response.json();
}
// POST /computer/upload-file - 上传单个文件
async uploadFile(
userId: string,
cId: string,
file: File,
filePath: string,
): Promise<any> {
const formData = new FormData();
formData.append("userId", userId);
formData.append("cId", cId);
formData.append("filePath", filePath);
formData.append("file", file);
const response = await fetch(
`${this.config.baseUrl}/computer/upload-file`,
{
method: "POST",
headers: this.getHeaders(),
body: formData,
},
);
if (!response.ok) {
throw new Error(`Failed to upload file: ${response.statusText}`);
}
return response.json();
}
// POST /computer/upload-files - 批量上传文件
async uploadFiles(
userId: string,
cId: string,
files: File[],
filePaths: string[],
): Promise<any> {
const formData = new FormData();
formData.append("userId", userId);
formData.append("cId", cId);
formData.append("filePaths", JSON.stringify(filePaths));
files.forEach((file, i) => {
formData.append("files", file);
});
const response = await fetch(
`${this.config.baseUrl}/computer/upload-files`,
{
method: "POST",
headers: this.getHeaders(),
body: formData,
},
);
if (!response.ok) {
throw new Error(`Failed to upload files: ${response.statusText}`);
}
return response.json();
}
// GET /computer/download-all-files - 下载所有文件
async downloadAllFiles(userId: string, cId: string): Promise<Blob> {
const params = new URLSearchParams({ userId, cId });
const response = await fetch(
`${this.config.baseUrl}/computer/download-all-files?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok) {
throw new Error(`Failed to download files: ${response.statusText}`);
}
return response.blob();
}
// ==================== Build Routes ====================
async startDev(projectId: string): Promise<any> {
const params = new URLSearchParams({ projectId });
const response = await fetch(
`${this.config.baseUrl}/build/start-dev?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok)
throw new Error(`Failed to start dev: ${response.statusText}`);
return response.json();
}
async stopDev(projectId: string, pid: string): Promise<any> {
const params = new URLSearchParams({ projectId, pid });
const response = await fetch(
`${this.config.baseUrl}/build/stop-dev?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok)
throw new Error(`Failed to stop dev: ${response.statusText}`);
return response.json();
}
async build(projectId: string): Promise<any> {
const params = new URLSearchParams({ projectId });
const response = await fetch(
`${this.config.baseUrl}/build/build?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok)
throw new Error(`Failed to build: ${response.statusText}`);
return response.json();
}
async restartDev(projectId: string): Promise<any> {
const params = new URLSearchParams({ projectId });
const response = await fetch(
`${this.config.baseUrl}/build/restart-dev?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok)
throw new Error(`Failed to restart dev: ${response.statusText}`);
return response.json();
}
async listDev(): Promise<{ success: boolean; list: any[] }> {
const response = await fetch(`${this.config.baseUrl}/build/list-dev`, {
method: "GET",
headers: this.getHeaders(),
});
if (!response.ok)
throw new Error(`Failed to list dev: ${response.statusText}`);
return response.json();
}
async getDevLog(
projectId: string,
startIndex: number = 1,
logType: string = "temp",
): Promise<any> {
const params = new URLSearchParams({
projectId,
startIndex: String(startIndex),
logType,
});
const response = await fetch(
`${this.config.baseUrl}/build/get-dev-log?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok)
throw new Error(`Failed to get dev log: ${response.statusText}`);
return response.json();
}
// ==================== Code Routes ====================
async updateAllFiles(
projectId: string,
codeVersion: string,
files: any[],
basePath?: string,
pid?: string,
): Promise<any> {
const response = await fetch(
`${this.config.baseUrl}/code/all-files-update`,
{
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ projectId, codeVersion, files, basePath, pid }),
},
);
if (!response.ok)
throw new Error(`Failed to update files: ${response.statusText}`);
return response.json();
}
async uploadSingleCodeFile(
projectId: string,
codeVersion: string,
file: File,
filePath: string,
): Promise<any> {
const formData = new FormData();
formData.append("projectId", projectId);
formData.append("codeVersion", codeVersion);
formData.append("filePath", filePath);
formData.append("file", file);
const response = await fetch(
`${this.config.baseUrl}/code/upload-single-file`,
{
method: "POST",
headers: this.getHeaders(),
body: formData,
},
);
if (!response.ok)
throw new Error(`Failed to upload code file: ${response.statusText}`);
return response.json();
}
async rollbackVersion(
projectId: string,
codeVersion: string,
rollbackTo: string,
): Promise<any> {
const response = await fetch(
`${this.config.baseUrl}/code/rollback-version`,
{
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ projectId, codeVersion, rollbackTo }),
},
);
if (!response.ok)
throw new Error(`Failed to rollback: ${response.statusText}`);
return response.json();
}
// ==================== Project Routes ====================
async createProject(projectId: string): Promise<any> {
const response = await fetch(
`${this.config.baseUrl}/project/create-project`,
{
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ projectId }),
},
);
if (!response.ok)
throw new Error(`Failed to create project: ${response.statusText}`);
return response.json();
}
async getProjectContent(
projectId: string,
command?: string,
proxyPath?: string,
): Promise<any> {
const params = new URLSearchParams({ projectId });
if (command) params.append("command", command);
if (proxyPath) params.append("proxyPath", proxyPath);
const response = await fetch(
`${this.config.baseUrl}/project/get-project-content?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok)
throw new Error(`Failed to get project content: ${response.statusText}`);
return response.json();
}
async backupVersion(projectId: string, codeVersion: string): Promise<any> {
const response = await fetch(
`${this.config.baseUrl}/project/backup-current-version`,
{
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ projectId, codeVersion }),
},
);
if (!response.ok)
throw new Error(`Failed to backup version: ${response.statusText}`);
return response.json();
}
async exportProject(
projectId: string,
codeVersion: string,
exportType: string = "zip",
config?: any,
): Promise<Blob> {
const response = await fetch(
`${this.config.baseUrl}/project/export-project`,
{
method: "POST",
headers: {
...this.getHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify({ projectId, codeVersion, exportType, config }),
},
);
if (!response.ok)
throw new Error(`Failed to export project: ${response.statusText}`);
return response.blob();
}
async deleteProject(projectId: string, pid?: string): Promise<any> {
const params = new URLSearchParams({ projectId });
if (pid) params.append("pid", pid);
const response = await fetch(
`${this.config.baseUrl}/project/delete-project?${params}`,
{
method: "GET",
headers: this.getHeaders(),
},
);
if (!response.ok)
throw new Error(`Failed to delete project: ${response.statusText}`);
return response.json();
}
// Check connection
async checkConnection(): Promise<boolean> {
try {
// 优先通过 IPC
if (window.electronAPI?.computer) {
const result = await window.electronAPI.computer.health();
return result.status === "healthy";
}
// 回退到 HTTP
const response = await fetch(`${this.config.baseUrl}/health`, {
method: "GET",
});
return response.ok;
} catch {
return false;
}
}
}
export const fileServerService = new FileServerService();

View File

@@ -0,0 +1,316 @@
export type IMPlatform = 'discord' | 'telegram' | 'dingtalk' | 'feishu';
export interface IMConfig {
platform: IMPlatform;
enabled: boolean;
token?: string;
botToken?: string;
apiKey?: string;
apiSecret?: string;
webhookUrl?: string;
allowedUsers?: string[];
autoReply?: boolean;
}
export interface IMLogin {
platform: IMPlatform;
userId: string;
username: string;
displayName: string;
avatar?: string;
}
export interface IMMessage {
id: string;
platform: IMPlatform;
sender: IMLogin;
content: string;
timestamp: number;
channelId?: string;
groupId?: string;
isGroup: boolean;
replyTo?: string;
}
export interface IMSendOptions {
content: string;
channelId?: string;
userId?: string;
replyTo?: string;
markdown?: boolean;
}
class IMService {
private configs: Map<IMPlatform, IMConfig> = new Map();
private handlers: Map<IMPlatform, (message: IMMessage) => void> = new Map();
private connections: Map<IMPlatform, boolean> = new Map();
constructor() {
// Initialize with empty configs
this.configs.set('discord', { platform: 'discord', enabled: false });
this.configs.set('telegram', { platform: 'telegram', enabled: false });
this.configs.set('dingtalk', { platform: 'dingtalk', enabled: false });
this.configs.set('feishu', { platform: 'feishu', enabled: false });
}
getConfig(platform: IMPlatform): IMConfig | undefined {
return this.configs.get(platform);
}
setConfig(platform: IMPlatform, config: Partial<IMConfig>): void {
const current = this.configs.get(platform) || { platform, enabled: false };
this.configs.set(platform, { ...current, ...config });
}
getEnabledPlatforms(): IMPlatform[] {
return Array.from(this.configs.entries())
.filter(([_, config]) => config.enabled)
.map(([platform]) => platform);
}
isConnected(platform: IMPlatform): boolean {
return this.connections.get(platform) || false;
}
onMessage(platform: IMPlatform, handler: (message: IMMessage) => void): void {
this.handlers.set(platform, handler);
}
async connect(platform: IMPlatform): Promise<{ success: boolean; error?: string }> {
const config = this.configs.get(platform);
if (!config || !config.enabled) {
return { success: false, error: 'Platform not enabled' };
}
try {
// Platform-specific connection logic
switch (platform) {
case 'discord':
return await this.connectDiscord(config);
case 'telegram':
return await this.connectTelegram(config);
case 'dingtalk':
return await this.connectDingtalk(config);
case 'feishu':
return await this.connectFeishu(config);
default:
return { success: false, error: 'Unknown platform' };
}
} catch (error) {
return { success: false, error: String(error) };
}
}
async disconnect(platform: IMPlatform): Promise<void> {
this.connections.set(platform, false);
}
async sendMessage(platform: IMPlatform, options: IMSendOptions): Promise<{ success: boolean; error?: string }> {
if (!this.connections.get(platform)) {
return { success: false, error: 'Not connected' };
}
try {
switch (platform) {
case 'discord':
return await this.sendDiscordMessage(options);
case 'telegram':
return await this.sendTelegramMessage(options);
case 'dingtalk':
return await this.sendDingtalkMessage(options);
case 'feishu':
return await this.sendFeishuMessage(options);
default:
return { success: false, error: 'Unknown platform' };
}
} catch (error) {
return { success: false, error: String(error) };
}
}
// Discord implementation
private async connectDiscord(config: IMConfig): Promise<{ success: boolean; error?: string }> {
if (!config.botToken) {
return { success: false, error: 'Bot token required' };
}
// Test API connection
try {
const response = await fetch('https://discord.com/api/v10/users/@me', {
headers: { 'Authorization': `Bot ${config.botToken}` }
});
if (response.ok) {
this.connections.set('discord', true);
return { success: true };
}
return { success: false, error: 'Invalid bot token' };
} catch (error) {
return { success: false, error: String(error) };
}
}
private async sendDiscordMessage(options: IMSendOptions): Promise<{ success: boolean; error?: string }> {
const config = this.configs.get('discord');
if (!config?.botToken || !options.channelId) {
return { success: false, error: 'Missing configuration' };
}
try {
const response = await fetch(`https://discord.com/api/v10/channels/${options.channelId}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bot ${config.botToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ content: options.content })
});
return { success: response.ok };
} catch (error) {
return { success: false, error: String(error) };
}
}
// Telegram implementation
private async connectTelegram(config: IMConfig): Promise<{ success: boolean; error?: string }> {
if (!config.botToken) {
return { success: false, error: 'Bot token required' };
}
try {
const response = await fetch(`https://api.telegram.org/bot${config.botToken}/getMe`);
const data = await response.json();
if (data.ok) {
this.connections.set('telegram', true);
return { success: true };
}
return { success: false, error: 'Invalid bot token' };
} catch (error) {
return { success: false, error: String(error) };
}
}
private async sendTelegramMessage(options: IMSendOptions): Promise<{ success: boolean; error?: string }> {
const config = this.configs.get('telegram');
if (!config?.botToken || !options.userId) {
return { success: false, error: 'Missing configuration' };
}
try {
const response = await fetch(`https://api.telegram.org/bot${config.botToken}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: options.userId,
text: options.content,
})
});
return { success: response.ok };
} catch (error) {
return { success: false, error: String(error) };
}
}
// DingTalk implementation
private async connectDingtalk(config: IMConfig): Promise<{ success: boolean; error?: string }> {
if (!config.appKey || !config.appSecret) {
return { success: false, error: 'AppKey and AppSecret required' };
}
try {
// Get access token
const tokenResponse = await fetch('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appKey: config.appKey,
appSecret: config.appSecret,
})
});
const data = await tokenResponse.json();
if (data.accessToken) {
this.connections.set('dingtalk', true);
return { success: true };
}
return { success: false, error: 'Failed to get access token' };
} catch (error) {
return { success: false, error: String(error) };
}
}
private async sendDingtalkMessage(options: IMSendOptions): Promise<{ success: boolean; error?: string }> {
// DingTalk webhook implementation
if (!options.userId) {
return { success: false, error: 'User ID required' };
}
return { success: true }; // Placeholder
}
// Feishu ( Lark ) implementation
private async connectFeishu(config: IMConfig): Promise<{ success: boolean; error?: string }> {
if (!config.appId || !config.appSecret) {
return { success: false, error: 'AppId and AppSecret required' };
}
try {
const response = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: config.appId,
app_secret: config.appSecret,
})
});
const data = await response.json();
if (data.tenant_access_token) {
this.connections.set('feishu', true);
return { success: true };
}
return { success: false, error: 'Failed to get tenant token' };
} catch (error) {
return { success: false, error: String(error) };
}
}
private async sendFeishuMessage(options: IMSendOptions): Promise<{ success: boolean; error?: string }> {
if (!options.userId) {
return { success: false, error: 'User ID required' };
}
return { success: true }; // Placeholder
}
// Load/save config
async loadConfigs(): Promise<void> {
try {
const saved = await window.electronAPI?.settings.get('im_configs');
if (saved) {
const configs = saved as IMConfig[];
for (const config of configs) {
this.configs.set(config.platform, config);
}
}
} catch (error) {
console.error('Failed to load IM configs:', error);
}
}
async saveConfigs(): Promise<void> {
try {
const configs = Array.from(this.configs.values());
await window.electronAPI?.settings.set('im_configs', configs);
} catch (error) {
console.error('Failed to save IM configs:', error);
}
}
}
export const imService = new IMService();

View File

@@ -0,0 +1,141 @@
import { LOCALHOST_IP, DEFAULT_LANPROXY_PORT } from "@shared/constants";
import { t } from "../core/i18n";
export interface LanproxyConfig {
enabled: boolean;
serverIp: string;
serverPort: number;
ssl: boolean;
}
export const defaultLanproxyConfig: LanproxyConfig = {
enabled: false,
serverIp: LOCALHOST_IP,
serverPort: DEFAULT_LANPROXY_PORT,
ssl: true,
};
export interface LanproxyStatus {
running: boolean;
pid?: number;
error?: string;
}
class LanproxyManager {
private config: LanproxyConfig = { ...defaultLanproxyConfig };
private status: LanproxyStatus = { running: false };
private process: ReturnType<typeof setInterval> | null = null;
getConfig(): LanproxyConfig {
return { ...this.config };
}
setConfig(config: Partial<LanproxyConfig>) {
this.config = { ...this.config, ...config };
}
getStatus(): LanproxyStatus {
return { ...this.status };
}
async loadConfig(): Promise<void> {
try {
const saved = await window.electronAPI?.settings.get("lanproxy_config");
if (saved) {
this.config = {
...defaultLanproxyConfig,
...(saved as LanproxyConfig),
};
}
} catch (error) {
console.error("Failed to load lanproxy config:", error);
}
}
async saveConfig(): Promise<void> {
try {
await window.electronAPI?.settings.set("lanproxy_config", this.config);
} catch (error) {
console.error("Failed to save lanproxy config:", error);
}
}
// Start lanproxy via IPC — clientKey 从 auth.saved_key 读取,不由 LanproxyManager 管理
async start(): Promise<{ success: boolean; error?: string }> {
if (this.status.running) {
return { success: true };
}
try {
// clientKey 始终从 auth.saved_key 读取(参考 Tauri 客户端)
const clientKey = (await window.electronAPI?.settings.get(
"auth.saved_key",
)) as string | null;
if (!clientKey) {
return {
success: false,
error: t("Claw.Errors.loginFirstForClientKey"),
};
}
const result = await window.electronAPI?.lanproxy.start({
serverIp: this.config.serverIp,
serverPort: this.config.serverPort,
clientKey,
ssl: this.config.ssl,
});
if (result?.success) {
this.status.running = true;
}
return result || { success: false, error: "IPC failed" };
} catch (error) {
return { success: false, error: String(error) };
}
}
// Stop lanproxy via IPC
async stop(): Promise<{ success: boolean; error?: string }> {
if (!this.status.running) {
return { success: true };
}
try {
const result = await window.electronAPI?.lanproxy.stop();
if (result?.success) {
this.status.running = false;
}
return result || { success: false, error: "IPC failed" };
} catch (error) {
return { success: false, error: String(error) };
}
}
// Get status via IPC
async checkStatus(): Promise<LanproxyStatus> {
try {
const status = await window.electronAPI?.lanproxy.status();
this.status = status || { running: false };
return this.status;
} catch (error) {
return { running: false, error: String(error) };
}
}
// Start periodic status check
startStatusCheck(intervalMs: number = 5000) {
this.stopStatusCheck();
this.process = setInterval(() => {
this.checkStatus();
}, intervalMs);
}
// Stop periodic status check
stopStatusCheck() {
if (this.process) {
clearInterval(this.process);
this.process = null;
}
}
}
export const lanproxyManager = new LanproxyManager();

View File

@@ -0,0 +1,314 @@
import { DEFAULT_SCHEDULER_MINUTE_INTERVAL } from '@shared/constants';
export interface ScheduledTask {
id: string;
name: string;
description: string;
enabled: boolean;
schedule: TaskSchedule;
action: TaskAction;
lastRun?: number;
nextRun?: number;
status: 'idle' | 'running' | 'success' | 'error';
lastError?: string;
}
export interface TaskSchedule {
type: 'once' | 'interval' | 'cron';
// For once
timestamp?: number;
// For interval
intervalMs?: number;
// For cron
cron?: string;
}
export interface TaskAction {
type: 'message' | 'command' | 'webhook';
content: string;
// For webhook
url?: string;
method?: 'GET' | 'POST';
headers?: Record<string, string>;
}
export interface ScheduledTaskLog {
id: string;
taskId: string;
timestamp: number;
status: 'success' | 'error';
output?: string;
error?: string;
}
class TaskScheduler {
private tasks: Map<string, ScheduledTask> = new Map();
private timers: Map<string, NodeJS.Timeout> = new Map();
private logs: Map<string, ScheduledTaskLog[]> = new Map();
private maxLogsPerTask = 50;
constructor() {
// Load tasks from storage on init
}
// Create a new task
createTask(task: Omit<ScheduledTask, 'id' | 'status' | 'lastRun' | 'nextRun'>): ScheduledTask {
const id = crypto.randomUUID();
const newTask: ScheduledTask = {
...task,
id,
status: 'idle',
};
this.tasks.set(id, newTask);
this.scheduleTask(newTask);
return newTask;
}
// Update a task
updateTask(id: string, updates: Partial<ScheduledTask>): ScheduledTask | null {
const task = this.tasks.get(id);
if (!task) return null;
const updatedTask = { ...task, ...updates };
this.tasks.set(id, updatedTask);
// Reschedule if schedule changed
if (updates.schedule) {
this.unscheduleTask(id);
this.scheduleTask(updatedTask);
}
return updatedTask;
}
// Delete a task
deleteTask(id: string): boolean {
this.unscheduleTask(id);
return this.tasks.delete(id);
}
// Get all tasks
getTasks(): ScheduledTask[] {
return Array.from(this.tasks.values());
}
// Get task by ID
getTask(id: string): ScheduledTask | undefined {
return this.tasks.get(id);
}
// Enable/disable task
toggleTask(id: string, enabled: boolean): ScheduledTask | null {
const task = this.tasks.get(id);
if (!task) return null;
task.enabled = enabled;
if (enabled) {
this.scheduleTask(task);
} else {
this.unscheduleTask(id);
}
return task;
}
// Run task immediately
async runTask(id: string): Promise<{ success: boolean; error?: string }> {
const task = this.tasks.get(id);
if (!task) {
return { success: false, error: 'Task not found' };
}
task.status = 'running';
this.tasks.set(id, task);
try {
const result = await this.executeAction(task.action);
task.status = 'success';
task.lastRun = Date.now();
task.lastError = undefined;
this.tasks.set(id, task);
this.addLog(task.id, { success: true, output: result });
return { success: true };
} catch (error) {
task.status = 'error';
task.lastError = String(error);
this.tasks.set(id, task);
this.addLog(task.id, { success: false, error: String(error) });
return { success: false, error: String(error) };
}
}
// Schedule a task
private scheduleTask(task: ScheduledTask): void {
if (!task.enabled) return;
const now = Date.now();
let nextRun: number;
switch (task.schedule.type) {
case 'once':
if (task.schedule.timestamp && task.schedule.timestamp > now) {
nextRun = task.schedule.timestamp;
} else {
return; // Past timestamp, don't schedule
}
break;
case 'interval':
if (task.schedule.intervalMs) {
nextRun = now + task.schedule.intervalMs;
} else {
return;
}
break;
case 'cron':
// Simple cron implementation - just use interval for now
nextRun = now + 60000; // Default 1 minute
break;
default:
return;
}
task.nextRun = nextRun;
this.tasks.set(task.id, task);
const delay = Math.max(0, nextRun - now);
const timer = setTimeout(async () => {
await this.runTask(task.id);
// Reschedule for next run
if (task.enabled && task.schedule.type !== 'once') {
this.scheduleTask(task);
}
}, delay);
this.timers.set(task.id, timer);
}
// Unschedule a task
private unscheduleTask(id: string): void {
const timer = this.timers.get(id);
if (timer) {
clearTimeout(timer);
this.timers.delete(id);
}
const task = this.tasks.get(id);
if (task) {
task.nextRun = undefined;
this.tasks.set(id, task);
}
}
// Execute task action
private async executeAction(action: TaskAction): Promise<string> {
switch (action.type) {
case 'message':
// This would trigger the AI to send a message
return `Message: ${action.content}`;
case 'command':
// Execute shell command
return `Command: ${action.content}`;
case 'webhook':
// Make HTTP request
try {
const response = await fetch(action.url!, {
method: action.method || 'GET',
headers: action.headers,
body: action.method === 'POST' ? action.content : undefined,
});
return `Webhook response: ${response.status}`;
} catch (error) {
throw new Error(`Webhook failed: ${error}`);
}
default:
throw new Error('Unknown action type');
}
}
// Add log entry
private addLog(taskId: string, result: { success: boolean; output?: string; error?: string }): void {
const log: ScheduledTaskLog = {
id: crypto.randomUUID(),
taskId,
timestamp: Date.now(),
status: result.success ? 'success' : 'error',
output: result.output,
error: result.error,
};
const taskLogs = this.logs.get(taskId) || [];
taskLogs.unshift(log);
// Keep only last N logs
if (taskLogs.length > this.maxLogsPerTask) {
taskLogs.pop();
}
this.logs.set(taskId, taskLogs);
}
// Get logs for a task
getLogs(taskId: string): ScheduledTaskLog[] {
return this.logs.get(taskId) || [];
}
// Load tasks from storage
async loadTasks(): Promise<void> {
try {
const saved = await window.electronAPI?.settings.get('scheduled_tasks');
if (saved) {
const tasks = saved as ScheduledTask[];
for (const task of tasks) {
this.tasks.set(task.id, task);
if (task.enabled) {
this.scheduleTask(task);
}
}
}
} catch (error) {
console.error('Failed to load scheduled tasks:', error);
}
}
// Save tasks to storage
async saveTasks(): Promise<void> {
try {
const tasks = Array.from(this.tasks.values());
await window.electronAPI?.settings.set('scheduled_tasks', tasks);
} catch (error) {
console.error('Failed to save scheduled tasks:', error);
}
}
// Stop all tasks
stopAll(): void {
for (const id of this.timers.keys()) {
this.unscheduleTask(id);
}
}
}
export const taskScheduler = new TaskScheduler();
// Common preset schedules
export const presetSchedules = {
everyMinute: { type: 'interval' as const, intervalMs: DEFAULT_SCHEDULER_MINUTE_INTERVAL },
everyHour: { type: 'interval' as const, intervalMs: 3600000 },
everyDay: { type: 'interval' as const, intervalMs: 86400000 },
everyWeek: { type: 'interval' as const, intervalMs: 604800000 },
};

View File

@@ -0,0 +1,277 @@
export interface SkillDefinition {
id: string;
name: string;
description: string;
enabled: boolean;
requiresPermission?: boolean;
icon?: string;
}
export interface SkillExecutionResult {
success: boolean;
output?: string;
error?: string;
requiresPermission?: boolean;
permissionDetails?: {
type: 'command' | 'file' | 'network';
title: string;
description: string;
details: Record<string, unknown>;
};
}
export interface SkillContext {
workingDir?: string;
userId: string;
sessionId: string;
requirePermission?: (details: SkillExecutionResult['permissionDetails']) => Promise<boolean>;
}
export interface BaseSkill {
id: string;
name: string;
description: string;
enabled: boolean;
requiresPermission: boolean;
execute(input: string, context: SkillContext): Promise<SkillExecutionResult>;
}
// Built-in skills
export const builtInSkills: BaseSkill[] = [];
class WebSearchSkill implements BaseSkill {
id = 'web-search';
name = 'Web Search';
description = 'Search the web for information';
enabled = true;
requiresPermission = false;
async execute(input: string, _context: SkillContext): Promise<SkillExecutionResult> {
// TODO: Implement web search
return {
success: true,
output: `Search results for: ${input}`,
};
}
}
class CalculatorSkill implements BaseSkill {
id = 'calculator';
name = 'Calculator';
description = 'Perform mathematical calculations';
enabled = true;
requiresPermission = false;
async execute(input: string, _context: SkillContext): Promise<SkillExecutionResult> {
try {
const sanitized = input.replace(/[^0-9+\-*/.()%]/g, '');
// eslint-disable-next-line no-new-func
const result = new Function(`"use strict"; return (${sanitized})`)();
return {
success: true,
output: String(result),
};
} catch {
return {
success: false,
error: 'Invalid expression',
};
}
}
}
class FileReadSkill implements BaseSkill {
id = 'file-read';
name = 'File Read';
description = 'Read content from a file';
enabled = true;
requiresPermission = true;
async execute(input: string, context: SkillContext): Promise<SkillExecutionResult> {
const filePath = input.trim();
if (context.requirePermission) {
const approved = await context.requirePermission({
type: 'file',
title: 'Read File',
description: `Read content from file: ${filePath}`,
details: { file: filePath },
});
if (!approved) {
return {
success: false,
error: 'Permission denied',
};
}
}
return {
success: true,
output: `File read: ${filePath}`,
};
}
}
class CommandRunSkill implements BaseSkill {
id = 'command-run';
name = 'Command Run';
description = 'Run shell commands';
enabled = true;
requiresPermission = true;
async execute(input: string, context: SkillContext): Promise<SkillExecutionResult> {
if (context.requirePermission) {
const approved = await context.requirePermission({
type: 'command',
title: 'Run Command',
description: `Execute shell command: ${input}`,
details: { command: 'sh', args: ['-c', input], env: {} },
});
if (!approved) {
return {
success: false,
error: 'Permission denied',
};
}
}
return {
success: true,
output: `Command would execute: ${input}`,
};
}
}
class NetworkFetchSkill implements BaseSkill {
id = 'network-fetch';
name = 'Network Fetch';
description = 'Fetch content from a URL';
enabled = true;
requiresPermission = true;
async execute(input: string, context: SkillContext): Promise<SkillExecutionResult> {
const url = input.trim();
if (context.requirePermission) {
const approved = await context.requirePermission({
type: 'network',
title: 'Network Request',
description: `Fetch content from: ${url}`,
details: { url },
});
if (!approved) {
return {
success: false,
error: 'Permission denied',
};
}
}
return {
success: true,
output: `Would fetch: ${url}`,
};
}
}
// Skill manager
class SkillManager {
private skills: Map<string, BaseSkill> = new Map();
register(skill: BaseSkill) {
this.skills.set(skill.id, skill);
}
getSkill(id: string): BaseSkill | undefined {
return this.skills.get(id);
}
listSkills(): SkillDefinition[] {
return Array.from(this.skills.values()).map((s) => ({
id: s.id,
name: s.name,
description: s.description,
enabled: s.enabled,
requiresPermission: s.requiresPermission,
}));
}
async executeSkill(
skillId: string,
input: string,
context: SkillContext,
requirePermission?: SkillContext['requirePermission']
): Promise<SkillExecutionResult> {
const skill = this.skills.get(skillId);
if (!skill) {
return { success: false, error: `Skill not found: ${skillId}` };
}
if (!skill.enabled) {
return { success: false, error: `Skill disabled: ${skillId}` };
}
const ctx = { ...context, requirePermission };
// Check if permission is required but not provided
if (skill.requiresPermission && !requirePermission) {
const result = await skill.execute(input, ctx);
return {
...result,
requiresPermission: true,
};
}
return skill.execute(input, ctx);
}
async executeAuto(
input: string,
context: SkillContext,
requirePermission?: SkillContext['requirePermission']
): Promise<SkillExecutionResult | null> {
const lowerInput = input.toLowerCase();
// Calculator
if (/^\d+[\s+\-*/%()\d]+$/.test(input.replace(/\s/g, ''))) {
return this.executeSkill('calculator', input, context, requirePermission);
}
// Web search
if (lowerInput.startsWith('search:') || lowerInput.startsWith('查找:') || lowerInput.startsWith('搜索:')) {
const query = input.replace(/^(search|查找|搜索):\s*/i, '');
return this.executeSkill('web-search', query, context, requirePermission);
}
// Command (starts with !)
if (lowerInput.startsWith('!') || lowerInput.startsWith('run:')) {
const cmd = input.replace(/^(!|run:)\s*/i, '');
return this.executeSkill('command-run', cmd, context, requirePermission);
}
// File read (starts with cat:)
if (lowerInput.startsWith('cat:')) {
const file = input.replace(/^cat:\s*/i, '');
return this.executeSkill('file-read', file, context, requirePermission);
}
// Network fetch (starts with fetch:)
if (lowerInput.startsWith('fetch:') || lowerInput.startsWith('curl:')) {
const url = input.replace(/^(fetch|curl):\s*/i, '');
return this.executeSkill('network-fetch', url, context, requirePermission);
}
return null;
}
}
export const skillManager = new SkillManager();
// Register built-in skills
skillManager.register(new WebSearchSkill());
skillManager.register(new CalculatorSkill());
skillManager.register(new FileReadSkill());
skillManager.register(new CommandRunSkill());
skillManager.register(new NetworkFetchSkill());