@code4bug/jarvis-agent 1.1.4 → 1.1.6

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 CHANGED
@@ -11,6 +11,10 @@
11
11
 
12
12
  在终端中与 AI 对话,支持多轮推理、工具调用、流式输出,开箱即用。
13
13
 
14
+ > 多智能体,不只是多几个助手,而是让需求分析、方案设计、编码实现、测试验证、代码评审等角色真正协同起来。
15
+ > 它可以用于多人协作流程构建、跨角色任务接力、复杂项目拆解与推进,也可以用于产品研发、缺陷修复、自动化交付等高频场景。
16
+ > 当多个智能体围绕同一目标持续沟通、分工与汇总时,AI 才真正从“单点应答”走向“团队作战”。
17
+
14
18
  ---
15
19
 
16
20
  ## 为什么叫 Jarvis
package/dist/cli.js CHANGED
@@ -1,9 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { APP_VERSION } from './config/constants.js';
3
+ import { ensureLoggerReady, logError, logInfo } from './core/logger.js';
3
4
  import { startJarvis } from './index.js';
4
5
  const arg = process.argv[2];
6
+ ensureLoggerReady();
7
+ logInfo('cli.launch', {
8
+ argv: process.argv.slice(2),
9
+ version: APP_VERSION,
10
+ });
5
11
  if (arg === '--version' || arg === '-v' || arg === 'version') {
12
+ logInfo('cli.version', { version: APP_VERSION });
6
13
  console.log(APP_VERSION);
7
14
  process.exit(0);
8
15
  }
16
+ process.on('uncaughtException', (error) => {
17
+ logError('process.uncaught_exception', error);
18
+ });
19
+ process.on('unhandledRejection', (reason) => {
20
+ logError('process.unhandled_rejection', reason);
21
+ });
9
22
  startJarvis();
@@ -3,7 +3,9 @@ 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
+ export declare const JARVIS_HOME_DIR: string;
6
7
  export declare const SESSIONS_DIR: string;
8
+ export declare const LOGS_DIR: string;
7
9
  /** 输入后是否隐藏 WelcomeHeader,默认 false(不隐藏) */
8
10
  export declare const HIDE_WELCOME_AFTER_INPUT = false;
9
11
  export declare const MODEL_NAME: string;
@@ -20,7 +20,9 @@ export const PROJECT_NAME = path.basename(process.cwd());
20
20
  export const MAX_ITERATIONS = 50;
21
21
  /** 会话存储目录(~/.jarvis/sessions/) */
22
22
  import os from 'os';
23
- export const SESSIONS_DIR = path.join(os.homedir(), '.jarvis', 'sessions');
23
+ export const JARVIS_HOME_DIR = path.join(os.homedir(), '.jarvis');
24
+ export const SESSIONS_DIR = path.join(JARVIS_HOME_DIR, 'sessions');
25
+ export const LOGS_DIR = path.join(JARVIS_HOME_DIR, 'logs');
24
26
  /** 输入后是否隐藏 WelcomeHeader,默认 false(不隐藏) */
25
27
  export const HIDE_WELCOME_AFTER_INPUT = false;
26
28
  /** 从配置文件获取当前模型名称 */
