@asframe/opencode-iflow-auth 1.0.0
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/README.md +278 -0
- package/dist/constants.d.ts +33 -0
- package/dist/constants.js +303 -0
- package/dist/iflow/apikey.d.ts +6 -0
- package/dist/iflow/apikey.js +17 -0
- package/dist/iflow/oauth.d.ts +20 -0
- package/dist/iflow/oauth.js +113 -0
- package/dist/iflow/proxy.d.ts +23 -0
- package/dist/iflow/proxy.js +435 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/plugin/accounts.d.ts +24 -0
- package/dist/plugin/accounts.js +205 -0
- package/dist/plugin/auth-page.d.ts +3 -0
- package/dist/plugin/auth-page.js +573 -0
- package/dist/plugin/cli.d.ts +11 -0
- package/dist/plugin/cli.js +77 -0
- package/dist/plugin/config/index.d.ts +2 -0
- package/dist/plugin/config/index.js +2 -0
- package/dist/plugin/config/loader.d.ts +3 -0
- package/dist/plugin/config/loader.js +110 -0
- package/dist/plugin/config/schema.d.ts +35 -0
- package/dist/plugin/config/schema.js +22 -0
- package/dist/plugin/errors.d.ts +14 -0
- package/dist/plugin/errors.js +25 -0
- package/dist/plugin/logger.d.ts +8 -0
- package/dist/plugin/logger.js +63 -0
- package/dist/plugin/server.d.ts +7 -0
- package/dist/plugin/server.js +98 -0
- package/dist/plugin/storage.d.ts +4 -0
- package/dist/plugin/storage.js +91 -0
- package/dist/plugin/token.d.ts +3 -0
- package/dist/plugin/token.js +26 -0
- package/dist/plugin/types.d.ts +58 -0
- package/dist/plugin/types.js +1 -0
- package/dist/plugin-iflow.d.ts +2 -0
- package/dist/plugin-iflow.js +141 -0
- package/dist/plugin-proxy.d.ts +2 -0
- package/dist/plugin-proxy.js +155 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +2 -0
- package/package.json +63 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { IFLOW_CONSTANTS } from '../constants.js';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
function base64URLEncode(buffer) {
|
|
4
|
+
return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
5
|
+
}
|
|
6
|
+
function generateState() {
|
|
7
|
+
return base64URLEncode(randomBytes(16));
|
|
8
|
+
}
|
|
9
|
+
export async function authorizeIFlowOAuth(port) {
|
|
10
|
+
const state = generateState();
|
|
11
|
+
const redirectUri = `http://localhost:${port}/oauth2callback`;
|
|
12
|
+
const params = new URLSearchParams({
|
|
13
|
+
loginMethod: 'phone',
|
|
14
|
+
type: 'phone',
|
|
15
|
+
redirect: redirectUri,
|
|
16
|
+
state,
|
|
17
|
+
client_id: IFLOW_CONSTANTS.CLIENT_ID
|
|
18
|
+
});
|
|
19
|
+
const authUrl = `${IFLOW_CONSTANTS.OAUTH_AUTHORIZE_URL}?${params.toString()}`;
|
|
20
|
+
return { authUrl, state, redirectUri };
|
|
21
|
+
}
|
|
22
|
+
export async function exchangeOAuthCode(code, redirectUri) {
|
|
23
|
+
const params = new URLSearchParams({
|
|
24
|
+
grant_type: 'authorization_code',
|
|
25
|
+
code,
|
|
26
|
+
redirect_uri: redirectUri,
|
|
27
|
+
client_id: IFLOW_CONSTANTS.CLIENT_ID,
|
|
28
|
+
client_secret: IFLOW_CONSTANTS.CLIENT_SECRET
|
|
29
|
+
});
|
|
30
|
+
const basicAuth = Buffer.from(`${IFLOW_CONSTANTS.CLIENT_ID}:${IFLOW_CONSTANTS.CLIENT_SECRET}`).toString('base64');
|
|
31
|
+
const response = await fetch(IFLOW_CONSTANTS.OAUTH_TOKEN_URL, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
35
|
+
Accept: 'application/json',
|
|
36
|
+
Authorization: `Basic ${basicAuth}`
|
|
37
|
+
},
|
|
38
|
+
body: params.toString()
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const errorText = await response.text().catch(() => '');
|
|
42
|
+
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
|
|
43
|
+
}
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
const userInfo = await fetchUserInfo(data.access_token);
|
|
46
|
+
const expiresIn = data.expires_in || 3600;
|
|
47
|
+
const expiresAt = Date.now() + expiresIn * 1000;
|
|
48
|
+
return {
|
|
49
|
+
accessToken: data.access_token,
|
|
50
|
+
refreshToken: data.refresh_token,
|
|
51
|
+
expiresAt,
|
|
52
|
+
apiKey: userInfo.apiKey,
|
|
53
|
+
email: userInfo.email,
|
|
54
|
+
authMethod: 'oauth'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function refreshOAuthToken(refreshToken) {
|
|
58
|
+
const params = new URLSearchParams({
|
|
59
|
+
grant_type: 'refresh_token',
|
|
60
|
+
refresh_token: refreshToken,
|
|
61
|
+
client_id: IFLOW_CONSTANTS.CLIENT_ID,
|
|
62
|
+
client_secret: IFLOW_CONSTANTS.CLIENT_SECRET
|
|
63
|
+
});
|
|
64
|
+
const basicAuth = Buffer.from(`${IFLOW_CONSTANTS.CLIENT_ID}:${IFLOW_CONSTANTS.CLIENT_SECRET}`).toString('base64');
|
|
65
|
+
const response = await fetch(IFLOW_CONSTANTS.OAUTH_TOKEN_URL, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
69
|
+
Accept: 'application/json',
|
|
70
|
+
Authorization: `Basic ${basicAuth}`
|
|
71
|
+
},
|
|
72
|
+
body: params.toString()
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const errorText = await response.text().catch(() => '');
|
|
76
|
+
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
|
|
77
|
+
}
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
const userInfo = await fetchUserInfo(data.access_token);
|
|
80
|
+
const expiresIn = data.expires_in || 3600;
|
|
81
|
+
const expiresAt = Date.now() + expiresIn * 1000;
|
|
82
|
+
return {
|
|
83
|
+
accessToken: data.access_token,
|
|
84
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
85
|
+
expiresAt,
|
|
86
|
+
apiKey: userInfo.apiKey,
|
|
87
|
+
email: userInfo.email,
|
|
88
|
+
authMethod: 'oauth'
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function fetchUserInfo(accessToken) {
|
|
92
|
+
const response = await fetch(`${IFLOW_CONSTANTS.USER_INFO_URL}?accessToken=${encodeURIComponent(accessToken)}`, {
|
|
93
|
+
headers: {
|
|
94
|
+
Accept: 'application/json'
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const errorText = await response.text().catch(() => '');
|
|
99
|
+
throw new Error(`User info fetch failed: ${response.status} ${errorText}`);
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
if (!data.success || !data.data) {
|
|
103
|
+
throw new Error('User info request not successful');
|
|
104
|
+
}
|
|
105
|
+
if (!data.data.apiKey) {
|
|
106
|
+
throw new Error('Missing apiKey in user info response');
|
|
107
|
+
}
|
|
108
|
+
const email = data.data.email || data.data.phone || 'oauth-user';
|
|
109
|
+
return {
|
|
110
|
+
apiKey: data.data.apiKey,
|
|
111
|
+
email
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare class IFlowCLIProxy {
|
|
2
|
+
private server;
|
|
3
|
+
private port;
|
|
4
|
+
private host;
|
|
5
|
+
private cliAvailable;
|
|
6
|
+
private cliChecked;
|
|
7
|
+
constructor(port?: number, host?: string);
|
|
8
|
+
start(): Promise<void>;
|
|
9
|
+
stop(): Promise<void>;
|
|
10
|
+
getBaseUrl(): string;
|
|
11
|
+
isCLIAvailable(): boolean;
|
|
12
|
+
private handleRequest;
|
|
13
|
+
private handleChatCompletions;
|
|
14
|
+
private handleDirectAPIRequest;
|
|
15
|
+
private handleCLINonStreamRequest;
|
|
16
|
+
private handleCLIStreamRequest;
|
|
17
|
+
private handleModels;
|
|
18
|
+
private callIFlowCLI;
|
|
19
|
+
private callIFlowCLIStream;
|
|
20
|
+
private buildPrompt;
|
|
21
|
+
}
|
|
22
|
+
export declare function getProxyInstance(): IFlowCLIProxy;
|
|
23
|
+
export declare function startProxy(): Promise<IFlowCLIProxy>;
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
import { createServer } from 'http';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
const IFLOW_PROXY_PORT = 19998;
|
|
5
|
+
const IFLOW_PROXY_HOST = '127.0.0.1';
|
|
6
|
+
const IFLOW_API_BASE = 'https://apis.iflow.cn';
|
|
7
|
+
const CLI_REQUIRED_MODELS = ['glm-5', 'glm-5-free', 'glm-5-thinking'];
|
|
8
|
+
const DEBUG = process.env.IFLOW_PROXY_DEBUG === 'true';
|
|
9
|
+
const AUTO_INSTALL_CLI = process.env.IFLOW_AUTO_INSTALL_CLI === 'true';
|
|
10
|
+
function log(...args) {
|
|
11
|
+
if (DEBUG) {
|
|
12
|
+
console.error('[IFlowProxy]', ...args);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function requiresCLI(model) {
|
|
16
|
+
return CLI_REQUIRED_MODELS.some(m => model.includes(m));
|
|
17
|
+
}
|
|
18
|
+
function checkIFlowCLI() {
|
|
19
|
+
try {
|
|
20
|
+
const result = execSync('iflow --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
21
|
+
const version = result.trim();
|
|
22
|
+
log('iflow CLI version:', version);
|
|
23
|
+
return { installed: true, version };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const errorMsg = error.message || 'Unknown error';
|
|
27
|
+
log('iflow CLI check failed:', errorMsg);
|
|
28
|
+
return { installed: false, error: errorMsg };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function installIFlowCLI() {
|
|
32
|
+
log('Attempting to install iflow CLI...');
|
|
33
|
+
console.error('[IFlowProxy] Installing iflow CLI...');
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
try {
|
|
36
|
+
const npm = spawn('npm', ['install', '-g', 'iflow-cli'], {
|
|
37
|
+
shell: true,
|
|
38
|
+
stdio: 'inherit'
|
|
39
|
+
});
|
|
40
|
+
npm.on('error', (err) => {
|
|
41
|
+
console.error('[IFlowProxy] Failed to install iflow CLI:', err.message);
|
|
42
|
+
resolve({ success: false, error: err.message });
|
|
43
|
+
});
|
|
44
|
+
npm.on('close', (code) => {
|
|
45
|
+
if (code === 0) {
|
|
46
|
+
console.error('[IFlowProxy] iflow CLI installed successfully!');
|
|
47
|
+
console.error('[IFlowProxy] Please run: iflow login');
|
|
48
|
+
resolve({ success: true });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.error('[IFlowProxy] Installation failed with code:', code);
|
|
52
|
+
resolve({ success: false, error: `npm install exited with code ${code}` });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('[IFlowProxy] Failed to start npm install:', error.message);
|
|
58
|
+
resolve({ success: false, error: error.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export class IFlowCLIProxy {
|
|
63
|
+
server = null;
|
|
64
|
+
port;
|
|
65
|
+
host;
|
|
66
|
+
cliAvailable = false;
|
|
67
|
+
cliChecked = false;
|
|
68
|
+
constructor(port = IFLOW_PROXY_PORT, host = IFLOW_PROXY_HOST) {
|
|
69
|
+
this.port = port;
|
|
70
|
+
this.host = host;
|
|
71
|
+
}
|
|
72
|
+
async start() {
|
|
73
|
+
if (this.server) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!this.cliChecked) {
|
|
77
|
+
let cliCheck = checkIFlowCLI();
|
|
78
|
+
if (!cliCheck.installed && AUTO_INSTALL_CLI) {
|
|
79
|
+
const installResult = await installIFlowCLI();
|
|
80
|
+
if (installResult.success) {
|
|
81
|
+
cliCheck = checkIFlowCLI();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.cliAvailable = cliCheck.installed;
|
|
85
|
+
this.cliChecked = true;
|
|
86
|
+
if (!cliCheck.installed) {
|
|
87
|
+
console.error('');
|
|
88
|
+
console.error('[IFlowProxy] ═══════════════════════════════════════════════════════════');
|
|
89
|
+
console.error('[IFlowProxy] WARNING: iflow CLI is not installed');
|
|
90
|
+
console.error('[IFlowProxy] ═══════════════════════════════════════════════════════════');
|
|
91
|
+
console.error('[IFlowProxy] To use GLM-5 models, please install iflow CLI:');
|
|
92
|
+
console.error('[IFlowProxy]');
|
|
93
|
+
console.error('[IFlowProxy] npm install -g iflow-cli');
|
|
94
|
+
console.error('[IFlowProxy] iflow login');
|
|
95
|
+
console.error('[IFlowProxy]');
|
|
96
|
+
console.error('[IFlowProxy] Or set IFLOW_AUTO_INSTALL_CLI=true to auto-install');
|
|
97
|
+
console.error('[IFlowProxy] ═══════════════════════════════════════════════════════════');
|
|
98
|
+
console.error('');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
log('iflow CLI is available:', cliCheck.version);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
this.server = createServer((req, res) => {
|
|
106
|
+
this.handleRequest(req, res);
|
|
107
|
+
});
|
|
108
|
+
this.server.on('error', (err) => {
|
|
109
|
+
if (err.code === 'EADDRINUSE') {
|
|
110
|
+
log(`Port ${this.port} already in use, assuming server is running`);
|
|
111
|
+
resolve();
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
reject(err);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
this.server.listen(this.port, this.host, () => {
|
|
118
|
+
log(`Smart proxy started on http://${this.host}:${this.port}`);
|
|
119
|
+
resolve();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async stop() {
|
|
124
|
+
if (!this.server) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
this.server.close(() => {
|
|
129
|
+
this.server = null;
|
|
130
|
+
log('Server stopped');
|
|
131
|
+
resolve();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
getBaseUrl() {
|
|
136
|
+
return `http://${this.host}:${this.port}`;
|
|
137
|
+
}
|
|
138
|
+
isCLIAvailable() {
|
|
139
|
+
return this.cliAvailable;
|
|
140
|
+
}
|
|
141
|
+
async handleRequest(req, res) {
|
|
142
|
+
if (req.method !== 'POST') {
|
|
143
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
144
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const url = req.url || '';
|
|
148
|
+
if (url === '/v1/chat/completions') {
|
|
149
|
+
await this.handleChatCompletions(req, res);
|
|
150
|
+
}
|
|
151
|
+
else if (url === '/v1/models') {
|
|
152
|
+
this.handleModels(res);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
156
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async handleChatCompletions(req, res) {
|
|
160
|
+
let body = '';
|
|
161
|
+
req.on('data', chunk => {
|
|
162
|
+
body += chunk.toString();
|
|
163
|
+
});
|
|
164
|
+
req.on('end', async () => {
|
|
165
|
+
try {
|
|
166
|
+
const request = JSON.parse(body);
|
|
167
|
+
const model = request.model;
|
|
168
|
+
const isStream = request.stream === true;
|
|
169
|
+
log(`Request for model: ${model}, requires CLI: ${requiresCLI(model)}`);
|
|
170
|
+
if (requiresCLI(model)) {
|
|
171
|
+
if (!this.cliAvailable) {
|
|
172
|
+
log(`CLI not available for model: ${model}`);
|
|
173
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
174
|
+
res.end(JSON.stringify({
|
|
175
|
+
error: 'iflow CLI is not installed. Please install it with: npm install -g iflow-cli && iflow login',
|
|
176
|
+
install_hint: 'npm install -g iflow-cli && iflow login'
|
|
177
|
+
}));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
log(`Using CLI for model: ${model}`);
|
|
181
|
+
if (isStream) {
|
|
182
|
+
await this.handleCLIStreamRequest(request, res);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
await this.handleCLINonStreamRequest(request, res);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
log(`Using direct API for model: ${model}`);
|
|
190
|
+
await this.handleDirectAPIRequest(request, res, isStream);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
log('Error parsing request:', error);
|
|
195
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
196
|
+
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async handleDirectAPIRequest(request, res, isStream) {
|
|
201
|
+
try {
|
|
202
|
+
const https = await import('https');
|
|
203
|
+
const requestBody = JSON.stringify({
|
|
204
|
+
...request,
|
|
205
|
+
model: request.model,
|
|
206
|
+
});
|
|
207
|
+
const options = {
|
|
208
|
+
hostname: 'apis.iflow.cn',
|
|
209
|
+
path: '/v1/chat/completions',
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'application/json',
|
|
213
|
+
'Content-Length': Buffer.byteLength(requestBody)
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const apiReq = https.request(options, (apiRes) => {
|
|
217
|
+
res.writeHead(apiRes.statusCode || 200, apiRes.headers);
|
|
218
|
+
apiRes.pipe(res);
|
|
219
|
+
});
|
|
220
|
+
apiReq.on('error', (err) => {
|
|
221
|
+
log('Direct API error:', err);
|
|
222
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
223
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
224
|
+
});
|
|
225
|
+
apiReq.write(requestBody);
|
|
226
|
+
apiReq.end();
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
log('Direct API error:', error);
|
|
230
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
231
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async handleCLINonStreamRequest(request, res) {
|
|
235
|
+
try {
|
|
236
|
+
const result = await this.callIFlowCLI(request);
|
|
237
|
+
const response = {
|
|
238
|
+
id: `iflow-${randomUUID()}`,
|
|
239
|
+
object: 'chat.completion',
|
|
240
|
+
created: Math.floor(Date.now() / 1000),
|
|
241
|
+
model: request.model,
|
|
242
|
+
choices: [{
|
|
243
|
+
index: 0,
|
|
244
|
+
message: {
|
|
245
|
+
role: 'assistant',
|
|
246
|
+
content: result.content
|
|
247
|
+
},
|
|
248
|
+
finish_reason: 'stop'
|
|
249
|
+
}],
|
|
250
|
+
usage: {
|
|
251
|
+
prompt_tokens: result.promptTokens || 0,
|
|
252
|
+
completion_tokens: result.completionTokens || 0,
|
|
253
|
+
total_tokens: (result.promptTokens || 0) + (result.completionTokens || 0)
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
257
|
+
res.end(JSON.stringify(response));
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
log('Error calling iflow CLI:', error);
|
|
261
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
262
|
+
res.end(JSON.stringify({ error: error.message || 'Internal server error' }));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async handleCLIStreamRequest(request, res) {
|
|
266
|
+
res.writeHead(200, {
|
|
267
|
+
'Content-Type': 'text/event-stream',
|
|
268
|
+
'Cache-Control': 'no-cache',
|
|
269
|
+
'Connection': 'keep-alive'
|
|
270
|
+
});
|
|
271
|
+
const chatId = `iflow-${randomUUID()}`;
|
|
272
|
+
const created = Math.floor(Date.now() / 1000);
|
|
273
|
+
try {
|
|
274
|
+
await this.callIFlowCLIStream(request, (content, done) => {
|
|
275
|
+
const chunk = {
|
|
276
|
+
id: chatId,
|
|
277
|
+
object: 'chat.completion.chunk',
|
|
278
|
+
created: created,
|
|
279
|
+
model: request.model,
|
|
280
|
+
choices: [{
|
|
281
|
+
index: 0,
|
|
282
|
+
delta: done ? {} : { content },
|
|
283
|
+
finish_reason: done ? 'stop' : null
|
|
284
|
+
}]
|
|
285
|
+
};
|
|
286
|
+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
287
|
+
if (done) {
|
|
288
|
+
res.write('data: [DONE]\n\n');
|
|
289
|
+
res.end();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
log('Error in stream:', error);
|
|
295
|
+
const errorChunk = {
|
|
296
|
+
id: chatId,
|
|
297
|
+
object: 'chat.completion.chunk',
|
|
298
|
+
created: created,
|
|
299
|
+
model: request.model,
|
|
300
|
+
choices: [{
|
|
301
|
+
index: 0,
|
|
302
|
+
delta: { content: `\n\n[Error: ${error.message}]` },
|
|
303
|
+
finish_reason: 'stop'
|
|
304
|
+
}]
|
|
305
|
+
};
|
|
306
|
+
res.write(`data: ${JSON.stringify(errorChunk)}\n\n`);
|
|
307
|
+
res.write('data: [DONE]\n\n');
|
|
308
|
+
res.end();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
handleModels(res) {
|
|
312
|
+
const models = [
|
|
313
|
+
{ id: 'glm-5', object: 'model', created: 1700000000, owned_by: 'iflow' },
|
|
314
|
+
{ id: 'glm-4.6', object: 'model', created: 1700000000, owned_by: 'iflow' },
|
|
315
|
+
{ id: 'deepseek-v3.2', object: 'model', created: 1700000000, owned_by: 'iflow' },
|
|
316
|
+
{ id: 'kimi-k2', object: 'model', created: 1700000000, owned_by: 'iflow' },
|
|
317
|
+
];
|
|
318
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
319
|
+
res.end(JSON.stringify({ object: 'list', data: models }));
|
|
320
|
+
}
|
|
321
|
+
async callIFlowCLI(request) {
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const prompt = this.buildPrompt(request.messages);
|
|
324
|
+
const args = [
|
|
325
|
+
'-m', request.model,
|
|
326
|
+
'--no-stream'
|
|
327
|
+
];
|
|
328
|
+
log(`Calling iflow with stdin, prompt length: ${prompt.length}`);
|
|
329
|
+
const iflow = spawn('iflow', args, {
|
|
330
|
+
shell: true,
|
|
331
|
+
windowsHide: true,
|
|
332
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
333
|
+
env: { ...process.env }
|
|
334
|
+
});
|
|
335
|
+
let stdout = '';
|
|
336
|
+
let stderr = '';
|
|
337
|
+
iflow.stdout?.on('data', (data) => {
|
|
338
|
+
stdout += data.toString();
|
|
339
|
+
});
|
|
340
|
+
iflow.stderr?.on('data', (data) => {
|
|
341
|
+
stderr += data.toString();
|
|
342
|
+
});
|
|
343
|
+
iflow.on('error', (err) => {
|
|
344
|
+
log('Failed to start iflow:', err);
|
|
345
|
+
reject(new Error(`Failed to start iflow: ${err.message}`));
|
|
346
|
+
});
|
|
347
|
+
iflow.on('close', (code) => {
|
|
348
|
+
if (code !== 0) {
|
|
349
|
+
log('iflow exited with code:', code, stderr);
|
|
350
|
+
reject(new Error(`iflow exited with code ${code}`));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const content = stdout.trim();
|
|
354
|
+
log('iflow response length:', content.length);
|
|
355
|
+
resolve({
|
|
356
|
+
content,
|
|
357
|
+
promptTokens: 0,
|
|
358
|
+
completionTokens: 0
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
iflow.stdin?.write(prompt);
|
|
362
|
+
iflow.stdin?.end();
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async callIFlowCLIStream(request, onChunk) {
|
|
366
|
+
return new Promise((resolve, reject) => {
|
|
367
|
+
const prompt = this.buildPrompt(request.messages);
|
|
368
|
+
const args = [
|
|
369
|
+
'-m', request.model
|
|
370
|
+
];
|
|
371
|
+
log(`Calling iflow (stream) with stdin, prompt length: ${prompt.length}`);
|
|
372
|
+
const iflow = spawn('iflow', args, {
|
|
373
|
+
shell: true,
|
|
374
|
+
windowsHide: true,
|
|
375
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
376
|
+
env: { ...process.env }
|
|
377
|
+
});
|
|
378
|
+
let resolved = false;
|
|
379
|
+
iflow.stdout?.on('data', (data) => {
|
|
380
|
+
const chunk = data.toString();
|
|
381
|
+
onChunk(chunk, false);
|
|
382
|
+
});
|
|
383
|
+
iflow.stderr?.on('data', () => { });
|
|
384
|
+
iflow.on('error', (err) => {
|
|
385
|
+
log('Failed to start iflow:', err);
|
|
386
|
+
if (!resolved) {
|
|
387
|
+
resolved = true;
|
|
388
|
+
reject(new Error(`Failed to start iflow: ${err.message}`));
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
iflow.on('close', (code) => {
|
|
392
|
+
if (code !== 0 && !resolved) {
|
|
393
|
+
log('iflow exited with code:', code);
|
|
394
|
+
resolved = true;
|
|
395
|
+
reject(new Error(`iflow exited with code ${code}`));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (!resolved) {
|
|
399
|
+
resolved = true;
|
|
400
|
+
onChunk('', true);
|
|
401
|
+
resolve();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
iflow.stdin?.write(prompt);
|
|
405
|
+
iflow.stdin?.end();
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
buildPrompt(messages) {
|
|
409
|
+
const parts = [];
|
|
410
|
+
for (const msg of messages) {
|
|
411
|
+
if (msg.role === 'system') {
|
|
412
|
+
parts.push(`System: ${msg.content}`);
|
|
413
|
+
}
|
|
414
|
+
else if (msg.role === 'user') {
|
|
415
|
+
parts.push(msg.content);
|
|
416
|
+
}
|
|
417
|
+
else if (msg.role === 'assistant') {
|
|
418
|
+
parts.push(`Assistant: ${msg.content}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return parts.join('\n\n');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let proxyInstance = null;
|
|
425
|
+
export function getProxyInstance() {
|
|
426
|
+
if (!proxyInstance) {
|
|
427
|
+
proxyInstance = new IFlowCLIProxy();
|
|
428
|
+
}
|
|
429
|
+
return proxyInstance;
|
|
430
|
+
}
|
|
431
|
+
export async function startProxy() {
|
|
432
|
+
const proxy = getProxyInstance();
|
|
433
|
+
await proxy.start();
|
|
434
|
+
return proxy;
|
|
435
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ManagedAccount, AccountSelectionStrategy, IFlowAuthDetails, RefreshParts } from './types.js';
|
|
2
|
+
export declare function generateAccountId(): string;
|
|
3
|
+
export declare function encodeRefreshToken(parts: RefreshParts): string;
|
|
4
|
+
export declare function decodeRefreshToken(encoded: string): RefreshParts;
|
|
5
|
+
export declare class AccountManager {
|
|
6
|
+
private accounts;
|
|
7
|
+
private cursor;
|
|
8
|
+
private strategy;
|
|
9
|
+
private lastToastTime;
|
|
10
|
+
constructor(accounts: ManagedAccount[], strategy?: AccountSelectionStrategy);
|
|
11
|
+
static loadFromDisk(strategy?: AccountSelectionStrategy): Promise<AccountManager>;
|
|
12
|
+
getAccountCount(): number;
|
|
13
|
+
getAccounts(): ManagedAccount[];
|
|
14
|
+
shouldShowToast(debounce?: number): boolean;
|
|
15
|
+
getMinWaitTime(): number;
|
|
16
|
+
getCurrentOrNext(): ManagedAccount | null;
|
|
17
|
+
addAccount(a: ManagedAccount): void;
|
|
18
|
+
removeAccount(a: ManagedAccount): void;
|
|
19
|
+
updateFromAuth(a: ManagedAccount, auth: IFlowAuthDetails): void;
|
|
20
|
+
markRateLimited(a: ManagedAccount, ms: number): void;
|
|
21
|
+
markUnhealthy(a: ManagedAccount, reason: string, recovery?: number): void;
|
|
22
|
+
saveToDisk(): Promise<void>;
|
|
23
|
+
toAuthDetails(a: ManagedAccount): IFlowAuthDetails;
|
|
24
|
+
}
|