@code4bug/jarvis-agent 1.1.7 → 1.2.1
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/commands/init.d.ts +1 -1
- package/dist/commands/init.js +416 -64
- package/dist/components/MarkdownText.d.ts +4 -0
- package/dist/components/MarkdownText.js +10 -3
- package/dist/components/MessageItem.js +4 -1
- package/dist/components/StatusBar.js +9 -6
- package/dist/components/WelcomeHeader.js +4 -2
- package/dist/components/setup/SetupConfirmStep.d.ts +8 -0
- package/dist/components/setup/SetupConfirmStep.js +12 -0
- package/dist/components/setup/SetupDoneStep.d.ts +7 -0
- package/dist/components/setup/SetupDoneStep.js +5 -0
- package/dist/components/setup/SetupFormStep.d.ts +11 -0
- package/dist/components/setup/SetupFormStep.js +44 -0
- package/dist/components/setup/SetupHeader.d.ts +9 -0
- package/dist/components/setup/SetupHeader.js +25 -0
- package/dist/components/setup/SetupProviderStep.d.ts +6 -0
- package/dist/components/setup/SetupProviderStep.js +20 -0
- package/dist/components/setup/SetupWelcomeStep.d.ts +5 -0
- package/dist/components/setup/SetupWelcomeStep.js +5 -0
- package/dist/config/bootstrap.d.ts +38 -0
- package/dist/config/bootstrap.js +155 -0
- package/dist/config/constants.d.ts +7 -6
- package/dist/config/constants.js +29 -16
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +4 -0
- package/dist/core/hint.js +3 -3
- package/dist/core/query.js +3 -2
- package/dist/hooks/useDoubleCtrlCExit.js +32 -15
- package/dist/index.js +2 -2
- package/dist/screens/AppBootstrap.d.ts +1 -0
- package/dist/screens/AppBootstrap.js +14 -0
- package/dist/screens/repl.js +6 -2
- package/dist/screens/setup/SetupWizard.d.ts +7 -0
- package/dist/screens/setup/SetupWizard.js +198 -0
- package/dist/screens/slashCommands.d.ts +1 -1
- package/dist/screens/slashCommands.js +2 -2
- package/dist/services/api/llm.js +2 -2
- package/dist/tools/createSkill.js +59 -1
- package/dist/tools/readFile.js +28 -3
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { BootstrapStatus } from '../../config/bootstrap.js';
|
|
2
|
+
interface SetupWizardProps {
|
|
3
|
+
initialStatus: BootstrapStatus;
|
|
4
|
+
onCompleted: () => void;
|
|
5
|
+
}
|
|
6
|
+
export default function SetupWizard({ initialStatus, onCompleted }: SetupWizardProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
4
|
+
import SetupHeader from '../../components/setup/SetupHeader.js';
|
|
5
|
+
import SetupWelcomeStep from '../../components/setup/SetupWelcomeStep.js';
|
|
6
|
+
import SetupProviderStep from '../../components/setup/SetupProviderStep.js';
|
|
7
|
+
import SetupFormStep from '../../components/setup/SetupFormStep.js';
|
|
8
|
+
import SetupConfirmStep from '../../components/setup/SetupConfirmStep.js';
|
|
9
|
+
import SetupDoneStep from '../../components/setup/SetupDoneStep.js';
|
|
10
|
+
import { buildJarvisConfig, getDefaultSetupForm, validateSetupForm, writeBootstrapConfig, } from '../../config/bootstrap.js';
|
|
11
|
+
import { useDoubleCtrlCExit } from '../../hooks/useDoubleCtrlCExit.js';
|
|
12
|
+
import { useTerminalWidth } from '../../hooks/useTerminalWidth.js';
|
|
13
|
+
const PROVIDERS = ['openai-compatible', 'ollama'];
|
|
14
|
+
const FORM_FIELDS = [
|
|
15
|
+
'profileName',
|
|
16
|
+
'apiUrl',
|
|
17
|
+
'apiKey',
|
|
18
|
+
'model',
|
|
19
|
+
'temperature',
|
|
20
|
+
'maxTokens',
|
|
21
|
+
];
|
|
22
|
+
function findFirstInvalidFieldIndex(errors) {
|
|
23
|
+
const firstField = FORM_FIELDS.findIndex((field) => Boolean(errors[field]));
|
|
24
|
+
return firstField >= 0 ? firstField : 0;
|
|
25
|
+
}
|
|
26
|
+
export default function SetupWizard({ initialStatus, onCompleted }) {
|
|
27
|
+
const width = useTerminalWidth();
|
|
28
|
+
const { exit } = useApp();
|
|
29
|
+
const { countdown, handleCtrlC } = useDoubleCtrlCExit(() => {
|
|
30
|
+
exit();
|
|
31
|
+
setTimeout(() => process.exit(0), 50);
|
|
32
|
+
});
|
|
33
|
+
const [step, setStep] = useState('welcome');
|
|
34
|
+
const [provider, setProvider] = useState('openai-compatible');
|
|
35
|
+
const [form, setForm] = useState(() => getDefaultSetupForm('openai-compatible'));
|
|
36
|
+
const [errors, setErrors] = useState({});
|
|
37
|
+
const [selectedFieldIndex, setSelectedFieldIndex] = useState(0);
|
|
38
|
+
const [editingField, setEditingField] = useState(null);
|
|
39
|
+
const [editBuffer, setEditBuffer] = useState('');
|
|
40
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
41
|
+
const [result, setResult] = useState(null);
|
|
42
|
+
const selectedField = FORM_FIELDS[selectedFieldIndex];
|
|
43
|
+
const preview = useMemo(() => JSON.stringify(buildJarvisConfig(form), null, 2), [form]);
|
|
44
|
+
function moveProvider(offset) {
|
|
45
|
+
const currentIndex = PROVIDERS.indexOf(provider);
|
|
46
|
+
const nextIndex = (currentIndex + offset + PROVIDERS.length) % PROVIDERS.length;
|
|
47
|
+
const nextProvider = PROVIDERS[nextIndex];
|
|
48
|
+
setProvider(nextProvider);
|
|
49
|
+
setForm(getDefaultSetupForm(nextProvider));
|
|
50
|
+
setErrors({});
|
|
51
|
+
}
|
|
52
|
+
function moveField(offset) {
|
|
53
|
+
setSelectedFieldIndex((current) => {
|
|
54
|
+
const next = current + offset;
|
|
55
|
+
if (next < 0)
|
|
56
|
+
return FORM_FIELDS.length - 1;
|
|
57
|
+
if (next >= FORM_FIELDS.length)
|
|
58
|
+
return 0;
|
|
59
|
+
return next;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function startEditing() {
|
|
63
|
+
setEditingField(selectedField);
|
|
64
|
+
setEditBuffer(form[selectedField]);
|
|
65
|
+
}
|
|
66
|
+
function startEditingField(field) {
|
|
67
|
+
setEditingField(field);
|
|
68
|
+
setEditBuffer(form[field]);
|
|
69
|
+
}
|
|
70
|
+
function stopEditing(save) {
|
|
71
|
+
if (!editingField)
|
|
72
|
+
return;
|
|
73
|
+
if (save) {
|
|
74
|
+
setForm((current) => ({ ...current, [editingField]: editBuffer }));
|
|
75
|
+
}
|
|
76
|
+
setEditingField(null);
|
|
77
|
+
setEditBuffer('');
|
|
78
|
+
}
|
|
79
|
+
async function submitConfig() {
|
|
80
|
+
const nextErrors = validateSetupForm(form);
|
|
81
|
+
setErrors(nextErrors);
|
|
82
|
+
if (Object.keys(nextErrors).length > 0) {
|
|
83
|
+
setSelectedFieldIndex(findFirstInvalidFieldIndex(nextErrors));
|
|
84
|
+
setStep('form');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
setIsSubmitting(true);
|
|
88
|
+
const config = buildJarvisConfig(form);
|
|
89
|
+
const writeResult = writeBootstrapConfig(config);
|
|
90
|
+
setIsSubmitting(false);
|
|
91
|
+
if (writeResult.success) {
|
|
92
|
+
setResult({
|
|
93
|
+
success: true,
|
|
94
|
+
message: '基础模型配置已完成,后续可直接启动进入聊天界面。',
|
|
95
|
+
configPath: writeResult.path,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
setResult({
|
|
100
|
+
success: false,
|
|
101
|
+
message: writeResult.message,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
setStep('done');
|
|
105
|
+
}
|
|
106
|
+
useInput((input, key) => {
|
|
107
|
+
if (key.ctrl && input === 'c') {
|
|
108
|
+
handleCtrlC();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (editingField) {
|
|
112
|
+
if (key.escape) {
|
|
113
|
+
stopEditing(false);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.return) {
|
|
117
|
+
stopEditing(true);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key.backspace || key.delete) {
|
|
121
|
+
setEditBuffer((current) => current.slice(0, -1));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (input) {
|
|
125
|
+
setEditBuffer((current) => current + input);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (step === 'welcome') {
|
|
130
|
+
if (key.return) {
|
|
131
|
+
setStep('provider');
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (step === 'provider') {
|
|
136
|
+
if (key.upArrow)
|
|
137
|
+
moveProvider(-1);
|
|
138
|
+
if (key.downArrow)
|
|
139
|
+
moveProvider(1);
|
|
140
|
+
if (key.escape)
|
|
141
|
+
setStep('welcome');
|
|
142
|
+
if (key.return) {
|
|
143
|
+
setErrors({});
|
|
144
|
+
setSelectedFieldIndex(0);
|
|
145
|
+
setStep('form');
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (step === 'form') {
|
|
150
|
+
if (key.upArrow)
|
|
151
|
+
moveField(-1);
|
|
152
|
+
if (key.downArrow || key.tab)
|
|
153
|
+
moveField(1);
|
|
154
|
+
if (key.escape) {
|
|
155
|
+
setErrors({});
|
|
156
|
+
setStep('provider');
|
|
157
|
+
}
|
|
158
|
+
if (key.return) {
|
|
159
|
+
const nextErrors = validateSetupForm(form);
|
|
160
|
+
setErrors(nextErrors);
|
|
161
|
+
if (Object.keys(nextErrors).length === 0) {
|
|
162
|
+
setStep('confirm');
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const invalidIndex = findFirstInvalidFieldIndex(nextErrors);
|
|
166
|
+
const invalidField = FORM_FIELDS[invalidIndex];
|
|
167
|
+
setSelectedFieldIndex(invalidIndex);
|
|
168
|
+
startEditingField(invalidField);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (input === 'e') {
|
|
172
|
+
startEditing();
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (step === 'confirm') {
|
|
177
|
+
if (key.escape) {
|
|
178
|
+
setStep('form');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (key.return && !isSubmitting) {
|
|
182
|
+
void submitConfig();
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (step === 'done' && result) {
|
|
187
|
+
if (key.return) {
|
|
188
|
+
if (result.success) {
|
|
189
|
+
onCompleted();
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
setStep('form');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(SetupHeader, { width: width, currentStep: step, status: initialStatus }), step === 'welcome' ? _jsx(SetupWelcomeStep, { reasonText: initialStatus.ok ? '配置状态正常' : initialStatus.message }) : null, step === 'provider' ? _jsx(SetupProviderStep, { provider: provider }) : null, step === 'form' ? (_jsx(SetupFormStep, { provider: provider, form: form, errors: errors, selectedField: selectedField, editingField: editingField, editBuffer: editBuffer })) : null, step === 'confirm' ? (_jsx(SetupConfirmStep, { form: form, preview: preview, isSubmitting: isSubmitting })) : null, step === 'done' && result ? (_jsx(SetupDoneStep, { success: result.success, message: result.message, configPath: result.configPath })) : null, _jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "gray", children: "\u5FEB\u6377\u952E\uFF1AEnter \u4E0B\u4E00\u6B65\u6216\u786E\u8BA4\uFF0CEsc \u8FD4\u56DE\uFF0Ce \u7F16\u8F91\u5F53\u524D\u5B57\u6BB5\uFF0CCtrl+C \u9000\u51FA" }), countdown !== null ? _jsxs(Text, { color: "yellow", children: ["\u518D\u6B21\u6309 Ctrl+C \u9000\u51FA\uFF08", countdown, "s\uFF09"] }) : null] })] }));
|
|
198
|
+
}
|
|
@@ -9,10 +9,10 @@ import { listPermanentAuthorizations, DANGER_RULES, } from '../core/safeguard.js
|
|
|
9
9
|
*
|
|
10
10
|
* 纯函数,返回要追加的系统消息。不涉及 React 状态。
|
|
11
11
|
*/
|
|
12
|
-
export function executeSlashCommand(cmdName) {
|
|
12
|
+
export async function executeSlashCommand(cmdName) {
|
|
13
13
|
switch (cmdName) {
|
|
14
14
|
case 'init': {
|
|
15
|
-
const result = executeInit();
|
|
15
|
+
const result = await executeInit();
|
|
16
16
|
return {
|
|
17
17
|
id: `init-${Date.now()}`,
|
|
18
18
|
type: 'system',
|
package/dist/services/api/llm.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { loadConfig, getActiveModel } from '../../config/loader.js';
|
|
10
10
|
import { getAgent } from '../../agents/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { getDefaultAgent } from '../../config/constants.js';
|
|
12
12
|
import { getActiveAgent } from '../../config/agentState.js';
|
|
13
13
|
import { getSystemInfoPrompt } from '../../config/systemInfo.js';
|
|
14
14
|
import { getCachedUserProfile } from '../../config/userProfile.js';
|
|
@@ -174,7 +174,7 @@ export class LLMServiceImpl {
|
|
|
174
174
|
return;
|
|
175
175
|
}
|
|
176
176
|
// 从当前激活的智能体加载 system prompt(运行时动态读取)
|
|
177
|
-
const currentAgent = getActiveAgent(
|
|
177
|
+
const currentAgent = getActiveAgent(getDefaultAgent());
|
|
178
178
|
const agent = getAgent(currentAgent);
|
|
179
179
|
const agentPrompt = agent?.systemPrompt
|
|
180
180
|
?? '你是一个强大的终端智能助手。回答时使用中文,简洁明了。当需要操作文件系统或执行命令时,请调用对应的工具。';
|
|
@@ -96,6 +96,64 @@ function ensureFrontmatterDescription(skillMd, description) {
|
|
|
96
96
|
// 兜底:在 frontmatter 末尾插入
|
|
97
97
|
return skillMd.replace(/\n---/, `\ndescription: ${description}\n---`);
|
|
98
98
|
}
|
|
99
|
+
function decodeLooseJsonString(value) {
|
|
100
|
+
return value
|
|
101
|
+
.replace(/\\"/g, '"')
|
|
102
|
+
.replace(/\\n/g, '\n')
|
|
103
|
+
.replace(/\\r/g, '\r')
|
|
104
|
+
.replace(/\\t/g, '\t')
|
|
105
|
+
.replace(/\\\\/g, '\\');
|
|
106
|
+
}
|
|
107
|
+
function escapeRegExp(value) {
|
|
108
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
109
|
+
}
|
|
110
|
+
function extractLooseField(jsonStr, key, nextKeys) {
|
|
111
|
+
const keyPattern = new RegExp(`"${escapeRegExp(key)}"\\s*:\\s*"`, 'm');
|
|
112
|
+
const keyMatch = keyPattern.exec(jsonStr);
|
|
113
|
+
if (!keyMatch)
|
|
114
|
+
return undefined;
|
|
115
|
+
const valueStart = keyMatch.index + keyMatch[0].length;
|
|
116
|
+
let valueEnd = jsonStr.length;
|
|
117
|
+
for (const nextKey of nextKeys) {
|
|
118
|
+
const nextPattern = new RegExp(`"\\s*,\\s*\\n\\s*"${escapeRegExp(nextKey)}"\\s*:`, 'm');
|
|
119
|
+
const tail = jsonStr.slice(valueStart);
|
|
120
|
+
const nextMatch = nextPattern.exec(tail);
|
|
121
|
+
if (nextMatch) {
|
|
122
|
+
valueEnd = Math.min(valueEnd, valueStart + nextMatch.index);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (nextKeys.length === 0) {
|
|
126
|
+
const tail = jsonStr.slice(valueStart);
|
|
127
|
+
const tailMatch = /"\s*\n?\s*}/m.exec(tail);
|
|
128
|
+
if (tailMatch) {
|
|
129
|
+
valueEnd = valueStart + tailMatch.index;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const rawValue = jsonStr.slice(valueStart, valueEnd);
|
|
133
|
+
return decodeLooseJsonString(rawValue);
|
|
134
|
+
}
|
|
135
|
+
function parseSkillJsonResult(jsonStr) {
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(jsonStr);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
const name = extractLooseField(jsonStr, 'name', ['description', 'argument_hint', 'skill_md', 'skill_py']);
|
|
141
|
+
const description = extractLooseField(jsonStr, 'description', ['argument_hint', 'skill_md', 'skill_py']);
|
|
142
|
+
const argument_hint = extractLooseField(jsonStr, 'argument_hint', ['skill_md', 'skill_py']);
|
|
143
|
+
const skill_md = extractLooseField(jsonStr, 'skill_md', ['skill_py']);
|
|
144
|
+
const skill_py = extractLooseField(jsonStr, 'skill_py', []);
|
|
145
|
+
if (!name || !description || !skill_md) {
|
|
146
|
+
throw new Error('无法从 LLM 输出中提取必要字段 (name, description, skill_md)');
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
name: name.trim(),
|
|
150
|
+
description: description.trim(),
|
|
151
|
+
argument_hint: argument_hint?.trim() || undefined,
|
|
152
|
+
skill_md,
|
|
153
|
+
skill_py: skill_py || undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
99
157
|
/**
|
|
100
158
|
* 调用 LLM 生成 skill 内容
|
|
101
159
|
*
|
|
@@ -179,7 +237,7 @@ async function callLLMForSkill(requirement) {
|
|
|
179
237
|
jsonStr = jsonBlockMatch[1].trim();
|
|
180
238
|
}
|
|
181
239
|
try {
|
|
182
|
-
const parsed =
|
|
240
|
+
const parsed = parseSkillJsonResult(jsonStr);
|
|
183
241
|
if (!parsed.name || !parsed.skill_md) {
|
|
184
242
|
throw new Error('LLM 返回的 JSON 缺少必要字段 (name, skill_md)');
|
|
185
243
|
}
|
package/dist/tools/readFile.js
CHANGED
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
// 单次读取文件大小上限:1MB
|
|
4
|
+
const MAX_FILE_SIZE = 1 * 1024 * 1024;
|
|
2
5
|
export const readFile = {
|
|
3
6
|
name: 'read_file',
|
|
4
7
|
description: '读取指定路径的文件内容',
|
|
5
8
|
parameters: {
|
|
6
|
-
path: { type: 'string', description: '
|
|
9
|
+
path: { type: 'string', description: '文件路径(支持绝对路径或相对于当前工作目录的相对路径)', required: true },
|
|
7
10
|
},
|
|
8
11
|
execute: async (args) => {
|
|
9
|
-
const
|
|
12
|
+
const rawPath = args.path;
|
|
13
|
+
if (!rawPath || !rawPath.trim()) {
|
|
14
|
+
throw new Error('path 参数不能为空');
|
|
15
|
+
}
|
|
16
|
+
// 统一解析为绝对路径,相对路径基于 process.cwd()
|
|
17
|
+
const filePath = path.isAbsolute(rawPath) ? rawPath : path.resolve(process.cwd(), rawPath);
|
|
10
18
|
try {
|
|
19
|
+
const stat = fs.statSync(filePath);
|
|
20
|
+
if (stat.isDirectory()) {
|
|
21
|
+
throw new Error(`路径是目录而非文件: ${filePath}`);
|
|
22
|
+
}
|
|
23
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
24
|
+
throw new Error(`文件过大(${(stat.size / 1024).toFixed(1)} KB),超过读取上限 ${MAX_FILE_SIZE / 1024} KB,请使用 search_files 或分段读取`);
|
|
25
|
+
}
|
|
11
26
|
return fs.readFileSync(filePath, 'utf-8');
|
|
12
27
|
}
|
|
13
28
|
catch (e) {
|
|
14
|
-
|
|
29
|
+
// 区分"文件不存在"和其他 IO 错误,给出更明确的提示
|
|
30
|
+
if (e.code === 'ENOENT') {
|
|
31
|
+
throw new Error(`文件不存在: ${filePath}`);
|
|
32
|
+
}
|
|
33
|
+
if (e.code === 'EACCES') {
|
|
34
|
+
throw new Error(`无权限读取文件: ${filePath}`);
|
|
35
|
+
}
|
|
36
|
+
// 重新抛出已包装的错误(如上面的 size/dir 检查)
|
|
37
|
+
throw e.message?.startsWith('文件') || e.message?.startsWith('路径')
|
|
38
|
+
? e
|
|
39
|
+
: new Error(`读取文件失败: ${e.message}`);
|
|
15
40
|
}
|
|
16
41
|
},
|
|
17
42
|
};
|