@userdaoo/iflow-api-bridge 0.1.1 → 0.1.2

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.
package/src/adapter.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * iFlow SDK 适配器(使用子进程模式)
3
- * 封装与 iFlow CLI 的通信
3
+ * 封装与 iFlow CLI 的通信,支持对话上下文管理
4
4
  */
5
5
 
6
6
  import { spawn, type ChildProcess } from 'child_process';
@@ -27,24 +27,115 @@ export interface IFlowAdapterOptions {
27
27
  apiKey?: string;
28
28
  baseUrl?: string;
29
29
  timeout?: number;
30
+ maxHistoryLength?: number;
31
+ }
32
+
33
+ interface Message {
34
+ role: 'user' | 'assistant' | 'system';
35
+ content: string;
36
+ }
37
+
38
+ interface Conversation {
39
+ messages: Message[];
40
+ lastAccessTime: number;
30
41
  }
31
42
 
32
- /**
33
- * iFlow 适配器 - 使用子进程模式
34
- */
35
43
  export class IFlowAdapter {
36
44
  private options: IFlowAdapterOptions;
37
- private defaultTimeout = 120000; // 2分钟默认超时
45
+ private defaultTimeout = 300000;
46
+ private conversations: Map<string, Conversation> = new Map();
47
+ private readonly maxHistoryLength: number;
48
+ private cleanupInterval: NodeJS.Timeout | null = null;
38
49
 
39
50
  constructor(options: IFlowAdapterOptions = {}) {
40
51
  this.options = options;
52
+ this.maxHistoryLength = options.maxHistoryLength || 20;
53
+ this.cleanupInterval = setInterval(() => {
54
+ this.cleanupExpiredConversations();
55
+ }, 30 * 60 * 1000);
56
+ }
57
+
58
+ private getConversation(conversationId: string): Conversation {
59
+ let conversation = this.conversations.get(conversationId);
60
+ if (!conversation) {
61
+ conversation = {
62
+ messages: [],
63
+ lastAccessTime: Date.now(),
64
+ };
65
+ this.conversations.set(conversationId, conversation);
66
+ } else {
67
+ conversation.lastAccessTime = Date.now();
68
+ }
69
+ return conversation;
41
70
  }
42
71
 
43
- /**
44
- * 发送消息并获取完整响应(非流式)
45
- */
46
- async sendMessage(prompt: string): Promise<IFlowResponse> {
47
- console.log('[Adapter] 发送消息(非流式):', prompt.substring(0, 100));
72
+ private buildPromptWithHistory(
73
+ conversationId: string,
74
+ systemMessage: string | undefined,
75
+ userMessage: string
76
+ ): string {
77
+ const conversation = this.getConversation(conversationId);
78
+ const parts: string[] = [];
79
+
80
+ if (systemMessage) {
81
+ parts.push(`System: ${systemMessage}`);
82
+ }
83
+
84
+ for (const msg of conversation.messages) {
85
+ if (msg.role === 'user') {
86
+ parts.push(`User: ${msg.content}`);
87
+ } else if (msg.role === 'assistant') {
88
+ parts.push(`Assistant: ${msg.content}`);
89
+ }
90
+ }
91
+
92
+ parts.push(`User: ${userMessage}`);
93
+ return parts.join('\n\n');
94
+ }
95
+
96
+ private saveMessage(conversationId: string, role: 'user' | 'assistant', content: string): void {
97
+ const conversation = this.getConversation(conversationId);
98
+ conversation.messages.push({ role, content });
99
+
100
+ if (conversation.messages.length > this.maxHistoryLength) {
101
+ const systemMessages = conversation.messages.filter(m => m.role === 'system');
102
+ const otherMessages = conversation.messages.filter(m => m.role !== 'system');
103
+ const keepCount = this.maxHistoryLength - systemMessages.length;
104
+ conversation.messages = [
105
+ ...systemMessages,
106
+ ...otherMessages.slice(-keepCount),
107
+ ];
108
+ }
109
+ conversation.lastAccessTime = Date.now();
110
+ }
111
+
112
+ private cleanupExpiredConversations(): void {
113
+ const now = Date.now();
114
+ const expireTime = 24 * 60 * 60 * 1000;
115
+ let cleaned = 0;
116
+
117
+ for (const [id, conversation] of this.conversations.entries()) {
118
+ if (now - conversation.lastAccessTime > expireTime) {
119
+ this.conversations.delete(id);
120
+ cleaned++;
121
+ }
122
+ }
123
+
124
+ if (cleaned > 0) {
125
+ console.log(`[Adapter] 清理了 ${cleaned} 个过期会话`);
126
+ }
127
+ }
128
+
129
+ async sendMessage(
130
+ conversationId: string,
131
+ systemMessage: string | undefined,
132
+ userMessage: string
133
+ ): Promise<IFlowResponse> {
134
+ const prompt = this.buildPromptWithHistory(conversationId, systemMessage, userMessage);
135
+ console.log(`[Adapter] 发送消息(非流式)会话: ${conversationId}`);
136
+ console.log(`[Adapter] 历史消息数: ${this.getConversation(conversationId).messages.length}`);
137
+
138
+ this.saveMessage(conversationId, 'user', userMessage);
48
139
 
49
140
  return new Promise((resolve, reject) => {
50
141
  const args = ['-p', prompt];
@@ -52,7 +143,7 @@ export class IFlowAdapter {
52
143
  args.push('-m', this.options.model);
53
144
  }
54
145
 
55
- console.log('[Adapter] 启动 iflow:', args.join(' '));
146
+ console.log('[Adapter] 启动 iflow:', args.slice(0, 3).join(' ') + '...');
56
147
  const child = spawn('iflow', args, {
57
148
  stdio: ['ignore', 'pipe', 'pipe'],
58
149
  });
@@ -82,11 +173,12 @@ export class IFlowAdapter {
82
173
  console.log('[Adapter] iFlow 退出,code:', code);
83
174
 
84
175
  if (code !== 0 && code !== null) {
85
- console.error('[Adapter] iFlow stderr:', stderr);
176
+ console.error('[Adapter] iFlow stderr:', stderr.substring(0, 500));
86
177
  }
87
178
 
88
- // 解析响应
89
179
  const content = this.parseResponse(stdout);
180
+ this.saveMessage(conversationId, 'assistant', content);
181
+
90
182
  resolve({
91
183
  content,
92
184
  stopReason: code === 0 ? 'end_turn' : 'error',
@@ -95,24 +187,31 @@ export class IFlowAdapter {
95
187
  });
96
188
  }
97
189
 
98
- /**
99
- * 发送消息并获取流式响应
100
- */
101
- async *sendMessageStream(prompt: string): AsyncGenerator<StreamChunk> {
102
- console.log('[Adapter] 发送消息(流式):', prompt.substring(0, 100));
190
+ async *sendMessageStream(
191
+ conversationId: string,
192
+ systemMessage: string | undefined,
193
+ userMessage: string
194
+ ): AsyncGenerator<StreamChunk> {
195
+ const prompt = this.buildPromptWithHistory(conversationId, systemMessage, userMessage);
196
+ console.log(`[Adapter] 发送消息(流式)会话: ${conversationId}`);
197
+ console.log(`[Adapter] 历史消息数: ${this.getConversation(conversationId).messages.length}`);
198
+
199
+ this.saveMessage(conversationId, 'user', userMessage);
103
200
 
104
201
  const args = ['-p', prompt];
105
202
  if (this.options.model) {
106
203
  args.push('-m', this.options.model);
107
204
  }
108
205
 
109
- console.log('[Adapter] 启动 iflow:', args.join(' '));
206
+ console.log('[Adapter] 启动 iflow:', args.slice(0, 3).join(' ') + '...');
110
207
  const child = spawn('iflow', args, {
111
208
  stdio: ['ignore', 'pipe', 'pipe'],
112
209
  });
113
210
 
114
211
  let buffer = '';
115
212
  let isDone = false;
213
+ let fullContent = '';
214
+
116
215
  const timeout = setTimeout(() => {
117
216
  if (!isDone) {
118
217
  child.kill('SIGTERM');
@@ -127,23 +226,20 @@ export class IFlowAdapter {
127
226
 
128
227
  child.stderr?.on('data', (data) => {
129
228
  const msg = data.toString();
130
- console.log('[Adapter] iFlow stderr:', msg.trim());
229
+ console.log('[Adapter] iFlow stderr:', msg.trim().substring(0, 200));
131
230
  });
132
231
 
133
- // 模拟流式输出 - 逐字符发送
134
232
  let lastSentIndex = 0;
135
233
  while (!isDone) {
136
- // 检查进程是否结束
137
234
  if (child.exitCode !== null) {
138
235
  isDone = true;
139
236
  }
140
237
 
141
- // 发送新内容
142
238
  if (buffer.length > lastSentIndex) {
143
239
  const newContent = buffer.slice(lastSentIndex);
144
240
  lastSentIndex = buffer.length;
241
+ fullContent += newContent;
145
242
 
146
- // 逐行或逐字符发送
147
243
  const lines = newContent.split('\n');
148
244
  for (const line of lines) {
149
245
  if (line.trim()) {
@@ -162,28 +258,27 @@ export class IFlowAdapter {
162
258
 
163
259
  clearTimeout(timeout);
164
260
 
165
- // 发送剩余内容
166
261
  if (buffer.length > lastSentIndex) {
262
+ const remaining = buffer.slice(lastSentIndex);
263
+ fullContent += remaining;
167
264
  yield {
168
265
  type: 'content',
169
- content: buffer.slice(lastSentIndex),
266
+ content: remaining,
170
267
  };
171
268
  }
172
269
 
270
+ const finalContent = this.parseResponse(fullContent);
271
+ this.saveMessage(conversationId, 'assistant', finalContent);
272
+
173
273
  yield { type: 'done' };
174
274
  }
175
275
 
176
- /**
177
- * 解析 iFlow 输出,提取实际回复内容
178
- */
179
276
  private parseResponse(stdout: string): string {
180
- // 尝试提取 <Execution Info> 之前的内容作为回复
181
277
  const executionInfoMatch = stdout.match(/<Execution Info>[\s\S]*$/);
182
278
  if (executionInfoMatch) {
183
279
  return stdout.substring(0, executionInfoMatch.index).trim();
184
280
  }
185
281
 
186
- // 如果没有 Execution Info,返回全部内容(去掉开头的警告)
187
282
  const lines = stdout.split('\n');
188
283
  const startIndex = lines.findIndex((line) =>
189
284
  !line.includes('DeprecationWarning') &&
@@ -198,30 +293,33 @@ export class IFlowAdapter {
198
293
  return stdout.trim();
199
294
  }
200
295
 
201
- /**
202
- * 连接到 iFlow
203
- */
204
296
  async connect(): Promise<void> {
205
- // 子进程模式不需要持久连接
206
- console.log('[Adapter] 子进程模式已就绪');
297
+ console.log('[Adapter] 子进程模式已就绪,支持上下文管理');
207
298
  }
208
299
 
209
- /**
210
- * 断开连接
211
- */
212
300
  disconnect(): void {
213
- // 子进程模式不需要断开连接
214
- console.log('[Adapter] 子进程模式断开(无操作)');
301
+ console.log('[Adapter] 子进程模式断开,清理资源...');
302
+ if (this.cleanupInterval) {
303
+ clearInterval(this.cleanupInterval);
304
+ this.cleanupInterval = null;
305
+ }
306
+ this.conversations.clear();
215
307
  }
216
308
 
217
- /**
218
- * 检查是否已连接
219
- */
220
309
  isConnected(): boolean {
221
- // 子进程模式总是"已连接"
222
310
  return true;
223
311
  }
312
+
313
+ getStats(): { conversationCount: number; totalMessages: number } {
314
+ let totalMessages = 0;
315
+ for (const conv of this.conversations.values()) {
316
+ totalMessages += conv.messages.length;
317
+ }
318
+ return {
319
+ conversationCount: this.conversations.size,
320
+ totalMessages,
321
+ };
322
+ }
224
323
  }
225
324
 
226
325
  export default IFlowAdapter;
227
-
@@ -12,23 +12,14 @@ import type {
12
12
  ToolCall,
13
13
  } from './types.js';
14
14
 
15
- /**
16
- * 生成唯一 ID
17
- */
18
15
  export function generateId(): string {
19
16
  return `chatcmpl-${Date.now().toString(36)}${Math.random().toString(36).substring(2, 10)}`;
20
17
  }
21
18
 
22
- /**
23
- * 获取当前时间戳
24
- */
25
19
  export function getTimestamp(): number {
26
20
  return Math.floor(Date.now() / 1000);
27
21
  }
28
22
 
29
- /**
30
- * 将 OpenAI 消息格式转换为 iFlow 文本格式
31
- */
32
23
  export function messagesToIFlowPrompt(messages: ChatCompletionMessage[]): string {
33
24
  return messages.map(msg => {
34
25
  if (typeof msg.content === 'string') {
@@ -38,9 +29,26 @@ export function messagesToIFlowPrompt(messages: ChatCompletionMessage[]): string
38
29
  }).join('\n\n');
39
30
  }
40
31
 
41
- /**
42
- * 创建流式响应块
43
- */
32
+ export function extractMessages(messages: ChatCompletionMessage[]): {
33
+ systemMessage: string | undefined;
34
+ userMessage: string;
35
+ } {
36
+ let systemMessage: string | undefined;
37
+ let userMessage = '';
38
+
39
+ for (const msg of messages) {
40
+ if (typeof msg.content !== 'string') continue;
41
+
42
+ if (msg.role === 'system') {
43
+ systemMessage = msg.content;
44
+ } else if (msg.role === 'user') {
45
+ userMessage = msg.content;
46
+ }
47
+ }
48
+
49
+ return { systemMessage, userMessage };
50
+ }
51
+
44
52
  export function createStreamChunk(
45
53
  id: string,
46
54
  model: string,
@@ -62,9 +70,6 @@ export function createStreamChunk(
62
70
  };
63
71
  }
64
72
 
65
- /**
66
- * 创建完整响应
67
- */
68
73
  export function createCompletionResponse(
69
74
  id: string,
70
75
  model: string,
@@ -91,17 +96,10 @@ export function createCompletionResponse(
91
96
  };
92
97
  }
93
98
 
94
- /**
95
- * 估算 token 数量(简化版)
96
- */
97
99
  export function estimateTokens(text: string): number {
98
- // 简化估算:假设平均每 4 个字符一个 token
99
100
  return Math.ceil(text.length / 4);
100
101
  }
101
102
 
102
- /**
103
- * 计算使用量
104
- */
105
103
  export function calculateUsage(prompt: string, completion: string): UsageInfo {
106
104
  const promptTokens = estimateTokens(prompt);
107
105
  const completionTokens = estimateTokens(completion);
@@ -113,30 +111,28 @@ export function calculateUsage(prompt: string, completion: string): UsageInfo {
113
111
  };
114
112
  }
115
113
 
116
- /**
117
- * SSE 格式化
118
- */
119
114
  export function formatSSE(data: unknown): string {
120
115
  return `data: ${JSON.stringify(data)}\n\n`;
121
116
  }
122
117
 
123
- /**
124
- * SSE 结束标记
125
- */
126
118
  export const SSE_DONE = 'data: [DONE]\n\n';
127
119
 
128
120
  /**
129
- * 支持的模型列表
121
+ * iFlow 支持的模型列表
122
+ * 基于用户实际的 iflow CLI 配置
130
123
  */
131
124
  export const AVAILABLE_MODELS = [
132
- { id: 'iflow-default', name: 'iFlow Default' },
133
- { id: 'iflow-claude', name: 'iFlow Claude' },
134
- { id: 'iflow-gpt-4', name: 'iFlow GPT-4' },
125
+ { id: 'glm-4.7', name: 'GLM-4.7 (Default)' },
126
+ { id: 'iflow-rome-30ba3b', name: 'iFlow-ROME-30BA3B (Preview)' },
127
+ { id: 'deepseek-v3.2', name: 'DeepSeek-V3.2' },
128
+ { id: 'glm-5', name: 'GLM-5' },
129
+ { id: 'qwen3-coder-plus', name: 'Qwen3-Coder-Plus' },
130
+ { id: 'kimi-k2-thinking', name: 'Kimi-K2-Thinking' },
131
+ { id: 'minimax-m2.5', name: 'MiniMax-M2.5' },
132
+ { id: 'kimi-k2.5', name: 'Kimi-K2.5' },
133
+ { id: 'kimi-k2-0905', name: 'Kimi-K2-0905' },
135
134
  ];
136
135
 
137
- /**
138
- * 获取默认模型 ID
139
- */
140
136
  export function getDefaultModel(): string {
141
- return 'kimi-k2.5';
142
- }
137
+ return 'glm-4.7';
138
+ }
package/src/server.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  import {
16
16
  generateId,
17
17
  getTimestamp,
18
- messagesToIFlowPrompt,
18
+ extractMessages,
19
19
  createCompletionResponse,
20
20
  createStreamChunk,
21
21
  calculateUsage,
@@ -43,6 +43,7 @@ export class IFlowAPIServer {
43
43
  this.app = express();
44
44
  this.adapter = new IFlowAdapter({
45
45
  model: options.model,
46
+ maxHistoryLength: 20,
46
47
  });
47
48
 
48
49
  this.setupMiddleware();
@@ -95,9 +96,12 @@ export class IFlowAPIServer {
95
96
  private setupRoutes(): void {
96
97
  // 健康检查
97
98
  this.app.get('/health', (req: Request, res: Response) => {
99
+ const stats = this.adapter.getStats();
98
100
  res.json({
99
101
  status: 'ok',
100
102
  connected: this.adapter.isConnected(),
103
+ conversations: stats.conversationCount,
104
+ totalMessages: stats.totalMessages,
101
105
  timestamp: new Date().toISOString(),
102
106
  });
103
107
  });
@@ -119,7 +123,6 @@ export class IFlowAPIServer {
119
123
  // 聊天完成
120
124
  this.app.post('/v1/chat/completions', async (req: Request, res: Response) => {
121
125
  console.log(`[${new Date().toISOString()}] 收到聊天请求`);
122
- console.log('请求体:', JSON.stringify(req.body, null, 2));
123
126
 
124
127
  try {
125
128
  const body = req.body as ChatCompletionRequest;
@@ -133,18 +136,28 @@ export class IFlowAPIServer {
133
136
  const isStream = body.stream === true;
134
137
  const model = body.model || getDefaultModel();
135
138
 
136
- console.log(`流式: ${isStream}, 模型: ${model}`);
139
+ // 提取 system 和 user 消息
140
+ const { systemMessage, userMessage } = extractMessages(body.messages);
141
+
142
+ if (!userMessage) {
143
+ return res.status(400).json(this.createError('未找到 user 消息', 'invalid_request_error'));
144
+ }
145
+
146
+ // 生成或使用已有的 conversation ID
147
+ // opencode 可能在 header 中传递,或者我们需要生成一个
148
+ let conversationId = req.headers['x-conversation-id'] as string | undefined;
149
+ if (!conversationId) {
150
+ // 从消息中生成一个稳定的 ID(基于 system + user 消息)
151
+ conversationId = `${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
152
+ }
137
153
 
138
- // 转换消息为 iFlow 提示词
139
- const prompt = messagesToIFlowPrompt(body.messages);
140
- console.log('提示词:', prompt.substring(0, 100) + '...');
154
+ console.log(`会话: ${conversationId}, 流式: ${isStream}, 模型: ${model}`);
155
+ console.log(`系统消息: ${systemMessage ? '有' : '无'}, 用户消息: ${userMessage.substring(0, 50)}...`);
141
156
 
142
157
  if (isStream) {
143
- console.log('处理流式响应...');
144
- await this.handleStreamResponse(res, prompt, model);
158
+ await this.handleStreamResponse(res, conversationId, systemMessage, userMessage, model);
145
159
  } else {
146
- console.log('处理非流式响应...');
147
- await this.handleNonStreamResponse(res, prompt, model);
160
+ await this.handleNonStreamResponse(res, conversationId, systemMessage, userMessage, model);
148
161
  }
149
162
  } catch (error) {
150
163
  console.error('处理请求错误:', error);
@@ -156,18 +169,24 @@ export class IFlowAPIServer {
156
169
  /**
157
170
  * 处理流式响应
158
171
  */
159
- private async handleStreamResponse(res: Response, prompt: string, model: string): Promise<void> {
172
+ private async handleStreamResponse(
173
+ res: Response,
174
+ conversationId: string,
175
+ systemMessage: string | undefined,
176
+ userMessage: string,
177
+ model: string
178
+ ): Promise<void> {
160
179
  const id = generateId();
161
180
 
162
181
  res.setHeader('Content-Type', 'text/event-stream');
163
182
  res.setHeader('Cache-Control', 'no-cache');
164
183
  res.setHeader('Connection', 'keep-alive');
165
184
 
166
- // 设置 60 秒超时
185
+ // 设置 5 分钟超时
167
186
  const timeout = setTimeout(() => {
168
187
  res.write(formatSSE(this.createError('请求超时', 'timeout_error')));
169
188
  res.end();
170
- }, 60000);
189
+ }, 300000);
171
190
 
172
191
  try {
173
192
  // 发送开始标记(角色)
@@ -187,7 +206,7 @@ export class IFlowAPIServer {
187
206
  // 流式发送内容
188
207
  let fullContent = '';
189
208
  let isDone = false;
190
- for await (const chunk of this.adapter.sendMessageStream(prompt)) {
209
+ for await (const chunk of this.adapter.sendMessageStream(conversationId, systemMessage, userMessage)) {
191
210
  if (isDone) break;
192
211
 
193
212
  switch (chunk.type) {
@@ -205,7 +224,6 @@ export class IFlowAPIServer {
205
224
  break;
206
225
 
207
226
  case 'done':
208
- // 收到完成信号,标记结束
209
227
  isDone = true;
210
228
  break;
211
229
 
@@ -238,19 +256,25 @@ export class IFlowAPIServer {
238
256
  /**
239
257
  * 处理非流式响应
240
258
  */
241
- private async handleNonStreamResponse(res: Response, prompt: string, model: string): Promise<void> {
259
+ private async handleNonStreamResponse(
260
+ res: Response,
261
+ conversationId: string,
262
+ systemMessage: string | undefined,
263
+ userMessage: string,
264
+ model: string
265
+ ): Promise<void> {
242
266
  const id = generateId();
243
267
 
244
- // 设置 60 秒超时
268
+ // 设置 5 分钟超时
245
269
  const timeout = setTimeout(() => {
246
270
  res.status(504).json(this.createError('请求超时', 'timeout_error'));
247
- }, 60000);
271
+ }, 300000);
248
272
 
249
273
  try {
250
- const response = await this.adapter.sendMessage(prompt);
274
+ const response = await this.adapter.sendMessage(conversationId, systemMessage, userMessage);
251
275
  clearTimeout(timeout);
252
276
 
253
- const usage = calculateUsage(prompt, response.content);
277
+ const usage = calculateUsage(userMessage, response.content);
254
278
 
255
279
  const completionResponse = createCompletionResponse(
256
280
  id,
@@ -317,6 +341,7 @@ export class IFlowAPIServer {
317
341
  console.log(`\n🚀 iFlow API 桥接服务已启动`);
318
342
  console.log(`📍 服务地址: http://${host}:${port}`);
319
343
  console.log(`🤖 使用模型: ${model}`);
344
+ console.log(`💬 支持上下文管理: 是`);
320
345
  console.log(`🔗 OpenAI API: http://${host}:${port}/v1/chat/completions`);
321
346
  console.log(`📋 模型列表: http://${host}:${port}/v1/models`);
322
347
  console.log(`❤️ 健康检查: http://${host}:${port}/health`);
@@ -338,4 +363,4 @@ export class IFlowAPIServer {
338
363
  async stop(): Promise<void> {
339
364
  await this.adapter.disconnect();
340
365
  }
341
- }
366
+ }