@ia-ccun/code-agent-claw 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,397 @@
1
+ import { spawn, ChildProcess } from 'child_process';
2
+ import * as readline from 'readline';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { AgentConfig, CommandResult, AgentEvent, EventCallback, AgentStatus } from '../types';
6
+ import { logger } from '../utils/logger';
7
+
8
+ /**
9
+ * aicode RPC 服务
10
+ * 负责管理 aicode 子进程,并通过 JSONL 协议进行通信
11
+ */
12
+ export class AgentRpcService {
13
+ private process: ChildProcess | null = null;
14
+ private rl: readline.Interface | null = null;
15
+ private pendingCommands: Map<string, {
16
+ resolve: (r: CommandResult) => void;
17
+ reject: (e: Error) => void;
18
+ }> = new Map();
19
+ private callbacks: Set<EventCallback> = new Set();
20
+ private config: AgentConfig | null = null;
21
+ private _status: AgentStatus = 'uninitialized';
22
+ private healthCheckInterval: NodeJS.Timeout | null = null;
23
+
24
+ /**
25
+ * 获取当前状态
26
+ */
27
+ get status(): AgentStatus {
28
+ return this._status;
29
+ }
30
+
31
+ /**
32
+ * 获取当前配置
33
+ */
34
+ get currentConfig(): AgentConfig | null {
35
+ return this.config;
36
+ }
37
+
38
+ /**
39
+ * 初始化 agent(保存配置并启动)
40
+ */
41
+ async initialize(config: AgentConfig): Promise<CommandResult> {
42
+ logger.info('[AgentRpc] Initializing with config:', config);
43
+
44
+ // 验证命令是否存在
45
+ const commandPath = config.command;
46
+ if (!fs.existsSync(commandPath)) {
47
+ const error = `aicode command not found: ${commandPath}`;
48
+ logger.error('[AgentRpc]', error);
49
+ return { success: false, message: error };
50
+ }
51
+
52
+ // 确保目录存在
53
+ this.ensureDir(config.sessionDir);
54
+ this.ensureDir(config.workingDir);
55
+
56
+ // 保存配置
57
+ this.config = config;
58
+
59
+ // 启动进程
60
+ return this.start();
61
+ }
62
+
63
+ /**
64
+ * 启动 aicode 进程
65
+ */
66
+ async start(): Promise<CommandResult> {
67
+ if (!this.config) {
68
+ return { success: false, message: 'Config not set' };
69
+ }
70
+
71
+ if (this._status === 'running') {
72
+ return { success: true, message: 'Already running' };
73
+ }
74
+
75
+ this._status = 'starting';
76
+ logger.info('[AgentRpc] Starting aicode process...');
77
+
78
+ try {
79
+ // 构建命令参数
80
+ const args: string[] = [
81
+ '--mode', 'rpc',
82
+ '--provider', this.config.provider,
83
+ '--model', this.config.model,
84
+ '--session-dir', this.config.sessionDir
85
+ ];
86
+
87
+ // noSession 模式下不传 session 参数或使用特殊处理
88
+ if (!this.config.noSession) {
89
+ // 如果需要会话模式,可以在这里添加
90
+ }
91
+
92
+ logger.info(`[AgentRpc] Command: ${this.config.command} ${args.join(' ')}`);
93
+ logger.info(`[AgentRpc] Working dir: ${this.config.workingDir}`);
94
+
95
+ // 启动子进程
96
+ this.process = spawn(this.config.command, args, {
97
+ cwd: this.config.workingDir,
98
+ env: { ...process.env },
99
+ stdio: ['pipe', 'pipe', 'pipe']
100
+ });
101
+
102
+ // 创建 readline 接口监听 stdout
103
+ this.rl = readline.createInterface({
104
+ input: this.process.stdout!,
105
+ crlfDelay: Infinity
106
+ });
107
+
108
+ // 监听 stdout 事件
109
+ this.rl.on('line', (line: string) => {
110
+ this.handleEvent(line);
111
+ });
112
+
113
+ // 监听 stderr
114
+ this.process.stderr?.on('data', (data: Buffer) => {
115
+ const line = data.toString().replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
116
+ if (line.trim()) {
117
+ logger.warn(`[aicode-stderr] ${line}`);
118
+ }
119
+ });
120
+
121
+ // 监听进程退出
122
+ this.process.on('exit', (code, signal) => {
123
+ logger.info(`[AgentRpc] Process exited with code ${code}, signal ${signal}`);
124
+ this._status = 'stopped';
125
+ this.stopHealthCheck();
126
+ });
127
+
128
+ this.process.on('error', (err) => {
129
+ logger.error('[AgentRpc] Process error:', err);
130
+ this._status = 'error';
131
+ });
132
+
133
+ // 启动健康检查
134
+ this.startHealthCheck();
135
+
136
+ this._status = 'running';
137
+ logger.info('[AgentRpc] aicode process started successfully');
138
+
139
+ return { success: true, message: 'Agent started' };
140
+ } catch (error: any) {
141
+ this._status = 'error';
142
+ logger.error('[AgentRpc] Failed to start:', error);
143
+ return { success: false, message: error.message };
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 处理事件
149
+ */
150
+ private handleEvent(jsonLine: string): void {
151
+ try {
152
+ const trimmed = jsonLine.trim();
153
+ if (!trimmed || !trimmed.startsWith('{')) {
154
+ return;
155
+ }
156
+
157
+ const event = JSON.parse(jsonLine);
158
+ const type = event.type;
159
+
160
+ logger.debug(`[AgentRpc] <<< type=${type}`);
161
+
162
+ // 处理命令响应 - RPC 模式特有
163
+ if (type === 'response') {
164
+ this.handleCommandResponse(event);
165
+ return;
166
+ }
167
+
168
+ // 构建 AgentEvent
169
+ const agentEvent: AgentEvent = {
170
+ type,
171
+ data: event
172
+ };
173
+
174
+ if (type === 'message_update') {
175
+ const msgEvent = event.assistantMessageEvent;
176
+ if (msgEvent) {
177
+ agentEvent.messageType = msgEvent.type;
178
+ agentEvent.delta = msgEvent.delta;
179
+ }
180
+ } else if (type === 'agent_end') {
181
+ agentEvent.success = true;
182
+ agentEvent.usage = event.usage;
183
+ }
184
+
185
+ // 通知回调
186
+ this.callbacks.forEach(cb => {
187
+ try {
188
+ cb(agentEvent);
189
+ } catch (e) {
190
+ logger.error('[AgentRpc] Event callback error:', e);
191
+ }
192
+ });
193
+ } catch (e) {
194
+ // 忽略解析错误
195
+ }
196
+ }
197
+
198
+ /**
199
+ * 处理命令响应
200
+ */
201
+ private handleCommandResponse(event: any): void {
202
+ const commandId = event.id;
203
+ const commandType = event.command;
204
+ const success = event.success;
205
+
206
+ logger.info(`[AgentRpc] Command response: id=${commandId}, command=${commandType}, success=${success}`);
207
+
208
+ const pending = this.pendingCommands.get(commandId);
209
+ if (pending) {
210
+ this.pendingCommands.delete(commandId);
211
+ if (success) {
212
+ pending.resolve({ success: true, message: commandType });
213
+ } else {
214
+ pending.resolve({ success: false, message: event.error || 'Command failed' });
215
+ }
216
+ } else {
217
+ logger.warn(`[AgentRpc] No pending command for id: ${commandId}`);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * 发送提示
223
+ */
224
+ async sendPrompt(message: string): Promise<CommandResult> {
225
+ return this.sendCommand('prompt', message);
226
+ }
227
+
228
+ /**
229
+ * 发送引导消息
230
+ */
231
+ async sendSteer(message: string): Promise<CommandResult> {
232
+ return this.sendCommand('steer', message);
233
+ }
234
+
235
+ /**
236
+ * 发送后续消息
237
+ */
238
+ async sendFollowUp(message: string): Promise<CommandResult> {
239
+ return this.sendCommand('follow_up', message);
240
+ }
241
+
242
+ /**
243
+ * 中止操作
244
+ */
245
+ async sendAbort(): Promise<CommandResult> {
246
+ return this.sendCommand('abort');
247
+ }
248
+
249
+ /**
250
+ * 创建新会话
251
+ */
252
+ async newSession(parentSession?: string): Promise<CommandResult> {
253
+ return this.sendCommand('new_session', undefined, parentSession);
254
+ }
255
+
256
+ /**
257
+ * 发送命令
258
+ */
259
+ private sendCommand(type: string, message?: string, extra?: string): Promise<CommandResult> {
260
+ return new Promise((resolve, reject) => {
261
+ if (!this.isRunning() || !this.process || !this.process.stdin) {
262
+ return resolve({ success: false, message: 'Agent is not running' });
263
+ }
264
+
265
+ const command: any = { type };
266
+ if (message) command.message = message;
267
+ if (extra) command.parentSession = extra;
268
+
269
+ // 生成命令ID
270
+ const commandId = Date.now() + '-' + Math.floor(Math.random() * 10000);
271
+ command.id = commandId;
272
+
273
+ this.pendingCommands.set(commandId, { resolve, reject });
274
+
275
+ try {
276
+ const jsonLine = JSON.stringify(command) + '\n';
277
+ logger.debug(`[AgentRpc] >>> ${jsonLine.trim()}`);
278
+ this.process.stdin.write(jsonLine);
279
+ } catch (e: any) {
280
+ this.pendingCommands.delete(commandId);
281
+ reject(e);
282
+ }
283
+ });
284
+ }
285
+
286
+ /**
287
+ * 检查是否运行中
288
+ */
289
+ isRunning(): boolean {
290
+ return this._status === 'running' && this.process?.pid !== undefined;
291
+ }
292
+
293
+ /**
294
+ * 停止 agent
295
+ */
296
+ stop(): void {
297
+ logger.info('[AgentRpc] Stopping agent...');
298
+ this._status = 'stopped';
299
+ this.stopHealthCheck();
300
+
301
+ try {
302
+ if (this.process?.stdin) {
303
+ this.process.stdin.end();
304
+ }
305
+ if (this.process) {
306
+ this.process.kill('SIGTERM');
307
+ setTimeout(() => {
308
+ if (this.process && !this.process.killed) {
309
+ this.process.kill('SIGKILL');
310
+ }
311
+ }, 3000);
312
+ }
313
+ } catch (e) {
314
+ logger.error('[AgentRpc] Error stopping:', e);
315
+ }
316
+
317
+ this.process = null;
318
+ this.rl = null;
319
+ logger.info('[AgentRpc] Agent stopped');
320
+ }
321
+
322
+ /**
323
+ * 注册事件回调
324
+ */
325
+ registerCallback(callback: EventCallback): void {
326
+ this.callbacks.add(callback);
327
+ }
328
+
329
+ /**
330
+ * 移除事件回调
331
+ */
332
+ removeCallback(callback: EventCallback): void {
333
+ this.callbacks.delete(callback);
334
+ }
335
+
336
+ /**
337
+ * 启动健康检查
338
+ */
339
+ private startHealthCheck(): void {
340
+ this.stopHealthCheck();
341
+
342
+ this.healthCheckInterval = setInterval(async () => {
343
+ if (!this.isRunning() && this._status === 'running') {
344
+ logger.warn('[AgentRpc] Process died, auto-restarting...');
345
+ this._status = 'restarting';
346
+
347
+ // 自动重启
348
+ if (this.config) {
349
+ try {
350
+ await this.start();
351
+ logger.info('[AgentRpc] Auto-restart successful');
352
+ } catch (e) {
353
+ logger.error('[AgentRpc] Auto-restart failed:', e);
354
+ this._status = 'error';
355
+ }
356
+ } else {
357
+ this._status = 'stopped';
358
+ }
359
+ }
360
+ }, 5000);
361
+ }
362
+
363
+ /**
364
+ * 停止健康检查
365
+ */
366
+ private stopHealthCheck(): void {
367
+ if (this.healthCheckInterval) {
368
+ clearInterval(this.healthCheckInterval);
369
+ this.healthCheckInterval = null;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * 确保目录存在
375
+ */
376
+ private ensureDir(dirPath: string): void {
377
+ if (!fs.existsSync(dirPath)) {
378
+ logger.info(`[AgentRpc] Creating directory: ${dirPath}`);
379
+ fs.mkdirSync(dirPath, { recursive: true });
380
+ }
381
+ }
382
+ }
383
+
384
+ /**
385
+ * 单例实例
386
+ */
387
+ let agentRpcService: AgentRpcService | null = null;
388
+
389
+ /**
390
+ * 获取 AgentRpcService 单例
391
+ */
392
+ export function getAgentRpcService(): AgentRpcService {
393
+ if (!agentRpcService) {
394
+ agentRpcService = new AgentRpcService();
395
+ }
396
+ return agentRpcService;
397
+ }
@@ -0,0 +1,60 @@
1
+ export interface ServerConfig {
2
+ port: number;
3
+ host: string;
4
+ }
5
+
6
+ export interface AgentConfig {
7
+ enabled: boolean;
8
+ command: string;
9
+ provider: string;
10
+ model: string;
11
+ noSession: boolean;
12
+ sessionDir: string;
13
+ workingDir: string;
14
+ }
15
+
16
+ export interface LoggingConfig {
17
+ level: string;
18
+ }
19
+
20
+ export interface Config {
21
+ server: ServerConfig;
22
+ agent: AgentConfig;
23
+ logging: LoggingConfig;
24
+ }
25
+
26
+ export interface CommandResult {
27
+ success: boolean;
28
+ message: string;
29
+ }
30
+
31
+ export interface AgentEvent {
32
+ type: string;
33
+ data?: any;
34
+ messageType?: string;
35
+ delta?: string;
36
+ success?: boolean;
37
+ usage?: {
38
+ completionTokens?: number;
39
+ promptTokens?: number;
40
+ };
41
+ }
42
+
43
+ export type EventCallback = (event: AgentEvent) => void;
44
+
45
+ export type AgentStatus = 'uninitialized' | 'starting' | 'running' | 'stopped' | 'error' | 'restarting';
46
+
47
+ export interface ConfigRequest {
48
+ command?: string;
49
+ provider?: string;
50
+ model?: string;
51
+ noSession?: boolean;
52
+ sessionDir?: string;
53
+ workingDir?: string;
54
+ }
55
+
56
+ export interface ApiResponse<T = any> {
57
+ success: boolean;
58
+ message?: string;
59
+ data?: T;
60
+ }
@@ -0,0 +1,13 @@
1
+ import pino from 'pino';
2
+
3
+ export const logger = pino({
4
+ level: process.env.LOG_LEVEL || 'info',
5
+ transport: process.env.NODE_ENV !== 'production' ? {
6
+ target: 'pino-pretty',
7
+ options: {
8
+ colorize: true,
9
+ translateTime: 'HH:MM:ss',
10
+ ignore: 'pid,hostname'
11
+ }
12
+ } : undefined
13
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ia-ccun/code-agent-claw",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Code Agent Claw WebUI - Node.js implementation with frontend configuration",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -3,12 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AICode Agent</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <link rel="stylesheet" href="juejin.css">
11
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
6
+ <title>AICode Claw</title>
12
7
  <style>
13
8
  /* ========================================
14
9
  ANTHROPIC STYLE - AI Agent Console
@@ -64,8 +59,8 @@
64
59
  --space-2xl: 32px;
65
60
 
66
61
  /* Typography - Inter for clean readability */
67
- --font-display: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
68
- --font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
62
+ --font-display: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
63
+ --font-mono: 'SF Mono', Consolas, 'PingFang SC', 'Microsoft YaHei Mono', monospace;
69
64
 
70
65
  /* Effects */
71
66
  --shadow-sm: 0 1px 3px rgba(61, 61, 58, 0.08);
@@ -262,7 +257,7 @@
262
257
  background: var(--bg-tertiary);
263
258
  padding: 2px 6px;
264
259
  border-radius: 4px;
265
- font-family: 'JetBrains Mono', monospace;
260
+ font-family: var(--font-mono);
266
261
  font-size: 0.75rem;
267
262
  color: var(--accent-green);
268
263
  }
@@ -1075,7 +1070,7 @@
1075
1070
  <div class="header-left">
1076
1071
  <div class="logo">
1077
1072
  <img class="logo-icon" src="aicode.svg" alt="AICode">
1078
- <span class="logo-text">AICode Agent</span>
1073
+ <span class="logo-text">AICode Claw</span>
1079
1074
  </div>
1080
1075
  <div class="status-badge">
1081
1076
  <div class="status-dot" id="statusDot"></div>
@@ -1163,7 +1158,7 @@
1163
1158
  <div id="responseTab" class="panel-content">
1164
1159
  <div class="empty-state" id="emptyState">
1165
1160
  <img class="empty-logo" src="aicode.svg" alt="AICode">
1166
- <div class="empty-title">你好,我是 AICode Agent</div>
1161
+ <div class="empty-title">你好,我是 AICode Claw</div>
1167
1162
  <div class="empty-desc">我可以帮助你完成以下任务:</div>
1168
1163
  <div class="empty-features">
1169
1164
  <div class="feature-item"><span class="feature-icon">📝</span> 代码编写与调试</div>
@@ -1236,12 +1231,6 @@
1236
1231
  <script>
1237
1232
  const API_BASE = '';
1238
1233
  let messages = [];
1239
-
1240
- // Configure marked
1241
- marked.setOptions({
1242
- breaks: true,
1243
- gfm: true
1244
- });
1245
1234
  let isStreaming = false;
1246
1235
  let lastRenderTime = 0;
1247
1236
  const RENDER_THROTTLE = 100;