@@ -0,0 +1,7 @@
1
+ export declare const MEMORY_FILE_PATH: string;
2
+ export declare function ensureMemoryHomeDir(): string;
3
+ export declare function ensureMemoryFile(): string;
4
+ export declare function readPersistentMemory(): string;
5
+ export declare function readPersistentMemoryForPrompt(maxChars?: number): string;
6
+ export declare function appendPersistentMemory(content: string): void;
7
+ export declare function replacePersistentMemory(content: string): void;
@@ -0,0 +1,55 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ export const MEMORY_FILE_PATH = path.join(os.homedir(), '.jarvis', 'MEMORY.md');
5
+ const MEMORY_FILE_HEADER = [
6
+ '# Jarvis 长期记忆',
7
+ '',
8
+ '> 这里沉淀可复用的经验、技能、偏好、约束与稳定环境事实。',
9
+ '> 避免写入一次性闲聊、临时输出、密钥或其它敏感信息。',
10
+ '',
11
+ ].join('\n');
12
+ export function ensureMemoryHomeDir() {
13
+ const dir = path.dirname(MEMORY_FILE_PATH);
14
+ if (!fs.existsSync(dir)) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+ return dir;
18
+ }
19
+ export function ensureMemoryFile() {
20
+ ensureMemoryHomeDir();
21
+ if (!fs.existsSync(MEMORY_FILE_PATH)) {
22
+ fs.writeFileSync(MEMORY_FILE_PATH, MEMORY_FILE_HEADER, 'utf-8');
23
+ }
24
+ return MEMORY_FILE_PATH;
25
+ }
26
+ export function readPersistentMemory() {
27
+ try {
28
+ ensureMemoryFile();
29
+ return fs.readFileSync(MEMORY_FILE_PATH, 'utf-8').trim();
30
+ }
31
+ catch {
32
+ return '';
33
+ }
34
+ }
35
+ export function readPersistentMemoryForPrompt(maxChars = 12000) {
36
+ const content = readPersistentMemory();
37
+ if (!content)
38
+ return '';
39
+ if (content.length <= maxChars)
40
+ return content;
41
+ return `...[已截断,仅保留最近 ${maxChars} 字符]\n${content.slice(-maxChars)}`;
42
+ }
43
+ export function appendPersistentMemory(content) {
44
+ const normalized = content.trim();
45
+ if (!normalized)
46
+ return;
47
+ ensureMemoryFile();
48
+ const current = fs.readFileSync(MEMORY_FILE_PATH, 'utf-8');
49
+ const suffix = current.endsWith('\n\n') ? '' : (current.endsWith('\n') ? '\n' : '\n\n');
50
+ fs.appendFileSync(MEMORY_FILE_PATH, `${suffix}${normalized}\n`, 'utf-8');
51
+ }
52
+ export function replacePersistentMemory(content) {
53
+ ensureMemoryFile();
54
+ fs.writeFileSync(MEMORY_FILE_PATH, `${content.trimEnd()}\n`, 'utf-8');
55
+ }
@@ -0,0 +1,4 @@
1
+ export declare const USER_PROFILE_PATH: string;
2
+ export declare function ensureJarvisHomeDir(): string;
3
+ export declare function readUserProfile(): string;
4
+ export declare function writeUserProfile(content: string): void;
@@ -0,0 +1,25 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ export const USER_PROFILE_PATH = path.join(os.homedir(), '.jarvis', 'USER.md');
5
+ export function ensureJarvisHomeDir() {
6
+ const dir = path.dirname(USER_PROFILE_PATH);
7
+ if (!fs.existsSync(dir)) {
8
+ fs.mkdirSync(dir, { recursive: true });
9
+ }
10
+ return dir;
11
+ }
12
+ export function readUserProfile() {
13
+ try {
14
+ if (!fs.existsSync(USER_PROFILE_PATH))
15
+ return '';
16
+ return fs.readFileSync(USER_PROFILE_PATH, 'utf-8').trim();
17
+ }
18
+ catch {
19
+ return '';
20
+ }
21
+ }
22
+ export function writeUserProfile(content) {
23
+ ensureJarvisHomeDir();
24
+ fs.writeFileSync(USER_PROFILE_PATH, content.trimEnd() + '\n', 'utf-8');
25
+ }
@@ -1,16 +1,3 @@
1
- /**
2
- * AgentMessageBus — SubAgent 间通讯总线(进程内单例)
3
- *
4
- * 提供发布/订阅机制,允许:
5
- * - SubAgent 向命名频道发布消息
6
- * - SubAgent 订阅频道,等待其他 Agent 发布的消息
7
- * - 主 Agent 观察所有频道历史
8
- *
9
- * 线程安全说明:
10
- * Node.js Worker 线程之间不共享内存,因此 MessageBus 运行在
11
- * 主线程(queryWorker)中,SubAgent Worker 通过 IPC 消息与其交互。
12
- * SubAgentBridge 负责在主线程侧代理 publish/subscribe 请求。
13
- */
14
1
  export interface BusMessage {
15
2
  /** 发布者 Agent 标识 */
16
3
  from: string;
@@ -11,6 +11,7 @@
11
11
  * 主线程(queryWorker)中,SubAgent Worker 通过 IPC 消息与其交互。
12
12
  * SubAgentBridge 负责在主线程侧代理 publish/subscribe 请求。
13
13
  */
14
+ import { logInfo } from './logger.js';
14
15
  class AgentMessageBus {
15
16
  /** 频道历史消息,key = channel */
16
17
  history = new Map();
@@ -24,6 +25,11 @@ class AgentMessageBus {
24
25
  */
25
26
  publish(from, channel, payload) {
26
27
  const msg = { from, channel, payload, timestamp: Date.now() };
28
+ logInfo('bus.publish', {
29
+ from,
30
+ channel,
31
+ payloadLength: payload.length,
32
+ });
27
33
  // 存入历史
28
34
  if (!this.history.has(channel))
29
35
  this.history.set(channel, []);
@@ -48,10 +54,19 @@ class AgentMessageBus {
48
54
  * @param fromOffset 从第几条开始消费(0-based),不传则只等新消息
49
55
  */
50
56
  subscribe(channel, timeoutMs = 30_000, fromOffset) {
57
+ logInfo('bus.subscribe', {
58
+ channel,
59
+ timeoutMs,
60
+ fromOffset,
61
+ });
51
62
  // 如果指定了 offset 且历史中已有该位置之后的消息,立即返回
52
63
  if (fromOffset !== undefined) {
53
64
  const history = this.history.get(channel) ?? [];
54
65
  if (fromOffset < history.length) {
66
+ logInfo('bus.subscribe.hit_history', {
67
+ channel,
68
+ fromOffset,
69
+ });
55
70
  return Promise.resolve(history[fromOffset]);
56
71
  }
57
72
  }
@@ -62,10 +77,16 @@ class AgentMessageBus {
62
77
  if (list) {
63
78
  this.waiters.set(channel, list.filter((w) => w.resolve !== resolve));
64
79
  }
80
+ logInfo('bus.subscribe.timeout', { channel, timeoutMs, fromOffset });
65
81
  resolve(null);
66
82
  }, timeoutMs);
67
83
  const wrappedResolve = (msg) => {
68
84
  clearTimeout(timer);
85
+ logInfo('bus.subscribe.received', {
86
+ channel,
87
+ fromOffset,
88
+ from: msg.from,
89
+ });
69
90
  resolve(msg);
70
91
  };
71
92
  if (!this.waiters.has(channel))
@@ -20,11 +20,13 @@ export declare class QueryEngine {
20
20
  private session;
21
21
  private transcript;
22
22
  private workerBridge;
23
+ private memoryUpdateQueue;
23
24
  constructor();
24
25
  /** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
25
26
  registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
26
27
  private createSession;
27
28
  private ensureSessionDir;
29
+ private createService;
28
30
  /** 处理用户输入(在独立 Worker 线程中执行) */
29
31
  handleQuery(userInput: string, callbacks: EngineCallbacks): Promise<void>;
30
32
  /** 终止当前任务(通知 Worker 中断) */
@@ -37,6 +39,7 @@ export declare class QueryEngine {
37
39
  switchAgent(agentName: string): void;
38
40
  /** 保存会话到文件 */
39
41
  private saveSession;
42
+ private schedulePersistentMemoryUpdate;
40
43
  /** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
41
44
  static listSessions(): {
42
45
  id: string;
@@ -9,32 +9,28 @@ import { SESSIONS_DIR } from '../config/constants.js';
9
9
  import { setActiveAgent } from '../config/agentState.js';
10
10
  import { clearAuthorizations } from './safeguard.js';
11
11
  import { agentUIBus } from './AgentRegistry.js';
12
+ import { logError, logInfo, logWarn } from './logger.js';
13
+ import { updateUserProfileFromInput } from '../services/userProfile.js';
14
+ import { updatePersistentMemoryFromConversation } from '../services/persistentMemory.js';
12
15
  export class QueryEngine {
13
16
  service;
14
17
  session;
15
18
  transcript = [];
16
19
  workerBridge = new WorkerBridge();
20
+ memoryUpdateQueue = Promise.resolve();
17
21
  constructor() {
18
- // 尝试从配置文件加载 LLM,失败则回退 MockService
19
- const config = loadConfig();
20
- const activeModel = getActiveModel(config);
21
- if (activeModel) {
22
- try {
23
- this.service = new LLMServiceImpl();
24
- }
25
- catch {
26
- this.service = new MockService();
27
- }
28
- }
29
- else {
30
- this.service = new MockService();
31
- }
22
+ this.service = this.createService();
32
23
  this.session = this.createSession();
33
24
  this.ensureSessionDir();
25
+ logInfo('engine.created', {
26
+ sessionId: this.session.id,
27
+ service: this.service.constructor.name,
28
+ });
34
29
  }
35
30
  /** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
36
31
  registerUIBus(onMessage, onUpdateMessage) {
37
32
  agentUIBus.register(onMessage, onUpdateMessage);
33
+ logInfo('engine.ui_bus_registered', { sessionId: this.session.id });
38
34
  }
39
35
  createSession() {
40
36
  return {
@@ -51,8 +47,27 @@ export class QueryEngine {
51
47
  fs.mkdirSync(SESSIONS_DIR, { recursive: true });
52
48
  }
53
49
  }
50
+ createService() {
51
+ const config = loadConfig();
52
+ const activeModel = getActiveModel(config);
53
+ if (activeModel) {
54
+ try {
55
+ return new LLMServiceImpl();
56
+ }
57
+ catch {
58
+ return new MockService();
59
+ }
60
+ }
61
+ return new MockService();
62
+ }
54
63
  /** 处理用户输入(在独立 Worker 线程中执行) */
55
64
  async handleQuery(userInput, callbacks) {
65
+ const previousTranscriptLength = this.transcript.length;
66
+ logInfo('query.received', {
67
+ sessionId: this.session.id,
68
+ inputLength: userInput.length,
69
+ preview: userInput.slice(0, 200),
70
+ });
56
71
  const userMsg = {
57
72
  id: uuid(),
58
73
  type: 'user',
@@ -62,6 +77,12 @@ export class QueryEngine {
62
77
  };
63
78
  callbacks.onMessage(userMsg);
64
79
  this.session.messages.push(userMsg);
80
+ if (this.transcript.length === 0) {
81
+ const updated = await updateUserProfileFromInput(userInput);
82
+ if (updated) {
83
+ this.service = this.createService();
84
+ }
85
+ }
65
86
  // 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
66
87
  const bridgeCallbacks = {
67
88
  onMessage: (msg) => {
@@ -87,8 +108,16 @@ export class QueryEngine {
87
108
  };
88
109
  try {
89
110
  this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
111
+ const recentTranscript = this.transcript.slice(previousTranscriptLength);
112
+ this.schedulePersistentMemoryUpdate(userInput, recentTranscript);
113
+ logInfo('query.completed', {
114
+ sessionId: this.session.id,
115
+ transcriptLength: this.transcript.length,
116
+ totalTokens: this.session.totalTokens,
117
+ });
90
118
  }
91
119
  catch (err) {
120
+ logError('query.failed', err, { sessionId: this.session.id });
92
121
  const errMsg = {
93
122
  id: uuid(),
94
123
  type: 'error',
@@ -104,38 +133,39 @@ export class QueryEngine {
104
133
  }
105
134
  /** 终止当前任务(通知 Worker 中断) */
106
135
  abort() {
136
+ logWarn('query.abort_requested', { sessionId: this.session.id });
107
137
  this.workerBridge.abort();
108
138
  }
109
139
  /** 重置会话 */
110
140
  reset() {
141
+ logInfo('session.reset', { sessionId: this.session.id });
111
142
  this.saveSession();
112
143
  this.session = this.createSession();
113
144
  this.transcript = [];
145
+ this.memoryUpdateQueue = Promise.resolve();
114
146
  clearAuthorizations();
115
147
  }
116
148
  /**
117
149
  * 切换智能体:持久化选择 + 重建 LLM service + 重置会话
118
150
  */
119
151
  switchAgent(agentName) {
152
+ logInfo('agent.switch.start', {
153
+ fromSessionId: this.session.id,
154
+ agentName,
155
+ });
120
156
  setActiveAgent(agentName);
121
157
  // 重建 LLM service 以加载新 agent 的 system prompt
122
- const config = loadConfig();
123
- const activeModel = getActiveModel(config);
124
- if (activeModel) {
125
- try {
126
- this.service = new LLMServiceImpl();
127
- }
128
- catch {
129
- this.service = new MockService();
130
- }
131
- }
132
- else {
133
- this.service = new MockService();
134
- }
158
+ this.service = this.createService();
135
159
  // 重置会话上下文
136
160
  this.saveSession();
137
161
  this.session = this.createSession();
138
162
  this.transcript = [];
163
+ this.memoryUpdateQueue = Promise.resolve();
164
+ logInfo('agent.switch.completed', {
165
+ sessionId: this.session.id,
166
+ agentName,
167
+ service: this.service.constructor.name,
168
+ });
139
169
  }
140
170
  /** 保存会话到文件 */
141
171
  saveSession() {
@@ -149,8 +179,29 @@ export class QueryEngine {
149
179
  }
150
180
  const filePath = path.join(SESSIONS_DIR, `${this.session.id}.json`);
151
181
  fs.writeFileSync(filePath, JSON.stringify(this.session, null, 2), 'utf-8');
182
+ logInfo('session.saved', {
183
+ sessionId: this.session.id,
184
+ filePath,
185
+ messageCount: this.session.messages.length,
186
+ });
152
187
  }
153
- catch { /* 静默失败 */ }
188
+ catch (error) {
189
+ logError('session.save_failed', error, { sessionId: this.session.id });
190
+ }
191
+ }
192
+ schedulePersistentMemoryUpdate(userInput, recentTranscript) {
193
+ if (!userInput.trim() || recentTranscript.length === 0)
194
+ return;
195
+ const sessionId = this.session.id;
196
+ this.memoryUpdateQueue = this.memoryUpdateQueue
197
+ .catch(() => { })
198
+ .then(async () => {
199
+ await updatePersistentMemoryFromConversation({
200
+ sessionId,
201
+ userInput,
202
+ recentTranscript,
203
+ });
204
+ });
154
205
  }
155
206
  /** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
156
207
  static listSessions() {
@@ -221,9 +272,15 @@ export class QueryEngine {
221
272
  });
222
273
  }
223
274
  }
275
+ logInfo('session.loaded', {
276
+ sessionId,
277
+ messageCount: cleanedMessages.length,
278
+ transcriptLength: this.transcript.length,
279
+ });
224
280
  return { session: this.session, messages: cleanedMessages };
225
281
  }
226
- catch {
282
+ catch (error) {
283
+ logError('session.load_failed', error, { sessionId });
227
284
  return null;
228
285
  }
229
286
  }
@@ -250,9 +307,14 @@ export class QueryEngine {
250
307
  }
251
308
  catch { /* 跳过删除失败的文件 */ }
252
309
  }
310
+ logInfo('session.clear_others', {
311
+ sessionId: this.session.id,
312
+ removedCount: count,
313
+ });
253
314
  return count;
254
315
  }
255
- catch {
316
+ catch (error) {
317
+ logError('session.clear_others_failed', error, { sessionId: this.session.id });
256
318
  return 0;
257
319
  }
258
320
  }
@@ -12,6 +12,7 @@ import { Worker } from 'worker_threads';
12
12
  import { fileURLToPath } from 'url';
13
13
  import path from 'path';
14
14
  import { agentMessageBus } from './AgentMessageBus.js';
15
+ import { logError, logInfo, logWarn } from './logger.js';
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
17
18
  /** 创建 SubAgent Worker(兼容 tsx 开发模式与编译后 .js) */
@@ -42,6 +43,10 @@ export class SubAgentBridge {
42
43
  return new Promise((resolve, reject) => {
43
44
  const worker = createSubAgentWorker();
44
45
  this.worker = worker;
46
+ logInfo('subagent_bridge.run.start', {
47
+ taskId: task.taskId,
48
+ allowedTools: task.allowedTools,
49
+ });
45
50
  // 收集 SubAgent 产生的所有消息,用于汇总结果
46
51
  const collectedMessages = [];
47
52
  let finalTranscript = [];
@@ -125,6 +130,11 @@ export class SubAgentBridge {
125
130
  finalTranscript = msg.transcript;
126
131
  this.worker = null;
127
132
  worker.terminate();
133
+ logInfo('subagent_bridge.run.done', {
134
+ taskId: msg.taskId,
135
+ transcriptLength: msg.transcript.length,
136
+ messageCount: collectedMessages.length,
137
+ });
128
138
  // 从 transcript 中提取最终输出文本
129
139
  resolve({
130
140
  taskId: msg.taskId,
@@ -137,6 +147,10 @@ export class SubAgentBridge {
137
147
  case 'error':
138
148
  this.worker = null;
139
149
  worker.terminate();
150
+ logError('subagent_bridge.run.failed', msg.message, {
151
+ taskId: msg.taskId,
152
+ messageCount: collectedMessages.length,
153
+ });
140
154
  resolve({
141
155
  taskId: msg.taskId,
142
156
  status: 'error',
@@ -150,12 +164,14 @@ export class SubAgentBridge {
150
164
  });
151
165
  worker.on('error', (err) => {
152
166
  this.worker = null;
167
+ logError('subagent_bridge.worker_error', err, { taskId: task.taskId });
153
168
  reject(err);
154
169
  });
155
170
  worker.on('exit', (code) => {
156
171
  if (this.worker) {
157
172
  // Worker 退出但未发送 done/error,说明异常终止
158
173
  this.worker = null;
174
+ logError('subagent_bridge.worker_exit_abnormal', undefined, { taskId: task.taskId, code });
159
175
  reject(new Error(`SubAgent Worker 意外退出,code=${code}`));
160
176
  }
161
177
  });
@@ -166,6 +182,7 @@ export class SubAgentBridge {
166
182
  /** 中断 SubAgent 执行 */
167
183
  abort() {
168
184
  if (this.worker) {
185
+ logWarn('subagent_bridge.abort_forwarded');
169
186
  const msg = { type: 'abort' };
170
187
  this.worker.postMessage(msg);
171
188
  }
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'url';
7
7
  import path from 'path';
8
8
  import { agentMessageBus } from './AgentMessageBus.js';
9
9
  import { spawnSubAgentInMainThread } from '../tools/spawnAgent.js';
10
+ import { logError, logInfo, logWarn } from './logger.js';
10
11
  // 兼容 ESM __dirname
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = path.dirname(__filename);
@@ -41,6 +42,10 @@ export class WorkerBridge {
41
42
  const workerTsPath = path.join(__dirname, 'queryWorker.ts');
42
43
  const worker = createWorker(workerTsPath);
43
44
  this.worker = worker;
45
+ logInfo('worker_bridge.run.start', {
46
+ inputLength: userInput.length,
47
+ transcriptLength: transcript.length,
48
+ });
44
49
  worker.on('message', async (msg) => {
45
50
  switch (msg.type) {
46
51
  case 'message':
@@ -143,22 +148,28 @@ export class WorkerBridge {
143
148
  case 'done':
144
149
  this.worker = null;
145
150
  worker.terminate();
151
+ logInfo('worker_bridge.run.done', {
152
+ transcriptLength: msg.transcript.length,
153
+ });
146
154
  resolve(msg.transcript);
147
155
  break;
148
156
  case 'error':
149
157
  this.worker = null;
150
158
  worker.terminate();
159
+ logError('worker_bridge.run.error', msg.message);
151
160
  reject(new Error(msg.message));
152
161
  break;
153
162
  }
154
163
  });
155
164
  worker.on('error', (err) => {
156
165
  this.worker = null;
166
+ logError('worker_bridge.worker_error', err);
157
167
  reject(err);
158
168
  });
159
169
  worker.on('exit', (code) => {
160
170
  if (code !== 0 && this.worker) {
161
171
  this.worker = null;
172
+ logError('worker_bridge.worker_exit_abnormal', undefined, { code });
162
173
  reject(new Error(`Worker 异常退出,code=${code}`));
163
174
  }
164
175
  });
@@ -170,6 +181,7 @@ export class WorkerBridge {
170
181
  /** 向 Worker 发送中断信号 */
171
182
  abort() {
172
183
  if (this.worker) {
184
+ logWarn('worker_bridge.abort_forwarded');
173
185
  const msg = { type: 'abort' };
174
186
  this.worker.postMessage(msg);
175
187
  }
@@ -0,0 +1,8 @@
1
+ export type LogLevel = 'INFO' | 'WARN' | 'ERROR';
2
+ export interface LogPayload {
3
+ [key: string]: unknown;
4
+ }
5
+ export declare function ensureLoggerReady(): string;
6
+ export declare function logInfo(event: string, payload?: LogPayload): void;
7
+ export declare function logWarn(event: string, payload?: LogPayload): void;
8
+ export declare function logError(event: string, error?: unknown, payload?: LogPayload): void;
@@ -0,0 +1,63 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { isMainThread, threadId } from 'worker_threads';
5
+ function resolveLogsDir() {
6
+ return path.join(os.homedir(), '.jarvis', 'logs');
7
+ }
8
+ function ensureLogsDir() {
9
+ const logsDir = resolveLogsDir();
10
+ if (!fs.existsSync(logsDir)) {
11
+ fs.mkdirSync(logsDir, { recursive: true });
12
+ }
13
+ return logsDir;
14
+ }
15
+ function getLogFilePath(date = new Date()) {
16
+ const logsDir = ensureLogsDir();
17
+ const day = date.toISOString().slice(0, 10);
18
+ return path.join(logsDir, `${day}.log`);
19
+ }
20
+ function normalizeError(error) {
21
+ if (error instanceof Error) {
22
+ return {
23
+ name: error.name,
24
+ message: error.message,
25
+ stack: error.stack,
26
+ };
27
+ }
28
+ return { message: String(error) };
29
+ }
30
+ function writeLog(level, event, payload = {}) {
31
+ try {
32
+ const now = new Date();
33
+ const line = JSON.stringify({
34
+ timestamp: now.toISOString(),
35
+ level,
36
+ event,
37
+ pid: process.pid,
38
+ threadId,
39
+ workerType: isMainThread ? 'main' : 'worker',
40
+ cwd: process.cwd(),
41
+ ...payload,
42
+ });
43
+ fs.appendFileSync(getLogFilePath(now), `${line}\n`, 'utf-8');
44
+ }
45
+ catch {
46
+ // 日志系统自身不能影响主流程
47
+ }
48
+ }
49
+ export function ensureLoggerReady() {
50
+ return ensureLogsDir();
51
+ }
52
+ export function logInfo(event, payload) {
53
+ writeLog('INFO', event, payload);
54
+ }
55
+ export function logWarn(event, payload) {
56
+ writeLog('WARN', event, payload);
57
+ }
58
+ export function logError(event, error, payload = {}) {
59
+ writeLog('ERROR', event, {
60
+ ...payload,
61
+ ...(error !== undefined ? { error: normalizeError(error) } : {}),
62
+ });
63
+ }