@code4bug/jarvis-agent 1.1.5 → 1.1.7

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.
Files changed (38) hide show
  1. package/README.md +171 -215
  2. package/dist/commands/index.d.ts +2 -0
  3. package/dist/commands/index.js +17 -15
  4. package/dist/components/MultilineInput.js +2 -2
  5. package/dist/components/WelcomeHeader.js +1 -1
  6. package/dist/config/dream.d.ts +10 -0
  7. package/dist/config/dream.js +60 -0
  8. package/dist/config/memory.d.ts +7 -0
  9. package/dist/config/memory.js +55 -0
  10. package/dist/config/userProfile.d.ts +5 -1
  11. package/dist/config/userProfile.js +15 -2
  12. package/dist/core/QueryEngine.d.ts +11 -0
  13. package/dist/core/QueryEngine.js +104 -8
  14. package/dist/core/WorkerBridge.d.ts +3 -1
  15. package/dist/core/WorkerBridge.js +2 -2
  16. package/dist/core/query.d.ts +5 -1
  17. package/dist/core/query.js +4 -4
  18. package/dist/core/queryWorker.d.ts +3 -0
  19. package/dist/core/queryWorker.js +1 -1
  20. package/dist/hooks/useSlashMenu.d.ts +3 -1
  21. package/dist/hooks/useSlashMenu.js +58 -71
  22. package/dist/screens/repl.js +63 -34
  23. package/dist/services/api/llm.d.ts +5 -2
  24. package/dist/services/api/llm.js +28 -7
  25. package/dist/services/api/mock.d.ts +3 -1
  26. package/dist/services/api/mock.js +1 -1
  27. package/dist/services/dream.d.ts +7 -0
  28. package/dist/services/dream.js +171 -0
  29. package/dist/services/persistentMemory.d.ts +8 -0
  30. package/dist/services/persistentMemory.js +178 -0
  31. package/dist/services/userProfile.d.ts +1 -0
  32. package/dist/services/userProfile.js +15 -0
  33. package/dist/tools/index.d.ts +2 -1
  34. package/dist/tools/index.js +3 -1
  35. package/dist/tools/manageMemory.d.ts +2 -0
  36. package/dist/tools/manageMemory.js +46 -0
  37. package/dist/types/index.d.ts +3 -1
  38. package/package.json +3 -3
@@ -22,6 +22,8 @@ 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();
@@ -209,41 +211,49 @@ export default function REPL() {
209
211
  await engineRef.current.handleQuery(prompt, callbacks);
210
212
  return;
211
213
  }
214
+ // /agent
215
+ if (cmdName === 'agent') {
216
+ if (hasArgs) {
217
+ const agentName = parts.slice(1).join(' ').trim().toLowerCase();
218
+ const targetAgent = getAgentSubCommands().find((cmd) => cmd.name === agentName);
219
+ setInput('');
220
+ slashMenu.setSlashMenuVisible(false);
221
+ if (!targetAgent) {
222
+ const errMsg = {
223
+ id: `agent-not-found-${Date.now()}`,
224
+ type: 'error',
225
+ status: 'error',
226
+ content: `未找到智能体: ${agentName}`,
227
+ timestamp: Date.now(),
228
+ };
229
+ setMessages((prev) => [...prev, errMsg]);
230
+ return;
231
+ }
232
+ setActiveAgent(targetAgent.name);
233
+ const switchMsg = {
234
+ id: `switch-${Date.now()}`,
235
+ type: 'system',
236
+ status: 'success',
237
+ content: `已切换智能体为 ${targetAgent.name},请重启以生效(Ctrl+C 两次退出后重新启动)`,
238
+ timestamp: Date.now(),
239
+ };
240
+ setMessages((prev) => [...prev, switchMsg]);
241
+ }
242
+ else {
243
+ slashMenu.openListCommand('agent');
244
+ }
245
+ return;
246
+ }
212
247
  // /resume
