@code4bug/jarvis-agent 1.1.8 → 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.js +4 -4
- 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/index.js +2 -2
- package/dist/screens/AppBootstrap.d.ts +1 -0
- package/dist/screens/AppBootstrap.js +14 -0
- package/dist/screens/setup/SetupWizard.d.ts +7 -0
- package/dist/screens/setup/SetupWizard.js +198 -0
- 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
package/dist/commands/init.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
|
-
import {
|
|
10
|
+
import { APP_VERSION, getAppName } from '../config/constants.js';
|
|
11
11
|
import { LLMServiceImpl, getDefaultConfig } from '../services/api/llm.js';
|
|
12
12
|
import { allTools } from '../tools/index.js';
|
|
13
13
|
import { loadAllAgents } from '../agents/index.js';
|
|
@@ -358,7 +358,7 @@ function renderJarvisMd(input) {
|
|
|
358
358
|
md.push(`- ${line}`);
|
|
359
359
|
}
|
|
360
360
|
md.push('');
|
|
361
|
-
md.push(`> 由 ${
|
|
361
|
+
md.push(`> 由 ${getAppName()} /init 自动生成`);
|
|
362
362
|
md.push('');
|
|
363
363
|
return md.join('\n');
|
|
364
364
|
}
|
|
@@ -508,7 +508,7 @@ function generateBasicJarvisMd(input) {
|
|
|
508
508
|
md.push('');
|
|
509
509
|
md.push('- 本文件为自动生成结果;若需更准确的业务背景,请补充 README 或项目文档。');
|
|
510
510
|
md.push('');
|
|
511
|
-
md.push(`> 由 ${
|
|
511
|
+
md.push(`> 由 ${getAppName()} /init 自动生成`);
|
|
512
512
|
md.push('');
|
|
513
513
|
return md.join('\n');
|
|
514
514
|
}
|
|
@@ -522,7 +522,7 @@ export async function executeInit() {
|
|
|
522
522
|
const dirTree = scanDirectoryTree(cwd);
|
|
523
523
|
// ===== 构建终端显示文本 =====
|
|
524
524
|
const display = [];
|
|
525
|
-
display.push(`${
|
|
525
|
+
display.push(`${getAppName()} ${APP_VERSION} - 项目初始化`);
|
|
526
526
|
display.push('');
|
|
527
527
|
// 项目基本信息
|
|
528
528
|
display.push('[ 项目信息 ]');
|
|
@@ -3,6 +3,10 @@ import React from 'react';
|
|
|
3
3
|
* Markdown 终端渲染组件
|
|
4
4
|
* 支持表格(动态列宽+自动换行)、代码块(深色背景+边框)、加粗、列表等
|
|
5
5
|
* 对流式不完整文本做容错补全
|
|
6
|
+
*
|
|
7
|
+
* 渲染策略:将 ANSI 字符串按 \n 拆行,每行用独立 <Text> 渲染。
|
|
8
|
+
* 直接将含 \n 的 ANSI 字符串塞入单个 <Text> 会导致 ink 布局引擎
|
|
9
|
+
* 与 ANSI 转义序列冲突,出现光标错位和输出错乱。
|
|
6
10
|
*/
|
|
7
11
|
declare function MarkdownText({ text, color }: {
|
|
8
12
|
text: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { useMemo } from 'react';
|
|
3
|
-
import { Text } from 'ink';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
4
|
import { Marked } from 'marked';
|
|
5
5
|
// @ts-ignore — marked-terminal 无内置类型声明
|
|
6
6
|
import { markedTerminal } from 'marked-terminal';
|
|
@@ -158,9 +158,16 @@ function renderMarkdown(text) {
|
|
|
158
158
|
* Markdown 终端渲染组件
|
|
159
159
|
* 支持表格(动态列宽+自动换行)、代码块(深色背景+边框)、加粗、列表等
|
|
160
160
|
* 对流式不完整文本做容错补全
|
|
161
|
+
*
|
|
162
|
+
* 渲染策略:将 ANSI 字符串按 \n 拆行,每行用独立 <Text> 渲染。
|
|
163
|
+
* 直接将含 \n 的 ANSI 字符串塞入单个 <Text> 会导致 ink 布局引擎
|
|
164
|
+
* 与 ANSI 转义序列冲突,出现光标错位和输出错乱。
|
|
161
165
|
*/
|
|
162
166
|
function MarkdownText({ text, color }) {
|
|
163
|
-
const
|
|
164
|
-
|
|
167
|
+
const lines = useMemo(() => {
|
|
168
|
+
const rendered = renderMarkdown(text);
|
|
169
|
+
return rendered.split('\n');
|
|
170
|
+
}, [text]);
|
|
171
|
+
return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { wrap: "wrap", color: color, children: line }, i))) }));
|
|
165
172
|
}
|
|
166
173
|
export default React.memo(MarkdownText);
|
|
@@ -79,8 +79,11 @@ function MessageItem({ msg, showDetails = false }) {
|
|
|
79
79
|
// 有 subAgentId 时:标题行 + 内容分两行,避免内容粘连
|
|
80
80
|
_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsxs(Text, { color: "blue", dimColor: true, children: [msg.subAgentId, " \u203A "] })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(MarkdownText, { text: msg.content }) })] })) : (_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content }) })] })), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
81
81
|
}
|
|
82
|
+
if (msg.type === 'system' && msg.content) {
|
|
83
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content, color: "gray" }) })] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
84
|
+
}
|
|
82
85
|
if (msg.status !== 'pending' && msg.content) {
|
|
83
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
86
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "row", alignItems: "flex-start", children: [_jsxs(Text, { color: dotColor, children: [dot, " "] }), _jsx(Box, { flexDirection: "column", flexShrink: 1, children: _jsx(MarkdownText, { text: msg.content, color: "gray" }) })] }), _jsx(MessageStats, { msg: msg, show: showDetails })] }));
|
|
84
87
|
}
|
|
85
88
|
return null;
|
|
86
89
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import {
|
|
4
|
+
import { PROJECT_NAME, getContextTokenLimit, getModelName, isThinkingModeToggleEnabled } from '../config/constants.js';
|
|
5
5
|
/** 生成 token 用量进度条 */
|
|
6
6
|
function tokenProgressBar(used, limit, barWidth) {
|
|
7
7
|
const ratio = Math.min(used / limit, 1);
|
|
@@ -12,16 +12,19 @@ function tokenProgressBar(used, limit, barWidth) {
|
|
|
12
12
|
return { bar, color };
|
|
13
13
|
}
|
|
14
14
|
function StatusBar({ width, totalTokens, activeAgents = 0 }) {
|
|
15
|
-
const
|
|
15
|
+
const modelName = getModelName();
|
|
16
|
+
const contextTokenLimit = getContextTokenLimit();
|
|
17
|
+
const thinkingModeToggleEnabled = isThinkingModeToggleEnabled();
|
|
18
|
+
const left = ` ${modelName} │ ${PROJECT_NAME}`;
|
|
16
19
|
// 右侧:智能体数量(有后台 Agent 时显示)+ token 进度条 + 思考模式切换(可选)
|
|
17
20
|
const agentPart = activeAgents > 0 ? `⬡ ${activeAgents} agent${activeAgents > 1 ? 's' : ''} │ ` : '';
|
|
18
|
-
const tokenLabel = `${totalTokens}/${
|
|
21
|
+
const tokenLabel = `${totalTokens}/${contextTokenLimit}`;
|
|
19
22
|
const barWidth = 10;
|
|
20
|
-
const { bar, color } = tokenProgressBar(totalTokens,
|
|
21
|
-
const effortPart =
|
|
23
|
+
const { bar, color } = tokenProgressBar(totalTokens, contextTokenLimit, barWidth);
|
|
24
|
+
const effortPart = thinkingModeToggleEnabled ? ' │ ● medium · /effort' : '';
|
|
22
25
|
// 右侧完整文本长度(用于计算间距)
|
|
23
26
|
const rightLen = agentPart.length + tokenLabel.length + 1 + barWidth + effortPart.length + 1;
|
|
24
27
|
const gap = Math.max(width - left.length - rightLen, 1);
|
|
25
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: left }), _jsx(Text, { children: ' '.repeat(gap) }), activeAgents > 0 && _jsx(Text, { color: "cyan", children: agentPart }), _jsxs(Text, { color: "gray", children: [tokenLabel, " "] }), _jsx(Text, { color: color, children: bar }),
|
|
28
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: left }), _jsx(Text, { children: ' '.repeat(gap) }), activeAgents > 0 && _jsx(Text, { color: "cyan", children: agentPart }), _jsxs(Text, { color: "gray", children: [tokenLabel, " "] }), _jsx(Text, { color: color, children: bar }), thinkingModeToggleEnabled && _jsx(Text, { color: "gray", children: effortPart }), _jsx(Text, { children: " " })] }));
|
|
26
29
|
}
|
|
27
30
|
export default React.memo(StatusBar);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
|
-
import {
|
|
4
|
+
import { APP_VERSION, getAppName, getModelName } from '../config/constants.js';
|
|
5
5
|
function truncatePath(p, max) {
|
|
6
6
|
if (p.length <= max)
|
|
7
7
|
return p;
|
|
@@ -20,6 +20,8 @@ const LOGO_COLORS = ['cyan', 'cyan', 'blueBright', 'blueBright', 'magenta', 'mag
|
|
|
20
20
|
function WelcomeHeader({ width }) {
|
|
21
21
|
const maxPath = Math.max(width - 10, 20);
|
|
22
22
|
const showLogo = width >= 52;
|
|
23
|
-
|
|
23
|
+
const appName = getAppName();
|
|
24
|
+
const modelName = getModelName();
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, width: width, children: [showLogo && (_jsx(Box, { flexDirection: "column", children: LOGO_LINES.map((line, i) => (_jsx(Text, { color: LOGO_COLORS[i], children: line }, i))) })), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray" }), _jsx(Text, { color: "white", bold: true, children: "Your AI-Powered Dev Companion" }), _jsx(Text, { color: "gray" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: "gray", children: "model " }), _jsx(Text, { color: "cyan", children: modelName }), _jsxs(Text, { color: "gray", children: [" ", appName, " "] }), _jsx(Text, { color: "magenta", children: APP_VERSION })] }), _jsx(Box, { children: _jsx(Text, { color: "gray", children: truncatePath(process.cwd(), maxPath) }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(width - 4, 48)) }) }), _jsxs(Box, { children: [_jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "init" }), _jsx(Text, { color: "gray", children: " \u521D\u59CB\u5316 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "help" }), _jsx(Text, { color: "gray", children: " \u5E2E\u52A9 " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "new" }), _jsx(Text, { color: "gray", children: " \u65B0\u4F1A\u8BDD " }), _jsx(Text, { color: "gray", children: "/" }), _jsx(Text, { color: "cyan", children: "agent" }), _jsx(Text, { color: "gray", children: " \u5207\u6362" })] }), _jsx(Text, { children: ' ' })] }));
|
|
24
26
|
}
|
|
25
27
|
export default React.memo(WelcomeHeader);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SetupFormData } from '../../config/bootstrap.js';
|
|
2
|
+
interface SetupConfirmStepProps {
|
|
3
|
+
form: SetupFormData;
|
|
4
|
+
preview: string;
|
|
5
|
+
isSubmitting: boolean;
|
|
6
|
+
}
|
|
7
|
+
export default function SetupConfirmStep({ form, preview, isSubmitting }: SetupConfirmStepProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
function maskApiKey(apiKey) {
|
|
4
|
+
if (!apiKey)
|
|
5
|
+
return '未填写';
|
|
6
|
+
if (apiKey.length <= 8)
|
|
7
|
+
return '*'.repeat(apiKey.length);
|
|
8
|
+
return `${apiKey.slice(0, 4)}${'*'.repeat(Math.max(apiKey.length - 8, 4))}${apiKey.slice(-4)}`;
|
|
9
|
+
}
|
|
10
|
+
export default function SetupConfirmStep({ form, preview, isSubmitting }) {
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "white", bold: true, children: "\u786E\u8BA4\u5199\u5165\u914D\u7F6E" }), _jsx(Text, { color: "gray", children: "\u786E\u8BA4\u65E0\u8BEF\u540E\u6309 Enter \u5199\u5165\u3002Esc \u53EF\u8FD4\u56DE\u4FEE\u6539\u3002" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["\u914D\u7F6E\u540D\u79F0\uFF1A", form.profileName] }), _jsxs(Text, { children: ["API \u5730\u5740\uFF1A", form.apiUrl] }), _jsxs(Text, { children: ["\u6A21\u578B ID\uFF1A", form.model] }), _jsxs(Text, { children: ["API Key\uFF1A", maskApiKey(form.apiKey)] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "JSON \u9884\u89C8" }), preview.split('\n').map((line, index) => (_jsx(Text, { color: "gray", children: line }, `${index}-${line}`)))] }), isSubmitting ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: "\u6B63\u5728\u5199\u5165\u914D\u7F6E..." }) })) : null] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export default function SetupDoneStep({ success, message, configPath }) {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: success ? 'green' : 'red', bold: true, children: success ? '配置写入成功' : '配置写入失败' }), _jsx(Text, { children: message }), configPath ? _jsxs(Text, { color: "gray", children: ["\u914D\u7F6E\u6587\u4EF6\uFF1A", configPath] }) : null, _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: success ? '按 Enter 进入 Jarvis' : '按 Enter 返回修改' }) })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ProviderType, SetupFormData, SetupValidationErrors } from '../../config/bootstrap.js';
|
|
2
|
+
interface SetupFormStepProps {
|
|
3
|
+
provider: ProviderType;
|
|
4
|
+
form: SetupFormData;
|
|
5
|
+
errors: SetupValidationErrors;
|
|
6
|
+
selectedField: keyof SetupFormData;
|
|
7
|
+
editingField: keyof SetupFormData | null;
|
|
8
|
+
editBuffer: string;
|
|
9
|
+
}
|
|
10
|
+
export default function SetupFormStep({ provider, form, errors, selectedField, editingField, editBuffer, }: SetupFormStepProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const FIELD_ORDER = [
|
|
4
|
+
'profileName',
|
|
5
|
+
'apiUrl',
|
|
6
|
+
'apiKey',
|
|
7
|
+
'model',
|
|
8
|
+
'temperature',
|
|
9
|
+
'maxTokens',
|
|
10
|
+
];
|
|
11
|
+
const FIELD_LABELS = {
|
|
12
|
+
profileName: '配置名称',
|
|
13
|
+
apiUrl: 'API 地址',
|
|
14
|
+
apiKey: 'API Key',
|
|
15
|
+
model: '模型 ID',
|
|
16
|
+
temperature: 'temperature',
|
|
17
|
+
maxTokens: 'max_tokens',
|
|
18
|
+
};
|
|
19
|
+
const FIELD_DESCRIPTIONS = {
|
|
20
|
+
profileName: '写入到 models 下的配置名,同时会被设置为默认激活模型。',
|
|
21
|
+
apiUrl: '完整的 Chat Completions 接口地址。',
|
|
22
|
+
apiKey: '鉴权密钥。显示时会自动脱敏。',
|
|
23
|
+
model: '服务端实际使用的模型标识。',
|
|
24
|
+
temperature: '可选,默认 0.1。',
|
|
25
|
+
maxTokens: '响应最大 token 数,建议保持默认即可。',
|
|
26
|
+
};
|
|
27
|
+
function maskValue(field, value) {
|
|
28
|
+
if (field !== 'apiKey')
|
|
29
|
+
return value || '未填写';
|
|
30
|
+
if (!value)
|
|
31
|
+
return '未填写';
|
|
32
|
+
if (value.length <= 8)
|
|
33
|
+
return '*'.repeat(value.length);
|
|
34
|
+
return `${value.slice(0, 4)}${'*'.repeat(Math.max(value.length - 8, 4))}${value.slice(-4)}`;
|
|
35
|
+
}
|
|
36
|
+
export default function SetupFormStep({ provider, form, errors, selectedField, editingField, editBuffer, }) {
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "white", bold: true, children: "\u586B\u5199\u914D\u7F6E\u53C2\u6570" }), _jsxs(Text, { color: "gray", children: ["\u5F53\u524D\u63A5\u5165\u65B9\u5F0F\uFF1A", provider === 'ollama' ? 'Ollama / 本地模型' : 'OpenAI 兼容接口'] }), _jsx(Text, { color: "gray", children: "\u4E0A\u4E0B\u952E\u5207\u5B57\u6BB5\uFF0C\u6309 e \u7F16\u8F91\u5F53\u524D\u5B57\u6BB5\uFF0CEnter \u6821\u9A8C\u5E76\u8FDB\u5165\u786E\u8BA4\u3002" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: FIELD_ORDER.map((field) => {
|
|
38
|
+
const selected = field === selectedField;
|
|
39
|
+
const editing = field === editingField;
|
|
40
|
+
const displayValue = editing ? editBuffer : maskValue(field, form[field]);
|
|
41
|
+
const error = errors[field];
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: selected ? 'cyan' : 'white', children: [selected ? '›' : ' ', " ", FIELD_LABELS[field], "\uFF1A", displayValue] }), _jsx(Text, { color: "gray", children: FIELD_DESCRIPTIONS[field] }), error ? _jsx(Text, { color: "red", children: error }) : null] }, field));
|
|
43
|
+
}) })] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BootstrapStatus } from '../../config/bootstrap.js';
|
|
2
|
+
type SetupStep = 'welcome' | 'provider' | 'form' | 'confirm' | 'done';
|
|
3
|
+
interface SetupHeaderProps {
|
|
4
|
+
width: number;
|
|
5
|
+
currentStep: SetupStep;
|
|
6
|
+
status: BootstrapStatus;
|
|
7
|
+
}
|
|
8
|
+
export default function SetupHeader({ width, currentStep, status }: SetupHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { getBootstrapConfigPath } from '../../config/bootstrap.js';
|
|
4
|
+
const STEPS = [
|
|
5
|
+
{ key: 'welcome', label: '欢迎' },
|
|
6
|
+
{ key: 'provider', label: '接入方式' },
|
|
7
|
+
{ key: 'form', label: '参数填写' },
|
|
8
|
+
{ key: 'confirm', label: '确认写入' },
|
|
9
|
+
{ key: 'done', label: '完成' },
|
|
10
|
+
];
|
|
11
|
+
function stepColor(currentStep, step) {
|
|
12
|
+
const currentIndex = STEPS.findIndex((item) => item.key === currentStep);
|
|
13
|
+
const stepIndex = STEPS.findIndex((item) => item.key === step);
|
|
14
|
+
if (stepIndex < currentIndex)
|
|
15
|
+
return 'green';
|
|
16
|
+
if (stepIndex === currentIndex)
|
|
17
|
+
return 'cyan';
|
|
18
|
+
return 'gray';
|
|
19
|
+
}
|
|
20
|
+
export default function SetupHeader({ width, currentStep, status }) {
|
|
21
|
+
const progress = STEPS
|
|
22
|
+
.map((step, index) => ({ ...step, index: index + 1 }))
|
|
23
|
+
.map((step) => (_jsxs(Text, { color: stepColor(currentStep, step.key), children: [step.index, ". ", step.label, ' '] }, step.key)));
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, width: width, children: [_jsx(Text, { color: "magenta", bold: true, children: "Jarvis \u9996\u6B21\u542F\u52A8\u914D\u7F6E" }), _jsx(Text, { color: "gray", children: status.ok ? '配置校验通过' : status.message }), _jsxs(Text, { color: "gray", children: ["\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84\uFF1A", getBootstrapConfigPath()] }), _jsx(Box, { marginTop: 1, children: progress }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.min(Math.max(width - 2, 20), 72)) })] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const OPTIONS = [
|
|
4
|
+
{
|
|
5
|
+
value: 'openai-compatible',
|
|
6
|
+
title: 'OpenAI 兼容接口',
|
|
7
|
+
desc: '适合 OpenAI、OneAPI、云厂商兼容接口等场景。',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
value: 'ollama',
|
|
11
|
+
title: 'Ollama / 本地模型',
|
|
12
|
+
desc: '自动带入本地默认地址,适合本机 Ollama 服务。',
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
export default function SetupProviderStep({ provider }) {
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "white", bold: true, children: "\u9009\u62E9\u63A5\u5165\u65B9\u5F0F" }), _jsx(Text, { color: "gray", children: "\u4E0A\u4E0B\u952E\u5207\u6362\uFF0CEnter \u786E\u8BA4\u540E\u8FDB\u5165\u53C2\u6570\u586B\u5199\u3002" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: OPTIONS.map((option) => {
|
|
17
|
+
const selected = option.value === provider;
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: selected ? 'cyan' : 'white', children: [selected ? '›' : ' ', " ", option.title] }), _jsx(Text, { color: "gray", children: option.desc })] }, option.value));
|
|
19
|
+
}) })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export default function SetupWelcomeStep({ reasonText }) {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "white", bold: true, children: "\u6B22\u8FCE\u4F7F\u7528 Jarvis" }), _jsx(Text, { color: "gray", children: "\u68C0\u6D4B\u5230\u5F53\u524D\u5C1A\u672A\u5B8C\u6210\u53EF\u7528\u6A21\u578B\u914D\u7F6E\uFF0C\u9996\u6B21\u542F\u52A8\u9700\u8981\u5148\u5B8C\u6210\u57FA\u7840\u5F15\u5BFC\u3002" }), _jsx(Text, { color: "gray", children: "\u5B8C\u6210\u540E\u4F1A\u76F4\u63A5\u8FDB\u5165\u4E3B\u754C\u9762\uFF0C\u65E0\u9700\u518D\u6B21\u6267\u884C\u989D\u5916\u547D\u4EE4\u3002" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "\u5F53\u524D\u72B6\u6001" }), _jsx(Text, { children: reasonText })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "\u672C\u6B21\u4F1A\u5B8C\u6210\u7684\u914D\u7F6E" }), _jsx(Text, { children: "1. \u8BBE\u7F6E\u9ED8\u8BA4\u6A21\u578B\u914D\u7F6E\u540D" }), _jsx(Text, { children: "2. \u5199\u5165 API \u5730\u5740\u4E0E API Key" }), _jsx(Text, { children: "3. \u5199\u5165\u6A21\u578B ID \u4E0E\u57FA\u7840\u53C2\u6570" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: "\u6309 Enter \u5F00\u59CB\u914D\u7F6E" }) })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { JarvisConfig } from './loader.js';
|
|
2
|
+
export type ProviderType = 'openai-compatible' | 'ollama';
|
|
3
|
+
export interface SetupFormData {
|
|
4
|
+
profileName: string;
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
model: string;
|
|
8
|
+
temperature: string;
|
|
9
|
+
maxTokens: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SetupValidationErrors {
|
|
12
|
+
profileName?: string;
|
|
13
|
+
apiUrl?: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
temperature?: string;
|
|
17
|
+
maxTokens?: string;
|
|
18
|
+
form?: string;
|
|
19
|
+
}
|
|
20
|
+
export type BootstrapStatus = {
|
|
21
|
+
ok: true;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
reason: 'missing' | 'invalid_json' | 'incomplete';
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
export declare function getBootstrapConfigPath(): string;
|
|
28
|
+
export declare function getDefaultSetupForm(provider: ProviderType): SetupFormData;
|
|
29
|
+
export declare function checkBootstrapStatus(): BootstrapStatus;
|
|
30
|
+
export declare function validateSetupForm(form: SetupFormData): SetupValidationErrors;
|
|
31
|
+
export declare function buildJarvisConfig(form: SetupFormData): JarvisConfig;
|
|
32
|
+
export declare function writeBootstrapConfig(config: JarvisConfig): {
|
|
33
|
+
success: true;
|
|
34
|
+
path: string;
|
|
35
|
+
} | {
|
|
36
|
+
success: false;
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { loadConfig, resetConfigCache } from './loader.js';
|
|
5
|
+
const JARVIS_HOME_DIR = path.join(os.homedir(), '.jarvis');
|
|
6
|
+
const CONFIG_PATH = path.join(JARVIS_HOME_DIR, 'config.json');
|
|
7
|
+
export function getBootstrapConfigPath() {
|
|
8
|
+
return CONFIG_PATH;
|
|
9
|
+
}
|
|
10
|
+
export function getDefaultSetupForm(provider) {
|
|
11
|
+
if (provider === 'ollama') {
|
|
12
|
+
return {
|
|
13
|
+
profileName: 'ollama',
|
|
14
|
+
apiUrl: 'http://127.0.0.1:11434/v1/chat/completions',
|
|
15
|
+
apiKey: 'ollama',
|
|
16
|
+
model: '',
|
|
17
|
+
temperature: '0.1',
|
|
18
|
+
maxTokens: '10000',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
profileName: 'default',
|
|
23
|
+
apiUrl: 'https://api.openai.com/v1/chat/completions',
|
|
24
|
+
apiKey: '',
|
|
25
|
+
model: '',
|
|
26
|
+
temperature: '0.1',
|
|
27
|
+
maxTokens: '10000',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function checkBootstrapStatus() {
|
|
31
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
32
|
+
return { ok: false, reason: 'missing', message: '未检测到全局配置文件,需要先完成首次配置。' };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
const modelName = parsed.system?.model;
|
|
38
|
+
if (!modelName) {
|
|
39
|
+
return { ok: false, reason: 'incomplete', message: '配置文件缺少 system.model,无法确定默认模型。' };
|
|
40
|
+
}
|
|
41
|
+
const activeModel = parsed.models?.[modelName];
|
|
42
|
+
if (!activeModel) {
|
|
43
|
+
return { ok: false, reason: 'incomplete', message: `配置文件缺少 models.${modelName},无法完成启动。` };
|
|
44
|
+
}
|
|
45
|
+
if (!activeModel.api_url || !activeModel.api_key || !activeModel.model) {
|
|
46
|
+
return { ok: false, reason: 'incomplete', message: '当前默认模型缺少 api_url、api_key 或 model 字段。' };
|
|
47
|
+
}
|
|
48
|
+
return { ok: true };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: 'invalid_json',
|
|
54
|
+
message: `配置文件解析失败:${error instanceof Error ? error.message : String(error)}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function validateSetupForm(form) {
|
|
59
|
+
const errors = {};
|
|
60
|
+
if (!form.profileName.trim()) {
|
|
61
|
+
errors.profileName = '配置名称不能为空';
|
|
62
|
+
}
|
|
63
|
+
else if (/\s/.test(form.profileName)) {
|
|
64
|
+
errors.profileName = '配置名称不能包含空格';
|
|
65
|
+
}
|
|
66
|
+
if (!form.apiUrl.trim()) {
|
|
67
|
+
errors.apiUrl = 'API 地址不能为空';
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
try {
|
|
71
|
+
new URL(form.apiUrl);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
errors.apiUrl = 'API 地址格式不合法';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!form.apiKey.trim()) {
|
|
78
|
+
errors.apiKey = 'API Key 不能为空';
|
|
79
|
+
}
|
|
80
|
+
if (!form.model.trim()) {
|
|
81
|
+
errors.model = '模型 ID 不能为空';
|
|
82
|
+
}
|
|
83
|
+
if (form.temperature.trim()) {
|
|
84
|
+
const temperature = Number(form.temperature);
|
|
85
|
+
if (Number.isNaN(temperature)) {
|
|
86
|
+
errors.temperature = 'temperature 必须是数字';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!form.maxTokens.trim()) {
|
|
90
|
+
errors.maxTokens = 'max_tokens 不能为空';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const maxTokens = Number(form.maxTokens);
|
|
94
|
+
if (!Number.isInteger(maxTokens) || maxTokens <= 0) {
|
|
95
|
+
errors.maxTokens = 'max_tokens 必须是正整数';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return errors;
|
|
99
|
+
}
|
|
100
|
+
export function buildJarvisConfig(form) {
|
|
101
|
+
const baseConfig = loadExistingConfig();
|
|
102
|
+
const temperature = form.temperature.trim() ? Number(form.temperature) : undefined;
|
|
103
|
+
const maxTokens = Number(form.maxTokens);
|
|
104
|
+
const config = {
|
|
105
|
+
system: {
|
|
106
|
+
...baseConfig.system,
|
|
107
|
+
model: form.profileName.trim(),
|
|
108
|
+
},
|
|
109
|
+
models: {
|
|
110
|
+
...baseConfig.models,
|
|
111
|
+
[form.profileName.trim()]: {
|
|
112
|
+
...(baseConfig.models?.[form.profileName.trim()] ?? {}),
|
|
113
|
+
api_url: form.apiUrl.trim(),
|
|
114
|
+
api_key: form.apiKey.trim(),
|
|
115
|
+
model: form.model.trim(),
|
|
116
|
+
...(temperature !== undefined ? { temperature } : {}),
|
|
117
|
+
max_tokens: maxTokens,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
return config;
|
|
122
|
+
}
|
|
123
|
+
function loadExistingConfig() {
|
|
124
|
+
try {
|
|
125
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
126
|
+
return { system: {}, models: {} };
|
|
127
|
+
}
|
|
128
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
129
|
+
const parsed = JSON.parse(raw);
|
|
130
|
+
return {
|
|
131
|
+
system: parsed.system ?? {},
|
|
132
|
+
models: parsed.models ?? {},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { system: {}, models: {} };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function writeBootstrapConfig(config) {
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(JARVIS_HOME_DIR)) {
|
|
142
|
+
fs.mkdirSync(JARVIS_HOME_DIR, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
145
|
+
resetConfigCache();
|
|
146
|
+
loadConfig();
|
|
147
|
+
return { success: true, path: CONFIG_PATH };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
message: error instanceof Error ? error.message : String(error),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -3,16 +3,18 @@ export declare const APP_VERSION: string;
|
|
|
3
3
|
export declare const PROJECT_NAME: string;
|
|
4
4
|
/** Agentic Loop 最大迭代次数 */
|
|
5
5
|
export declare const MAX_ITERATIONS = 50;
|
|
6
|
+
/** 会话存储目录(~/.jarvis/sessions/) */
|
|
6
7
|
export declare const JARVIS_HOME_DIR: string;
|
|
7
8
|
export declare const SESSIONS_DIR: string;
|
|
8
9
|
export declare const LOGS_DIR: string;
|
|
9
10
|
/** 输入后是否隐藏 WelcomeHeader,默认 false(不隐藏) */
|
|
10
11
|
export declare const HIDE_WELCOME_AFTER_INPUT = false;
|
|
11
|
-
|
|
12
|
+
/** 从配置文件获取当前模型名称 */
|
|
13
|
+
export declare function getModelName(): string;
|
|
12
14
|
/** 是否支持思考/非思考模式切换,默认 false(隐藏该功能) */
|
|
13
|
-
export declare
|
|
15
|
+
export declare function isThinkingModeToggleEnabled(): boolean;
|
|
14
16
|
/** 上下文 token 上限 */
|
|
15
|
-
export declare
|
|
17
|
+
export declare function getContextTokenLimit(): number;
|
|
16
18
|
/** 默认智能体名称(硬编码兜底值) */
|
|
17
19
|
export declare const DEFAULT_AGENT_FALLBACK = "Jarvis";
|
|
18
20
|
/** 智能体定义文件目录(相对项目根目录) */
|
|
@@ -22,6 +24,5 @@ export declare const DEFAULT_AGENT_COLOR = "green";
|
|
|
22
24
|
/** 智能体默认标识符 */
|
|
23
25
|
export declare const DEFAULT_AGENT_EMOJI = ">";
|
|
24
26
|
/** 当前激活的智能体名称 — 启动时从 ~/.jarvis/agent.json 读取,运行时可切换 */
|
|
25
|
-
export declare
|
|
26
|
-
|
|
27
|
-
export declare const APP_NAME: string;
|
|
27
|
+
export declare function getDefaultAgent(): string;
|
|
28
|
+
export declare function getAppName(): string;
|
package/dist/config/constants.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { loadConfig, getActiveModel } from './loader.js';
|
|
6
|
+
import { getAgent } from '../agents/index.js';
|
|
7
|
+
import { getActiveAgent } from './agentState.js';
|
|
4
8
|
/** 从 package.json 动态读取版本号 */
|
|
5
9
|
function resolveAppVersion() {
|
|
6
10
|
try {
|
|
@@ -19,29 +23,40 @@ export const PROJECT_NAME = path.basename(process.cwd());
|
|
|
19
23
|
/** Agentic Loop 最大迭代次数 */
|
|
20
24
|
export const MAX_ITERATIONS = 50;
|
|
21
25
|
/** 会话存储目录(~/.jarvis/sessions/) */
|
|
22
|
-
import os from 'os';
|
|
23
26
|
export const JARVIS_HOME_DIR = path.join(os.homedir(), '.jarvis');
|
|
24
27
|
export const SESSIONS_DIR = path.join(JARVIS_HOME_DIR, 'sessions');
|
|
25
28
|
export const LOGS_DIR = path.join(JARVIS_HOME_DIR, 'logs');
|
|
26
29
|
/** 输入后是否隐藏 WelcomeHeader,默认 false(不隐藏) */
|
|
27
30
|
export const HIDE_WELCOME_AFTER_INPUT = false;
|
|
28
31
|
/** 从配置文件获取当前模型名称 */
|
|
29
|
-
|
|
30
|
-
const _cfg = loadConfig();
|
|
31
|
-
function resolveModelName() {
|
|
32
|
+
export function getModelName() {
|
|
32
33
|
try {
|
|
33
|
-
const
|
|
34
|
-
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
const active = getActiveModel(config);
|
|
36
|
+
return active?.model ?? config.system.model ?? 'unknown';
|
|
35
37
|
}
|
|
36
38
|
catch {
|
|
37
39
|
return 'unknown';
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
|
-
export const MODEL_NAME = resolveModelName();
|
|
41
42
|
/** 是否支持思考/非思考模式切换,默认 false(隐藏该功能) */
|
|
42
|
-
export
|
|
43
|
+
export function isThinkingModeToggleEnabled() {
|
|
44
|
+
try {
|
|
45
|
+
return loadConfig().system.enable_thinking_mode_toggle ?? false;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
43
51
|
/** 上下文 token 上限 */
|
|
44
|
-
export
|
|
52
|
+
export function getContextTokenLimit() {
|
|
53
|
+
try {
|
|
54
|
+
return loadConfig().system.context_token_limit ?? 18000;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return 18000;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
45
60
|
// ===== 智能体默认配置 =====
|
|
46
61
|
/** 默认智能体名称(硬编码兜底值) */
|
|
47
62
|
export const DEFAULT_AGENT_FALLBACK = 'Jarvis';
|
|
@@ -52,18 +67,16 @@ export const DEFAULT_AGENT_COLOR = 'green';
|
|
|
52
67
|
/** 智能体默认标识符 */
|
|
53
68
|
export const DEFAULT_AGENT_EMOJI = '>';
|
|
54
69
|
// ===== 动态应用名称(跟随激活智能体) =====
|
|
55
|
-
import { getAgent } from '../agents/index.js';
|
|
56
|
-
import { getActiveAgent } from './agentState.js';
|
|
57
70
|
/** 当前激活的智能体名称 — 启动时从 ~/.jarvis/agent.json 读取,运行时可切换 */
|
|
58
|
-
export
|
|
59
|
-
|
|
71
|
+
export function getDefaultAgent() {
|
|
72
|
+
return getActiveAgent(DEFAULT_AGENT_FALLBACK);
|
|
73
|
+
}
|
|
74
|
+
export function getAppName() {
|
|
60
75
|
try {
|
|
61
|
-
const agent = getAgent(
|
|
76
|
+
const agent = getAgent(getDefaultAgent());
|
|
62
77
|
return agent?.meta.name ?? 'Jarvis';
|
|
63
78
|
}
|
|
64
79
|
catch {
|
|
65
80
|
return 'Jarvis';
|
|
66
81
|
}
|
|
67
82
|
}
|
|
68
|
-
/** 应用名称 — 取自当前激活智能体的 name */
|
|
69
|
-
export const APP_NAME = resolveAppName();
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -28,5 +28,7 @@ export interface JarvisConfig {
|
|
|
28
28
|
}
|
|
29
29
|
/** 加载合并后的最终配置(结果会被缓存,多次调用只解析一次) */
|
|
30
30
|
export declare function loadConfig(): JarvisConfig;
|
|
31
|
+
/** 清空配置缓存,供首次引导写入后重新加载 */
|
|
32
|
+
export declare function resetConfigCache(): void;
|
|
31
33
|
/** 根据当前配置获取活跃模型配置 */
|
|
32
34
|
export declare function getActiveModel(config: JarvisConfig): ModelConfig | null;
|
package/dist/config/loader.js
CHANGED
|
@@ -55,6 +55,10 @@ export function loadConfig() {
|
|
|
55
55
|
_cachedConfig = mergeConfigs(globalCfg, localCfg);
|
|
56
56
|
return _cachedConfig;
|
|
57
57
|
}
|
|
58
|
+
/** 清空配置缓存,供首次引导写入后重新加载 */
|
|
59
|
+
export function resetConfigCache() {
|
|
60
|
+
_cachedConfig = null;
|
|
61
|
+
}
|
|
58
62
|
/** 根据当前配置获取活跃模型配置 */
|
|
59
63
|
export function getActiveModel(config) {
|
|
60
64
|
const modelName = config.system.model;
|
package/dist/core/hint.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { getAgent } from '../agents/index.js';
|
|
10
|
-
import {
|
|
10
|
+
import { getDefaultAgent } from '../config/constants.js';
|
|
11
11
|
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
12
12
|
import { getDefaultConfig } from '../services/api/llm.js';
|
|
13
13
|
/** 安全读取 JSON 文件 */
|
|
@@ -155,7 +155,7 @@ export function getFallbackHint(agentName) {
|
|
|
155
155
|
if (agentName) {
|
|
156
156
|
return FALLBACK_HINTS[agentName.toLowerCase()] ?? DEFAULT_HINT;
|
|
157
157
|
}
|
|
158
|
-
const agent = getAgent(
|
|
158
|
+
const agent = getAgent(getDefaultAgent());
|
|
159
159
|
const name = agent?.meta.name ?? 'Jarvis';
|
|
160
160
|
return FALLBACK_HINTS[name.toLowerCase()] ?? DEFAULT_HINT;
|
|
161
161
|
}
|
|
@@ -180,7 +180,7 @@ function normalizeHint(raw) {
|
|
|
180
180
|
* @returns 生成的提示文本,失败时返回静态兜底
|
|
181
181
|
*/
|
|
182
182
|
export async function generateAgentHint() {
|
|
183
|
-
const agent = getAgent(
|
|
183
|
+
const agent = getAgent(getDefaultAgent());
|
|
184
184
|
const agentName = agent?.meta.name ?? 'Jarvis';
|
|
185
185
|
const fallback = getFallbackHint(agentName);
|
|
186
186
|
// 检查 LLM 是否可用
|
package/dist/core/query.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Worker } from 'worker_threads';
|
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { findToolMerged as findTool } from '../tools/index.js';
|
|
6
|
-
import { MAX_ITERATIONS,
|
|
6
|
+
import { MAX_ITERATIONS, getContextTokenLimit } from '../config/constants.js';
|
|
7
7
|
import { sanitizeOutput, validateCommand, authorizeCommand, authorizeRule } from './safeguard.js';
|
|
8
8
|
import { logError, logInfo, logWarn } from './logger.js';
|
|
9
9
|
// 兼容 ESM __dirname
|
|
@@ -27,6 +27,7 @@ function estimateTokens(text) {
|
|
|
27
27
|
* - 确保总估算 token 不超过 CONTEXT_TOKEN_LIMIT
|
|
28
28
|
*/
|
|
29
29
|
function compressTranscript(transcript) {
|
|
30
|
+
const contextTokenLimit = getContextTokenLimit();
|
|
30
31
|
// 单条工具结果最大字符数
|
|
31
32
|
const MAX_TOOL_RESULT_CHARS = 3000;
|
|
32
33
|
// 旧条目压缩到的字符数
|
|
@@ -51,7 +52,7 @@ function compressTranscript(transcript) {
|
|
|
51
52
|
return m.content;
|
|
52
53
|
return JSON.stringify(m.content);
|
|
53
54
|
}).join(''));
|
|
54
|
-
if (totalTokens <=
|
|
55
|
+
if (totalTokens <= contextTokenLimit)
|
|
55
56
|
return result;
|
|
56
57
|
// 找出所有 tool_result 的索引,保留最近 KEEP_RECENT 条,其余压缩
|
|
57
58
|
const toolResultIndices = result
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from 'ink';
|
|
3
3
|
import { logInfo } from './core/logger.js';
|
|
4
|
-
import
|
|
4
|
+
import AppBootstrap from './screens/AppBootstrap.js';
|
|
5
5
|
export function startJarvis() {
|
|
6
6
|
logInfo('app.render.start');
|
|
7
|
-
render(_jsx(
|
|
7
|
+
render(_jsx(AppBootstrap, {}), { exitOnCtrlC: false });
|
|
8
8
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function AppBootstrap(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import REPL from './repl.js';
|
|
4
|
+
import SetupWizard from './setup/SetupWizard.js';
|
|
5
|
+
import { checkBootstrapStatus } from '../config/bootstrap.js';
|
|
6
|
+
export default function AppBootstrap() {
|
|
7
|
+
const [status, setStatus] = useState(() => checkBootstrapStatus());
|
|
8
|
+
if (status.ok) {
|
|
9
|
+
return _jsx(REPL, {});
|
|
10
|
+
}
|
|
11
|
+
return (_jsx(SetupWizard, { initialStatus: status, onCompleted: () => {
|
|
12
|
+
setStatus(checkBootstrapStatus());
|
|
13
|
+
} }));
|
|
14
|
+
}
|
|
@@ -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
|
+
}
|
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
|
};
|