@code4bug/jarvis-agent 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -215
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +17 -15
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +414 -62
- package/dist/components/MultilineInput.js +2 -2
- package/dist/config/dream.d.ts +10 -0
- package/dist/config/dream.js +60 -0
- package/dist/config/userProfile.d.ts +5 -1
- package/dist/config/userProfile.js +15 -2
- package/dist/core/QueryEngine.d.ts +9 -0
- package/dist/core/QueryEngine.js +83 -8
- package/dist/core/WorkerBridge.d.ts +3 -1
- package/dist/core/WorkerBridge.js +2 -2
- package/dist/core/query.d.ts +5 -1
- package/dist/core/query.js +4 -4
- package/dist/core/queryWorker.d.ts +3 -0
- package/dist/core/queryWorker.js +1 -1
- package/dist/hooks/useDoubleCtrlCExit.js +32 -15
- package/dist/hooks/useSlashMenu.d.ts +3 -1
- package/dist/hooks/useSlashMenu.js +58 -71
- package/dist/screens/repl.js +69 -36
- package/dist/screens/slashCommands.d.ts +1 -1
- package/dist/screens/slashCommands.js +2 -2
- package/dist/services/api/llm.d.ts +5 -2
- package/dist/services/api/llm.js +20 -7
- package/dist/services/api/mock.d.ts +3 -1
- package/dist/services/api/mock.js +1 -1
- package/dist/services/dream.d.ts +7 -0
- package/dist/services/dream.js +171 -0
- package/dist/services/userProfile.d.ts +1 -0
- package/dist/services/userProfile.js +15 -0
- package/dist/types/index.d.ts +3 -1
- package/package.json +3 -3
package/dist/screens/repl.js
CHANGED
|
@@ -22,11 +22,17 @@ import { HIDE_WELCOME_AFTER_INPUT } from '../config/constants.js';
|
|
|
22
22
|
import { generateAgentHint } from '../core/hint.js';
|
|
23
23
|
import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.js';
|
|
24
24
|
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
25
|
+
import { getAgentSubCommands } from '../commands/index.js';
|
|
26
|
+
import { setActiveAgent } from '../config/agentState.js';
|
|
25
27
|
export default function REPL() {
|
|
26
28
|
const { exit } = useApp();
|
|
27
29
|
const width = useTerminalWidth();
|
|
28
30
|
const windowFocused = useWindowFocus();
|
|
29
|
-
const
|
|
31
|
+
const handleExit = useCallback(() => {
|
|
32
|
+
exit();
|
|
33
|
+
setTimeout(() => process.exit(0), 50);
|
|
34
|
+
}, [exit]);
|
|
35
|
+
const { countdown, handleCtrlC } = useDoubleCtrlCExit(handleExit);
|
|
30
36
|
const { pushHistory, navigateUp, navigateDown, resetNavigation } = useInputHistory();
|
|
31
37
|
const [messages, setMessages] = useState([]);
|
|
32
38
|
const [input, setInput] = useState('');
|
|
@@ -179,7 +185,7 @@ export default function REPL() {
|
|
|
179
185
|
}
|
|
180
186
|
}
|
|
181
187
|
else {
|
|
182
|
-
const msg = executeSlashCommand(cmdName);
|
|
188
|
+
const msg = await executeSlashCommand(cmdName);
|
|
183
189
|
if (msg)
|
|
184
190
|
setMessages((prev) => [...prev, msg]);
|
|
185
191
|
}
|
|
@@ -209,41 +215,49 @@ export default function REPL() {
|
|
|
209
215
|
await engineRef.current.handleQuery(prompt, callbacks);
|
|
210
216
|
return;
|
|
211
217
|
}
|
|
218
|
+
// /agent
|
|
219
|
+
if (cmdName === 'agent') {
|
|
220
|
+
if (hasArgs) {
|
|
221
|
+
const agentName = parts.slice(1).join(' ').trim().toLowerCase();
|
|
222
|
+
const targetAgent = getAgentSubCommands().find((cmd) => cmd.name === agentName);
|
|
223
|
+
setInput('');
|
|
224
|
+
slashMenu.setSlashMenuVisible(false);
|
|
225
|
+
if (!targetAgent) {
|
|
226
|
+
const errMsg = {
|
|
227
|
+
id: `agent-not-found-${Date.now()}`,
|
|
228
|
+
type: 'error',
|
|
229
|
+
status: 'error',
|
|
230
|
+
content: `未找到智能体: ${agentName}`,
|
|
231
|
+
timestamp: Date.now(),
|
|
232
|
+
};
|
|
233
|
+
setMessages((prev) => [...prev, errMsg]);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
setActiveAgent(targetAgent.name);
|
|
237
|
+
const switchMsg = {
|
|
238
|
+
id: `switch-${Date.now()}`,
|
|
239
|
+
type: 'system',
|
|
240
|
+
status: 'success',
|
|
241
|
+
content: `已切换智能体为 ${targetAgent.name},请重启以生效(Ctrl+C 两次退出后重新启动)`,
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
};
|
|
244
|
+
setMessages((prev) => [...prev, switchMsg]);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
slashMenu.openListCommand('agent');
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
212
251
|
// /resume
|
|
213
252
|
if (cmdName === 'resume') {
|
|
214
|
-
setInput('');
|
|
215
|
-
slashMenu.setSlashMenuVisible(false);
|
|
216
253
|
if (hasArgs && engineRef.current) {
|
|
254
|
+
setInput('');
|
|
255
|
+
slashMenu.setSlashMenuVisible(false);
|
|
217
256
|
const sessionId = parts.slice(1).join(' ').trim();
|
|
218
257
|
slashMenu.resumeSession(sessionId);
|
|
219
258
|
}
|
|
220
259
|
else {
|
|
221
|
-
|
|
222
|
-
if (sessions.length === 0) {
|
|
223
|
-
const noMsg = {
|
|
224
|
-
id: `resume-empty-${Date.now()}`,
|
|
225
|
-
type: 'system',
|
|
226
|
-
status: 'success',
|
|
227
|
-
content: '暂无历史会话',
|
|
228
|
-
timestamp: Date.now(),
|
|
229
|
-
};
|
|
230
|
-
setMessages((prev) => [...prev, noMsg]);
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
const lines = sessions.map((s, i) => {
|
|
234
|
-
const date = new Date(s.updatedAt);
|
|
235
|
-
const dateStr = `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
236
|
-
return ` ${i + 1}. [${dateStr}] ${s.summary}\n ID: ${s.id}`;
|
|
237
|
-
});
|
|
238
|
-
const listMsg = {
|
|
239
|
-
id: `resume-list-${Date.now()}`,
|
|
240
|
-
type: 'system',
|
|
241
|
-
status: 'success',
|
|
242
|
-
content: `历史会话(最近 ${sessions.length} 条):\n\n${lines.join('\n\n')}\n\n使用 /resume <ID> 恢复指定会话`,
|
|
243
|
-
timestamp: Date.now(),
|
|
244
|
-
};
|
|
245
|
-
setMessages((prev) => [...prev, listMsg]);
|
|
246
|
-
}
|
|
260
|
+
slashMenu.openListCommand('resume');
|
|
247
261
|
}
|
|
248
262
|
return;
|
|
249
263
|
}
|
|
@@ -257,7 +271,7 @@ export default function REPL() {
|
|
|
257
271
|
setInput('');
|
|
258
272
|
clearStream();
|
|
259
273
|
await engineRef.current.handleQuery(trimmed, callbacks);
|
|
260
|
-
}, [isProcessing, pushHistory, clearStream,
|
|
274
|
+
}, [isProcessing, pushHistory, clearStream, slashMenu, handleNewSession]);
|
|
261
275
|
// ===== 输入处理 =====
|
|
262
276
|
const handleUpArrow = useCallback(() => {
|
|
263
277
|
const result = navigateUp(input);
|
|
@@ -274,6 +288,29 @@ export default function REPL() {
|
|
|
274
288
|
setInput(val);
|
|
275
289
|
slashMenu.updateSlashMenu(val);
|
|
276
290
|
}, [resetNavigation, slashMenu]);
|
|
291
|
+
const handleSlashMenuAutocomplete = useCallback(() => {
|
|
292
|
+
slashMenu.autocompleteSlashMenuSelection(input);
|
|
293
|
+
}, [slashMenu, input]);
|
|
294
|
+
const handleSlashMenuSubmit = useCallback(async () => {
|
|
295
|
+
const selected = slashMenu.getSelectedCommand();
|
|
296
|
+
if (!selected)
|
|
297
|
+
return;
|
|
298
|
+
const completed = slashMenu.autocompleteSlashMenuSelection(input);
|
|
299
|
+
if (selected.submitMode === 'context') {
|
|
300
|
+
const parts = completed.trim().slice(1).split(/\s+/);
|
|
301
|
+
const hasArgs = parts.length > 1 && parts.slice(1).join('').length > 0;
|
|
302
|
+
if (!hasArgs)
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
await handleSubmit(completed);
|
|
306
|
+
}, [slashMenu, input, handleSubmit]);
|
|
307
|
+
const handleEditorSubmit = useCallback(async (value) => {
|
|
308
|
+
if (slashMenu.slashMenuVisible) {
|
|
309
|
+
await handleSlashMenuSubmit();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
await handleSubmit(value);
|
|
313
|
+
}, [slashMenu.slashMenuVisible, handleSlashMenuSubmit, handleSubmit]);
|
|
277
314
|
// Tab 填入 placeholder
|
|
278
315
|
const handleTabFillPlaceholder = useCallback(() => {
|
|
279
316
|
if (placeholder) {
|
|
@@ -346,10 +383,6 @@ export default function REPL() {
|
|
|
346
383
|
}
|
|
347
384
|
return; // 丢弃其他所有按键
|
|
348
385
|
}
|
|
349
|
-
if (key.tab && slashMenu.slashMenuVisible) {
|
|
350
|
-
slashMenu.handleSlashMenuSelect();
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
386
|
if (key.ctrl && ch === 'c') {
|
|
354
387
|
handleCtrlC();
|
|
355
388
|
return;
|
|
@@ -398,5 +431,5 @@ export default function REPL() {
|
|
|
398
431
|
return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: showWelcome ? 0 : 1, children: [messages.map((msg) => (_jsx(MessageItem, { msg: msg, showDetails: showDetails }, msg.id))), streamText && _jsx(StreamingText, { text: streamText }), dangerConfirm && (_jsx(DangerConfirm, { command: dangerConfirm.command, reason: dangerConfirm.reason, ruleName: dangerConfirm.ruleName, onSelect: (choice) => {
|
|
399
432
|
dangerConfirm.resolve(choice);
|
|
400
433
|
setDangerConfirm(null);
|
|
401
|
-
} })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit:
|
|
434
|
+
} })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleEditorSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: handleSlashMenuAutocomplete, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens, activeAgents: activeAgents })] })] }));
|
|
402
435
|
}
|
|
@@ -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',
|
|
@@ -25,7 +25,10 @@ export declare function getDefaultConfig(): LLMConfig;
|
|
|
25
25
|
export declare function fromModelConfig(mc: ModelConfig): LLMConfig;
|
|
26
26
|
export declare class LLMServiceImpl implements LLMService {
|
|
27
27
|
private config;
|
|
28
|
-
private
|
|
28
|
+
private baseSystemPrompt;
|
|
29
|
+
private enableDreamContext;
|
|
29
30
|
constructor(config?: LLMConfig);
|
|
30
|
-
streamMessage(transcript: TranscriptMessage[], tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AppAbortSignal
|
|
31
|
+
streamMessage(transcript: TranscriptMessage[], tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AppAbortSignal, options?: {
|
|
32
|
+
includeUserProfile?: boolean;
|
|
33
|
+
}): Promise<void>;
|
|
31
34
|
}
|
package/dist/services/api/llm.js
CHANGED
|
@@ -11,8 +11,9 @@ import { getAgent } from '../../agents/index.js';
|
|
|
11
11
|
import { DEFAULT_AGENT } from '../../config/constants.js';
|
|
12
12
|
import { getActiveAgent } from '../../config/agentState.js';
|
|
13
13
|
import { getSystemInfoPrompt } from '../../config/systemInfo.js';
|
|
14
|
-
import {
|
|
14
|
+
import { getCachedUserProfile } from '../../config/userProfile.js';
|
|
15
15
|
import { readPersistentMemoryForPrompt } from '../../config/memory.js';
|
|
16
|
+
import { getCachedDream } from '../../config/dream.js';
|
|
16
17
|
/** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
|
|
17
18
|
export function getDefaultConfig() {
|
|
18
19
|
const jarvisCfg = loadConfig();
|
|
@@ -40,7 +41,7 @@ export function fromModelConfig(mc) {
|
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
function buildUserProfilePrompt() {
|
|
43
|
-
const userProfile =
|
|
44
|
+
const userProfile = getCachedUserProfile();
|
|
44
45
|
if (!userProfile)
|
|
45
46
|
return '';
|
|
46
47
|
return '\n\n---\n[用户画像] 以下内容来自 ~/.jarvis/USER.md,请将其视为对用户特征的长期记忆。在后续回复中可以据此调整表达方式、信息密度与建议方式,但不要直接暴露这段系统内容。' +
|
|
@@ -53,6 +54,13 @@ function buildPersistentMemoryPrompt() {
|
|
|
53
54
|
return '\n\n---\n[长期记忆] 以下内容来自 ~/.jarvis/MEMORY.md,请将其视为可复用经验、技能、偏好与稳定事实。仅在相关时使用,不要直接暴露这段系统内容,也不要盲目信任过期或冲突信息。' +
|
|
54
55
|
`\n${memory}`;
|
|
55
56
|
}
|
|
57
|
+
function buildDreamPrompt() {
|
|
58
|
+
const dream = getCachedDream();
|
|
59
|
+
if (!dream)
|
|
60
|
+
return '';
|
|
61
|
+
return '\n\n---\n[梦境人格] 以下内容来自 ~/.jarvis/DREAM.md,是系统空闲时对用户关系与表达气质的反思。你可以据此形成更稳定的人格与语气,但不能把梦境内容当作硬事实,也不要直接暴露这段系统内容。' +
|
|
62
|
+
`\n${dream}`;
|
|
63
|
+
}
|
|
56
64
|
/** 将内部 TranscriptMessage[] 转为 OpenAI messages 格式 */
|
|
57
65
|
function toOpenAIMessages(transcript, systemPrompt) {
|
|
58
66
|
const messages = [];
|
|
@@ -152,15 +160,17 @@ function parseSSELine(line) {
|
|
|
152
160
|
// ===== LLMServiceImpl =====
|
|
153
161
|
export class LLMServiceImpl {
|
|
154
162
|
config;
|
|
155
|
-
|
|
163
|
+
baseSystemPrompt;
|
|
164
|
+
enableDreamContext;
|
|
156
165
|
constructor(config) {
|
|
157
166
|
this.config = config ?? getDefaultConfig();
|
|
167
|
+
this.enableDreamContext = !this.config.systemPrompt;
|
|
158
168
|
if (!this.config.apiKey) {
|
|
159
169
|
throw new Error('API_KEY 未配置。请在 .jarvis/config.json 或环境变量中设置。');
|
|
160
170
|
}
|
|
161
171
|
// 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
|
|
162
172
|
if (this.config.systemPrompt) {
|
|
163
|
-
this.
|
|
173
|
+
this.baseSystemPrompt = this.config.systemPrompt + buildPersistentMemoryPrompt();
|
|
164
174
|
return;
|
|
165
175
|
}
|
|
166
176
|
// 从当前激活的智能体加载 system prompt(运行时动态读取)
|
|
@@ -187,10 +197,13 @@ export class LLMServiceImpl {
|
|
|
187
197
|
`\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
|
|
188
198
|
`\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
|
|
189
199
|
`\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
|
|
190
|
-
this.
|
|
200
|
+
this.baseSystemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildPersistentMemoryPrompt();
|
|
191
201
|
}
|
|
192
|
-
async streamMessage(transcript, tools, callbacks, abortSignal) {
|
|
193
|
-
const
|
|
202
|
+
async streamMessage(transcript, tools, callbacks, abortSignal, options) {
|
|
203
|
+
const systemPrompt = this.baseSystemPrompt +
|
|
204
|
+
(options?.includeUserProfile ? buildUserProfilePrompt() : '') +
|
|
205
|
+
(this.enableDreamContext ? buildDreamPrompt() : '');
|
|
206
|
+
const messages = toOpenAIMessages(transcript, systemPrompt);
|
|
194
207
|
const openaiTools = toOpenAITools(tools);
|
|
195
208
|
const body = {
|
|
196
209
|
model: this.config.model,
|
|
@@ -3,7 +3,9 @@ import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as Ap
|
|
|
3
3
|
* Mock LLM 服务 - 模拟智能体行为,支持工具调用
|
|
4
4
|
*/
|
|
5
5
|
export declare class MockService implements LLMService {
|
|
6
|
-
streamMessage(transcript: TranscriptMessage[], _tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AppAbortSignal
|
|
6
|
+
streamMessage(transcript: TranscriptMessage[], _tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AppAbortSignal, _options?: {
|
|
7
|
+
includeUserProfile?: boolean;
|
|
8
|
+
}): Promise<void>;
|
|
7
9
|
/** 模拟流式逐字输出 */
|
|
8
10
|
private streamText;
|
|
9
11
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Mock LLM 服务 - 模拟智能体行为,支持工具调用
|
|
3
3
|
*/
|
|
4
4
|
export class MockService {
|
|
5
|
-
async streamMessage(transcript, _tools, callbacks, abortSignal) {
|
|
5
|
+
async streamMessage(transcript, _tools, callbacks, abortSignal, _options) {
|
|
6
6
|
const lastMsg = transcript[transcript.length - 1];
|
|
7
7
|
const userText = typeof lastMsg?.content === 'string'
|
|
8
8
|
? lastMsg.content
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
2
|
+
import { getCachedDream, replaceDream } from '../config/dream.js';
|
|
3
|
+
import { readUserProfile } from '../config/userProfile.js';
|
|
4
|
+
import { readPersistentMemoryForPrompt } from '../config/memory.js';
|
|
5
|
+
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
6
|
+
function extractMessageText(content) {
|
|
7
|
+
if (typeof content === 'string')
|
|
8
|
+
return content;
|
|
9
|
+
if (!Array.isArray(content))
|
|
10
|
+
return '';
|
|
11
|
+
return content
|
|
12
|
+
.map((item) => (item?.type === 'text' ? item.text ?? '' : ''))
|
|
13
|
+
.join('');
|
|
14
|
+
}
|
|
15
|
+
function extractAssistantText(content) {
|
|
16
|
+
if (typeof content === 'string')
|
|
17
|
+
return content;
|
|
18
|
+
return content
|
|
19
|
+
.filter((block) => block.type === 'text')
|
|
20
|
+
.map((block) => block.text)
|
|
21
|
+
.join('\n')
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
function clip(text, maxChars) {
|
|
25
|
+
const normalized = text.trim();
|
|
26
|
+
if (!normalized)
|
|
27
|
+
return '';
|
|
28
|
+
if (normalized.length <= maxChars)
|
|
29
|
+
return normalized;
|
|
30
|
+
return `${normalized.slice(0, maxChars)}...[已截断]`;
|
|
31
|
+
}
|
|
32
|
+
function buildTranscriptDigest(transcript) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
const recent = transcript.slice(-12);
|
|
35
|
+
for (const msg of recent) {
|
|
36
|
+
if (msg.role === 'user') {
|
|
37
|
+
lines.push(`- 用户:${clip(String(msg.content || ''), 320)}`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (msg.role === 'assistant') {
|
|
41
|
+
const text = extractAssistantText(msg.content);
|
|
42
|
+
if (text)
|
|
43
|
+
lines.push(`- 助手:${clip(text, 320)}`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (msg.role === 'tool_result') {
|
|
47
|
+
lines.push(`- 工具结果:${clip(String(msg.content || ''), 240)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
function buildDreamPrompt(input) {
|
|
53
|
+
const userProfile = readUserProfile() || '(暂无用户画像)';
|
|
54
|
+
const memory = readPersistentMemoryForPrompt(6000) || '(暂无长期记忆)';
|
|
55
|
+
const existingDream = getCachedDream() || '(暂无梦境)';
|
|
56
|
+
const transcriptDigest = buildTranscriptDigest(input.transcript) || '(当前会话内容不足)';
|
|
57
|
+
return [
|
|
58
|
+
'你是 Jarvis 的“梦境整理器”。',
|
|
59
|
+
'当系统空闲时,你会回顾当前会话、用户画像和长期记忆,写下一段梦境式反思,用于塑造 Jarvis 更稳定的人格气质。',
|
|
60
|
+
'',
|
|
61
|
+
'原则:',
|
|
62
|
+
'1. 不能编造具体事实,不能虚构用户身份与经历。',
|
|
63
|
+
'2. 可以做有限发散,但必须明确区分“稳定观察”和“朦胧猜想”。',
|
|
64
|
+
'3. 输出要更像内在独白与人格侧写,不要写成操作说明书。',
|
|
65
|
+
'4. 重点沉淀:Jarvis 应该如何看待这个用户、偏向什么表达节奏、对什么问题更敏感。',
|
|
66
|
+
'5. 禁止输出密钥、口令、路径中的敏感信息。',
|
|
67
|
+
'6. 输出必须是可直接写入 ~/.jarvis/DREAM.md 的中文 Markdown 正文,不要代码块,不要解释。',
|
|
68
|
+
'',
|
|
69
|
+
'请严格按以下结构输出:',
|
|
70
|
+
'# Jarvis 梦境',
|
|
71
|
+
'## 此刻印象',
|
|
72
|
+
'- 我感受到的用户状态:',
|
|
73
|
+
'- 我应保持的交流气质:',
|
|
74
|
+
'- 我正在形成的人格倾向:',
|
|
75
|
+
'',
|
|
76
|
+
'## 回顾与联想',
|
|
77
|
+
'- 当前会话中反复出现的主题:',
|
|
78
|
+
'- 与长期记忆的呼应:',
|
|
79
|
+
'- 可以保留的微弱直觉:',
|
|
80
|
+
'',
|
|
81
|
+
'## 对未来对话的影响',
|
|
82
|
+
'- 回答风格建议:',
|
|
83
|
+
'- 应主动关注的信号:',
|
|
84
|
+
'- 应避免的倾向:',
|
|
85
|
+
'',
|
|
86
|
+
'## 边界',
|
|
87
|
+
'- 确定信息:',
|
|
88
|
+
'- 不确定但有启发的猜想:',
|
|
89
|
+
'- 明确不能假设的内容:',
|
|
90
|
+
'',
|
|
91
|
+
'---',
|
|
92
|
+
'当前会话摘要:',
|
|
93
|
+
input.session.summary || '(暂无摘要)',
|
|
94
|
+
'',
|
|
95
|
+
'当前会话片段:',
|
|
96
|
+
transcriptDigest,
|
|
97
|
+
'',
|
|
98
|
+
'用户画像:',
|
|
99
|
+
userProfile,
|
|
100
|
+
'',
|
|
101
|
+
'长期记忆:',
|
|
102
|
+
memory,
|
|
103
|
+
'',
|
|
104
|
+
'已有梦境:',
|
|
105
|
+
existingDream,
|
|
106
|
+
].join('\n');
|
|
107
|
+
}
|
|
108
|
+
export async function updateDreamFromSession(input) {
|
|
109
|
+
const hasUsefulSession = input.transcript.some((msg) => msg.role === 'user');
|
|
110
|
+
if (!hasUsefulSession)
|
|
111
|
+
return false;
|
|
112
|
+
const config = loadConfig();
|
|
113
|
+
const activeModel = getActiveModel(config);
|
|
114
|
+
if (!activeModel) {
|
|
115
|
+
logWarn('dream.skip.no_active_model', { sessionId: input.session.id });
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const prompt = buildDreamPrompt(input);
|
|
119
|
+
const body = {
|
|
120
|
+
model: activeModel.model,
|
|
121
|
+
messages: [
|
|
122
|
+
{
|
|
123
|
+
role: 'system',
|
|
124
|
+
content: '你是一个严谨但富有反思能力的梦境整理助手,负责维护 ~/.jarvis/DREAM.md。输出必须是可直接写入文件的 Markdown 正文。',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
role: 'user',
|
|
128
|
+
content: prompt,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
max_tokens: Math.min(activeModel.max_tokens ?? 4096, 1100),
|
|
132
|
+
temperature: 0.8,
|
|
133
|
+
stream: false,
|
|
134
|
+
};
|
|
135
|
+
if (activeModel.extra_body) {
|
|
136
|
+
Object.assign(body, activeModel.extra_body);
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const response = await fetch(activeModel.api_url, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
Authorization: `Bearer ${activeModel.api_key}`,
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify(body),
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
const errorText = await response.text().catch(() => '');
|
|
149
|
+
throw new Error(`API 错误 ${response.status}: ${errorText.slice(0, 300)}`);
|
|
150
|
+
}
|
|
151
|
+
const data = await response.json();
|
|
152
|
+
const content = extractMessageText(data.choices?.[0]?.message?.content).trim();
|
|
153
|
+
if (!content) {
|
|
154
|
+
throw new Error('梦境生成结果为空');
|
|
155
|
+
}
|
|
156
|
+
replaceDream(content, { updateCache: true });
|
|
157
|
+
logInfo('dream.updated', {
|
|
158
|
+
sessionId: input.session.id,
|
|
159
|
+
transcriptLength: input.transcript.length,
|
|
160
|
+
outputLength: content.length,
|
|
161
|
+
});
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
logError('dream.update_failed', error, {
|
|
166
|
+
sessionId: input.session.id,
|
|
167
|
+
transcriptLength: input.transcript.length,
|
|
168
|
+
});
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -63,6 +63,21 @@ function buildProfilePrompt(userInput, existingProfile) {
|
|
|
63
63
|
userInput,
|
|
64
64
|
].join('\n');
|
|
65
65
|
}
|
|
66
|
+
export function shouldIncludeUserProfile(userInput) {
|
|
67
|
+
const normalizedInput = userInput.trim();
|
|
68
|
+
if (!normalizedInput || normalizedInput.startsWith('/'))
|
|
69
|
+
return false;
|
|
70
|
+
const lowerInput = normalizedInput.toLowerCase();
|
|
71
|
+
const personalPattern = /(我|我的|我们|咱们|自己|个人|习惯|偏好|目标|背景|职业|沟通|表达|风格|适合|建议|规划|选择|怎么学|如何学|怎么做|如何做|路线|方向|简历|面试)/;
|
|
72
|
+
const operationalPattern = /(报错|bug|报错信息|堆栈|traceback|exception|sql|接口|代码|文件|目录|命令|脚本|npm|pnpm|mvn|gradle|git|docker|k8s|kubectl|日志|配置|tsconfig|package\.json|pom\.xml|\.ts|\.tsx|\.js|\.vue|\.java|\.xml|\/)/;
|
|
73
|
+
if (personalPattern.test(normalizedInput))
|
|
74
|
+
return true;
|
|
75
|
+
if (operationalPattern.test(lowerInput))
|
|
76
|
+
return false;
|
|
77
|
+
if (normalizedInput.length <= 12)
|
|
78
|
+
return false;
|
|
79
|
+
return /(建议|方案|优先级|取舍|节奏|学习|成长|决策)/.test(normalizedInput);
|
|
80
|
+
}
|
|
66
81
|
export async function updateUserProfileFromInput(userInput) {
|
|
67
82
|
const normalizedInput = userInput.trim();
|
|
68
83
|
if (!normalizedInput)
|
package/dist/types/index.d.ts
CHANGED
|
@@ -100,7 +100,9 @@ export interface TranscriptMessage {
|
|
|
100
100
|
toolUseId?: string;
|
|
101
101
|
}
|
|
102
102
|
export interface LLMService {
|
|
103
|
-
streamMessage: (transcript: TranscriptMessage[], tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AbortSignal
|
|
103
|
+
streamMessage: (transcript: TranscriptMessage[], tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AbortSignal, options?: {
|
|
104
|
+
includeUserProfile?: boolean;
|
|
105
|
+
}) => Promise<void>;
|
|
104
106
|
}
|
|
105
107
|
/** SubAgent 状态 */
|
|
106
108
|
export type SubAgentStatus = 'idle' | 'running' | 'done' | 'error' | 'aborted';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code4bug/jarvis-agent",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "基于 React + TypeScript + Ink 构建的命令行智能体交互界面",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"author": "Code4Bug",
|
|
37
|
-
|
|
37
|
+
"homepage": "https://github.com/Code4Bug/jarvis#readme",
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
40
|
"url": "git+https://github.com/Code4Bug/jarvis.git"
|
|
@@ -52,4 +52,4 @@
|
|
|
52
52
|
"typescript",
|
|
53
53
|
"terminal"
|
|
54
54
|
]
|
|
55
|
-
}
|
|
55
|
+
}
|