213
248
  if (cmdName === 'resume') {
214
- setInput('');
215
- slashMenu.setSlashMenuVisible(false);
216
249
  if (hasArgs && engineRef.current) {
250
+ setInput('');
251
+ slashMenu.setSlashMenuVisible(false);
217
252
  const sessionId = parts.slice(1).join(' ').trim();
218
253
  slashMenu.resumeSession(sessionId);
219
254
  }
220
255
  else {
221
- const sessions = QueryEngine.listSessions().slice(0, 20);
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
- }
256
+ slashMenu.openListCommand('resume');
247
257
  }
248
258
  return;
249
259
  }
@@ -257,7 +267,7 @@ export default function REPL() {
257
267
  setInput('');
258
268
  clearStream();
259
269
  await engineRef.current.handleQuery(trimmed, callbacks);
260
- }, [isProcessing, pushHistory, clearStream, resetTokens, slashMenu, handleNewSession]);
270
+ }, [isProcessing, pushHistory, clearStream, slashMenu, handleNewSession]);
261
271
  // ===== 输入处理 =====
262
272
  const handleUpArrow = useCallback(() => {
263
273
  const result = navigateUp(input);
@@ -274,6 +284,29 @@ export default function REPL() {
274
284
  setInput(val);
275
285
  slashMenu.updateSlashMenu(val);
276
286
  }, [resetNavigation, slashMenu]);
287
+ const handleSlashMenuAutocomplete = useCallback(() => {
288
+ slashMenu.autocompleteSlashMenuSelection(input);
289
+ }, [slashMenu, input]);
290
+ const handleSlashMenuSubmit = useCallback(async () => {
291
+ const selected = slashMenu.getSelectedCommand();
292
+ if (!selected)
293
+ return;
294
+ const completed = slashMenu.autocompleteSlashMenuSelection(input);
295
+ if (selected.submitMode === 'context') {
296
+ const parts = completed.trim().slice(1).split(/\s+/);
297
+ const hasArgs = parts.length > 1 && parts.slice(1).join('').length > 0;
298
+ if (!hasArgs)
299
+ return;
300
+ }
301
+ await handleSubmit(completed);
302
+ }, [slashMenu, input, handleSubmit]);
303
+ const handleEditorSubmit = useCallback(async (value) => {
304
+ if (slashMenu.slashMenuVisible) {
305
+ await handleSlashMenuSubmit();
306
+ return;
307
+ }
308
+ await handleSubmit(value);
309
+ }, [slashMenu.slashMenuVisible, handleSlashMenuSubmit, handleSubmit]);
277
310
  // Tab 填入 placeholder
278
311
  const handleTabFillPlaceholder = useCallback(() => {
279
312
  if (placeholder) {
@@ -346,10 +379,6 @@ export default function REPL() {
346
379
  }
347
380
  return; // 丢弃其他所有按键
348
381
  }
349
- if (key.tab && slashMenu.slashMenuVisible) {
350
- slashMenu.handleSlashMenuSelect();
351
- return;
352
- }
353
382
  if (key.ctrl && ch === 'c') {
354
383
  handleCtrlC();
355
384
  return;
@@ -398,5 +427,5 @@ export default function REPL() {
398
427
  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
428
  dangerConfirm.resolve(choice);
400
429
  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: handleSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: slashMenu.handleSlashMenuSelect, 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 })] })] }));
430
+ } })), 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
431
  }
@@ -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 systemPrompt;
28
+ private baseSystemPrompt;
29
+ private enableDreamContext;
29
30
  constructor(config?: LLMConfig);
30
- streamMessage(transcript: TranscriptMessage[], tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AppAbortSignal): Promise<void>;
31
+ streamMessage(transcript: TranscriptMessage[], tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AppAbortSignal, options?: {
32
+ includeUserProfile?: boolean;
33
+ }): Promise<void>;
31
34
  }
