shenxiang-ai-cli 0.1.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/dist/index.js +2813 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2813 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/chat.ts
|
|
7
|
+
import readline from "readline";
|
|
8
|
+
|
|
9
|
+
// src/core/providers/openai-compatible.ts
|
|
10
|
+
import OpenAI from "openai";
|
|
11
|
+
var OpenAICompatibleProvider = class {
|
|
12
|
+
name;
|
|
13
|
+
constructor(name) {
|
|
14
|
+
this.name = name;
|
|
15
|
+
}
|
|
16
|
+
async *chatStream(messages, tools, config3) {
|
|
17
|
+
const isKimi = this.name === "kimi";
|
|
18
|
+
if (isKimi) {
|
|
19
|
+
yield* this.kimiChatStream(messages, tools, config3);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const client = new OpenAI({
|
|
23
|
+
apiKey: config3.apiKey,
|
|
24
|
+
baseURL: config3.baseUrl
|
|
25
|
+
});
|
|
26
|
+
const openaiTools = tools.map((t2) => ({
|
|
27
|
+
type: "function",
|
|
28
|
+
function: {
|
|
29
|
+
name: t2.name,
|
|
30
|
+
description: t2.description,
|
|
31
|
+
parameters: t2.parameters
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
34
|
+
const openaiMessages = messages.map((m) => {
|
|
35
|
+
if (m.role === "tool") {
|
|
36
|
+
return {
|
|
37
|
+
role: "tool",
|
|
38
|
+
content: m.content || "",
|
|
39
|
+
tool_call_id: m.tool_call_id || ""
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (m.role === "assistant" && m.tool_calls) {
|
|
43
|
+
return {
|
|
44
|
+
role: "assistant",
|
|
45
|
+
content: m.content,
|
|
46
|
+
tool_calls: m.tool_calls.map((tc) => ({
|
|
47
|
+
id: tc.id,
|
|
48
|
+
type: "function",
|
|
49
|
+
function: {
|
|
50
|
+
name: tc.function.name,
|
|
51
|
+
arguments: tc.function.arguments
|
|
52
|
+
}
|
|
53
|
+
}))
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
role: m.role,
|
|
58
|
+
content: m.content || ""
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
const stream = await client.chat.completions.create({
|
|
63
|
+
model: config3.model,
|
|
64
|
+
messages: openaiMessages,
|
|
65
|
+
tools: openaiTools.length > 0 ? openaiTools : void 0,
|
|
66
|
+
stream: true,
|
|
67
|
+
stream_options: { include_usage: true },
|
|
68
|
+
temperature: 0.3,
|
|
69
|
+
max_tokens: 8192
|
|
70
|
+
});
|
|
71
|
+
const toolCallAccumulator = /* @__PURE__ */ new Map();
|
|
72
|
+
let tokenUsage;
|
|
73
|
+
for await (const chunk of stream) {
|
|
74
|
+
if (chunk.usage) {
|
|
75
|
+
tokenUsage = {
|
|
76
|
+
promptTokens: chunk.usage.prompt_tokens || 0,
|
|
77
|
+
completionTokens: chunk.usage.completion_tokens || 0,
|
|
78
|
+
totalTokens: chunk.usage.total_tokens || 0
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const choice = chunk.choices[0];
|
|
82
|
+
if (!choice) continue;
|
|
83
|
+
const delta = choice.delta;
|
|
84
|
+
if (delta.content) {
|
|
85
|
+
yield { type: "text", content: delta.content };
|
|
86
|
+
}
|
|
87
|
+
if (delta.tool_calls) {
|
|
88
|
+
for (const tc of delta.tool_calls) {
|
|
89
|
+
const idx = tc.index;
|
|
90
|
+
if (!toolCallAccumulator.has(idx)) {
|
|
91
|
+
toolCallAccumulator.set(idx, { id: tc.id || "", functionName: tc.function?.name || "", arguments: "" });
|
|
92
|
+
}
|
|
93
|
+
const acc = toolCallAccumulator.get(idx);
|
|
94
|
+
if (tc.id) acc.id = tc.id;
|
|
95
|
+
if (tc.function?.name) acc.functionName = tc.function.name;
|
|
96
|
+
if (tc.function?.arguments) acc.arguments += tc.function.arguments;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (choice.finish_reason === "tool_calls" || choice.finish_reason === "stop") {
|
|
100
|
+
for (const [_, acc] of toolCallAccumulator) {
|
|
101
|
+
yield { type: "tool_call", toolCall: { id: acc.id, type: "function", function: { name: acc.functionName, arguments: acc.arguments } } };
|
|
102
|
+
}
|
|
103
|
+
yield { type: "done", usage: tokenUsage };
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const [_, acc] of toolCallAccumulator) {
|
|
108
|
+
yield { type: "tool_call", toolCall: { id: acc.id, type: "function", function: { name: acc.functionName, arguments: acc.arguments } } };
|
|
109
|
+
}
|
|
110
|
+
yield { type: "done", usage: tokenUsage };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
113
|
+
yield { type: "error", error: msg };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Kimi-specific streaming using raw fetch to handle reasoning_content
|
|
118
|
+
*/
|
|
119
|
+
async *kimiChatStream(messages, tools, config3) {
|
|
120
|
+
const baseUrl = config3.baseUrl || "https://api.moonshot.cn/v1";
|
|
121
|
+
const kimiMessages = messages.map((m) => {
|
|
122
|
+
if (m.role === "tool") {
|
|
123
|
+
return {
|
|
124
|
+
role: "tool",
|
|
125
|
+
content: m.content || "",
|
|
126
|
+
tool_call_id: m.tool_call_id || ""
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (m.role === "assistant" && m.tool_calls) {
|
|
130
|
+
return {
|
|
131
|
+
role: "assistant",
|
|
132
|
+
content: m.content || "",
|
|
133
|
+
reasoning_content: m.reasoning_content || "",
|
|
134
|
+
tool_calls: m.tool_calls.map((tc) => ({
|
|
135
|
+
id: tc.id,
|
|
136
|
+
type: "function",
|
|
137
|
+
function: {
|
|
138
|
+
name: tc.function.name,
|
|
139
|
+
arguments: tc.function.arguments
|
|
140
|
+
}
|
|
141
|
+
}))
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (m.role === "assistant") {
|
|
145
|
+
const msg = {
|
|
146
|
+
role: "assistant",
|
|
147
|
+
content: m.content || ""
|
|
148
|
+
};
|
|
149
|
+
if (m.reasoning_content !== void 0) {
|
|
150
|
+
msg.reasoning_content = m.reasoning_content;
|
|
151
|
+
}
|
|
152
|
+
return msg;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
role: m.role,
|
|
156
|
+
content: m.content || ""
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
const kimiTools = tools.map((t2) => ({
|
|
160
|
+
type: "function",
|
|
161
|
+
function: {
|
|
162
|
+
name: t2.name,
|
|
163
|
+
description: t2.description,
|
|
164
|
+
parameters: t2.parameters
|
|
165
|
+
}
|
|
166
|
+
}));
|
|
167
|
+
const body = {
|
|
168
|
+
model: config3.model,
|
|
169
|
+
messages: kimiMessages,
|
|
170
|
+
stream: true,
|
|
171
|
+
stream_options: { include_usage: true },
|
|
172
|
+
temperature: 1,
|
|
173
|
+
max_tokens: 8192
|
|
174
|
+
};
|
|
175
|
+
if (kimiTools.length > 0) {
|
|
176
|
+
body.tools = kimiTools;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: {
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
"Authorization": `Bearer ${config3.apiKey}`
|
|
184
|
+
},
|
|
185
|
+
body: JSON.stringify(body)
|
|
186
|
+
});
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const errText = await response.text();
|
|
189
|
+
yield { type: "error", error: `${response.status} ${errText}` };
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const reader = response.body?.getReader();
|
|
193
|
+
if (!reader) {
|
|
194
|
+
yield { type: "error", error: "No response stream" };
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const decoder = new TextDecoder();
|
|
198
|
+
let buffer = "";
|
|
199
|
+
const toolCallAccumulator = /* @__PURE__ */ new Map();
|
|
200
|
+
let tokenUsage;
|
|
201
|
+
let reasoningContent = "";
|
|
202
|
+
while (true) {
|
|
203
|
+
const { done, value } = await reader.read();
|
|
204
|
+
if (done) break;
|
|
205
|
+
buffer += decoder.decode(value, { stream: true });
|
|
206
|
+
const lines = buffer.split("\n");
|
|
207
|
+
buffer = lines.pop() || "";
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
const trimmed = line.trim();
|
|
210
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
211
|
+
const data = trimmed.slice(6);
|
|
212
|
+
if (data === "[DONE]") continue;
|
|
213
|
+
let parsed;
|
|
214
|
+
try {
|
|
215
|
+
parsed = JSON.parse(data);
|
|
216
|
+
} catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (parsed.usage) {
|
|
220
|
+
tokenUsage = {
|
|
221
|
+
promptTokens: parsed.usage.prompt_tokens || 0,
|
|
222
|
+
completionTokens: parsed.usage.completion_tokens || 0,
|
|
223
|
+
totalTokens: parsed.usage.total_tokens || 0
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const choice = parsed.choices?.[0];
|
|
227
|
+
if (!choice) continue;
|
|
228
|
+
const delta = choice.delta || {};
|
|
229
|
+
if (delta.reasoning_content) {
|
|
230
|
+
reasoningContent += delta.reasoning_content;
|
|
231
|
+
}
|
|
232
|
+
if (delta.content) {
|
|
233
|
+
yield { type: "text", content: delta.content };
|
|
234
|
+
}
|
|
235
|
+
if (delta.tool_calls) {
|
|
236
|
+
for (const tc of delta.tool_calls) {
|
|
237
|
+
const idx = tc.index ?? 0;
|
|
238
|
+
if (!toolCallAccumulator.has(idx)) {
|
|
239
|
+
toolCallAccumulator.set(idx, { id: tc.id || "", functionName: tc.function?.name || "", arguments: "" });
|
|
240
|
+
}
|
|
241
|
+
const acc = toolCallAccumulator.get(idx);
|
|
242
|
+
if (tc.id) acc.id = tc.id;
|
|
243
|
+
if (tc.function?.name) acc.functionName = tc.function.name;
|
|
244
|
+
if (tc.function?.arguments) acc.arguments += tc.function.arguments;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (choice.finish_reason === "tool_calls" || choice.finish_reason === "stop") {
|
|
248
|
+
for (const [_, acc] of toolCallAccumulator) {
|
|
249
|
+
const toolCall = {
|
|
250
|
+
id: acc.id,
|
|
251
|
+
type: "function",
|
|
252
|
+
function: { name: acc.functionName, arguments: acc.arguments }
|
|
253
|
+
};
|
|
254
|
+
yield { type: "tool_call", toolCall };
|
|
255
|
+
}
|
|
256
|
+
yield {
|
|
257
|
+
type: "done",
|
|
258
|
+
usage: tokenUsage,
|
|
259
|
+
content: reasoningContent || void 0
|
|
260
|
+
};
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const [_, acc] of toolCallAccumulator) {
|
|
266
|
+
yield { type: "tool_call", toolCall: { id: acc.id, type: "function", function: { name: acc.functionName, arguments: acc.arguments } } };
|
|
267
|
+
}
|
|
268
|
+
yield { type: "done", usage: tokenUsage, content: reasoningContent || void 0 };
|
|
269
|
+
} catch (err) {
|
|
270
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
271
|
+
yield { type: "error", error: msg };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// src/core/providers/anthropic.ts
|
|
277
|
+
var AnthropicProvider = class {
|
|
278
|
+
name = "anthropic";
|
|
279
|
+
async *chatStream(messages, tools, config3) {
|
|
280
|
+
const baseUrl = config3.baseUrl || "https://api.anthropic.com";
|
|
281
|
+
const systemMessage = messages.find((m) => m.role === "system");
|
|
282
|
+
const nonSystemMessages = messages.filter((m) => m.role !== "system");
|
|
283
|
+
const anthropicMessages = nonSystemMessages.map((m) => {
|
|
284
|
+
if (m.role === "assistant" && m.tool_calls) {
|
|
285
|
+
return {
|
|
286
|
+
role: "assistant",
|
|
287
|
+
content: [
|
|
288
|
+
...m.content ? [{ type: "text", text: m.content }] : [],
|
|
289
|
+
...m.tool_calls.map((tc) => ({
|
|
290
|
+
type: "tool_use",
|
|
291
|
+
id: tc.id,
|
|
292
|
+
name: tc.function.name,
|
|
293
|
+
input: JSON.parse(tc.function.arguments)
|
|
294
|
+
}))
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (m.role === "tool") {
|
|
299
|
+
return {
|
|
300
|
+
role: "user",
|
|
301
|
+
content: [{
|
|
302
|
+
type: "tool_result",
|
|
303
|
+
tool_use_id: m.tool_call_id,
|
|
304
|
+
content: m.content || ""
|
|
305
|
+
}]
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return { role: m.role, content: m.content || "" };
|
|
309
|
+
});
|
|
310
|
+
const anthropicTools = tools.map((t2) => ({
|
|
311
|
+
name: t2.name,
|
|
312
|
+
description: t2.description,
|
|
313
|
+
input_schema: t2.parameters
|
|
314
|
+
}));
|
|
315
|
+
const body = {
|
|
316
|
+
model: config3.model,
|
|
317
|
+
max_tokens: 8192,
|
|
318
|
+
system: systemMessage?.content || "",
|
|
319
|
+
messages: anthropicMessages,
|
|
320
|
+
tools: anthropicTools.length > 0 ? anthropicTools : void 0,
|
|
321
|
+
stream: true,
|
|
322
|
+
temperature: 0.3
|
|
323
|
+
};
|
|
324
|
+
try {
|
|
325
|
+
const response = await fetch(`${baseUrl}/v1/messages`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: {
|
|
328
|
+
"Content-Type": "application/json",
|
|
329
|
+
"x-api-key": config3.apiKey,
|
|
330
|
+
"anthropic-version": "2023-06-01"
|
|
331
|
+
},
|
|
332
|
+
body: JSON.stringify(body)
|
|
333
|
+
});
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
const errorText = await response.text();
|
|
336
|
+
yield { type: "error", error: `Anthropic API error (${response.status}): ${errorText}` };
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const reader = response.body?.getReader();
|
|
340
|
+
if (!reader) {
|
|
341
|
+
yield { type: "error", error: "No response stream available" };
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const decoder = new TextDecoder();
|
|
345
|
+
let buffer = "";
|
|
346
|
+
let currentToolId = "";
|
|
347
|
+
let currentToolName = "";
|
|
348
|
+
let currentToolArgs = "";
|
|
349
|
+
while (true) {
|
|
350
|
+
const { done, value } = await reader.read();
|
|
351
|
+
if (done) break;
|
|
352
|
+
buffer += decoder.decode(value, { stream: true });
|
|
353
|
+
const lines = buffer.split("\n");
|
|
354
|
+
buffer = lines.pop() || "";
|
|
355
|
+
for (const line of lines) {
|
|
356
|
+
if (!line.startsWith("data: ")) continue;
|
|
357
|
+
const data = line.slice(6).trim();
|
|
358
|
+
if (data === "[DONE]") continue;
|
|
359
|
+
try {
|
|
360
|
+
const event = JSON.parse(data);
|
|
361
|
+
if (event.type === "content_block_start") {
|
|
362
|
+
if (event.content_block?.type === "tool_use") {
|
|
363
|
+
currentToolId = event.content_block.id;
|
|
364
|
+
currentToolName = event.content_block.name;
|
|
365
|
+
currentToolArgs = "";
|
|
366
|
+
}
|
|
367
|
+
} else if (event.type === "content_block_delta") {
|
|
368
|
+
if (event.delta?.type === "text_delta") {
|
|
369
|
+
yield { type: "text", content: event.delta.text };
|
|
370
|
+
} else if (event.delta?.type === "input_json_delta") {
|
|
371
|
+
currentToolArgs += event.delta.partial_json || "";
|
|
372
|
+
}
|
|
373
|
+
} else if (event.type === "content_block_stop") {
|
|
374
|
+
if (currentToolId) {
|
|
375
|
+
const toolCall = {
|
|
376
|
+
id: currentToolId,
|
|
377
|
+
type: "function",
|
|
378
|
+
function: {
|
|
379
|
+
name: currentToolName,
|
|
380
|
+
arguments: currentToolArgs || "{}"
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
yield { type: "tool_call", toolCall };
|
|
384
|
+
currentToolId = "";
|
|
385
|
+
currentToolName = "";
|
|
386
|
+
currentToolArgs = "";
|
|
387
|
+
}
|
|
388
|
+
} else if (event.type === "message_stop") {
|
|
389
|
+
yield { type: "done" };
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
yield { type: "done" };
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
399
|
+
yield { type: "error", error: msg };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// src/core/providers/types.ts
|
|
405
|
+
var MODEL_PROVIDERS = {
|
|
406
|
+
// Kimi (月之暗面 Moonshot)
|
|
407
|
+
"kimi-k2.5": { provider: "kimi", displayName: "Kimi K2.5 (\u6700\u65B0\u591A\u6A21\u6001)" },
|
|
408
|
+
"kimi-k2-0905-preview": { provider: "kimi", displayName: "Kimi K2" },
|
|
409
|
+
"moonshot-v1-auto": { provider: "kimi", displayName: "Moonshot v1 Auto" },
|
|
410
|
+
// OpenAI
|
|
411
|
+
"gpt-4o": { provider: "openai", displayName: "GPT-4o" },
|
|
412
|
+
"gpt-4o-mini": { provider: "openai", displayName: "GPT-4o Mini" },
|
|
413
|
+
// Anthropic
|
|
414
|
+
"claude-sonnet-4-20250514": { provider: "anthropic", displayName: "Claude Sonnet 4" },
|
|
415
|
+
"claude-3-5-sonnet-20241022": { provider: "anthropic", displayName: "Claude 3.5 Sonnet" },
|
|
416
|
+
// DeepSeek
|
|
417
|
+
"deepseek-chat": { provider: "deepseek", displayName: "DeepSeek Chat" },
|
|
418
|
+
"deepseek-coder": { provider: "deepseek", displayName: "DeepSeek Coder" },
|
|
419
|
+
// Qwen (通义千问)
|
|
420
|
+
"qwen-turbo": { provider: "qwen", displayName: "\u901A\u4E49\u5343\u95EE Turbo" },
|
|
421
|
+
"qwen-plus": { provider: "qwen", displayName: "\u901A\u4E49\u5343\u95EE Plus" }
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// src/core/providers/index.ts
|
|
425
|
+
var providers = {
|
|
426
|
+
openai: new OpenAICompatibleProvider("openai"),
|
|
427
|
+
deepseek: new OpenAICompatibleProvider("deepseek"),
|
|
428
|
+
qwen: new OpenAICompatibleProvider("qwen"),
|
|
429
|
+
kimi: new OpenAICompatibleProvider("kimi"),
|
|
430
|
+
anthropic: new AnthropicProvider()
|
|
431
|
+
};
|
|
432
|
+
var DEFAULT_BASE_URLS = {
|
|
433
|
+
openai: "https://api.openai.com/v1",
|
|
434
|
+
deepseek: "https://api.deepseek.com",
|
|
435
|
+
qwen: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
436
|
+
kimi: "https://api.moonshot.cn/v1",
|
|
437
|
+
anthropic: "https://api.anthropic.com"
|
|
438
|
+
};
|
|
439
|
+
function getProvider(model) {
|
|
440
|
+
const info = MODEL_PROVIDERS[model];
|
|
441
|
+
if (!info) {
|
|
442
|
+
return providers.openai;
|
|
443
|
+
}
|
|
444
|
+
return providers[info.provider] || providers.openai;
|
|
445
|
+
}
|
|
446
|
+
function getProviderName(model) {
|
|
447
|
+
return MODEL_PROVIDERS[model]?.provider || "openai";
|
|
448
|
+
}
|
|
449
|
+
function getDefaultBaseUrl(providerName) {
|
|
450
|
+
return DEFAULT_BASE_URLS[providerName] || DEFAULT_BASE_URLS.openai;
|
|
451
|
+
}
|
|
452
|
+
function listModels() {
|
|
453
|
+
return Object.entries(MODEL_PROVIDERS).map(([id, info]) => ({
|
|
454
|
+
id,
|
|
455
|
+
name: info.displayName,
|
|
456
|
+
provider: info.provider
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/core/tools/file-read.ts
|
|
461
|
+
import fs from "fs/promises";
|
|
462
|
+
import path from "path";
|
|
463
|
+
import { glob } from "glob";
|
|
464
|
+
var readFileTool = {
|
|
465
|
+
definition: {
|
|
466
|
+
name: "read_file",
|
|
467
|
+
description: "Read the contents of a file. Returns the file content with line numbers.",
|
|
468
|
+
parameters: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
path: {
|
|
472
|
+
type: "string",
|
|
473
|
+
description: "Relative path to the file from the project root"
|
|
474
|
+
},
|
|
475
|
+
start_line: {
|
|
476
|
+
type: "number",
|
|
477
|
+
description: "Start line number (1-based). Optional."
|
|
478
|
+
},
|
|
479
|
+
end_line: {
|
|
480
|
+
type: "number",
|
|
481
|
+
description: "End line number (1-based, inclusive). Optional."
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
required: ["path"]
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
handler: async (args) => {
|
|
488
|
+
try {
|
|
489
|
+
const filePath = path.resolve(process.cwd(), args.path);
|
|
490
|
+
const stat = await fs.stat(filePath);
|
|
491
|
+
if (stat.size > 1024 * 1024) {
|
|
492
|
+
return {
|
|
493
|
+
success: false,
|
|
494
|
+
output: "",
|
|
495
|
+
error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Use start_line/end_line to read a portion.`
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
499
|
+
".png",
|
|
500
|
+
".jpg",
|
|
501
|
+
".jpeg",
|
|
502
|
+
".gif",
|
|
503
|
+
".ico",
|
|
504
|
+
".webp",
|
|
505
|
+
".svg",
|
|
506
|
+
".woff",
|
|
507
|
+
".woff2",
|
|
508
|
+
".ttf",
|
|
509
|
+
".eot",
|
|
510
|
+
".zip",
|
|
511
|
+
".tar",
|
|
512
|
+
".gz",
|
|
513
|
+
".7z",
|
|
514
|
+
".rar",
|
|
515
|
+
".exe",
|
|
516
|
+
".dll",
|
|
517
|
+
".so",
|
|
518
|
+
".dylib",
|
|
519
|
+
".pdf",
|
|
520
|
+
".doc",
|
|
521
|
+
".docx",
|
|
522
|
+
".xls",
|
|
523
|
+
".xlsx",
|
|
524
|
+
".mp3",
|
|
525
|
+
".mp4",
|
|
526
|
+
".wav",
|
|
527
|
+
".avi",
|
|
528
|
+
".mov"
|
|
529
|
+
]);
|
|
530
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
531
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
532
|
+
return {
|
|
533
|
+
success: false,
|
|
534
|
+
output: "",
|
|
535
|
+
error: `Binary file (${ext}) cannot be read as text.`
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
539
|
+
const lines = content.split("\n");
|
|
540
|
+
const start = Math.max(1, args.start_line || 1);
|
|
541
|
+
const end = Math.min(lines.length, args.end_line || lines.length);
|
|
542
|
+
const effectiveEnd = !args.start_line && !args.end_line && lines.length > 500 ? 500 : end;
|
|
543
|
+
const numbered = lines.slice(start - 1, effectiveEnd).map((line, i) => `${String(start + i).padStart(5)}| ${line}`).join("\n");
|
|
544
|
+
let output = `File: ${args.path} (${lines.length} lines)
|
|
545
|
+
${numbered}`;
|
|
546
|
+
if (effectiveEnd < lines.length && !args.end_line) {
|
|
547
|
+
output += `
|
|
548
|
+
... (${lines.length - effectiveEnd} more lines, use start_line/end_line to read more)`;
|
|
549
|
+
}
|
|
550
|
+
return { success: true, output };
|
|
551
|
+
} catch (err) {
|
|
552
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
553
|
+
if (msg.includes("ENOENT")) {
|
|
554
|
+
return { success: false, output: "", error: `File not found: ${args.path}` };
|
|
555
|
+
}
|
|
556
|
+
if (msg.includes("EACCES")) {
|
|
557
|
+
return { success: false, output: "", error: `Permission denied: ${args.path}` };
|
|
558
|
+
}
|
|
559
|
+
return { success: false, output: "", error: `Failed to read file: ${msg}` };
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
requiresConfirmation: false
|
|
563
|
+
};
|
|
564
|
+
var listFilesTool = {
|
|
565
|
+
definition: {
|
|
566
|
+
name: "list_files",
|
|
567
|
+
description: "List files and directories in a path. Returns a tree-like structure.",
|
|
568
|
+
parameters: {
|
|
569
|
+
type: "object",
|
|
570
|
+
properties: {
|
|
571
|
+
path: {
|
|
572
|
+
type: "string",
|
|
573
|
+
description: 'Relative directory path to list. Defaults to "."'
|
|
574
|
+
},
|
|
575
|
+
pattern: {
|
|
576
|
+
type: "string",
|
|
577
|
+
description: 'Glob pattern to filter files. E.g. "**/*.ts"'
|
|
578
|
+
},
|
|
579
|
+
max_depth: {
|
|
580
|
+
type: "number",
|
|
581
|
+
description: "Maximum directory depth. Default 3."
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
required: []
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
handler: async (args) => {
|
|
588
|
+
try {
|
|
589
|
+
const targetPath = path.resolve(process.cwd(), args.path || ".");
|
|
590
|
+
const pattern = args.pattern || "**/*";
|
|
591
|
+
const maxDepth = args.max_depth || 3;
|
|
592
|
+
const files = await glob(pattern, {
|
|
593
|
+
cwd: targetPath,
|
|
594
|
+
maxDepth,
|
|
595
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", ".next/**", ".nuxt/**", "coverage/**"],
|
|
596
|
+
mark: true
|
|
597
|
+
});
|
|
598
|
+
if (files.length === 0) {
|
|
599
|
+
return { success: true, output: "No files found matching the pattern." };
|
|
600
|
+
}
|
|
601
|
+
const sorted = files.sort();
|
|
602
|
+
const output = `Directory: ${args.path || "."}
|
|
603
|
+
Files (${sorted.length}):
|
|
604
|
+
${sorted.map((f) => ` ${f}`).join("\n")}`;
|
|
605
|
+
return { success: true, output };
|
|
606
|
+
} catch (err) {
|
|
607
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
608
|
+
return { success: false, output: "", error: `Failed to list files: ${msg}` };
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
requiresConfirmation: false
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// src/core/tools/file-write.ts
|
|
615
|
+
import fs2 from "fs/promises";
|
|
616
|
+
import path2 from "path";
|
|
617
|
+
var writeFileTool = {
|
|
618
|
+
definition: {
|
|
619
|
+
name: "write_file",
|
|
620
|
+
description: "Create or overwrite a file with the given content. Parent directories are created automatically.",
|
|
621
|
+
parameters: {
|
|
622
|
+
type: "object",
|
|
623
|
+
properties: {
|
|
624
|
+
path: {
|
|
625
|
+
type: "string",
|
|
626
|
+
description: "Relative path to the file from the project root"
|
|
627
|
+
},
|
|
628
|
+
content: {
|
|
629
|
+
type: "string",
|
|
630
|
+
description: "The full content to write to the file"
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
required: ["path", "content"]
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
handler: async (args) => {
|
|
637
|
+
try {
|
|
638
|
+
const filePath = path2.resolve(process.cwd(), args.path);
|
|
639
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
640
|
+
await fs2.writeFile(filePath, args.content, "utf-8");
|
|
641
|
+
const lines = args.content.split("\n").length;
|
|
642
|
+
return {
|
|
643
|
+
success: true,
|
|
644
|
+
output: `File written: ${args.path} (${lines} lines)`
|
|
645
|
+
};
|
|
646
|
+
} catch (err) {
|
|
647
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
648
|
+
return { success: false, output: "", error: `Failed to write file: ${msg}` };
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
requiresConfirmation: true
|
|
652
|
+
};
|
|
653
|
+
var editFileTool = {
|
|
654
|
+
definition: {
|
|
655
|
+
name: "edit_file",
|
|
656
|
+
description: "Edit a file by replacing a specific string with new content. The old_string must match exactly (including whitespace and indentation).",
|
|
657
|
+
parameters: {
|
|
658
|
+
type: "object",
|
|
659
|
+
properties: {
|
|
660
|
+
path: {
|
|
661
|
+
type: "string",
|
|
662
|
+
description: "Relative path to the file"
|
|
663
|
+
},
|
|
664
|
+
old_string: {
|
|
665
|
+
type: "string",
|
|
666
|
+
description: "The exact string to find and replace"
|
|
667
|
+
},
|
|
668
|
+
new_string: {
|
|
669
|
+
type: "string",
|
|
670
|
+
description: "The replacement string"
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
required: ["path", "old_string", "new_string"]
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
handler: async (args) => {
|
|
677
|
+
try {
|
|
678
|
+
const filePath = path2.resolve(process.cwd(), args.path);
|
|
679
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
680
|
+
const oldStr = args.old_string;
|
|
681
|
+
const newStr = args.new_string;
|
|
682
|
+
if (!content.includes(oldStr)) {
|
|
683
|
+
return {
|
|
684
|
+
success: false,
|
|
685
|
+
output: "",
|
|
686
|
+
error: `String not found in file. Make sure old_string matches exactly.`
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const count = content.split(oldStr).length - 1;
|
|
690
|
+
if (count > 1) {
|
|
691
|
+
return {
|
|
692
|
+
success: false,
|
|
693
|
+
output: "",
|
|
694
|
+
error: `Found ${count} occurrences of old_string. Please provide more context to uniquely identify the target.`
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const newContent = content.replace(oldStr, newStr);
|
|
698
|
+
await fs2.writeFile(filePath, newContent, "utf-8");
|
|
699
|
+
const addedLines = newStr.split("\n").length;
|
|
700
|
+
const removedLines = oldStr.split("\n").length;
|
|
701
|
+
return {
|
|
702
|
+
success: true,
|
|
703
|
+
output: `File edited: ${args.path} (+${addedLines} -${removedLines} lines)`
|
|
704
|
+
};
|
|
705
|
+
} catch (err) {
|
|
706
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
707
|
+
return { success: false, output: "", error: `Failed to edit file: ${msg}` };
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
requiresConfirmation: true
|
|
711
|
+
};
|
|
712
|
+
var deleteFileTool = {
|
|
713
|
+
definition: {
|
|
714
|
+
name: "delete_file",
|
|
715
|
+
description: "Delete a file from the project.",
|
|
716
|
+
parameters: {
|
|
717
|
+
type: "object",
|
|
718
|
+
properties: {
|
|
719
|
+
path: {
|
|
720
|
+
type: "string",
|
|
721
|
+
description: "Relative path to the file to delete"
|
|
722
|
+
}
|
|
723
|
+
},
|
|
724
|
+
required: ["path"]
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
handler: async (args) => {
|
|
728
|
+
try {
|
|
729
|
+
const filePath = path2.resolve(process.cwd(), args.path);
|
|
730
|
+
await fs2.unlink(filePath);
|
|
731
|
+
return { success: true, output: `File deleted: ${args.path}` };
|
|
732
|
+
} catch (err) {
|
|
733
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
734
|
+
return { success: false, output: "", error: `Failed to delete file: ${msg}` };
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
requiresConfirmation: true
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// src/core/tools/search.ts
|
|
741
|
+
import fs3 from "fs/promises";
|
|
742
|
+
import path3 from "path";
|
|
743
|
+
import { glob as glob2 } from "glob";
|
|
744
|
+
var searchCodeTool = {
|
|
745
|
+
definition: {
|
|
746
|
+
name: "search_code",
|
|
747
|
+
description: "Search for a pattern in project files. Returns matching lines with context. Similar to grep/ripgrep.",
|
|
748
|
+
parameters: {
|
|
749
|
+
type: "object",
|
|
750
|
+
properties: {
|
|
751
|
+
pattern: {
|
|
752
|
+
type: "string",
|
|
753
|
+
description: "Text or regex pattern to search for"
|
|
754
|
+
},
|
|
755
|
+
file_pattern: {
|
|
756
|
+
type: "string",
|
|
757
|
+
description: 'Glob pattern to filter files. E.g. "**/*.ts", "src/**/*.vue". Defaults to all files.'
|
|
758
|
+
},
|
|
759
|
+
case_sensitive: {
|
|
760
|
+
type: "string",
|
|
761
|
+
description: 'Whether search is case sensitive. "true" or "false". Default "true".'
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
required: ["pattern"]
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
handler: async (args) => {
|
|
768
|
+
try {
|
|
769
|
+
const searchPattern = args.pattern;
|
|
770
|
+
const filePattern = args.file_pattern || "**/*";
|
|
771
|
+
const caseSensitive = args.case_sensitive !== "false";
|
|
772
|
+
const files = await glob2(filePattern, {
|
|
773
|
+
cwd: process.cwd(),
|
|
774
|
+
nodir: true,
|
|
775
|
+
ignore: [
|
|
776
|
+
"node_modules/**",
|
|
777
|
+
".git/**",
|
|
778
|
+
"dist/**",
|
|
779
|
+
".next/**",
|
|
780
|
+
".nuxt/**",
|
|
781
|
+
"coverage/**",
|
|
782
|
+
"*.lock",
|
|
783
|
+
"package-lock.json",
|
|
784
|
+
"*.png",
|
|
785
|
+
"*.jpg",
|
|
786
|
+
"*.gif",
|
|
787
|
+
"*.ico",
|
|
788
|
+
"*.woff*",
|
|
789
|
+
"*.ttf"
|
|
790
|
+
]
|
|
791
|
+
});
|
|
792
|
+
const regex = new RegExp(
|
|
793
|
+
searchPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
|
|
794
|
+
caseSensitive ? "g" : "gi"
|
|
795
|
+
);
|
|
796
|
+
const results = [];
|
|
797
|
+
let matchCount = 0;
|
|
798
|
+
const maxResults = 50;
|
|
799
|
+
for (const file of files) {
|
|
800
|
+
if (matchCount >= maxResults) break;
|
|
801
|
+
try {
|
|
802
|
+
const filePath = path3.resolve(process.cwd(), file);
|
|
803
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
804
|
+
const lines = content.split("\n");
|
|
805
|
+
for (let i = 0; i < lines.length; i++) {
|
|
806
|
+
if (matchCount >= maxResults) break;
|
|
807
|
+
if (regex.test(lines[i])) {
|
|
808
|
+
results.push(`${file}:${i + 1}: ${lines[i].trim()}`);
|
|
809
|
+
matchCount++;
|
|
810
|
+
regex.lastIndex = 0;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
} catch {
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (results.length === 0) {
|
|
817
|
+
return { success: true, output: `No matches found for "${searchPattern}"` };
|
|
818
|
+
}
|
|
819
|
+
const output = `Found ${matchCount} matches${matchCount >= maxResults ? " (limited to 50)" : ""}:
|
|
820
|
+
${results.join("\n")}`;
|
|
821
|
+
return { success: true, output };
|
|
822
|
+
} catch (err) {
|
|
823
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
824
|
+
return { success: false, output: "", error: `Search failed: ${msg}` };
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
requiresConfirmation: false
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// src/core/tools/shell.ts
|
|
831
|
+
import { exec } from "child_process";
|
|
832
|
+
var MAX_OUTPUT_LENGTH = 1e4;
|
|
833
|
+
var shellTool = {
|
|
834
|
+
definition: {
|
|
835
|
+
name: "run_command",
|
|
836
|
+
description: "Execute a shell command in the project directory. Use for installing packages, running scripts, building, testing, etc. Requires user confirmation.",
|
|
837
|
+
parameters: {
|
|
838
|
+
type: "object",
|
|
839
|
+
properties: {
|
|
840
|
+
command: {
|
|
841
|
+
type: "string",
|
|
842
|
+
description: "The shell command to execute"
|
|
843
|
+
},
|
|
844
|
+
working_directory: {
|
|
845
|
+
type: "string",
|
|
846
|
+
description: "Working directory relative to project root. Optional."
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
required: ["command"]
|
|
850
|
+
}
|
|
851
|
+
},
|
|
852
|
+
handler: async (args) => {
|
|
853
|
+
const command = args.command;
|
|
854
|
+
const cwd = args.working_directory ? `${process.cwd()}/${args.working_directory}` : process.cwd();
|
|
855
|
+
return new Promise((resolve) => {
|
|
856
|
+
const child = exec(command, {
|
|
857
|
+
cwd,
|
|
858
|
+
timeout: 6e4,
|
|
859
|
+
// 60s timeout
|
|
860
|
+
maxBuffer: 1024 * 1024 * 5,
|
|
861
|
+
// 5MB
|
|
862
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
863
|
+
}, (error, stdout, stderr) => {
|
|
864
|
+
let output = "";
|
|
865
|
+
if (stdout) output += stdout;
|
|
866
|
+
if (stderr) output += (output ? "\n" : "") + stderr;
|
|
867
|
+
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
868
|
+
output = output.substring(0, MAX_OUTPUT_LENGTH) + `
|
|
869
|
+
... (output truncated, ${output.length} chars total)`;
|
|
870
|
+
}
|
|
871
|
+
if (error) {
|
|
872
|
+
resolve({
|
|
873
|
+
success: false,
|
|
874
|
+
output: output || error.message,
|
|
875
|
+
error: `Command failed with exit code ${error.code}: ${error.message}`
|
|
876
|
+
});
|
|
877
|
+
} else {
|
|
878
|
+
resolve({
|
|
879
|
+
success: true,
|
|
880
|
+
output: output || "(no output)"
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
},
|
|
886
|
+
requiresConfirmation: true
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/core/tools/git.ts
|
|
890
|
+
import simpleGit from "simple-git";
|
|
891
|
+
function getGit() {
|
|
892
|
+
return simpleGit(process.cwd());
|
|
893
|
+
}
|
|
894
|
+
var gitStatusTool = {
|
|
895
|
+
definition: {
|
|
896
|
+
name: "git_status",
|
|
897
|
+
description: "Show the current git status including staged, unstaged, and untracked files.",
|
|
898
|
+
parameters: {
|
|
899
|
+
type: "object",
|
|
900
|
+
properties: {},
|
|
901
|
+
required: []
|
|
902
|
+
}
|
|
903
|
+
},
|
|
904
|
+
handler: async () => {
|
|
905
|
+
try {
|
|
906
|
+
const git = getGit();
|
|
907
|
+
const status = await git.status();
|
|
908
|
+
const lines = [];
|
|
909
|
+
if (status.staged.length > 0) {
|
|
910
|
+
lines.push("Staged:");
|
|
911
|
+
status.staged.forEach((f) => lines.push(` + ${f}`));
|
|
912
|
+
}
|
|
913
|
+
if (status.modified.length > 0) {
|
|
914
|
+
lines.push("Modified:");
|
|
915
|
+
status.modified.forEach((f) => lines.push(` ~ ${f}`));
|
|
916
|
+
}
|
|
917
|
+
if (status.not_added.length > 0) {
|
|
918
|
+
lines.push("Untracked:");
|
|
919
|
+
status.not_added.forEach((f) => lines.push(` ? ${f}`));
|
|
920
|
+
}
|
|
921
|
+
if (status.deleted.length > 0) {
|
|
922
|
+
lines.push("Deleted:");
|
|
923
|
+
status.deleted.forEach((f) => lines.push(` - ${f}`));
|
|
924
|
+
}
|
|
925
|
+
if (lines.length === 0) {
|
|
926
|
+
lines.push("Working directory clean.");
|
|
927
|
+
}
|
|
928
|
+
lines.push(`
|
|
929
|
+
Branch: ${status.current || "HEAD detached"}`);
|
|
930
|
+
if (status.ahead > 0) lines.push(`Ahead: ${status.ahead} commit(s)`);
|
|
931
|
+
if (status.behind > 0) lines.push(`Behind: ${status.behind} commit(s)`);
|
|
932
|
+
return { success: true, output: lines.join("\n") };
|
|
933
|
+
} catch (err) {
|
|
934
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
935
|
+
return { success: false, output: "", error: `Git status failed: ${msg}` };
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
requiresConfirmation: false
|
|
939
|
+
};
|
|
940
|
+
var gitDiffTool = {
|
|
941
|
+
definition: {
|
|
942
|
+
name: "git_diff",
|
|
943
|
+
description: "Show git diff of changes. Can diff staged or unstaged changes, or between commits.",
|
|
944
|
+
parameters: {
|
|
945
|
+
type: "object",
|
|
946
|
+
properties: {
|
|
947
|
+
staged: {
|
|
948
|
+
type: "string",
|
|
949
|
+
description: 'If "true", show staged changes. Default shows unstaged.'
|
|
950
|
+
},
|
|
951
|
+
file: {
|
|
952
|
+
type: "string",
|
|
953
|
+
description: "Specific file to diff. Optional."
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
required: []
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
handler: async (args) => {
|
|
960
|
+
try {
|
|
961
|
+
const git = getGit();
|
|
962
|
+
const isStaged = args.staged === "true";
|
|
963
|
+
const file = args.file;
|
|
964
|
+
const diffArgs = isStaged ? ["--cached"] : [];
|
|
965
|
+
if (file) diffArgs.push("--", file);
|
|
966
|
+
const diff = await git.diff(diffArgs);
|
|
967
|
+
if (!diff) {
|
|
968
|
+
return { success: true, output: "No changes." };
|
|
969
|
+
}
|
|
970
|
+
const maxLen = 5e3;
|
|
971
|
+
const output = diff.length > maxLen ? diff.substring(0, maxLen) + `
|
|
972
|
+
... (diff truncated, ${diff.length} chars total)` : diff;
|
|
973
|
+
return { success: true, output };
|
|
974
|
+
} catch (err) {
|
|
975
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
976
|
+
return { success: false, output: "", error: `Git diff failed: ${msg}` };
|
|
977
|
+
}
|
|
978
|
+
},
|
|
979
|
+
requiresConfirmation: false
|
|
980
|
+
};
|
|
981
|
+
var gitCommitTool = {
|
|
982
|
+
definition: {
|
|
983
|
+
name: "git_commit",
|
|
984
|
+
description: "Stage files and create a git commit. If no files specified, stages all changes.",
|
|
985
|
+
parameters: {
|
|
986
|
+
type: "object",
|
|
987
|
+
properties: {
|
|
988
|
+
message: {
|
|
989
|
+
type: "string",
|
|
990
|
+
description: "Commit message"
|
|
991
|
+
},
|
|
992
|
+
files: {
|
|
993
|
+
type: "array",
|
|
994
|
+
description: "Files to stage. If empty, stages all changes.",
|
|
995
|
+
items: { type: "string" }
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
required: ["message"]
|
|
999
|
+
}
|
|
1000
|
+
},
|
|
1001
|
+
handler: async (args) => {
|
|
1002
|
+
try {
|
|
1003
|
+
const git = getGit();
|
|
1004
|
+
const message = args.message;
|
|
1005
|
+
const files = args.files;
|
|
1006
|
+
if (files && files.length > 0) {
|
|
1007
|
+
await git.add(files);
|
|
1008
|
+
} else {
|
|
1009
|
+
await git.add(".");
|
|
1010
|
+
}
|
|
1011
|
+
const result = await git.commit(message);
|
|
1012
|
+
return {
|
|
1013
|
+
success: true,
|
|
1014
|
+
output: `Committed: ${result.commit}
|
|
1015
|
+
${result.summary.changes} file(s) changed, ${result.summary.insertions} insertions, ${result.summary.deletions} deletions`
|
|
1016
|
+
};
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1019
|
+
return { success: false, output: "", error: `Git commit failed: ${msg}` };
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
requiresConfirmation: true
|
|
1023
|
+
};
|
|
1024
|
+
var gitLogTool = {
|
|
1025
|
+
definition: {
|
|
1026
|
+
name: "git_log",
|
|
1027
|
+
description: "Show recent git commit history.",
|
|
1028
|
+
parameters: {
|
|
1029
|
+
type: "object",
|
|
1030
|
+
properties: {
|
|
1031
|
+
count: {
|
|
1032
|
+
type: "number",
|
|
1033
|
+
description: "Number of commits to show. Default 10."
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
required: []
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
handler: async (args) => {
|
|
1040
|
+
try {
|
|
1041
|
+
const git = getGit();
|
|
1042
|
+
const count = args.count || 10;
|
|
1043
|
+
const log = await git.log({ maxCount: count });
|
|
1044
|
+
const lines = log.all.map(
|
|
1045
|
+
(c) => `${c.hash.substring(0, 7)} ${c.date.substring(0, 10)} ${c.message}`
|
|
1046
|
+
);
|
|
1047
|
+
return {
|
|
1048
|
+
success: true,
|
|
1049
|
+
output: lines.length > 0 ? lines.join("\n") : "No commits yet."
|
|
1050
|
+
};
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1053
|
+
return { success: false, output: "", error: `Git log failed: ${msg}` };
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
requiresConfirmation: false
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// src/ui/renderer.ts
|
|
1060
|
+
import { Marked } from "marked";
|
|
1061
|
+
import { markedTerminal } from "marked-terminal";
|
|
1062
|
+
|
|
1063
|
+
// src/ui/theme.ts
|
|
1064
|
+
import chalk from "chalk";
|
|
1065
|
+
var theme = {
|
|
1066
|
+
// Brand colors
|
|
1067
|
+
brand: chalk.hex("#A78BFA"),
|
|
1068
|
+
brandBold: chalk.hex("#A78BFA").bold,
|
|
1069
|
+
brandDim: chalk.hex("#7C3AED"),
|
|
1070
|
+
// Status
|
|
1071
|
+
success: chalk.hex("#34D399"),
|
|
1072
|
+
error: chalk.hex("#F87171"),
|
|
1073
|
+
warning: chalk.hex("#FBBF24"),
|
|
1074
|
+
info: chalk.hex("#60A5FA"),
|
|
1075
|
+
// Text
|
|
1076
|
+
dim: chalk.gray,
|
|
1077
|
+
bold: chalk.bold,
|
|
1078
|
+
italic: chalk.italic,
|
|
1079
|
+
muted: chalk.hex("#6B7280"),
|
|
1080
|
+
// Code
|
|
1081
|
+
code: chalk.bgHex("#1F2937").hex("#E5E7EB"),
|
|
1082
|
+
filePath: chalk.underline.hex("#60A5FA"),
|
|
1083
|
+
// Roles
|
|
1084
|
+
user: chalk.hex("#34D399").bold,
|
|
1085
|
+
assistant: chalk.hex("#A78BFA").bold,
|
|
1086
|
+
system: chalk.gray,
|
|
1087
|
+
tool: chalk.hex("#FBBF24"),
|
|
1088
|
+
// Token stats
|
|
1089
|
+
tokenLabel: chalk.hex("#6B7280"),
|
|
1090
|
+
tokenValue: chalk.hex("#A78BFA")
|
|
1091
|
+
};
|
|
1092
|
+
function banner() {
|
|
1093
|
+
const line = theme.brandDim("\u2501".repeat(46));
|
|
1094
|
+
const border = theme.brandDim("\u2503");
|
|
1095
|
+
const topLeft = theme.brandDim("\u250F");
|
|
1096
|
+
const topRight = theme.brandDim("\u2513");
|
|
1097
|
+
const botLeft = theme.brandDim("\u2517");
|
|
1098
|
+
const botRight = theme.brandDim("\u251B");
|
|
1099
|
+
return `
|
|
1100
|
+
${topLeft}${line}${topRight}
|
|
1101
|
+
${border} ${border}
|
|
1102
|
+
${border} ${theme.brandBold("\u6C88\u7FD4\u7684AI\u52A9\u624B")} ${theme.muted("v0.1.0")} ${border}
|
|
1103
|
+
${border} ${theme.dim("\u7EC8\u7AEF\u91CC\u7684AI\u5168\u6808\u5F00\u53D1\u642D\u6863")} ${border}
|
|
1104
|
+
${border} ${border}
|
|
1105
|
+
${botLeft}${line}${botRight}`;
|
|
1106
|
+
}
|
|
1107
|
+
function separator() {
|
|
1108
|
+
return theme.dim("\u2500".repeat(46));
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/ui/renderer.ts
|
|
1112
|
+
var marked = new Marked(markedTerminal());
|
|
1113
|
+
function streamChunk(text) {
|
|
1114
|
+
process.stdout.write(text);
|
|
1115
|
+
}
|
|
1116
|
+
function printToolCall(name, description) {
|
|
1117
|
+
console.log(theme.tool(` \u25B8 ${description}`));
|
|
1118
|
+
}
|
|
1119
|
+
function printToolResult(result, truncate = 500) {
|
|
1120
|
+
const display = result.length > truncate ? result.substring(0, truncate) + theme.dim(`
|
|
1121
|
+
... (${result.length} \u5B57\u7B26)`) : result;
|
|
1122
|
+
const indented = display.split("\n").map((l) => " " + l).join("\n");
|
|
1123
|
+
console.log(theme.dim(indented));
|
|
1124
|
+
}
|
|
1125
|
+
function printTokenUsage(usage, durationMs) {
|
|
1126
|
+
const parts = [];
|
|
1127
|
+
parts.push(theme.tokenLabel(" tokens: "));
|
|
1128
|
+
parts.push(theme.tokenValue(`\u2191${usage.promptTokens}`));
|
|
1129
|
+
parts.push(theme.tokenLabel(" + "));
|
|
1130
|
+
parts.push(theme.tokenValue(`\u2193${usage.completionTokens}`));
|
|
1131
|
+
parts.push(theme.tokenLabel(" = "));
|
|
1132
|
+
parts.push(theme.brandBold(`${usage.totalTokens}`));
|
|
1133
|
+
if (durationMs && durationMs > 0) {
|
|
1134
|
+
const seconds = (durationMs / 1e3).toFixed(1);
|
|
1135
|
+
const tokPerSec = usage.completionTokens > 0 ? (usage.completionTokens / (durationMs / 1e3)).toFixed(0) : "0";
|
|
1136
|
+
parts.push(theme.tokenLabel(` \u23F1 ${seconds}s`));
|
|
1137
|
+
parts.push(theme.tokenLabel(` (${tokPerSec} tok/s)`));
|
|
1138
|
+
}
|
|
1139
|
+
console.log(parts.join(""));
|
|
1140
|
+
}
|
|
1141
|
+
function printError(message) {
|
|
1142
|
+
console.log(theme.error(` \u2717 ${message}`));
|
|
1143
|
+
}
|
|
1144
|
+
function printSuccess(message) {
|
|
1145
|
+
console.log(theme.success(` \u2713 ${message}`));
|
|
1146
|
+
}
|
|
1147
|
+
function printInfo(message) {
|
|
1148
|
+
console.log(theme.info(` \u2139 ${message}`));
|
|
1149
|
+
}
|
|
1150
|
+
function printWarning(message) {
|
|
1151
|
+
console.log(theme.warning(` \u26A0 ${message}`));
|
|
1152
|
+
}
|
|
1153
|
+
function printSeparator() {
|
|
1154
|
+
console.log(separator());
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// src/i18n/zh.ts
|
|
1158
|
+
var zh = {
|
|
1159
|
+
welcome: "\u6B22\u8FCE\u4F7F\u7528\u6C88\u7FD4\u7684AI\u52A9\u624B \u2014 \u4F60\u7684AI\u5168\u6808\u5F00\u53D1\u642D\u6863",
|
|
1160
|
+
description: "\u6C88\u7FD4\u7684AI\u52A9\u624B - \u7EC8\u7AEF\u91CC\u7684AI\u5168\u6808\u5F00\u53D1\u642D\u6863",
|
|
1161
|
+
prompt: "\u4F60: ",
|
|
1162
|
+
thinking: "AI \u601D\u8003\u4E2D...",
|
|
1163
|
+
exit: "\u518D\u89C1\uFF01\u795D\u7F16\u7801\u6109\u5FEB \u{1F680}",
|
|
1164
|
+
exitHint: "\u8F93\u5165 /exit \u9000\u51FA, /help \u67E5\u770B\u5E2E\u52A9",
|
|
1165
|
+
help: `
|
|
1166
|
+
\u53EF\u7528\u547D\u4EE4:
|
|
1167
|
+
/help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F
|
|
1168
|
+
/exit \u9000\u51FA DevPilot
|
|
1169
|
+
/clear \u6E05\u9664\u5BF9\u8BDD\u5386\u53F2
|
|
1170
|
+
/model \u5207\u6362AI\u6A21\u578B
|
|
1171
|
+
/status \u67E5\u770B\u9879\u76EE\u72B6\u6001
|
|
1172
|
+
/config \u67E5\u770B/\u4FEE\u6539\u914D\u7F6E
|
|
1173
|
+
/login \u767B\u5F55 DevPilot \u8D26\u6237
|
|
1174
|
+
/logout \u9000\u51FA\u767B\u5F55
|
|
1175
|
+
/usage \u67E5\u770B\u7528\u91CF\u7EDF\u8BA1
|
|
1176
|
+
|
|
1177
|
+
\u5FEB\u6377\u952E:
|
|
1178
|
+
Ctrl+C \u4E2D\u65AD\u5F53\u524D\u64CD\u4F5C
|
|
1179
|
+
Ctrl+D \u9000\u51FA DevPilot
|
|
1180
|
+
`,
|
|
1181
|
+
errors: {
|
|
1182
|
+
noApiKey: "\u672A\u914D\u7F6EAPI\u5BC6\u94A5\u3002\u8BF7\u8FD0\u884C devpilot config \u6216 devpilot login \u914D\u7F6E\u3002",
|
|
1183
|
+
networkError: "\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002",
|
|
1184
|
+
apiError: "AI\u670D\u52A1\u8FD4\u56DE\u9519\u8BEF: {message}",
|
|
1185
|
+
unknownCommand: "\u672A\u77E5\u547D\u4EE4: {command}\u3002\u8F93\u5165 /help \u67E5\u770B\u5E2E\u52A9\u3002",
|
|
1186
|
+
fileNotFound: "\u6587\u4EF6\u4E0D\u5B58\u5728: {path}",
|
|
1187
|
+
permissionDenied: "\u6743\u9650\u4E0D\u8DB3: {path}",
|
|
1188
|
+
tokenLimit: "\u5BF9\u8BDD\u4E0A\u4E0B\u6587\u8FC7\u957F\uFF0C\u5DF2\u81EA\u52A8\u88C1\u526A\u65E9\u671F\u6D88\u606F\u3002",
|
|
1189
|
+
rateLimited: "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
|
|
1190
|
+
notLoggedIn: "\u8BF7\u5148\u767B\u5F55: devpilot login",
|
|
1191
|
+
dailyLimitReached: "\u4ECA\u65E5\u514D\u8D39\u989D\u5EA6\u5DF2\u7528\u5B8C\u3002\u5347\u7EA7\u5230\u4ED8\u8D39\u7248\u83B7\u53D6\u66F4\u591A\u7528\u91CF\u3002"
|
|
1192
|
+
},
|
|
1193
|
+
tool: {
|
|
1194
|
+
readFile: "\u{1F4D6} \u8BFB\u53D6\u6587\u4EF6: {path}",
|
|
1195
|
+
writeFile: "\u{1F4DD} \u5199\u5165\u6587\u4EF6: {path}",
|
|
1196
|
+
editFile: "\u270F\uFE0F \u7F16\u8F91\u6587\u4EF6: {path}",
|
|
1197
|
+
deleteFile: "\u{1F5D1}\uFE0F \u5220\u9664\u6587\u4EF6: {path}",
|
|
1198
|
+
search: "\u{1F50D} \u641C\u7D22: {pattern}",
|
|
1199
|
+
shell: "\u{1F5A5}\uFE0F \u6267\u884C\u547D\u4EE4: {command}",
|
|
1200
|
+
shellConfirm: "\u662F\u5426\u5141\u8BB8\u6267\u884C\u6B64\u547D\u4EE4\uFF1F(y/n)",
|
|
1201
|
+
gitStatus: "\u{1F4CA} \u67E5\u770BGit\u72B6\u6001",
|
|
1202
|
+
gitDiff: "\u{1F4CB} \u67E5\u770BGit\u5DEE\u5F02",
|
|
1203
|
+
gitCommit: "\u{1F4BE} Git\u63D0\u4EA4: {message}",
|
|
1204
|
+
listFiles: "\u{1F4C1} \u5217\u51FA\u6587\u4EF6: {path}"
|
|
1205
|
+
},
|
|
1206
|
+
auth: {
|
|
1207
|
+
loginPrompt: "\u8BF7\u8F93\u5165\u4F60\u7684\u90AE\u7BB1\u5730\u5740:",
|
|
1208
|
+
passwordPrompt: "\u8BF7\u8F93\u5165\u5BC6\u7801:",
|
|
1209
|
+
loginSuccess: "\u767B\u5F55\u6210\u529F\uFF01\u6B22\u8FCE\u56DE\u6765, {email}",
|
|
1210
|
+
loginFailed: "\u767B\u5F55\u5931\u8D25: {message}",
|
|
1211
|
+
registerPrompt: "\u8BE5\u90AE\u7BB1\u5C1A\u672A\u6CE8\u518C\uFF0C\u662F\u5426\u7ACB\u5373\u6CE8\u518C\uFF1F",
|
|
1212
|
+
registerSuccess: "\u6CE8\u518C\u6210\u529F\uFF01\u6B22\u8FCE\u4F7F\u7528 DevPilot\u3002",
|
|
1213
|
+
logoutSuccess: "\u5DF2\u9000\u51FA\u767B\u5F55\u3002"
|
|
1214
|
+
},
|
|
1215
|
+
project: {
|
|
1216
|
+
detected: "\u68C0\u6D4B\u5230\u9879\u76EE\u7C7B\u578B: {type}",
|
|
1217
|
+
framework: "\u6846\u67B6: {framework}",
|
|
1218
|
+
noProject: "\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u9879\u76EE\u76EE\u5F55\uFF0C\u5C06\u4EE5\u901A\u7528\u6A21\u5F0F\u8FD0\u884C\u3002"
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
// src/i18n/en.ts
|
|
1223
|
+
var en = {
|
|
1224
|
+
welcome: "Welcome to ShenXiang's AI Assistant \u2014 Your AI Full-Stack Dev Companion",
|
|
1225
|
+
description: "ShenXiang's AI Assistant - AI full-stack dev companion in your terminal",
|
|
1226
|
+
prompt: "You: ",
|
|
1227
|
+
thinking: "AI thinking...",
|
|
1228
|
+
exit: "Goodbye! Happy coding \u{1F680}",
|
|
1229
|
+
exitHint: "Type /exit to quit, /help for help",
|
|
1230
|
+
help: `
|
|
1231
|
+
Available commands:
|
|
1232
|
+
/help Show help
|
|
1233
|
+
/exit Exit DevPilot
|
|
1234
|
+
/clear Clear conversation history
|
|
1235
|
+
/model Switch AI model
|
|
1236
|
+
/status Show project status
|
|
1237
|
+
/config View/edit configuration
|
|
1238
|
+
/login Login to DevPilot
|
|
1239
|
+
/logout Logout
|
|
1240
|
+
/usage Show usage stats
|
|
1241
|
+
|
|
1242
|
+
Shortcuts:
|
|
1243
|
+
Ctrl+C Interrupt current operation
|
|
1244
|
+
Ctrl+D Exit DevPilot
|
|
1245
|
+
`,
|
|
1246
|
+
errors: {
|
|
1247
|
+
noApiKey: "No API key configured. Run devpilot config or devpilot login.",
|
|
1248
|
+
networkError: "Network error. Please check your connection.",
|
|
1249
|
+
apiError: "AI service error: {message}",
|
|
1250
|
+
unknownCommand: "Unknown command: {command}. Type /help for help.",
|
|
1251
|
+
fileNotFound: "File not found: {path}",
|
|
1252
|
+
permissionDenied: "Permission denied: {path}",
|
|
1253
|
+
tokenLimit: "Conversation too long, early messages trimmed.",
|
|
1254
|
+
rateLimited: "Rate limited. Please wait a moment.",
|
|
1255
|
+
notLoggedIn: "Please login first: devpilot login",
|
|
1256
|
+
dailyLimitReached: "Daily free quota reached. Upgrade for more usage."
|
|
1257
|
+
},
|
|
1258
|
+
tool: {
|
|
1259
|
+
readFile: "\u{1F4D6} Reading file: {path}",
|
|
1260
|
+
writeFile: "\u{1F4DD} Writing file: {path}",
|
|
1261
|
+
editFile: "\u270F\uFE0F Editing file: {path}",
|
|
1262
|
+
deleteFile: "\u{1F5D1}\uFE0F Deleting file: {path}",
|
|
1263
|
+
search: "\u{1F50D} Searching: {pattern}",
|
|
1264
|
+
shell: "\u{1F5A5}\uFE0F Running command: {command}",
|
|
1265
|
+
shellConfirm: "Allow this command? (y/n)",
|
|
1266
|
+
gitStatus: "\u{1F4CA} Git status",
|
|
1267
|
+
gitDiff: "\u{1F4CB} Git diff",
|
|
1268
|
+
gitCommit: "\u{1F4BE} Git commit: {message}",
|
|
1269
|
+
listFiles: "\u{1F4C1} Listing files: {path}"
|
|
1270
|
+
},
|
|
1271
|
+
auth: {
|
|
1272
|
+
loginPrompt: "Enter your email:",
|
|
1273
|
+
passwordPrompt: "Enter your password:",
|
|
1274
|
+
loginSuccess: "Login successful! Welcome back, {email}",
|
|
1275
|
+
loginFailed: "Login failed: {message}",
|
|
1276
|
+
registerPrompt: "Email not registered. Register now?",
|
|
1277
|
+
registerSuccess: "Registration successful! Welcome to DevPilot.",
|
|
1278
|
+
logoutSuccess: "Logged out."
|
|
1279
|
+
},
|
|
1280
|
+
project: {
|
|
1281
|
+
detected: "Project type detected: {type}",
|
|
1282
|
+
framework: "Framework: {framework}",
|
|
1283
|
+
noProject: "Not a project directory. Running in general mode."
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// src/i18n/index.ts
|
|
1288
|
+
var locales = { zh, en };
|
|
1289
|
+
var currentLocale = "zh";
|
|
1290
|
+
function setLocale(locale) {
|
|
1291
|
+
currentLocale = locale;
|
|
1292
|
+
}
|
|
1293
|
+
function t(key, params) {
|
|
1294
|
+
const keys = key.split(".");
|
|
1295
|
+
let value = locales[currentLocale];
|
|
1296
|
+
for (const k of keys) {
|
|
1297
|
+
if (value && typeof value === "object" && k in value) {
|
|
1298
|
+
value = value[k];
|
|
1299
|
+
} else {
|
|
1300
|
+
return key;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof value !== "string") return key;
|
|
1304
|
+
if (!params) return value;
|
|
1305
|
+
return value.replace(/\{(\w+)\}/g, (_, k) => params[k] ?? `{${k}}`);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/utils/input.ts
|
|
1309
|
+
var _questionFn = null;
|
|
1310
|
+
function registerQuestionFn(fn) {
|
|
1311
|
+
_questionFn = fn;
|
|
1312
|
+
}
|
|
1313
|
+
async function askQuestion(prompt) {
|
|
1314
|
+
if (_questionFn) {
|
|
1315
|
+
return _questionFn(prompt);
|
|
1316
|
+
}
|
|
1317
|
+
return new Promise((resolve) => {
|
|
1318
|
+
process.stdout.write(prompt);
|
|
1319
|
+
const onData = (data) => {
|
|
1320
|
+
process.stdin.removeListener("data", onData);
|
|
1321
|
+
resolve(data.toString().trim());
|
|
1322
|
+
};
|
|
1323
|
+
process.stdin.once("data", onData);
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/core/tools/index.ts
|
|
1328
|
+
var toolRegistry = /* @__PURE__ */ new Map();
|
|
1329
|
+
function registerAllTools() {
|
|
1330
|
+
const tools = [
|
|
1331
|
+
readFileTool,
|
|
1332
|
+
listFilesTool,
|
|
1333
|
+
writeFileTool,
|
|
1334
|
+
editFileTool,
|
|
1335
|
+
deleteFileTool,
|
|
1336
|
+
searchCodeTool,
|
|
1337
|
+
shellTool,
|
|
1338
|
+
gitStatusTool,
|
|
1339
|
+
gitDiffTool,
|
|
1340
|
+
gitCommitTool,
|
|
1341
|
+
gitLogTool
|
|
1342
|
+
];
|
|
1343
|
+
for (const tool of tools) {
|
|
1344
|
+
toolRegistry.set(tool.definition.name, tool);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function getToolDefinitions() {
|
|
1348
|
+
return Array.from(toolRegistry.values()).map((t2) => t2.definition);
|
|
1349
|
+
}
|
|
1350
|
+
async function confirmExecution(toolName, args) {
|
|
1351
|
+
let description = toolName;
|
|
1352
|
+
if (toolName === "run_command") description = `\u6267\u884C\u547D\u4EE4: ${args.command}`;
|
|
1353
|
+
else if (toolName === "write_file") description = `\u5199\u5165\u6587\u4EF6: ${args.path}`;
|
|
1354
|
+
else if (toolName === "edit_file") description = `\u7F16\u8F91\u6587\u4EF6: ${args.path}`;
|
|
1355
|
+
else if (toolName === "delete_file") description = `\u5220\u9664\u6587\u4EF6: ${args.path}`;
|
|
1356
|
+
else if (toolName === "git_commit") description = `Git\u63D0\u4EA4: ${args.message}`;
|
|
1357
|
+
const answer = await askQuestion(` \u26A1 ${description}
|
|
1358
|
+
\u5141\u8BB8\u6267\u884C\uFF1F(y/n) `);
|
|
1359
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
1360
|
+
}
|
|
1361
|
+
async function executeTool(name, args, autoConfirm = false) {
|
|
1362
|
+
const tool = toolRegistry.get(name);
|
|
1363
|
+
if (!tool) {
|
|
1364
|
+
return { success: false, output: "", error: `Unknown tool: ${name}` };
|
|
1365
|
+
}
|
|
1366
|
+
const descMap = {
|
|
1367
|
+
read_file: t("tool.readFile", { path: String(args.path || "") }),
|
|
1368
|
+
list_files: t("tool.listFiles", { path: String(args.path || ".") }),
|
|
1369
|
+
write_file: t("tool.writeFile", { path: String(args.path || "") }),
|
|
1370
|
+
edit_file: t("tool.editFile", { path: String(args.path || "") }),
|
|
1371
|
+
delete_file: t("tool.deleteFile", { path: String(args.path || "") }),
|
|
1372
|
+
search_code: t("tool.search", { pattern: String(args.pattern || "") }),
|
|
1373
|
+
run_command: t("tool.shell", { command: String(args.command || "") }),
|
|
1374
|
+
git_status: t("tool.gitStatus"),
|
|
1375
|
+
git_diff: t("tool.gitDiff"),
|
|
1376
|
+
git_commit: t("tool.gitCommit", { message: String(args.message || "") }),
|
|
1377
|
+
git_log: "\u{1F4DC} \u67E5\u770BGit\u65E5\u5FD7"
|
|
1378
|
+
};
|
|
1379
|
+
printToolCall(name, descMap[name] || name);
|
|
1380
|
+
if (tool.requiresConfirmation && !autoConfirm) {
|
|
1381
|
+
const confirmed = await confirmExecution(name, args);
|
|
1382
|
+
if (!confirmed) {
|
|
1383
|
+
return { success: false, output: "", error: "User declined execution." };
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
const result = await tool.handler(args);
|
|
1387
|
+
printToolResult(result.error || result.output);
|
|
1388
|
+
return result;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/core/context.ts
|
|
1392
|
+
import fs4 from "fs/promises";
|
|
1393
|
+
import path4 from "path";
|
|
1394
|
+
import { glob as glob3 } from "glob";
|
|
1395
|
+
async function buildProjectContext(rootDir) {
|
|
1396
|
+
const ctx = {
|
|
1397
|
+
rootDir,
|
|
1398
|
+
projectType: "unknown",
|
|
1399
|
+
frameworks: [],
|
|
1400
|
+
packageManager: "npm",
|
|
1401
|
+
languages: [],
|
|
1402
|
+
structure: "",
|
|
1403
|
+
hasGit: false
|
|
1404
|
+
};
|
|
1405
|
+
try {
|
|
1406
|
+
await fs4.access(path4.join(rootDir, ".git"));
|
|
1407
|
+
ctx.hasGit = true;
|
|
1408
|
+
} catch {
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
const pkgPath = path4.join(rootDir, "package.json");
|
|
1412
|
+
const pkg = JSON.parse(await fs4.readFile(pkgPath, "utf-8"));
|
|
1413
|
+
ctx.projectName = pkg.name;
|
|
1414
|
+
ctx.projectType = "node";
|
|
1415
|
+
ctx.languages.push("javascript", "typescript");
|
|
1416
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1417
|
+
if (allDeps["next"]) ctx.frameworks.push("Next.js");
|
|
1418
|
+
if (allDeps["nuxt"] || allDeps["nuxt3"]) ctx.frameworks.push("Nuxt");
|
|
1419
|
+
if (allDeps["react"]) ctx.frameworks.push("React");
|
|
1420
|
+
if (allDeps["vue"]) ctx.frameworks.push("Vue");
|
|
1421
|
+
if (allDeps["svelte"] || allDeps["@sveltejs/kit"]) ctx.frameworks.push("Svelte");
|
|
1422
|
+
if (allDeps["express"]) ctx.frameworks.push("Express");
|
|
1423
|
+
if (allDeps["@nestjs/core"]) ctx.frameworks.push("NestJS");
|
|
1424
|
+
if (allDeps["koa"]) ctx.frameworks.push("Koa");
|
|
1425
|
+
if (allDeps["fastify"]) ctx.frameworks.push("Fastify");
|
|
1426
|
+
if (allDeps["hono"]) ctx.frameworks.push("Hono");
|
|
1427
|
+
if (allDeps["tailwindcss"]) ctx.frameworks.push("Tailwind CSS");
|
|
1428
|
+
if (allDeps["prisma"] || allDeps["@prisma/client"]) ctx.frameworks.push("Prisma");
|
|
1429
|
+
if (allDeps["drizzle-orm"]) ctx.frameworks.push("Drizzle ORM");
|
|
1430
|
+
if (allDeps["typeorm"]) ctx.frameworks.push("TypeORM");
|
|
1431
|
+
if (allDeps["mongoose"]) ctx.frameworks.push("Mongoose");
|
|
1432
|
+
try {
|
|
1433
|
+
await fs4.access(path4.join(rootDir, "pnpm-lock.yaml"));
|
|
1434
|
+
ctx.packageManager = "pnpm";
|
|
1435
|
+
} catch {
|
|
1436
|
+
try {
|
|
1437
|
+
await fs4.access(path4.join(rootDir, "yarn.lock"));
|
|
1438
|
+
ctx.packageManager = "yarn";
|
|
1439
|
+
} catch {
|
|
1440
|
+
try {
|
|
1441
|
+
await fs4.access(path4.join(rootDir, "bun.lockb"));
|
|
1442
|
+
ctx.packageManager = "bun";
|
|
1443
|
+
} catch {
|
|
1444
|
+
ctx.packageManager = "npm";
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
} catch {
|
|
1449
|
+
}
|
|
1450
|
+
try {
|
|
1451
|
+
await fs4.access(path4.join(rootDir, "requirements.txt"));
|
|
1452
|
+
ctx.projectType = ctx.projectType === "node" ? "fullstack" : "python";
|
|
1453
|
+
ctx.languages.push("python");
|
|
1454
|
+
} catch {
|
|
1455
|
+
}
|
|
1456
|
+
try {
|
|
1457
|
+
await fs4.access(path4.join(rootDir, "pyproject.toml"));
|
|
1458
|
+
ctx.projectType = ctx.projectType === "node" ? "fullstack" : "python";
|
|
1459
|
+
if (!ctx.languages.includes("python")) ctx.languages.push("python");
|
|
1460
|
+
} catch {
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
await fs4.access(path4.join(rootDir, "go.mod"));
|
|
1464
|
+
ctx.projectType = "go";
|
|
1465
|
+
ctx.languages.push("go");
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
ctx.structure = await buildStructureTree(rootDir);
|
|
1469
|
+
return ctx;
|
|
1470
|
+
}
|
|
1471
|
+
async function buildStructureTree(rootDir, maxDepth = 2) {
|
|
1472
|
+
const files = await glob3("**/*", {
|
|
1473
|
+
cwd: rootDir,
|
|
1474
|
+
maxDepth,
|
|
1475
|
+
mark: true,
|
|
1476
|
+
ignore: [
|
|
1477
|
+
"node_modules/**",
|
|
1478
|
+
".git/**",
|
|
1479
|
+
"dist/**",
|
|
1480
|
+
".next/**",
|
|
1481
|
+
".nuxt/**",
|
|
1482
|
+
"coverage/**",
|
|
1483
|
+
".turbo/**"
|
|
1484
|
+
]
|
|
1485
|
+
});
|
|
1486
|
+
if (files.length === 0) return "(empty directory)";
|
|
1487
|
+
const sorted = files.sort();
|
|
1488
|
+
const limited = sorted.slice(0, 100);
|
|
1489
|
+
let tree = limited.map((f) => ` ${f}`).join("\n");
|
|
1490
|
+
if (sorted.length > 100) {
|
|
1491
|
+
tree += `
|
|
1492
|
+
... and ${sorted.length - 100} more files`;
|
|
1493
|
+
}
|
|
1494
|
+
return tree;
|
|
1495
|
+
}
|
|
1496
|
+
function generateSystemPrompt(ctx) {
|
|
1497
|
+
const parts = [];
|
|
1498
|
+
parts.push(`\u4F60\u662F\u6C88\u7FD4\u7684AI\u52A9\u624B\uFF0C\u4E00\u4E2A\u4E13\u4E1A\u7684AI\u5168\u6808\u5F00\u53D1\u52A9\u624B\u3002\u4F60\u6B63\u5728\u5E2E\u52A9\u5F00\u53D1\u8005\u5728\u7EC8\u7AEF\u4E2D\u7F16\u5199\u548C\u4FEE\u6539\u4EE3\u7801\u3002`);
|
|
1499
|
+
parts.push(`\u4F60\u7684\u98CE\u683C\uFF1A\u4E13\u4E1A\u4F46\u53CB\u597D\uFF0C\u89E3\u91CA\u6E05\u6670\uFF0C\u4EE3\u7801\u8D28\u91CF\u9AD8\u3002\u4F18\u5148\u4F7F\u7528\u4E2D\u6587\u56DE\u590D\u3002`);
|
|
1500
|
+
parts.push(`
|
|
1501
|
+
## \u5DE5\u4F5C\u539F\u5219`);
|
|
1502
|
+
parts.push(`1. \u5728\u4FEE\u6539\u6587\u4EF6\u524D\u5148\u9605\u8BFB\u6587\u4EF6\u5185\u5BB9\uFF0C\u786E\u4FDD\u7406\u89E3\u4E0A\u4E0B\u6587`);
|
|
1503
|
+
parts.push(`2. \u505A\u51FA\u7684\u4FEE\u6539\u8981\u7CBE\u786E\uFF0C\u4F7F\u7528 edit_file \u8FDB\u884C\u5C40\u90E8\u7F16\u8F91\u800C\u975E\u8986\u5199\u6574\u4E2A\u6587\u4EF6`);
|
|
1504
|
+
parts.push(`3. \u6D89\u53CA\u7834\u574F\u6027\u64CD\u4F5C\uFF08\u5220\u9664\u6587\u4EF6\u3001\u6267\u884C\u547D\u4EE4\uFF09\u65F6\u8981\u8C28\u614E`);
|
|
1505
|
+
parts.push(`4. \u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u4EE3\u7801\u98CE\u683C\u548C\u7EA6\u5B9A`);
|
|
1506
|
+
parts.push(`5. \u5982\u679C\u4E0D\u786E\u5B9A\uFF0C\u5148\u641C\u7D22\u4EE3\u7801\u4E86\u89E3\u73B0\u6709\u5B9E\u73B0`);
|
|
1507
|
+
parts.push(`6. \u6267\u884C\u547D\u4EE4\u65F6\u4F7F\u7528\u9879\u76EE\u7684\u5305\u7BA1\u7406\u5668\uFF08\u5982 ${ctx.packageManager}\uFF09`);
|
|
1508
|
+
parts.push(`7. \u6D89\u53CA\u591A\u4E2A\u6587\u4EF6\u7684\u4FEE\u6539\u65F6\uFF0C\u5148\u8BF4\u660E\u8BA1\u5212\u518D\u9010\u6B65\u6267\u884C`);
|
|
1509
|
+
if (ctx.projectType !== "unknown") {
|
|
1510
|
+
parts.push(`
|
|
1511
|
+
## \u5F53\u524D\u9879\u76EE\u4FE1\u606F`);
|
|
1512
|
+
parts.push(`- \u9879\u76EE\u76EE\u5F55: ${ctx.rootDir}`);
|
|
1513
|
+
if (ctx.projectName) parts.push(`- \u9879\u76EE\u540D\u79F0: ${ctx.projectName}`);
|
|
1514
|
+
parts.push(`- \u9879\u76EE\u7C7B\u578B: ${ctx.projectType}`);
|
|
1515
|
+
if (ctx.frameworks.length > 0) parts.push(`- \u4F7F\u7528\u6846\u67B6: ${ctx.frameworks.join(", ")}`);
|
|
1516
|
+
if (ctx.languages.length > 0) parts.push(`- \u7F16\u7A0B\u8BED\u8A00: ${ctx.languages.join(", ")}`);
|
|
1517
|
+
parts.push(`- \u5305\u7BA1\u7406\u5668: ${ctx.packageManager}`);
|
|
1518
|
+
parts.push(`- Git: ${ctx.hasGit ? "\u5DF2\u521D\u59CB\u5316" : "\u672A\u521D\u59CB\u5316"}`);
|
|
1519
|
+
}
|
|
1520
|
+
if (ctx.structure) {
|
|
1521
|
+
parts.push(`
|
|
1522
|
+
## \u76EE\u5F55\u7ED3\u6784`);
|
|
1523
|
+
parts.push("```");
|
|
1524
|
+
parts.push(ctx.structure);
|
|
1525
|
+
parts.push("```");
|
|
1526
|
+
}
|
|
1527
|
+
parts.push(`
|
|
1528
|
+
## \u5168\u6808\u5F00\u53D1\u4E13\u4E1A\u77E5\u8BC6`);
|
|
1529
|
+
parts.push(`\u4F60\u7CBE\u901A\u4EE5\u4E0B\u5168\u6808\u5F00\u53D1\u6280\u672F\u6808\uFF0C\u80FD\u6839\u636E\u9879\u76EE\u5B9E\u9645\u4F7F\u7528\u7684\u6846\u67B6\u63D0\u4F9B\u6700\u4F73\u5B9E\u8DF5\uFF1A`);
|
|
1530
|
+
parts.push(``);
|
|
1531
|
+
parts.push(`### \u524D\u7AEF`);
|
|
1532
|
+
parts.push(`- React (Hooks, Server Components, Suspense), Vue 3 (Composition API), Svelte`);
|
|
1533
|
+
parts.push(`- Next.js (App Router / Pages Router), Nuxt 3, SvelteKit`);
|
|
1534
|
+
parts.push(`- Tailwind CSS, CSS Modules, Styled Components`);
|
|
1535
|
+
parts.push(`- \u72B6\u6001\u7BA1\u7406: Zustand, Pinia, Redux Toolkit, Jotai`);
|
|
1536
|
+
parts.push(`- \u8868\u5355: React Hook Form, Formik, VeeValidate`);
|
|
1537
|
+
parts.push(``);
|
|
1538
|
+
parts.push(`### \u540E\u7AEF`);
|
|
1539
|
+
parts.push(`- Express, Fastify, Koa, Hono, NestJS`);
|
|
1540
|
+
parts.push(`- RESTful API \u8BBE\u8BA1, GraphQL`);
|
|
1541
|
+
parts.push(`- \u8BA4\u8BC1: JWT, OAuth2, Session`);
|
|
1542
|
+
parts.push(`- \u4E2D\u95F4\u4EF6\u6A21\u5F0F, \u9519\u8BEF\u5904\u7406, \u8BF7\u6C42\u9A8C\u8BC1 (Zod, Joi)`);
|
|
1543
|
+
parts.push(``);
|
|
1544
|
+
parts.push(`### \u6570\u636E\u5E93`);
|
|
1545
|
+
parts.push(`- PostgreSQL, MySQL, MongoDB, Redis`);
|
|
1546
|
+
parts.push(`- ORM: Prisma, Drizzle, TypeORM, Mongoose`);
|
|
1547
|
+
parts.push(`- \u6570\u636E\u5E93\u8FC1\u79FB, \u79CD\u5B50\u6570\u636E, \u67E5\u8BE2\u4F18\u5316`);
|
|
1548
|
+
parts.push(``);
|
|
1549
|
+
parts.push(`### \u5DE5\u7A0B\u5316`);
|
|
1550
|
+
parts.push(`- TypeScript \u7C7B\u578B\u8BBE\u8BA1, \u6CDB\u578B, \u7C7B\u578B\u5B88\u536B`);
|
|
1551
|
+
parts.push(`- \u6D4B\u8BD5: Vitest, Jest, Playwright, Cypress`);
|
|
1552
|
+
parts.push(`- CI/CD, Docker, \u73AF\u5883\u53D8\u91CF\u7BA1\u7406`);
|
|
1553
|
+
parts.push(`- ESLint, Prettier, Husky, lint-staged`);
|
|
1554
|
+
if (ctx.frameworks.length > 0) {
|
|
1555
|
+
parts.push(`
|
|
1556
|
+
## \u5F53\u524D\u9879\u76EE\u6846\u67B6\u6307\u5357`);
|
|
1557
|
+
for (const fw of ctx.frameworks) {
|
|
1558
|
+
const guide = getFrameworkGuide(fw);
|
|
1559
|
+
if (guide) parts.push(guide);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
parts.push(`
|
|
1563
|
+
## \u53EF\u7528\u5DE5\u5177`);
|
|
1564
|
+
parts.push(`\u4F60\u53EF\u4EE5\u4F7F\u7528\u4EE5\u4E0B\u5DE5\u5177\u6765\u5E2E\u52A9\u5F00\u53D1\u8005\uFF1A`);
|
|
1565
|
+
parts.push(`- read_file: \u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9\uFF08\u652F\u6301\u884C\u8303\u56F4\uFF09`);
|
|
1566
|
+
parts.push(`- list_files: \u5217\u51FA\u76EE\u5F55\u7ED3\u6784\uFF08\u652F\u6301glob\u6A21\u5F0F\uFF09`);
|
|
1567
|
+
parts.push(`- write_file: \u521B\u5EFA\u6216\u8986\u5199\u6587\u4EF6\uFF08\u81EA\u52A8\u521B\u5EFA\u7236\u76EE\u5F55\uFF09`);
|
|
1568
|
+
parts.push(`- edit_file: \u7CBE\u786E\u7F16\u8F91\u6587\u4EF6\u4E2D\u7684\u7279\u5B9A\u5185\u5BB9\uFF08\u5B57\u7B26\u4E32\u5339\u914D\u66FF\u6362\uFF09`);
|
|
1569
|
+
parts.push(`- delete_file: \u5220\u9664\u6587\u4EF6`);
|
|
1570
|
+
parts.push(`- search_code: \u5728\u9879\u76EE\u4E2D\u641C\u7D22\u4EE3\u7801\u6A21\u5F0F`);
|
|
1571
|
+
parts.push(`- run_command: \u6267\u884C\u7EC8\u7AEF\u547D\u4EE4\uFF08\u9700\u7528\u6237\u786E\u8BA4\uFF09`);
|
|
1572
|
+
parts.push(`- git_status: \u67E5\u770BGit\u72B6\u6001`);
|
|
1573
|
+
parts.push(`- git_diff: \u67E5\u770BGit\u5DEE\u5F02\uFF08\u652F\u6301staged/unstaged\uFF09`);
|
|
1574
|
+
parts.push(`- git_commit: \u6682\u5B58\u5E76\u63D0\u4EA4\u4EE3\u7801`);
|
|
1575
|
+
parts.push(`- git_log: \u67E5\u770B\u63D0\u4EA4\u5386\u53F2`);
|
|
1576
|
+
parts.push(`
|
|
1577
|
+
## \u56DE\u590D\u683C\u5F0F`);
|
|
1578
|
+
parts.push(`- \u4F7F\u7528 Markdown \u683C\u5F0F\u56DE\u590D`);
|
|
1579
|
+
parts.push(`- \u4EE3\u7801\u5757\u4F7F\u7528\u6B63\u786E\u7684\u8BED\u8A00\u6807\u6CE8`);
|
|
1580
|
+
parts.push(`- \u91CD\u8981\u64CD\u4F5C\u524D\u5148\u89E3\u91CA\u539F\u56E0`);
|
|
1581
|
+
parts.push(`- \u4FEE\u6539\u5B8C\u6210\u540E\u7ED9\u51FA\u7B80\u8981\u603B\u7ED3`);
|
|
1582
|
+
return parts.join("\n");
|
|
1583
|
+
}
|
|
1584
|
+
function getFrameworkGuide(framework) {
|
|
1585
|
+
const guides = {
|
|
1586
|
+
"Next.js": `### Next.js
|
|
1587
|
+
- \u4F7F\u7528 App Router\uFF08app/ \u76EE\u5F55\uFF09\u65F6\u9075\u5FAA RSC \u89C4\u8303\uFF0C\u9ED8\u8BA4\u7EC4\u4EF6\u4E3A Server Component
|
|
1588
|
+
- \u5BA2\u6237\u7AEF\u4EA4\u4E92\u7EC4\u4EF6\u9700\u5728\u9876\u90E8\u6DFB\u52A0 'use client' \u6307\u4EE4
|
|
1589
|
+
- API \u8DEF\u7531\u653E\u5728 app/api/ \u76EE\u5F55\u4E0B\uFF0C\u4F7F\u7528 route.ts \u6587\u4EF6
|
|
1590
|
+
- \u5229\u7528 loading.ts, error.ts, layout.ts \u7B49\u7EA6\u5B9A\u6587\u4EF6
|
|
1591
|
+
- \u56FE\u7247\u4F7F\u7528 next/image\uFF0C\u94FE\u63A5\u4F7F\u7528 next/link`,
|
|
1592
|
+
"Nuxt": `### Nuxt 3
|
|
1593
|
+
- \u9875\u9762\u653E\u5728 pages/ \u76EE\u5F55\uFF0C\u81EA\u52A8\u8DEF\u7531
|
|
1594
|
+
- \u7EC4\u4EF6\u653E\u5728 components/ \u76EE\u5F55\uFF0C\u81EA\u52A8\u5BFC\u5165
|
|
1595
|
+
- \u670D\u52A1\u7AEFAPI\u653E\u5728 server/api/ \u76EE\u5F55
|
|
1596
|
+
- \u4F7F\u7528 composables/ \u76EE\u5F55\u5B58\u653E\u7EC4\u5408\u5F0F\u51FD\u6570
|
|
1597
|
+
- \u4F7F\u7528 useFetch/useAsyncData \u83B7\u53D6\u6570\u636E`,
|
|
1598
|
+
"React": `### React
|
|
1599
|
+
- \u4F7F\u7528\u51FD\u6570\u7EC4\u4EF6 + Hooks \u6A21\u5F0F
|
|
1600
|
+
- \u9075\u5FAA Hooks \u4F7F\u7528\u89C4\u5219\uFF08\u4E0D\u5728\u6761\u4EF6/\u5FAA\u73AF\u4E2D\u8C03\u7528\uFF09
|
|
1601
|
+
- \u5408\u7406\u62C6\u5206\u7EC4\u4EF6\uFF0C\u4FDD\u6301\u5355\u4E00\u804C\u8D23
|
|
1602
|
+
- \u4F7F\u7528 useMemo/useCallback \u907F\u514D\u4E0D\u5FC5\u8981\u7684\u91CD\u6E32\u67D3`,
|
|
1603
|
+
"Vue": `### Vue 3
|
|
1604
|
+
- \u4F18\u5148\u4F7F\u7528 Composition API (setup / <script setup>)
|
|
1605
|
+
- \u4F7F\u7528 ref/reactive \u7BA1\u7406\u54CD\u5E94\u5F0F\u72B6\u6001
|
|
1606
|
+
- \u4F7F\u7528 computed \u521B\u5EFA\u6D3E\u751F\u72B6\u6001
|
|
1607
|
+
- \u9075\u5FAA Props \u5355\u5411\u6570\u636E\u6D41`,
|
|
1608
|
+
"Express": `### Express
|
|
1609
|
+
- \u4F7F\u7528\u8DEF\u7531\u5206\u7EC4\u548C\u4E2D\u95F4\u4EF6
|
|
1610
|
+
- \u7EDF\u4E00\u9519\u8BEF\u5904\u7406\u4E2D\u95F4\u4EF6
|
|
1611
|
+
- \u8F93\u5165\u9A8C\u8BC1\u4F7F\u7528 zod \u6216 joi
|
|
1612
|
+
- \u8DEF\u7531\u53C2\u6570\u7C7B\u578B\u5B89\u5168`,
|
|
1613
|
+
"NestJS": `### NestJS
|
|
1614
|
+
- \u9075\u5FAA\u6A21\u5757\u5316\u67B6\u6784\uFF08Module, Controller, Service\uFF09
|
|
1615
|
+
- \u4F7F\u7528 DTO \u8FDB\u884C\u8BF7\u6C42\u9A8C\u8BC1
|
|
1616
|
+
- \u5229\u7528\u4F9D\u8D56\u6CE8\u5165
|
|
1617
|
+
- Guards \u548C Interceptors \u5904\u7406\u6A2A\u5207\u5173\u6CE8\u70B9`,
|
|
1618
|
+
"Prisma": `### Prisma
|
|
1619
|
+
- Schema \u4FEE\u6539\u540E\u8FD0\u884C prisma generate
|
|
1620
|
+
- \u4F7F\u7528 prisma migrate dev \u7BA1\u7406\u8FC1\u79FB
|
|
1621
|
+
- \u5229\u7528 Prisma Client \u7684\u7C7B\u578B\u5B89\u5168\u67E5\u8BE2
|
|
1622
|
+
- \u4F7F\u7528 include/select \u4F18\u5316\u67E5\u8BE2`,
|
|
1623
|
+
"Drizzle ORM": `### Drizzle ORM
|
|
1624
|
+
- Schema \u5B9A\u4E49\u5728 TypeScript \u4E2D
|
|
1625
|
+
- \u4F7F\u7528 drizzle-kit \u7BA1\u7406\u8FC1\u79FB
|
|
1626
|
+
- \u5229\u7528\u67E5\u8BE2\u6784\u5EFA\u5668\u7684\u7C7B\u578B\u63A8\u65AD
|
|
1627
|
+
- \u4F7F\u7528 .where() \u94FE\u5F0F\u8C03\u7528\u6784\u5EFA\u67E5\u8BE2`,
|
|
1628
|
+
"Tailwind CSS": `### Tailwind CSS
|
|
1629
|
+
- \u4F7F\u7528 utility-first \u65B9\u5F0F\u7F16\u5199\u6837\u5F0F
|
|
1630
|
+
- \u590D\u6742\u7EC4\u4EF6\u63D0\u53D6\u81EA\u5B9A\u4E49\u7EC4\u4EF6\u7C7B
|
|
1631
|
+
- \u5229\u7528 @apply \u51CF\u5C11\u91CD\u590D
|
|
1632
|
+
- \u54CD\u5E94\u5F0F\u8BBE\u8BA1\u4F7F\u7528 sm/md/lg/xl \u524D\u7F00`
|
|
1633
|
+
};
|
|
1634
|
+
return guides[framework] || null;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// src/utils/config.ts
|
|
1638
|
+
import Conf from "conf";
|
|
1639
|
+
var defaults = {
|
|
1640
|
+
locale: "zh",
|
|
1641
|
+
model: "kimi-k2.5",
|
|
1642
|
+
apiBaseUrl: "http://localhost:3210"
|
|
1643
|
+
};
|
|
1644
|
+
var config = new Conf({
|
|
1645
|
+
projectName: "devpilot",
|
|
1646
|
+
defaults
|
|
1647
|
+
});
|
|
1648
|
+
function getConfig() {
|
|
1649
|
+
return {
|
|
1650
|
+
locale: config.get("locale"),
|
|
1651
|
+
model: config.get("model"),
|
|
1652
|
+
apiBaseUrl: config.get("apiBaseUrl"),
|
|
1653
|
+
authToken: config.get("authToken"),
|
|
1654
|
+
userEmail: config.get("userEmail"),
|
|
1655
|
+
openaiApiKey: config.get("openaiApiKey"),
|
|
1656
|
+
anthropicApiKey: config.get("anthropicApiKey"),
|
|
1657
|
+
deepseekApiKey: config.get("deepseekApiKey"),
|
|
1658
|
+
kimiApiKey: config.get("kimiApiKey")
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
function setConfig(key, value) {
|
|
1662
|
+
config.set(key, value);
|
|
1663
|
+
}
|
|
1664
|
+
function getConfigPath() {
|
|
1665
|
+
return config.path;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/core/agent.ts
|
|
1669
|
+
import ora from "ora";
|
|
1670
|
+
var MAX_CONTEXT_MESSAGES = 50;
|
|
1671
|
+
var MAX_TOOL_ITERATIONS = 20;
|
|
1672
|
+
var Agent = class {
|
|
1673
|
+
messages = [];
|
|
1674
|
+
tools = [];
|
|
1675
|
+
context = null;
|
|
1676
|
+
isRunning = false;
|
|
1677
|
+
/** Accumulated token usage for the entire chat turn */
|
|
1678
|
+
turnUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1679
|
+
turnStartTime = 0;
|
|
1680
|
+
/**
|
|
1681
|
+
* Initialize the agent: register tools, build project context
|
|
1682
|
+
*/
|
|
1683
|
+
async init() {
|
|
1684
|
+
registerAllTools();
|
|
1685
|
+
this.tools = getToolDefinitions();
|
|
1686
|
+
this.context = await buildProjectContext(process.cwd());
|
|
1687
|
+
const systemPrompt = generateSystemPrompt(this.context);
|
|
1688
|
+
this.messages = [{ role: "system", content: systemPrompt }];
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Get project context
|
|
1692
|
+
*/
|
|
1693
|
+
getContext() {
|
|
1694
|
+
return this.context;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* Send a user message and get AI response (with tool execution loop)
|
|
1698
|
+
*/
|
|
1699
|
+
async chat(userMessage) {
|
|
1700
|
+
if (this.isRunning) {
|
|
1701
|
+
printWarning("\u6B63\u5728\u5904\u7406\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u4E0A\u4E00\u6761\u6D88\u606F\u5B8C\u6210...");
|
|
1702
|
+
return "";
|
|
1703
|
+
}
|
|
1704
|
+
this.isRunning = true;
|
|
1705
|
+
this.turnUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1706
|
+
this.turnStartTime = Date.now();
|
|
1707
|
+
try {
|
|
1708
|
+
this.messages.push({ role: "user", content: userMessage });
|
|
1709
|
+
this.trimMessages();
|
|
1710
|
+
const response = await this.runAgentLoop();
|
|
1711
|
+
const duration = Date.now() - this.turnStartTime;
|
|
1712
|
+
if (this.turnUsage.totalTokens > 0) {
|
|
1713
|
+
printTokenUsage(this.turnUsage, duration);
|
|
1714
|
+
}
|
|
1715
|
+
return response;
|
|
1716
|
+
} finally {
|
|
1717
|
+
this.isRunning = false;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* The main agent loop: call AI, execute tools, repeat
|
|
1722
|
+
*/
|
|
1723
|
+
async runAgentLoop() {
|
|
1724
|
+
let iteration = 0;
|
|
1725
|
+
let finalResponse = "";
|
|
1726
|
+
while (iteration < MAX_TOOL_ITERATIONS) {
|
|
1727
|
+
iteration++;
|
|
1728
|
+
const config3 = this.getProviderConfig();
|
|
1729
|
+
const provider = getProvider(config3.model);
|
|
1730
|
+
let responseText = "";
|
|
1731
|
+
const toolCalls = [];
|
|
1732
|
+
let hasError = false;
|
|
1733
|
+
let reasoningContent = "";
|
|
1734
|
+
const spinner = ora({ text: t("thinking"), spinner: "dots" }).start();
|
|
1735
|
+
let spinnerStopped = false;
|
|
1736
|
+
try {
|
|
1737
|
+
for await (const chunk of provider.chatStream(this.messages, this.tools, config3)) {
|
|
1738
|
+
if (!spinnerStopped && (chunk.type === "text" || chunk.type === "tool_call")) {
|
|
1739
|
+
spinner.stop();
|
|
1740
|
+
spinnerStopped = true;
|
|
1741
|
+
if (chunk.type === "text") {
|
|
1742
|
+
process.stdout.write("\n");
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
switch (chunk.type) {
|
|
1746
|
+
case "text":
|
|
1747
|
+
responseText += chunk.content || "";
|
|
1748
|
+
streamChunk(chunk.content || "");
|
|
1749
|
+
break;
|
|
1750
|
+
case "tool_call":
|
|
1751
|
+
if (chunk.toolCall) {
|
|
1752
|
+
toolCalls.push(chunk.toolCall);
|
|
1753
|
+
}
|
|
1754
|
+
break;
|
|
1755
|
+
case "error":
|
|
1756
|
+
hasError = true;
|
|
1757
|
+
printError(t("errors.apiError", { message: chunk.error || "Unknown error" }));
|
|
1758
|
+
break;
|
|
1759
|
+
case "done":
|
|
1760
|
+
if (chunk.usage) {
|
|
1761
|
+
this.turnUsage.promptTokens += chunk.usage.promptTokens;
|
|
1762
|
+
this.turnUsage.completionTokens += chunk.usage.completionTokens;
|
|
1763
|
+
this.turnUsage.totalTokens += chunk.usage.totalTokens;
|
|
1764
|
+
}
|
|
1765
|
+
if (chunk.content) {
|
|
1766
|
+
reasoningContent = chunk.content;
|
|
1767
|
+
}
|
|
1768
|
+
break;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
if (!spinnerStopped) spinner.stop();
|
|
1773
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1774
|
+
printError(t("errors.apiError", { message: msg }));
|
|
1775
|
+
return "";
|
|
1776
|
+
}
|
|
1777
|
+
if (!spinnerStopped) spinner.stop();
|
|
1778
|
+
if (responseText) {
|
|
1779
|
+
process.stdout.write("\n");
|
|
1780
|
+
}
|
|
1781
|
+
if (hasError) return "";
|
|
1782
|
+
if (toolCalls.length === 0) {
|
|
1783
|
+
const msg = { role: "assistant", content: responseText };
|
|
1784
|
+
if (reasoningContent) msg.reasoning_content = reasoningContent;
|
|
1785
|
+
this.messages.push(msg);
|
|
1786
|
+
finalResponse = responseText;
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
const assistantMsg = {
|
|
1790
|
+
role: "assistant",
|
|
1791
|
+
content: responseText || null,
|
|
1792
|
+
tool_calls: toolCalls
|
|
1793
|
+
};
|
|
1794
|
+
if (reasoningContent) assistantMsg.reasoning_content = reasoningContent;
|
|
1795
|
+
this.messages.push(assistantMsg);
|
|
1796
|
+
console.log();
|
|
1797
|
+
for (const tc of toolCalls) {
|
|
1798
|
+
let args;
|
|
1799
|
+
try {
|
|
1800
|
+
args = JSON.parse(tc.function.arguments);
|
|
1801
|
+
} catch {
|
|
1802
|
+
args = {};
|
|
1803
|
+
}
|
|
1804
|
+
const result = await executeTool(tc.function.name, args);
|
|
1805
|
+
this.messages.push({
|
|
1806
|
+
role: "tool",
|
|
1807
|
+
content: result.error || result.output,
|
|
1808
|
+
tool_call_id: tc.id,
|
|
1809
|
+
name: tc.function.name
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
if (iteration >= MAX_TOOL_ITERATIONS) {
|
|
1814
|
+
printWarning("\u5DF2\u8FBE\u5230\u6700\u5927\u5DE5\u5177\u8C03\u7528\u6B21\u6570\uFF0C\u505C\u6B62\u6267\u884C\u3002");
|
|
1815
|
+
}
|
|
1816
|
+
return finalResponse;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Get provider configuration based on current settings
|
|
1820
|
+
*/
|
|
1821
|
+
getProviderConfig() {
|
|
1822
|
+
const cfg = getConfig();
|
|
1823
|
+
const model = cfg.model;
|
|
1824
|
+
const providerName = getProviderName(model);
|
|
1825
|
+
let apiKey = "";
|
|
1826
|
+
if (cfg.authToken) {
|
|
1827
|
+
apiKey = cfg.authToken;
|
|
1828
|
+
} else {
|
|
1829
|
+
switch (providerName) {
|
|
1830
|
+
case "openai":
|
|
1831
|
+
apiKey = cfg.openaiApiKey || "";
|
|
1832
|
+
break;
|
|
1833
|
+
case "anthropic":
|
|
1834
|
+
apiKey = cfg.anthropicApiKey || "";
|
|
1835
|
+
break;
|
|
1836
|
+
case "deepseek":
|
|
1837
|
+
apiKey = cfg.deepseekApiKey || "";
|
|
1838
|
+
break;
|
|
1839
|
+
case "kimi":
|
|
1840
|
+
apiKey = cfg.kimiApiKey || "";
|
|
1841
|
+
break;
|
|
1842
|
+
case "qwen":
|
|
1843
|
+
apiKey = cfg.openaiApiKey || "";
|
|
1844
|
+
break;
|
|
1845
|
+
default:
|
|
1846
|
+
apiKey = cfg.openaiApiKey || "";
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
let baseUrl;
|
|
1850
|
+
if (cfg.authToken) {
|
|
1851
|
+
baseUrl = `${cfg.apiBaseUrl}/api/ai/proxy`;
|
|
1852
|
+
} else {
|
|
1853
|
+
baseUrl = getDefaultBaseUrl(providerName);
|
|
1854
|
+
}
|
|
1855
|
+
return { apiKey, baseUrl, model };
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Trim old messages to prevent context overflow
|
|
1859
|
+
*/
|
|
1860
|
+
trimMessages() {
|
|
1861
|
+
if (this.messages.length > MAX_CONTEXT_MESSAGES) {
|
|
1862
|
+
const systemMsg = this.messages[0];
|
|
1863
|
+
const recentMessages = this.messages.slice(-MAX_CONTEXT_MESSAGES + 1);
|
|
1864
|
+
this.messages = [systemMsg, ...recentMessages];
|
|
1865
|
+
printWarning(t("errors.tokenLimit"));
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* Clear conversation history (keep system prompt)
|
|
1870
|
+
*/
|
|
1871
|
+
clearHistory() {
|
|
1872
|
+
const systemMsg = this.messages[0];
|
|
1873
|
+
this.messages = [systemMsg];
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Get current model name
|
|
1877
|
+
*/
|
|
1878
|
+
getCurrentModel() {
|
|
1879
|
+
return getConfig().model;
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* Cancel running operation
|
|
1883
|
+
*/
|
|
1884
|
+
cancel() {
|
|
1885
|
+
this.isRunning = false;
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
|
|
1889
|
+
// src/commands/chat.ts
|
|
1890
|
+
function question(rl, prompt) {
|
|
1891
|
+
return new Promise((resolve, reject) => {
|
|
1892
|
+
const onClose = () => reject(new Error("readline closed"));
|
|
1893
|
+
rl.once("close", onClose);
|
|
1894
|
+
rl.question(prompt, (answer) => {
|
|
1895
|
+
rl.removeListener("close", onClose);
|
|
1896
|
+
resolve(answer);
|
|
1897
|
+
});
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
async function chatCommand() {
|
|
1901
|
+
const config3 = getConfig();
|
|
1902
|
+
setLocale(config3.locale);
|
|
1903
|
+
console.log(banner());
|
|
1904
|
+
const currentModel = config3.model;
|
|
1905
|
+
console.log(theme.dim(` \u6A21\u578B ${theme.brandBold(currentModel)} \xB7 ${t("exitHint")}`));
|
|
1906
|
+
const hasAnyKey = config3.authToken || config3.kimiApiKey || config3.openaiApiKey || config3.anthropicApiKey || config3.deepseekApiKey;
|
|
1907
|
+
if (!hasAnyKey) {
|
|
1908
|
+
console.log();
|
|
1909
|
+
printWarning("\u5C1A\u672A\u914D\u7F6EAPI\u5BC6\u94A5\u3002\u8BF7\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u4E4B\u4E00:");
|
|
1910
|
+
console.log(theme.dim(" sxai set-key kimi <your-key> # Kimi (\u6708\u4E4B\u6697\u9762)"));
|
|
1911
|
+
console.log(theme.dim(" sxai set-key deepseek <your-key>"));
|
|
1912
|
+
console.log(theme.dim(" sxai set-key openai <your-key>"));
|
|
1913
|
+
console.log(theme.dim(" sxai config # \u4EA4\u4E92\u5F0F\u914D\u7F6E"));
|
|
1914
|
+
}
|
|
1915
|
+
const agent = new Agent();
|
|
1916
|
+
try {
|
|
1917
|
+
await agent.init();
|
|
1918
|
+
const projectCtx = agent.getContext();
|
|
1919
|
+
if (projectCtx && projectCtx.projectType !== "unknown") {
|
|
1920
|
+
const parts = [projectCtx.projectType];
|
|
1921
|
+
if (projectCtx.frameworks.length > 0) {
|
|
1922
|
+
parts.push(projectCtx.frameworks.join(", "));
|
|
1923
|
+
}
|
|
1924
|
+
console.log(theme.dim(` \u9879\u76EE ${theme.info(parts.join(" \xB7 "))}`));
|
|
1925
|
+
}
|
|
1926
|
+
printSeparator();
|
|
1927
|
+
console.log();
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
printError(`\u521D\u59CB\u5316\u5931\u8D25: ${err instanceof Error ? err.message : err}`);
|
|
1930
|
+
process.exit(1);
|
|
1931
|
+
}
|
|
1932
|
+
const rl = readline.createInterface({
|
|
1933
|
+
input: process.stdin,
|
|
1934
|
+
output: process.stdout,
|
|
1935
|
+
terminal: true
|
|
1936
|
+
});
|
|
1937
|
+
registerQuestionFn((prompt2) => question(rl, prompt2));
|
|
1938
|
+
let ctrlCCount = 0;
|
|
1939
|
+
process.on("SIGINT", () => {
|
|
1940
|
+
ctrlCCount++;
|
|
1941
|
+
if (ctrlCCount >= 2) {
|
|
1942
|
+
console.log();
|
|
1943
|
+
printSuccess(t("exit"));
|
|
1944
|
+
process.exit(0);
|
|
1945
|
+
}
|
|
1946
|
+
console.log(theme.dim("\n(\u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA)"));
|
|
1947
|
+
setTimeout(() => {
|
|
1948
|
+
ctrlCCount = 0;
|
|
1949
|
+
}, 2e3);
|
|
1950
|
+
});
|
|
1951
|
+
const prompt = theme.user(t("prompt"));
|
|
1952
|
+
while (true) {
|
|
1953
|
+
let input;
|
|
1954
|
+
try {
|
|
1955
|
+
input = await question(rl, prompt);
|
|
1956
|
+
} catch {
|
|
1957
|
+
break;
|
|
1958
|
+
}
|
|
1959
|
+
input = input.trim();
|
|
1960
|
+
if (!input) continue;
|
|
1961
|
+
ctrlCCount = 0;
|
|
1962
|
+
if (input.startsWith("/")) {
|
|
1963
|
+
const result = await handleSlashCommand(input, agent);
|
|
1964
|
+
if (result === "exit") break;
|
|
1965
|
+
continue;
|
|
1966
|
+
}
|
|
1967
|
+
try {
|
|
1968
|
+
await agent.chat(input);
|
|
1969
|
+
} catch (err) {
|
|
1970
|
+
if (err instanceof Error && err.message.includes("API key")) {
|
|
1971
|
+
printError("API\u5BC6\u94A5\u672A\u914D\u7F6E\u6216\u65E0\u6548\u3002\u8BF7\u8FD0\u884C sxai config \u914D\u7F6E\u3002");
|
|
1972
|
+
} else if (err instanceof Error && (err.message.includes("ECONNREFUSED") || err.message.includes("fetch failed"))) {
|
|
1973
|
+
printError("\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u6216API\u670D\u52A1\u662F\u5426\u53EF\u7528\u3002");
|
|
1974
|
+
} else {
|
|
1975
|
+
printError(`\u9519\u8BEF: ${err instanceof Error ? err.message : err}`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
console.log();
|
|
1979
|
+
}
|
|
1980
|
+
rl.close();
|
|
1981
|
+
console.log();
|
|
1982
|
+
printSuccess(t("exit"));
|
|
1983
|
+
process.exit(0);
|
|
1984
|
+
}
|
|
1985
|
+
async function handleSlashCommand(input, agent) {
|
|
1986
|
+
const [cmd, ...args] = input.split(" ");
|
|
1987
|
+
switch (cmd) {
|
|
1988
|
+
case "/exit":
|
|
1989
|
+
case "/quit":
|
|
1990
|
+
case "/q":
|
|
1991
|
+
return "exit";
|
|
1992
|
+
case "/help":
|
|
1993
|
+
case "/h":
|
|
1994
|
+
console.log(t("help"));
|
|
1995
|
+
break;
|
|
1996
|
+
case "/clear":
|
|
1997
|
+
agent.clearHistory();
|
|
1998
|
+
printSuccess("\u5BF9\u8BDD\u5386\u53F2\u5DF2\u6E05\u9664\u3002");
|
|
1999
|
+
break;
|
|
2000
|
+
case "/model": {
|
|
2001
|
+
const models = listModels();
|
|
2002
|
+
const currentModel = agent.getCurrentModel();
|
|
2003
|
+
console.log("\n\u53EF\u7528\u6A21\u578B:");
|
|
2004
|
+
for (const m of models) {
|
|
2005
|
+
const marker = m.id === currentModel ? theme.success(" \u25C9") : " \u25CB";
|
|
2006
|
+
console.log(`${marker} ${theme.bold(m.name)} ${theme.dim(`(${m.id})`)}`);
|
|
2007
|
+
}
|
|
2008
|
+
if (args[0]) {
|
|
2009
|
+
const target = args[0];
|
|
2010
|
+
if (models.find((m) => m.id === target)) {
|
|
2011
|
+
setConfig("model", target);
|
|
2012
|
+
printSuccess(`\u6A21\u578B\u5DF2\u5207\u6362\u4E3A: ${target}`);
|
|
2013
|
+
} else {
|
|
2014
|
+
printError(`\u672A\u77E5\u6A21\u578B: ${target}`);
|
|
2015
|
+
}
|
|
2016
|
+
} else {
|
|
2017
|
+
console.log(theme.dim("\n\u4F7F\u7528 /model <model-id> \u5207\u6362\u6A21\u578B"));
|
|
2018
|
+
}
|
|
2019
|
+
break;
|
|
2020
|
+
}
|
|
2021
|
+
case "/status": {
|
|
2022
|
+
const ctx = agent.getContext();
|
|
2023
|
+
if (ctx) {
|
|
2024
|
+
console.log(`
|
|
2025
|
+
\u9879\u76EE: ${ctx.projectName || ctx.rootDir}`);
|
|
2026
|
+
console.log(`\u7C7B\u578B: ${ctx.projectType}`);
|
|
2027
|
+
console.log(`\u6846\u67B6: ${ctx.frameworks.join(", ") || "\u65E0"}`);
|
|
2028
|
+
console.log(`\u8BED\u8A00: ${ctx.languages.join(", ") || "\u672A\u77E5"}`);
|
|
2029
|
+
console.log(`\u5305\u7BA1\u7406\u5668: ${ctx.packageManager}`);
|
|
2030
|
+
console.log(`Git: ${ctx.hasGit ? "\u662F" : "\u5426"}`);
|
|
2031
|
+
}
|
|
2032
|
+
break;
|
|
2033
|
+
}
|
|
2034
|
+
case "/config": {
|
|
2035
|
+
const config3 = getConfig();
|
|
2036
|
+
console.log("\n\u5F53\u524D\u914D\u7F6E:");
|
|
2037
|
+
console.log(` \u6A21\u578B: ${config3.model}`);
|
|
2038
|
+
console.log(` \u8BED\u8A00: ${config3.locale}`);
|
|
2039
|
+
console.log(` \u540E\u7AEF: ${config3.apiBaseUrl}`);
|
|
2040
|
+
console.log(` \u767B\u5F55: ${config3.userEmail || "\u672A\u767B\u5F55"}`);
|
|
2041
|
+
console.log(` Kimi Key: ${config3.kimiApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
|
|
2042
|
+
console.log(` OpenAI Key: ${config3.openaiApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
|
|
2043
|
+
console.log(` Anthropic Key: ${config3.anthropicApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
|
|
2044
|
+
console.log(` DeepSeek Key: ${config3.deepseekApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
|
|
2045
|
+
break;
|
|
2046
|
+
}
|
|
2047
|
+
case "/login":
|
|
2048
|
+
printInfo("\u8BF7\u4F7F\u7528 sxai login \u547D\u4EE4\u767B\u5F55\u3002");
|
|
2049
|
+
break;
|
|
2050
|
+
case "/logout":
|
|
2051
|
+
setConfig("authToken", void 0);
|
|
2052
|
+
setConfig("userEmail", void 0);
|
|
2053
|
+
printSuccess(t("auth.logoutSuccess"));
|
|
2054
|
+
break;
|
|
2055
|
+
default:
|
|
2056
|
+
printError(t("errors.unknownCommand", { command: cmd }));
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
// src/commands/config.ts
|
|
2061
|
+
import inquirer from "inquirer";
|
|
2062
|
+
async function configCommand() {
|
|
2063
|
+
const config3 = getConfig();
|
|
2064
|
+
console.log(`
|
|
2065
|
+
\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84: ${getConfigPath()}
|
|
2066
|
+
`);
|
|
2067
|
+
const { action } = await inquirer.prompt([{
|
|
2068
|
+
type: "list",
|
|
2069
|
+
name: "action",
|
|
2070
|
+
message: "\u9009\u62E9\u8981\u914D\u7F6E\u7684\u9879\u76EE:",
|
|
2071
|
+
choices: [
|
|
2072
|
+
{ name: "\u8BBE\u7F6EAI\u6A21\u578B", value: "model" },
|
|
2073
|
+
{ name: "\u8BBE\u7F6EKimi API Key (\u6708\u4E4B\u6697\u9762)", value: "kimi" },
|
|
2074
|
+
{ name: "\u8BBE\u7F6EOpenAI API Key", value: "openai" },
|
|
2075
|
+
{ name: "\u8BBE\u7F6EAnthropic API Key", value: "anthropic" },
|
|
2076
|
+
{ name: "\u8BBE\u7F6EDeepSeek API Key", value: "deepseek" },
|
|
2077
|
+
{ name: "\u8BBE\u7F6E\u540E\u7AEF\u670D\u52A1\u5730\u5740", value: "apiBaseUrl" },
|
|
2078
|
+
{ name: "\u8BBE\u7F6E\u754C\u9762\u8BED\u8A00", value: "locale" },
|
|
2079
|
+
{ name: "\u67E5\u770B\u5F53\u524D\u914D\u7F6E", value: "show" },
|
|
2080
|
+
{ name: "\u9000\u51FA", value: "exit" }
|
|
2081
|
+
]
|
|
2082
|
+
}]);
|
|
2083
|
+
switch (action) {
|
|
2084
|
+
case "model": {
|
|
2085
|
+
const models = listModels();
|
|
2086
|
+
const { model } = await inquirer.prompt([{
|
|
2087
|
+
type: "list",
|
|
2088
|
+
name: "model",
|
|
2089
|
+
message: "\u9009\u62E9AI\u6A21\u578B:",
|
|
2090
|
+
choices: models.map((m) => ({
|
|
2091
|
+
name: `${m.name} (${m.provider})`,
|
|
2092
|
+
value: m.id
|
|
2093
|
+
})),
|
|
2094
|
+
default: config3.model
|
|
2095
|
+
}]);
|
|
2096
|
+
setConfig("model", model);
|
|
2097
|
+
printSuccess(`\u6A21\u578B\u5DF2\u8BBE\u7F6E\u4E3A: ${model}`);
|
|
2098
|
+
break;
|
|
2099
|
+
}
|
|
2100
|
+
case "kimi": {
|
|
2101
|
+
const { key } = await inquirer.prompt([{
|
|
2102
|
+
type: "password",
|
|
2103
|
+
name: "key",
|
|
2104
|
+
message: "\u8F93\u5165Kimi API Key (\u6708\u4E4B\u6697\u9762):",
|
|
2105
|
+
mask: "*"
|
|
2106
|
+
}]);
|
|
2107
|
+
if (key) {
|
|
2108
|
+
setConfig("kimiApiKey", key);
|
|
2109
|
+
printSuccess("Kimi API Key \u5DF2\u4FDD\u5B58\u3002");
|
|
2110
|
+
}
|
|
2111
|
+
break;
|
|
2112
|
+
}
|
|
2113
|
+
case "openai": {
|
|
2114
|
+
const { key } = await inquirer.prompt([{
|
|
2115
|
+
type: "password",
|
|
2116
|
+
name: "key",
|
|
2117
|
+
message: "\u8F93\u5165OpenAI API Key:",
|
|
2118
|
+
mask: "*"
|
|
2119
|
+
}]);
|
|
2120
|
+
if (key) {
|
|
2121
|
+
setConfig("openaiApiKey", key);
|
|
2122
|
+
printSuccess("OpenAI API Key \u5DF2\u4FDD\u5B58\u3002");
|
|
2123
|
+
}
|
|
2124
|
+
break;
|
|
2125
|
+
}
|
|
2126
|
+
case "anthropic": {
|
|
2127
|
+
const { key } = await inquirer.prompt([{
|
|
2128
|
+
type: "password",
|
|
2129
|
+
name: "key",
|
|
2130
|
+
message: "\u8F93\u5165Anthropic API Key:",
|
|
2131
|
+
mask: "*"
|
|
2132
|
+
}]);
|
|
2133
|
+
if (key) {
|
|
2134
|
+
setConfig("anthropicApiKey", key);
|
|
2135
|
+
printSuccess("Anthropic API Key \u5DF2\u4FDD\u5B58\u3002");
|
|
2136
|
+
}
|
|
2137
|
+
break;
|
|
2138
|
+
}
|
|
2139
|
+
case "deepseek": {
|
|
2140
|
+
const { key } = await inquirer.prompt([{
|
|
2141
|
+
type: "password",
|
|
2142
|
+
name: "key",
|
|
2143
|
+
message: "\u8F93\u5165DeepSeek API Key:",
|
|
2144
|
+
mask: "*"
|
|
2145
|
+
}]);
|
|
2146
|
+
if (key) {
|
|
2147
|
+
setConfig("deepseekApiKey", key);
|
|
2148
|
+
printSuccess("DeepSeek API Key \u5DF2\u4FDD\u5B58\u3002");
|
|
2149
|
+
}
|
|
2150
|
+
break;
|
|
2151
|
+
}
|
|
2152
|
+
case "apiBaseUrl": {
|
|
2153
|
+
const { url } = await inquirer.prompt([{
|
|
2154
|
+
type: "input",
|
|
2155
|
+
name: "url",
|
|
2156
|
+
message: "\u8F93\u5165\u540E\u7AEF\u670D\u52A1\u5730\u5740:",
|
|
2157
|
+
default: config3.apiBaseUrl
|
|
2158
|
+
}]);
|
|
2159
|
+
setConfig("apiBaseUrl", url);
|
|
2160
|
+
printSuccess(`\u540E\u7AEF\u5730\u5740\u5DF2\u8BBE\u7F6E\u4E3A: ${url}`);
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
2163
|
+
case "locale": {
|
|
2164
|
+
const { locale } = await inquirer.prompt([{
|
|
2165
|
+
type: "list",
|
|
2166
|
+
name: "locale",
|
|
2167
|
+
message: "\u9009\u62E9\u754C\u9762\u8BED\u8A00:",
|
|
2168
|
+
choices: [
|
|
2169
|
+
{ name: "\u4E2D\u6587", value: "zh" },
|
|
2170
|
+
{ name: "English", value: "en" }
|
|
2171
|
+
],
|
|
2172
|
+
default: config3.locale
|
|
2173
|
+
}]);
|
|
2174
|
+
setConfig("locale", locale);
|
|
2175
|
+
printSuccess(`\u8BED\u8A00\u5DF2\u8BBE\u7F6E\u4E3A: ${locale}`);
|
|
2176
|
+
break;
|
|
2177
|
+
}
|
|
2178
|
+
case "show": {
|
|
2179
|
+
console.log("\n\u5F53\u524D\u914D\u7F6E:");
|
|
2180
|
+
console.log(` \u6A21\u578B: ${config3.model}`);
|
|
2181
|
+
console.log(` \u8BED\u8A00: ${config3.locale}`);
|
|
2182
|
+
console.log(` \u540E\u7AEF: ${config3.apiBaseUrl}`);
|
|
2183
|
+
console.log(` \u767B\u5F55: ${config3.userEmail || "\u672A\u767B\u5F55"}`);
|
|
2184
|
+
console.log(` Kimi Key: ${config3.kimiApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
|
|
2185
|
+
console.log(` OpenAI Key: ${config3.openaiApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
|
|
2186
|
+
console.log(` Anthropic Key: ${config3.anthropicApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
|
|
2187
|
+
console.log(` DeepSeek Key: ${config3.deepseekApiKey ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`);
|
|
2188
|
+
break;
|
|
2189
|
+
}
|
|
2190
|
+
case "exit":
|
|
2191
|
+
break;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/commands/login.ts
|
|
2196
|
+
import inquirer2 from "inquirer";
|
|
2197
|
+
async function loginCommand() {
|
|
2198
|
+
const config3 = getConfig();
|
|
2199
|
+
if (config3.authToken && config3.userEmail) {
|
|
2200
|
+
printInfo(`\u5F53\u524D\u5DF2\u767B\u5F55: ${config3.userEmail}`);
|
|
2201
|
+
const { relogin } = await inquirer2.prompt([{
|
|
2202
|
+
type: "confirm",
|
|
2203
|
+
name: "relogin",
|
|
2204
|
+
message: "\u662F\u5426\u91CD\u65B0\u767B\u5F55\uFF1F",
|
|
2205
|
+
default: false
|
|
2206
|
+
}]);
|
|
2207
|
+
if (!relogin) return;
|
|
2208
|
+
}
|
|
2209
|
+
console.log(theme.brandBold("\n\u{1F511} \u767B\u5F55\u6C88\u7FD4\u7684AI\u52A9\u624B\n"));
|
|
2210
|
+
const { email } = await inquirer2.prompt([{
|
|
2211
|
+
type: "input",
|
|
2212
|
+
name: "email",
|
|
2213
|
+
message: "\u90AE\u7BB1:",
|
|
2214
|
+
validate: (input) => {
|
|
2215
|
+
if (!input.includes("@")) return "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740";
|
|
2216
|
+
return true;
|
|
2217
|
+
}
|
|
2218
|
+
}]);
|
|
2219
|
+
const { password } = await inquirer2.prompt([{
|
|
2220
|
+
type: "password",
|
|
2221
|
+
name: "password",
|
|
2222
|
+
message: "\u5BC6\u7801:",
|
|
2223
|
+
mask: "*"
|
|
2224
|
+
}]);
|
|
2225
|
+
try {
|
|
2226
|
+
const response = await fetch(`${config3.apiBaseUrl}/api/auth/login`, {
|
|
2227
|
+
method: "POST",
|
|
2228
|
+
headers: { "Content-Type": "application/json" },
|
|
2229
|
+
body: JSON.stringify({ email, password })
|
|
2230
|
+
});
|
|
2231
|
+
if (response.ok) {
|
|
2232
|
+
const data = await response.json();
|
|
2233
|
+
setConfig("authToken", data.token);
|
|
2234
|
+
setConfig("userEmail", data.user.email);
|
|
2235
|
+
console.log();
|
|
2236
|
+
printSuccess("\u767B\u5F55\u6210\u529F\uFF01");
|
|
2237
|
+
console.log(theme.dim(` \u90AE\u7BB1: ${data.user.email}`));
|
|
2238
|
+
console.log(theme.dim(` \u8BA1\u5212: ${data.user.plan}`));
|
|
2239
|
+
if (data.user.role === "admin") {
|
|
2240
|
+
console.log(theme.error(` \u89D2\u8272: \u7BA1\u7406\u5458`));
|
|
2241
|
+
}
|
|
2242
|
+
console.log();
|
|
2243
|
+
} else if (response.status === 404) {
|
|
2244
|
+
printInfo("\u8BE5\u90AE\u7BB1\u5C1A\u672A\u6CE8\u518C");
|
|
2245
|
+
const { register } = await inquirer2.prompt([{
|
|
2246
|
+
type: "confirm",
|
|
2247
|
+
name: "register",
|
|
2248
|
+
message: "\u662F\u5426\u7ACB\u5373\u6CE8\u518C\uFF1F",
|
|
2249
|
+
default: true
|
|
2250
|
+
}]);
|
|
2251
|
+
if (register) {
|
|
2252
|
+
const regResponse = await fetch(`${config3.apiBaseUrl}/api/auth/register`, {
|
|
2253
|
+
method: "POST",
|
|
2254
|
+
headers: { "Content-Type": "application/json" },
|
|
2255
|
+
body: JSON.stringify({ email, password })
|
|
2256
|
+
});
|
|
2257
|
+
if (regResponse.ok) {
|
|
2258
|
+
const data = await regResponse.json();
|
|
2259
|
+
setConfig("authToken", data.token);
|
|
2260
|
+
setConfig("userEmail", data.user.email);
|
|
2261
|
+
printSuccess("\u6CE8\u518C\u5E76\u767B\u5F55\u6210\u529F\uFF01");
|
|
2262
|
+
} else {
|
|
2263
|
+
const err = await regResponse.json();
|
|
2264
|
+
printError(`\u6CE8\u518C\u5931\u8D25: ${err.error || "\u672A\u77E5\u9519\u8BEF"}`);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
} else {
|
|
2268
|
+
const err = await response.json();
|
|
2269
|
+
printError(`\u767B\u5F55\u5931\u8D25: ${err.error || "\u672A\u77E5\u9519\u8BEF"}`);
|
|
2270
|
+
}
|
|
2271
|
+
} catch (err) {
|
|
2272
|
+
printError("\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u540E\u7AEF\u670D\u52A1\u662F\u5426\u8FD0\u884C\u3002");
|
|
2273
|
+
console.log(theme.dim(` \u540E\u7AEF\u5730\u5740: ${config3.apiBaseUrl}`));
|
|
2274
|
+
console.log(theme.dim(" \u4F7F\u7528 sxai config \u4FEE\u6539\u540E\u7AEF\u5730\u5740"));
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
// src/commands/register.ts
|
|
2279
|
+
import inquirer3 from "inquirer";
|
|
2280
|
+
async function registerCommand() {
|
|
2281
|
+
const config3 = getConfig();
|
|
2282
|
+
if (config3.authToken && config3.userEmail) {
|
|
2283
|
+
printInfo(`\u5F53\u524D\u5DF2\u767B\u5F55: ${config3.userEmail}`);
|
|
2284
|
+
const { proceed } = await inquirer3.prompt([{
|
|
2285
|
+
type: "confirm",
|
|
2286
|
+
name: "proceed",
|
|
2287
|
+
message: "\u662F\u5426\u6CE8\u518C\u65B0\u8D26\u6237\uFF1F\uFF08\u5C06\u8986\u76D6\u5F53\u524D\u767B\u5F55\u72B6\u6001\uFF09",
|
|
2288
|
+
default: false
|
|
2289
|
+
}]);
|
|
2290
|
+
if (!proceed) return;
|
|
2291
|
+
}
|
|
2292
|
+
console.log(theme.brandBold("\n\u{1F4DD} \u6CE8\u518C\u6C88\u7FD4\u7684AI\u52A9\u624B\u8D26\u6237\n"));
|
|
2293
|
+
const { email } = await inquirer3.prompt([{
|
|
2294
|
+
type: "input",
|
|
2295
|
+
name: "email",
|
|
2296
|
+
message: "\u90AE\u7BB1:",
|
|
2297
|
+
validate: (input) => {
|
|
2298
|
+
if (!input.includes("@")) return "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740";
|
|
2299
|
+
return true;
|
|
2300
|
+
}
|
|
2301
|
+
}]);
|
|
2302
|
+
const { name } = await inquirer3.prompt([{
|
|
2303
|
+
type: "input",
|
|
2304
|
+
name: "name",
|
|
2305
|
+
message: "\u6635\u79F0 (\u53EF\u9009):",
|
|
2306
|
+
default: email.split("@")[0]
|
|
2307
|
+
}]);
|
|
2308
|
+
const { password } = await inquirer3.prompt([{
|
|
2309
|
+
type: "password",
|
|
2310
|
+
name: "password",
|
|
2311
|
+
message: "\u5BC6\u7801 (\u81F3\u5C116\u4F4D):",
|
|
2312
|
+
mask: "*",
|
|
2313
|
+
validate: (input) => {
|
|
2314
|
+
if (input.length < 6) return "\u5BC6\u7801\u81F3\u5C116\u4F4D";
|
|
2315
|
+
return true;
|
|
2316
|
+
}
|
|
2317
|
+
}]);
|
|
2318
|
+
const { confirmPassword } = await inquirer3.prompt([{
|
|
2319
|
+
type: "password",
|
|
2320
|
+
name: "confirmPassword",
|
|
2321
|
+
message: "\u786E\u8BA4\u5BC6\u7801:",
|
|
2322
|
+
mask: "*"
|
|
2323
|
+
}]);
|
|
2324
|
+
if (password !== confirmPassword) {
|
|
2325
|
+
printError("\u4E24\u6B21\u5BC6\u7801\u4E0D\u4E00\u81F4");
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
try {
|
|
2329
|
+
const response = await fetch(`${config3.apiBaseUrl}/api/auth/register`, {
|
|
2330
|
+
method: "POST",
|
|
2331
|
+
headers: { "Content-Type": "application/json" },
|
|
2332
|
+
body: JSON.stringify({ email, password, name })
|
|
2333
|
+
});
|
|
2334
|
+
if (response.ok) {
|
|
2335
|
+
const data = await response.json();
|
|
2336
|
+
setConfig("authToken", data.token);
|
|
2337
|
+
setConfig("userEmail", data.user.email);
|
|
2338
|
+
console.log();
|
|
2339
|
+
printSuccess("\u6CE8\u518C\u6210\u529F\uFF01");
|
|
2340
|
+
console.log(theme.dim(` \u90AE\u7BB1: ${data.user.email}`));
|
|
2341
|
+
console.log(theme.dim(` \u6635\u79F0: ${data.user.name}`));
|
|
2342
|
+
console.log(theme.dim(` \u8BA1\u5212: ${data.user.plan}`));
|
|
2343
|
+
console.log(theme.dim(` \u6BCF\u65E5\u8BF7\u6C42: ${data.user.dailyLimit}\u6B21`));
|
|
2344
|
+
console.log(theme.dim(` \u514D\u8D39Token: ${formatTokens(data.user.totalFreeTokens)}`));
|
|
2345
|
+
console.log();
|
|
2346
|
+
console.log(theme.info("\u73B0\u5728\u53EF\u4EE5\u76F4\u63A5\u4F7F\u7528 sxai \u5F00\u59CB\u5BF9\u8BDD\u4E86\uFF01"));
|
|
2347
|
+
} else {
|
|
2348
|
+
const err = await response.json();
|
|
2349
|
+
printError(`\u6CE8\u518C\u5931\u8D25: ${err.error || "\u672A\u77E5\u9519\u8BEF"}`);
|
|
2350
|
+
}
|
|
2351
|
+
} catch (err) {
|
|
2352
|
+
printError("\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u540E\u7AEF\u670D\u52A1\u662F\u5426\u8FD0\u884C\u3002");
|
|
2353
|
+
console.log(theme.dim(` \u540E\u7AEF\u5730\u5740: ${config3.apiBaseUrl}`));
|
|
2354
|
+
console.log(theme.dim(" \u4F7F\u7528 sxai config \u4FEE\u6539\u540E\u7AEF\u5730\u5740"));
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
function formatTokens(n) {
|
|
2358
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2359
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
|
|
2360
|
+
return String(n);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// src/commands/account.ts
|
|
2364
|
+
async function accountCommand() {
|
|
2365
|
+
const config3 = getConfig();
|
|
2366
|
+
if (!config3.authToken) {
|
|
2367
|
+
printWarning("\u5C1A\u672A\u767B\u5F55\u3002\u8BF7\u5148\u6CE8\u518C\u6216\u767B\u5F55:");
|
|
2368
|
+
console.log(theme.dim(" sxai register # \u6CE8\u518C\u65B0\u8D26\u6237"));
|
|
2369
|
+
console.log(theme.dim(" sxai login # \u767B\u5F55\u5DF2\u6709\u8D26\u6237"));
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
try {
|
|
2373
|
+
const response = await fetch(`${config3.apiBaseUrl}/api/auth/me`, {
|
|
2374
|
+
headers: { "Authorization": `Bearer ${config3.authToken}` }
|
|
2375
|
+
});
|
|
2376
|
+
if (response.status === 401) {
|
|
2377
|
+
printError("\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55: sxai login");
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
if (!response.ok) {
|
|
2381
|
+
printError("\u83B7\u53D6\u8D26\u6237\u4FE1\u606F\u5931\u8D25");
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
const data = await response.json();
|
|
2385
|
+
const { user, quota, usage } = data;
|
|
2386
|
+
console.log();
|
|
2387
|
+
console.log(theme.brandBold(" \u{1F4CB} \u8D26\u6237\u4FE1\u606F"));
|
|
2388
|
+
console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2389
|
+
console.log(` \u90AE\u7BB1 ${theme.info(user.email)}`);
|
|
2390
|
+
console.log(` \u6635\u79F0 ${user.name || "-"}`);
|
|
2391
|
+
console.log(` \u89D2\u8272 ${user.role === "admin" ? theme.error("\u7BA1\u7406\u5458") : "\u666E\u901A\u7528\u6237"}`);
|
|
2392
|
+
console.log(` \u8BA1\u5212 ${planLabel(user.plan)}`);
|
|
2393
|
+
console.log(` \u72B6\u6001 ${user.isActive ? theme.success("\u2713 \u6B63\u5E38") : theme.error("\u2717 \u5DF2\u7981\u7528")}`);
|
|
2394
|
+
console.log(` \u6CE8\u518C ${new Date(user.createdAt).toLocaleDateString("zh-CN")}`);
|
|
2395
|
+
if (quota) {
|
|
2396
|
+
console.log();
|
|
2397
|
+
console.log(theme.brandBold(" \u{1F4CA} \u989D\u5EA6"));
|
|
2398
|
+
console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2399
|
+
const dailyBar = progressBar(quota.daily.used, quota.daily.limit);
|
|
2400
|
+
console.log(` \u4ECA\u65E5\u8BF7\u6C42 ${dailyBar} ${quota.daily.used}/${quota.daily.limit}`);
|
|
2401
|
+
const monthBar = progressBar(quota.monthlyTokens.used, quota.monthlyTokens.limit);
|
|
2402
|
+
console.log(` \u672C\u6708Token ${monthBar} ${fmtTokens(quota.monthlyTokens.used)}/${fmtTokens(quota.monthlyTokens.limit)}`);
|
|
2403
|
+
const lifeBar = progressBar(quota.lifetimeTokens.used, quota.lifetimeTokens.limit);
|
|
2404
|
+
console.log(` \u514D\u8D39\u989D\u5EA6 ${lifeBar} ${fmtTokens(quota.lifetimeTokens.used)}/${fmtTokens(quota.lifetimeTokens.limit)}`);
|
|
2405
|
+
if (!quota.allowed) {
|
|
2406
|
+
console.log();
|
|
2407
|
+
printWarning(quota.reason || "\u989D\u5EA6\u5DF2\u7528\u5B8C");
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
if (usage) {
|
|
2411
|
+
console.log();
|
|
2412
|
+
console.log(theme.brandBold(" \u{1F4C8} \u7528\u91CF\u7EDF\u8BA1"));
|
|
2413
|
+
console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2414
|
+
console.log(` \u4ECA\u65E5 ${usage.today.requests}\u6B21\u8BF7\u6C42 \xB7 ${fmtTokens(usage.today.tokens)} tokens`);
|
|
2415
|
+
console.log(` \u672C\u6708 ${usage.thisMonth.requests}\u6B21\u8BF7\u6C42 \xB7 ${fmtTokens(usage.thisMonth.tokens)} tokens`);
|
|
2416
|
+
console.log(` \u7D2F\u8BA1 ${usage.lifetime.requests}\u6B21\u8BF7\u6C42 \xB7 ${fmtTokens(usage.lifetime.tokens)} tokens`);
|
|
2417
|
+
}
|
|
2418
|
+
console.log();
|
|
2419
|
+
} catch (err) {
|
|
2420
|
+
printError("\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u540E\u7AEF\u670D\u52A1\u662F\u5426\u8FD0\u884C\u3002");
|
|
2421
|
+
console.log(theme.dim(` \u540E\u7AEF\u5730\u5740: ${config3.apiBaseUrl}`));
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
function planLabel(plan) {
|
|
2425
|
+
switch (plan) {
|
|
2426
|
+
case "free":
|
|
2427
|
+
return theme.dim("\u514D\u8D39\u7248");
|
|
2428
|
+
case "pro":
|
|
2429
|
+
return theme.success("\u4E13\u4E1A\u7248");
|
|
2430
|
+
case "enterprise":
|
|
2431
|
+
return theme.info("\u4F01\u4E1A\u7248");
|
|
2432
|
+
default:
|
|
2433
|
+
return plan;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
function fmtTokens(n) {
|
|
2437
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2438
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
|
|
2439
|
+
return String(n);
|
|
2440
|
+
}
|
|
2441
|
+
function progressBar(used, limit, width = 16) {
|
|
2442
|
+
if (limit === 0) return theme.dim("[" + "\u2591".repeat(width) + "]");
|
|
2443
|
+
const ratio = Math.min(1, used / limit);
|
|
2444
|
+
const filled = Math.round(ratio * width);
|
|
2445
|
+
const empty = width - filled;
|
|
2446
|
+
const color = ratio < 0.6 ? theme.success : ratio < 0.85 ? theme.warning : theme.error;
|
|
2447
|
+
return color("[" + "\u2588".repeat(filled) + "\u2591".repeat(empty) + "]");
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/commands/admin.ts
|
|
2451
|
+
import inquirer4 from "inquirer";
|
|
2452
|
+
async function adminCommand() {
|
|
2453
|
+
const config3 = getConfig();
|
|
2454
|
+
if (!config3.authToken) {
|
|
2455
|
+
printWarning("\u8BF7\u5148\u767B\u5F55\u7BA1\u7406\u5458\u8D26\u6237: sxai login");
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
console.log(theme.brandBold("\n\u2699\uFE0F \u7BA1\u7406\u5458\u63A7\u5236\u53F0\n"));
|
|
2459
|
+
const { action } = await inquirer4.prompt([{
|
|
2460
|
+
type: "list",
|
|
2461
|
+
name: "action",
|
|
2462
|
+
message: "\u9009\u62E9\u64CD\u4F5C:",
|
|
2463
|
+
choices: [
|
|
2464
|
+
{ name: "\u{1F4CB} \u67E5\u770B\u6240\u6709\u7528\u6237", value: "list" },
|
|
2465
|
+
{ name: "\u{1F3AF} \u8BBE\u7F6E\u7528\u6237\u989D\u5EA6", value: "quota" },
|
|
2466
|
+
{ name: "\u{1F4CA} \u4FEE\u6539\u7528\u6237\u8BA1\u5212", value: "plan" },
|
|
2467
|
+
{ name: "\u{1F451} \u7BA1\u7406\u7528\u6237\u89D2\u8272", value: "role" },
|
|
2468
|
+
{ name: "\u{1F504} \u91CD\u7F6E\u7528\u91CF\u8BA1\u6570", value: "reset" },
|
|
2469
|
+
{ name: "\u{1F6AB} \u542F\u7528/\u7981\u7528\u7528\u6237", value: "status" },
|
|
2470
|
+
{ name: "\u{1F4C8} \u5E73\u53F0\u7EDF\u8BA1", value: "stats" },
|
|
2471
|
+
{ name: "\u9000\u51FA", value: "exit" }
|
|
2472
|
+
]
|
|
2473
|
+
}]);
|
|
2474
|
+
switch (action) {
|
|
2475
|
+
case "list":
|
|
2476
|
+
await listUsers(config3);
|
|
2477
|
+
break;
|
|
2478
|
+
case "quota":
|
|
2479
|
+
await setUserQuota(config3);
|
|
2480
|
+
break;
|
|
2481
|
+
case "plan":
|
|
2482
|
+
await setUserPlan(config3);
|
|
2483
|
+
break;
|
|
2484
|
+
case "role":
|
|
2485
|
+
await setUserRole(config3);
|
|
2486
|
+
break;
|
|
2487
|
+
case "reset":
|
|
2488
|
+
await resetUsage(config3);
|
|
2489
|
+
break;
|
|
2490
|
+
case "status":
|
|
2491
|
+
await toggleUserStatus(config3);
|
|
2492
|
+
break;
|
|
2493
|
+
case "stats":
|
|
2494
|
+
await showStats(config3);
|
|
2495
|
+
break;
|
|
2496
|
+
case "exit":
|
|
2497
|
+
break;
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
async function adminFetch(config3, path5, options) {
|
|
2501
|
+
const response = await fetch(`${config3.apiBaseUrl}${path5}`, {
|
|
2502
|
+
...options,
|
|
2503
|
+
headers: {
|
|
2504
|
+
"Content-Type": "application/json",
|
|
2505
|
+
"Authorization": `Bearer ${config3.authToken}`,
|
|
2506
|
+
...options?.headers || {}
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
if (response.status === 403) {
|
|
2510
|
+
printError("\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650\u3002\u5F53\u524D\u8D26\u6237\u4E0D\u662F\u7BA1\u7406\u5458\u3002");
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
if (response.status === 401) {
|
|
2514
|
+
printError("\u767B\u5F55\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55: sxai login");
|
|
2515
|
+
return null;
|
|
2516
|
+
}
|
|
2517
|
+
return response;
|
|
2518
|
+
}
|
|
2519
|
+
async function listUsers(config3) {
|
|
2520
|
+
try {
|
|
2521
|
+
const resp = await adminFetch(config3, "/api/admin/users");
|
|
2522
|
+
if (!resp) return;
|
|
2523
|
+
const data = await resp.json();
|
|
2524
|
+
console.log(theme.dim(`
|
|
2525
|
+
\u5171 ${data.pagination.total} \u4E2A\u7528\u6237
|
|
2526
|
+
`));
|
|
2527
|
+
console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2528
|
+
console.log(` ${"\u90AE\u7BB1".padEnd(28)} ${"\u8BA1\u5212".padEnd(8)} ${"\u89D2\u8272".padEnd(8)} ${"\u65E5\u9650".padEnd(6)} ${"\u514D\u8D39\u989D\u5EA6".padEnd(12)} ${"\u5DF2\u7528"}`);
|
|
2529
|
+
console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2530
|
+
for (const u of data.users) {
|
|
2531
|
+
const status = u.isActive ? "" : theme.error(" [\u7981\u7528]");
|
|
2532
|
+
const roleTag = u.role === "admin" ? theme.error("\u7BA1\u7406\u5458") : "\u7528\u6237 ";
|
|
2533
|
+
const planTag = u.plan === "free" ? theme.dim("\u514D\u8D39 ") : u.plan === "pro" ? theme.success("\u4E13\u4E1A ") : theme.info("\u4F01\u4E1A ");
|
|
2534
|
+
console.log(
|
|
2535
|
+
` ${(u.email + status).padEnd(28)} ${planTag} ${roleTag} ${String(u.dailyLimit).padEnd(6)} ${fmtTokens2(u.totalFreeTokens).padEnd(12)} ${fmtTokens2(u.totalTokensUsed)}`
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
console.log();
|
|
2539
|
+
} catch {
|
|
2540
|
+
printError("\u83B7\u53D6\u7528\u6237\u5217\u8868\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
async function setUserQuota(config3) {
|
|
2544
|
+
const users = await fetchUserList(config3);
|
|
2545
|
+
if (!users) return;
|
|
2546
|
+
const { userId } = await inquirer4.prompt([{
|
|
2547
|
+
type: "list",
|
|
2548
|
+
name: "userId",
|
|
2549
|
+
message: "\u9009\u62E9\u7528\u6237:",
|
|
2550
|
+
choices: users.map((u) => ({ name: `${u.email} (${u.plan})`, value: u.id }))
|
|
2551
|
+
}]);
|
|
2552
|
+
const selectedUser = users.find((u) => u.id === userId);
|
|
2553
|
+
console.log(theme.dim(`
|
|
2554
|
+
\u5F53\u524D\u989D\u5EA6: \u65E5\u9650=${selectedUser.dailyLimit}\u6B21 \u6708Token=${fmtTokens2(selectedUser.monthlyTokenLimit)} \u514D\u8D39\u603B\u989D=${fmtTokens2(selectedUser.totalFreeTokens)}
|
|
2555
|
+
`));
|
|
2556
|
+
const { dailyLimit } = await inquirer4.prompt([{
|
|
2557
|
+
type: "number",
|
|
2558
|
+
name: "dailyLimit",
|
|
2559
|
+
message: "\u6BCF\u65E5\u8BF7\u6C42\u4E0A\u9650:",
|
|
2560
|
+
default: selectedUser.dailyLimit
|
|
2561
|
+
}]);
|
|
2562
|
+
const { monthlyTokenLimit } = await inquirer4.prompt([{
|
|
2563
|
+
type: "number",
|
|
2564
|
+
name: "monthlyTokenLimit",
|
|
2565
|
+
message: "\u6BCF\u6708Token\u4E0A\u9650:",
|
|
2566
|
+
default: selectedUser.monthlyTokenLimit
|
|
2567
|
+
}]);
|
|
2568
|
+
const { totalFreeTokens } = await inquirer4.prompt([{
|
|
2569
|
+
type: "number",
|
|
2570
|
+
name: "totalFreeTokens",
|
|
2571
|
+
message: "\u514D\u8D39Token\u603B\u989D:",
|
|
2572
|
+
default: selectedUser.totalFreeTokens
|
|
2573
|
+
}]);
|
|
2574
|
+
try {
|
|
2575
|
+
const resp = await adminFetch(config3, `/api/admin/users/${userId}/quota`, {
|
|
2576
|
+
method: "PUT",
|
|
2577
|
+
body: JSON.stringify({ dailyLimit, monthlyTokenLimit, totalFreeTokens })
|
|
2578
|
+
});
|
|
2579
|
+
if (!resp) return;
|
|
2580
|
+
if (resp.ok) {
|
|
2581
|
+
const data = await resp.json();
|
|
2582
|
+
printSuccess(data.message);
|
|
2583
|
+
} else {
|
|
2584
|
+
const err = await resp.json();
|
|
2585
|
+
printError(err.error);
|
|
2586
|
+
}
|
|
2587
|
+
} catch {
|
|
2588
|
+
printError("\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
async function setUserPlan(config3) {
|
|
2592
|
+
const users = await fetchUserList(config3);
|
|
2593
|
+
if (!users) return;
|
|
2594
|
+
const { userId } = await inquirer4.prompt([{
|
|
2595
|
+
type: "list",
|
|
2596
|
+
name: "userId",
|
|
2597
|
+
message: "\u9009\u62E9\u7528\u6237:",
|
|
2598
|
+
choices: users.map((u) => ({ name: `${u.email} (\u5F53\u524D: ${u.plan})`, value: u.id }))
|
|
2599
|
+
}]);
|
|
2600
|
+
const { plan } = await inquirer4.prompt([{
|
|
2601
|
+
type: "list",
|
|
2602
|
+
name: "plan",
|
|
2603
|
+
message: "\u9009\u62E9\u8BA1\u5212:",
|
|
2604
|
+
choices: [
|
|
2605
|
+
{ name: "\u514D\u8D39\u7248 (50\u6B21/\u5929, 500K tokens/\u6708, 1M \u603B\u989D)", value: "free" },
|
|
2606
|
+
{ name: "\u4E13\u4E1A\u7248 (500\u6B21/\u5929, 5M tokens/\u6708, 50M \u603B\u989D)", value: "pro" },
|
|
2607
|
+
{ name: "\u4F01\u4E1A\u7248 (5000\u6B21/\u5929, 50M tokens/\u6708, 500M \u603B\u989D)", value: "enterprise" }
|
|
2608
|
+
]
|
|
2609
|
+
}]);
|
|
2610
|
+
try {
|
|
2611
|
+
const resp = await adminFetch(config3, `/api/admin/users/${userId}/plan`, {
|
|
2612
|
+
method: "PUT",
|
|
2613
|
+
body: JSON.stringify({ plan })
|
|
2614
|
+
});
|
|
2615
|
+
if (!resp) return;
|
|
2616
|
+
if (resp.ok) {
|
|
2617
|
+
const data = await resp.json();
|
|
2618
|
+
printSuccess(data.message);
|
|
2619
|
+
} else {
|
|
2620
|
+
const err = await resp.json();
|
|
2621
|
+
printError(err.error);
|
|
2622
|
+
}
|
|
2623
|
+
} catch {
|
|
2624
|
+
printError("\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
async function setUserRole(config3) {
|
|
2628
|
+
const users = await fetchUserList(config3);
|
|
2629
|
+
if (!users) return;
|
|
2630
|
+
const { userId } = await inquirer4.prompt([{
|
|
2631
|
+
type: "list",
|
|
2632
|
+
name: "userId",
|
|
2633
|
+
message: "\u9009\u62E9\u7528\u6237:",
|
|
2634
|
+
choices: users.map((u) => ({
|
|
2635
|
+
name: `${u.email} (${u.role === "admin" ? "\u7BA1\u7406\u5458" : "\u666E\u901A\u7528\u6237"})`,
|
|
2636
|
+
value: u.id
|
|
2637
|
+
}))
|
|
2638
|
+
}]);
|
|
2639
|
+
const { role } = await inquirer4.prompt([{
|
|
2640
|
+
type: "list",
|
|
2641
|
+
name: "role",
|
|
2642
|
+
message: "\u8BBE\u7F6E\u89D2\u8272:",
|
|
2643
|
+
choices: [
|
|
2644
|
+
{ name: "\u666E\u901A\u7528\u6237", value: "user" },
|
|
2645
|
+
{ name: "\u7BA1\u7406\u5458", value: "admin" }
|
|
2646
|
+
]
|
|
2647
|
+
}]);
|
|
2648
|
+
try {
|
|
2649
|
+
const resp = await adminFetch(config3, `/api/admin/users/${userId}/role`, {
|
|
2650
|
+
method: "PUT",
|
|
2651
|
+
body: JSON.stringify({ role })
|
|
2652
|
+
});
|
|
2653
|
+
if (!resp) return;
|
|
2654
|
+
if (resp.ok) {
|
|
2655
|
+
const data = await resp.json();
|
|
2656
|
+
printSuccess(data.message);
|
|
2657
|
+
} else {
|
|
2658
|
+
const err = await resp.json();
|
|
2659
|
+
printError(err.error);
|
|
2660
|
+
}
|
|
2661
|
+
} catch {
|
|
2662
|
+
printError("\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
async function resetUsage(config3) {
|
|
2666
|
+
const users = await fetchUserList(config3);
|
|
2667
|
+
if (!users) return;
|
|
2668
|
+
const { userId } = await inquirer4.prompt([{
|
|
2669
|
+
type: "list",
|
|
2670
|
+
name: "userId",
|
|
2671
|
+
message: "\u9009\u62E9\u7528\u6237:",
|
|
2672
|
+
choices: users.map((u) => ({
|
|
2673
|
+
name: `${u.email} (\u5DF2\u7528 ${fmtTokens2(u.totalTokensUsed)} tokens)`,
|
|
2674
|
+
value: u.id
|
|
2675
|
+
}))
|
|
2676
|
+
}]);
|
|
2677
|
+
const { confirm } = await inquirer4.prompt([{
|
|
2678
|
+
type: "confirm",
|
|
2679
|
+
name: "confirm",
|
|
2680
|
+
message: "\u786E\u5B9A\u8981\u91CD\u7F6E\u8BE5\u7528\u6237\u7684\u7D2F\u8BA1\u7528\u91CF\u5417\uFF1F",
|
|
2681
|
+
default: false
|
|
2682
|
+
}]);
|
|
2683
|
+
if (!confirm) return;
|
|
2684
|
+
try {
|
|
2685
|
+
const resp = await adminFetch(config3, `/api/admin/users/${userId}/reset-usage`, {
|
|
2686
|
+
method: "POST"
|
|
2687
|
+
});
|
|
2688
|
+
if (!resp) return;
|
|
2689
|
+
if (resp.ok) {
|
|
2690
|
+
printSuccess("\u7528\u91CF\u5DF2\u91CD\u7F6E");
|
|
2691
|
+
} else {
|
|
2692
|
+
const err = await resp.json();
|
|
2693
|
+
printError(err.error);
|
|
2694
|
+
}
|
|
2695
|
+
} catch {
|
|
2696
|
+
printError("\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
async function toggleUserStatus(config3) {
|
|
2700
|
+
const users = await fetchUserList(config3);
|
|
2701
|
+
if (!users) return;
|
|
2702
|
+
const { userId } = await inquirer4.prompt([{
|
|
2703
|
+
type: "list",
|
|
2704
|
+
name: "userId",
|
|
2705
|
+
message: "\u9009\u62E9\u7528\u6237:",
|
|
2706
|
+
choices: users.map((u) => ({
|
|
2707
|
+
name: `${u.email} (${u.isActive ? "\u6B63\u5E38" : "\u5DF2\u7981\u7528"})`,
|
|
2708
|
+
value: u.id
|
|
2709
|
+
}))
|
|
2710
|
+
}]);
|
|
2711
|
+
const user = users.find((u) => u.id === userId);
|
|
2712
|
+
const newStatus = !user.isActive;
|
|
2713
|
+
try {
|
|
2714
|
+
const resp = await adminFetch(config3, `/api/admin/users/${userId}/status`, {
|
|
2715
|
+
method: "PUT",
|
|
2716
|
+
body: JSON.stringify({ isActive: newStatus })
|
|
2717
|
+
});
|
|
2718
|
+
if (!resp) return;
|
|
2719
|
+
if (resp.ok) {
|
|
2720
|
+
const data = await resp.json();
|
|
2721
|
+
printSuccess(data.message);
|
|
2722
|
+
} else {
|
|
2723
|
+
const err = await resp.json();
|
|
2724
|
+
printError(err.error);
|
|
2725
|
+
}
|
|
2726
|
+
} catch {
|
|
2727
|
+
printError("\u64CD\u4F5C\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
async function showStats(config3) {
|
|
2731
|
+
try {
|
|
2732
|
+
const resp = await adminFetch(config3, "/api/admin/stats");
|
|
2733
|
+
if (!resp) return;
|
|
2734
|
+
const data = await resp.json();
|
|
2735
|
+
console.log(theme.brandBold("\n \u{1F4C8} \u5E73\u53F0\u7EDF\u8BA1"));
|
|
2736
|
+
console.log(theme.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2737
|
+
console.log(` \u7528\u6237\u603B\u6570 ${data.users.total}`);
|
|
2738
|
+
console.log(` \u6D3B\u8DC3\u7528\u6237 ${data.users.active}`);
|
|
2739
|
+
console.log(` \u603B\u8BF7\u6C42\u6570 ${data.usage.totalRequests}`);
|
|
2740
|
+
console.log(` \u603BToken\u7528\u91CF ${fmtTokens2(Number(data.usage.totalTokens))}`);
|
|
2741
|
+
console.log();
|
|
2742
|
+
} catch {
|
|
2743
|
+
printError("\u83B7\u53D6\u7EDF\u8BA1\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u3002");
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
async function fetchUserList(config3) {
|
|
2747
|
+
try {
|
|
2748
|
+
const resp = await adminFetch(config3, "/api/admin/users?limit=100");
|
|
2749
|
+
if (!resp) return null;
|
|
2750
|
+
const data = await resp.json();
|
|
2751
|
+
if (!data.users || data.users.length === 0) {
|
|
2752
|
+
printInfo("\u6682\u65E0\u7528\u6237");
|
|
2753
|
+
return null;
|
|
2754
|
+
}
|
|
2755
|
+
return data.users;
|
|
2756
|
+
} catch {
|
|
2757
|
+
printError("\u83B7\u53D6\u7528\u6237\u5217\u8868\u5931\u8D25");
|
|
2758
|
+
return null;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
function fmtTokens2(n) {
|
|
2762
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
2763
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
|
|
2764
|
+
return String(n);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// src/index.ts
|
|
2768
|
+
var program = new Command();
|
|
2769
|
+
var config2 = getConfig();
|
|
2770
|
+
setLocale(config2.locale);
|
|
2771
|
+
program.name("sxai").description(t("description")).version("0.1.0");
|
|
2772
|
+
program.command("chat", { isDefault: true }).description("\u542F\u52A8\u4EA4\u4E92\u5F0FAI\u5BF9\u8BDD\uFF08\u9ED8\u8BA4\u547D\u4EE4\uFF09").option("-m, --model <model>", "\u6307\u5B9AAI\u6A21\u578B", config2.model).action(async (options) => {
|
|
2773
|
+
if (options.model && options.model !== config2.model) {
|
|
2774
|
+
setConfig("model", options.model);
|
|
2775
|
+
}
|
|
2776
|
+
await chatCommand();
|
|
2777
|
+
});
|
|
2778
|
+
program.command("register").description("\u6CE8\u518C\u65B0\u8D26\u6237").action(async () => {
|
|
2779
|
+
await registerCommand();
|
|
2780
|
+
});
|
|
2781
|
+
program.command("login").description("\u767B\u5F55\u8D26\u6237").action(async () => {
|
|
2782
|
+
await loginCommand();
|
|
2783
|
+
});
|
|
2784
|
+
program.command("logout").description("\u9000\u51FA\u767B\u5F55").action(() => {
|
|
2785
|
+
setConfig("authToken", void 0);
|
|
2786
|
+
setConfig("userEmail", void 0);
|
|
2787
|
+
printSuccess("\u5DF2\u9000\u51FA\u767B\u5F55");
|
|
2788
|
+
});
|
|
2789
|
+
program.command("account").alias("me").description("\u67E5\u770B\u8D26\u6237\u4FE1\u606F\u3001\u989D\u5EA6\u548C\u7528\u91CF").action(async () => {
|
|
2790
|
+
await accountCommand();
|
|
2791
|
+
});
|
|
2792
|
+
program.command("admin").description("\u7BA1\u7406\u5458\u63A7\u5236\u53F0\uFF08\u7BA1\u7406\u7528\u6237\u3001\u8BBE\u7F6E\u989D\u5EA6\uFF09").action(async () => {
|
|
2793
|
+
await adminCommand();
|
|
2794
|
+
});
|
|
2795
|
+
program.command("config").description("\u914D\u7F6E\u6C88\u7FD4\u7684AI\u52A9\u624B").action(async () => {
|
|
2796
|
+
await configCommand();
|
|
2797
|
+
});
|
|
2798
|
+
program.command("set-key <provider> <key>").description("\u8BBE\u7F6EAPI\u5BC6\u94A5 (provider: openai, anthropic, deepseek, kimi)").action((provider, key) => {
|
|
2799
|
+
const keyMap = {
|
|
2800
|
+
openai: "openaiApiKey",
|
|
2801
|
+
anthropic: "anthropicApiKey",
|
|
2802
|
+
deepseek: "deepseekApiKey",
|
|
2803
|
+
kimi: "kimiApiKey"
|
|
2804
|
+
};
|
|
2805
|
+
const configKey = keyMap[provider];
|
|
2806
|
+
if (!configKey) {
|
|
2807
|
+
console.error(`\u672A\u77E5\u7684\u63D0\u4F9B\u5546: ${provider}\u3002\u652F\u6301: openai, anthropic, deepseek, kimi`);
|
|
2808
|
+
process.exit(1);
|
|
2809
|
+
}
|
|
2810
|
+
setConfig(configKey, key);
|
|
2811
|
+
printSuccess(`${provider} API Key \u5DF2\u4FDD\u5B58\u3002`);
|
|
2812
|
+
});
|
|
2813
|
+
program.parse();
|