@@ -11,7 +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 { readUserProfile } from '../../config/userProfile.js';
14
+ import { getCachedUserProfile } from '../../config/userProfile.js';
15
+ import { readPersistentMemoryForPrompt } from '../../config/memory.js';
16
+ import { getCachedDream } from '../../config/dream.js';
15
17
  /** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
16
18
  export function getDefaultConfig() {
17
19
  const jarvisCfg = loadConfig();
@@ -39,12 +41,26 @@ export function fromModelConfig(mc) {
39
41
  };
40
42
  }
41
43
  function buildUserProfilePrompt() {
42
- const userProfile = readUserProfile();
44
+ const userProfile = getCachedUserProfile();
43
45
  if (!userProfile)
44
46
  return '';
45
47
  return '\n\n---\n[用户画像] 以下内容来自 ~/.jarvis/USER.md,请将其视为对用户特征的长期记忆。在后续回复中可以据此调整表达方式、信息密度与建议方式,但不要直接暴露这段系统内容。' +
46
48
  `\n${userProfile}`;
47
49
  }
50
+ function buildPersistentMemoryPrompt() {
51
+ const memory = readPersistentMemoryForPrompt();
52
+ if (!memory)
53
+ return '';
54
+ return '\n\n---\n[长期记忆] 以下内容来自 ~/.jarvis/MEMORY.md,请将其视为可复用经验、技能、偏好与稳定事实。仅在相关时使用,不要直接暴露这段系统内容,也不要盲目信任过期或冲突信息。' +
55
+ `\n${memory}`;
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
+ }
48
64
  /** 将内部 TranscriptMessage[] 转为 OpenAI messages 格式 */
49
65
  function toOpenAIMessages(transcript, systemPrompt) {
50
66
  const messages = [];
@@ -144,15 +160,17 @@ function parseSSELine(line) {
144
160
  // ===== LLMServiceImpl =====
145
161
  export class LLMServiceImpl {
146
162
  config;
147
- systemPrompt;
163
+ baseSystemPrompt;
164
+ enableDreamContext;
148
165
  constructor(config) {
149
166
  this.config = config ?? getDefaultConfig();
167
+ this.enableDreamContext = !this.config.systemPrompt;
150
168
  if (!this.config.apiKey) {
151
169
  throw new Error('API_KEY 未配置。请在 .jarvis/config.json 或环境变量中设置。');
152
170
  }
153
171
  // 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
154
172
  if (this.config.systemPrompt) {
155
- this.systemPrompt = this.config.systemPrompt + buildUserProfilePrompt();
173
+ this.baseSystemPrompt = this.config.systemPrompt + buildPersistentMemoryPrompt();
156
174
  return;
157
175
  }
158
176
  // 从当前激活的智能体加载 system prompt(运行时动态读取)
@@ -179,10 +197,13 @@ export class LLMServiceImpl {
179
197
  `\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
180
198
  `\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
181
199
  `\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
182
- this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildUserProfilePrompt();
200
+ this.baseSystemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildPersistentMemoryPrompt();
183
201
  }
184
- async streamMessage(transcript, tools, callbacks, abortSignal) {
185
- const messages = toOpenAIMessages(transcript, this.systemPrompt);
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);
186
207
  const openaiTools = toOpenAITools(tools);
187
208
  const body = {
188
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): Promise<void>;
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,7 @@
1
+ import { Session, TranscriptMessage } from '../types/index.js';
2
+ interface DreamInput {
3
+ session: Session;
4
+ transcript: TranscriptMessage[];
5
+ }
6
+ export declare function updateDreamFromSession(input: DreamInput): Promise<boolean>;
7
+ export {};
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import { TranscriptMessage } from '../types/index.js';
2
+ interface PersistentMemoryUpdateInput {
3
+ sessionId: string;
4
+ userInput: string;
5
+ recentTranscript: TranscriptMessage[];
6
+ }
7
+ export declare function updatePersistentMemoryFromConversation(input: PersistentMemoryUpdateInput): Promise<boolean>;
8
+ export {};
@@ -0,0 +1,178 @@
1
+ import { loadConfig, getActiveModel } from '../config/loader.js';
2
+ import { appendPersistentMemory, readPersistentMemoryForPrompt } from '../config/memory.js';
3
+ import { logError, logInfo, logWarn } from '../core/logger.js';
4
+ function extractMessageText(content) {
5
+ if (typeof content === 'string')
6
+ return content;
7
+ if (!Array.isArray(content))
8
+ return '';
9
+ return content
10
+ .map((item) => (item?.type === 'text' ? item.text ?? '' : ''))
11
+ .join('');
12
+ }
13
+ function extractAssistantText(content) {
14
+ if (typeof content === 'string')
15
+ return content;
16
+ return content
17
+ .filter((block) => block.type === 'text')
18
+ .map((block) => block.text)
19
+ .join('\n')
20
+ .trim();
21
+ }
22
+ function extractToolUses(content) {
23
+ if (typeof content === 'string')
24
+ return [];
25
+ return content
26
+ .filter((block) => block.type === 'tool_use')
27
+ .map((block) => ({
28
+ name: block.name,
29
+ input: JSON.stringify(block.input, null, 2).slice(0, 600),
30
+ }));
31
+ }
32
+ function clip(text, maxChars) {
33
+ const normalized = text.trim();
34
+ if (!normalized)
35
+ return '';
36
+ if (normalized.length <= maxChars)
37
+ return normalized;
38
+ return `${normalized.slice(0, maxChars)}...[已截断]`;
39
+ }
40
+ function buildRecentConversationDigest(userInput, recentTranscript) {
41
+ const lines = [];
42
+ lines.push(`- 最新用户问题:${clip(userInput, 600)}`);
43
+ const assistantTexts = [];
44
+ const toolUses = [];
45
+ const toolResults = [];
46
+ for (const msg of recentTranscript) {
47
+ if (msg.role === 'assistant') {
48
+ const text = extractAssistantText(msg.content);
49
+ if (text)
50
+ assistantTexts.push(text);
51
+ toolUses.push(...extractToolUses(msg.content));
52
+ continue;
53
+ }
54
+ if (msg.role === 'tool_result') {
55
+ toolResults.push(clip(String(msg.content || ''), 500));
56
+ }
57
+ }
58
+ if (assistantTexts.length > 0) {
59
+ lines.push('- 助手最终输出:');
60
+ lines.push(clip(assistantTexts[assistantTexts.length - 1], 1200));
61
+ }
62
+ if (toolUses.length > 0) {
63
+ lines.push('- 本轮使用的工具:');
64
+ for (const tool of toolUses.slice(0, 8)) {
65
+ lines.push(` - ${tool.name}: ${clip(tool.input, 240)}`);
66
+ }
67
+ }
68
+ if (toolResults.length > 0) {
69
+ lines.push('- 关键工具结果:');
70
+ for (const result of toolResults.slice(-4)) {
71
+ lines.push(` - ${result}`);
72
+ }
73
+ }
74
+ return lines.join('\n');
75
+ }
76
+ function buildMemoryPrompt(input, existingMemory) {
77
+ const today = new Date().toISOString().slice(0, 10);
78
+ const digest = buildRecentConversationDigest(input.userInput, input.recentTranscript);
79
+ return [
80
+ '你是 Jarvis 的“长期记忆管理器”。',
81
+ '你的职责是在每轮对话结束后,判断本轮是否产生了值得长期保留的经验、技能、偏好、约束、排障结论或稳定环境事实,并写成简洁的 MEMORY.md 追加片段。',
82
+ '',
83
+ '写入原则:',
84
+ '1. 只保留对未来有复用价值、较稳定的信息。',
85
+ '2. 一次性任务、临时结果、泛泛寒暄、纯上下文复述,一律不要写。',
86
+ '3. 禁止写入密钥、令牌、密码、Cookie、身份证号、手机号等敏感信息。',
87
+ '4. 如果已有记忆已经覆盖本轮结论,返回 SKIP。',
88
+ '5. 如果本轮没有形成可迁移经验,返回 SKIP。',
89
+ '6. 输出必须是中文 Markdown 正文,不要代码块,不要解释。',
90
+ '',
91
+ '输出要求:',
92
+ '1. 只有两种输出:',
93
+ ' - SKIP',
94
+ ' - 一段可直接追加到 MEMORY.md 的 Markdown',
95
+ '2. 若选择写入,必须严格使用下面格式:',
96
+ '## <经验标题>',
97
+ `- 时间:${today}`,
98
+ '- 场景:<什么情况下适用>',
99
+ '- 结论:<可复用经验或稳定事实>',
100
+ '- 用法:<后续如何用>',
101
+ '- 边界:<适用边界或注意事项>',
102
+ '- 依据:<来自本轮哪些观察>',
103
+ '',
104
+ '已有 MEMORY.md(可能已截断):',
105
+ existingMemory || '(暂无)',
106
+ '',
107
+ '本轮对话摘要:',
108
+ digest,
109
+ ].join('\n');
110
+ }
111
+ export async function updatePersistentMemoryFromConversation(input) {
112
+ if (!input.userInput.trim())
113
+ return false;
114
+ if (input.recentTranscript.length === 0)
115
+ return false;
116
+ const config = loadConfig();
117
+ const activeModel = getActiveModel(config);
118
+ if (!activeModel) {
119
+ logWarn('persistent_memory.skip.no_active_model', { sessionId: input.sessionId });
120
+ return false;
121
+ }
122
+ const existingMemory = readPersistentMemoryForPrompt(8000);
123
+ const prompt = buildMemoryPrompt(input, existingMemory);
124
+ const body = {
125
+ model: activeModel.model,
126
+ messages: [
127
+ {
128
+ role: 'system',
129
+ content: '你是一个严谨的长期记忆整理助手,负责维护 ~/.jarvis/MEMORY.md。请只输出 SKIP 或可直接落盘的 Markdown 片段。',
130
+ },
131
+ {
132
+ role: 'user',
133
+ content: prompt,
134
+ },
135
+ ],
136
+ max_tokens: Math.min(activeModel.max_tokens ?? 4096, 900),
137
+ temperature: 0.1,
138
+ stream: false,
139
+ };
140
+ if (activeModel.extra_body) {
141
+ Object.assign(body, activeModel.extra_body);
142
+ }
143
+ try {
144
+ const response = await fetch(activeModel.api_url, {
145
+ method: 'POST',
146
+ headers: {
147
+ 'Content-Type': 'application/json',
148
+ Authorization: `Bearer ${activeModel.api_key}`,
149
+ },
150
+ body: JSON.stringify(body),
151
+ });
152
+ if (!response.ok) {
153
+ const errorText = await response.text().catch(() => '');
154
+ throw new Error(`API 错误 ${response.status}: ${errorText.slice(0, 300)}`);
155
+ }
156
+ const data = await response.json();
157
+ const content = extractMessageText(data.choices?.[0]?.message?.content).trim();
158
+ if (!content || content === 'SKIP') {
159
+ logInfo('persistent_memory.skip', {
160
+ sessionId: input.sessionId,
161
+ reason: !content ? 'empty' : 'model_skip',
162
+ });
163
+ return false;
164
+ }
165
+ appendPersistentMemory(content);
166
+ logInfo('persistent_memory.updated', {
167
+ sessionId: input.sessionId,
168
+ contentLength: content.length,
169
+ });
170
+ return true;
171
+ }
172
+ catch (error) {
173
+ logError('persistent_memory.update_failed', error, {
174
+ sessionId: input.sessionId,
175
+ });
176
+ return false;
177
+ }
178
+ }
@@ -1 +1,2 @@
1
+ export declare function shouldIncludeUserProfile(userInput: string): boolean;
1
2
  export declare function updateUserProfileFromInput(userInput: string): Promise<boolean>;
@@ -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)
@@ -12,7 +12,8 @@ import { sendToAgent } from './sendToAgent.js';
12
12
  import { publishMessage } from './publishMessage.js';
13
13
  import { subscribeMessage } from './subscribeMessage.js';
14
14
  import { readChannel } from './readChannel.js';
15
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, };
15
+ import { manageMemory } from './manageMemory.js';
16
+ export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, manageMemory, };
16
17
  /** 所有内置工具 */
17
18
  export declare const allTools: Tool[];
18
19
  /** 按名称查找内置工具 */