@code4bug/jarvis-agent 1.1.4 → 1.1.5

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,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))
@@ -25,6 +25,7 @@ export declare class QueryEngine {
25
25
  registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
26
26
  private createSession;
27
27
  private ensureSessionDir;
28
+ private createService;
28
29
  /** 处理用户输入(在独立 Worker 线程中执行) */
29
30
  handleQuery(userInput: string, callbacks: EngineCallbacks): Promise<void>;
30
31
  /** 终止当前任务(通知 Worker 中断) */
@@ -9,32 +9,26 @@ 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';
12
14
  export class QueryEngine {
13
15
  service;
14
16
  session;
15
17
  transcript = [];
16
18
  workerBridge = new WorkerBridge();
17
19
  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
- }
20
+ this.service = this.createService();
32
21
  this.session = this.createSession();
33
22
  this.ensureSessionDir();
23
+ logInfo('engine.created', {
24
+ sessionId: this.session.id,
25
+ service: this.service.constructor.name,
26
+ });
34
27
  }
35
28
  /** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
36
29
  registerUIBus(onMessage, onUpdateMessage) {
37
30
  agentUIBus.register(onMessage, onUpdateMessage);
31
+ logInfo('engine.ui_bus_registered', { sessionId: this.session.id });
38
32
  }
39
33
  createSession() {
40
34
  return {
@@ -51,8 +45,26 @@ export class QueryEngine {
51
45
  fs.mkdirSync(SESSIONS_DIR, { recursive: true });
52
46
  }
53
47
  }
48
+ createService() {
49
+ const config = loadConfig();
50
+ const activeModel = getActiveModel(config);
51
+ if (activeModel) {
52
+ try {
53
+ return new LLMServiceImpl();
54
+ }
55
+ catch {
56
+ return new MockService();
57
+ }
58
+ }
59
+ return new MockService();
60
+ }
54
61
  /** 处理用户输入(在独立 Worker 线程中执行) */
55
62
  async handleQuery(userInput, callbacks) {
63
+ logInfo('query.received', {
64
+ sessionId: this.session.id,
65
+ inputLength: userInput.length,
66
+ preview: userInput.slice(0, 200),
67
+ });
56
68
  const userMsg = {
57
69
  id: uuid(),
58
70
  type: 'user',
@@ -62,6 +74,12 @@ export class QueryEngine {
62
74
  };
63
75
  callbacks.onMessage(userMsg);
64
76
  this.session.messages.push(userMsg);
77
+ if (this.transcript.length === 0) {
78
+ const updated = await updateUserProfileFromInput(userInput);
79
+ if (updated) {
80
+ this.service = this.createService();
81
+ }
82
+ }
65
83
  // 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
66
84
  const bridgeCallbacks = {
67
85
  onMessage: (msg) => {
@@ -87,8 +105,14 @@ export class QueryEngine {
87
105
  };
88
106
  try {
89
107
  this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
108
+ logInfo('query.completed', {
109
+ sessionId: this.session.id,
110
+ transcriptLength: this.transcript.length,
111
+ totalTokens: this.session.totalTokens,
112
+ });
90
113
  }
91
114
  catch (err) {
115
+ logError('query.failed', err, { sessionId: this.session.id });
92
116
  const errMsg = {
93
117
  id: uuid(),
94
118
  type: 'error',
@@ -104,10 +128,12 @@ export class QueryEngine {
104
128
  }
105
129
  /** 终止当前任务(通知 Worker 中断) */
106
130
  abort() {
131
+ logWarn('query.abort_requested', { sessionId: this.session.id });
107
132
  this.workerBridge.abort();
108
133
  }
109
134
  /** 重置会话 */
110
135
  reset() {
136
+ logInfo('session.reset', { sessionId: this.session.id });
111
137
  this.saveSession();
112
138
  this.session = this.createSession();
113
139
  this.transcript = [];
@@ -117,25 +143,22 @@ export class QueryEngine {
117
143
  * 切换智能体:持久化选择 + 重建 LLM service + 重置会话
118
144
  */
119
145
  switchAgent(agentName) {
146
+ logInfo('agent.switch.start', {
147
+ fromSessionId: this.session.id,
148
+ agentName,
149
+ });
120
150
  setActiveAgent(agentName);
121
151
  // 重建 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
- }
152
+ this.service = this.createService();
135
153
  // 重置会话上下文
136
154
  this.saveSession();
137
155
  this.session = this.createSession();
138
156
  this.transcript = [];
157
+ logInfo('agent.switch.completed', {
158
+ sessionId: this.session.id,
159
+ agentName,
160
+ service: this.service.constructor.name,
161
+ });
139
162
  }
140
163
  /** 保存会话到文件 */
141
164
  saveSession() {
@@ -149,8 +172,15 @@ export class QueryEngine {
149
172
  }
150
173
  const filePath = path.join(SESSIONS_DIR, `${this.session.id}.json`);
151
174
  fs.writeFileSync(filePath, JSON.stringify(this.session, null, 2), 'utf-8');
175
+ logInfo('session.saved', {
176
+ sessionId: this.session.id,
177
+ filePath,
178
+ messageCount: this.session.messages.length,
179
+ });
180
+ }
181
+ catch (error) {
182
+ logError('session.save_failed', error, { sessionId: this.session.id });
152
183
  }
153
- catch { /* 静默失败 */ }
154
184
  }
155
185
  /** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
156
186
  static listSessions() {
@@ -221,9 +251,15 @@ export class QueryEngine {
221
251
  });
222
252
  }
223
253
  }
254
+ logInfo('session.loaded', {
255
+ sessionId,
256
+ messageCount: cleanedMessages.length,
257
+ transcriptLength: this.transcript.length,
258
+ });
224
259
  return { session: this.session, messages: cleanedMessages };
225
260
  }
226
- catch {
261
+ catch (error) {
262
+ logError('session.load_failed', error, { sessionId });
227
263
  return null;
228
264
  }
229
265
  }
@@ -250,9 +286,14 @@ export class QueryEngine {
250
286
  }
251
287
  catch { /* 跳过删除失败的文件 */ }
252
288
  }
289
+ logInfo('session.clear_others', {
290
+ sessionId: this.session.id,
291
+ removedCount: count,
292
+ });
253
293
  return count;
254
294
  }
255
- catch {
295
+ catch (error) {
296
+ logError('session.clear_others_failed', error, { sessionId: this.session.id });
256
297
  return 0;
257
298
  }
258
299
  }
@@ -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
+ }
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import { findToolMerged as findTool } from '../tools/index.js';
6
6
  import { MAX_ITERATIONS, CONTEXT_TOKEN_LIMIT } from '../config/constants.js';
7
7
  import { sanitizeOutput, validateCommand, authorizeCommand, authorizeRule } from './safeguard.js';
8
+ import { logError, logInfo, logWarn } from './logger.js';
8
9
  // 兼容 ESM __dirname
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
@@ -74,6 +75,10 @@ function compressTranscript(transcript) {
74
75
  * 单轮 Agentic Loop:推理 → 工具调用 → 循环
75
76
  */
76
77
  export async function executeQuery(userInput, transcript, _tools, service, callbacks, abortSignal) {
78
+ logInfo('agent_loop.start', {
79
+ inputLength: userInput.length,
80
+ initialTranscriptLength: transcript.length,
81
+ });
77
82
  const localTranscript = [...transcript];
78
83
  localTranscript.push({ role: 'user', content: userInput });
79
84
  const loopState = {
@@ -85,8 +90,19 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
85
90
  callbacks.onLoopStateChange({ ...loopState });
86
91
  while (loopState.iteration < MAX_ITERATIONS && !abortSignal.aborted) {
87
92
  loopState.iteration++;
93
+ logInfo('agent_loop.iteration.start', {
94
+ iteration: loopState.iteration,
95
+ transcriptLength: localTranscript.length,
96
+ });
88
97
  callbacks.onLoopStateChange({ ...loopState });
89
98
  const result = await runOneIteration(compressTranscript(localTranscript), _tools, service, callbacks, abortSignal);
99
+ logInfo('agent_loop.iteration.result', {
100
+ iteration: loopState.iteration,
101
+ textLength: result.text.length,
102
+ toolCallCount: result.toolCalls.length,
103
+ duration: result.duration,
104
+ tokenCount: result.tokenCount,
105
+ });
90
106
  // 构建 assistant transcript 块
91
107
  const assistantBlocks = [];
92
108
  if (result.text)
@@ -102,6 +118,10 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
102
118
  break;
103
119
  // 中断发生在推理阶段
104
120
  if (abortSignal.aborted) {
121
+ logWarn('agent_loop.aborted_before_tool_execution', {
122
+ iteration: loopState.iteration,
123
+ toolCallCount: result.toolCalls.length,
124
+ });
105
125
  for (const tc of result.toolCalls) {
106
126
  const skippedResult = `[用户中断] 工具 ${tc.name} 未执行(用户按下 ESC 中断)`;
107
127
  localTranscript.push({ role: 'tool_result', toolUseId: tc.id, content: skippedResult });
@@ -145,11 +165,20 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
145
165
  loopState.aborted = abortSignal.aborted;
146
166
  callbacks.onLoopStateChange({ ...loopState });
147
167
  if (abortSignal.aborted) {
168
+ logWarn('agent_loop.aborted', {
169
+ finalIteration: loopState.iteration,
170
+ transcriptLength: localTranscript.length,
171
+ });
148
172
  localTranscript.push({
149
173
  role: 'user',
150
174
  content: '[系统提示] 用户中断了上一轮回复(按下 ESC)。上一条助手消息可能不完整,请在后续回复中注意这一点。',
151
175
  });
152
176
  }
177
+ logInfo('agent_loop.done', {
178
+ finalIteration: loopState.iteration,
179
+ aborted: abortSignal.aborted,
180
+ transcriptLength: localTranscript.length,
181
+ });
153
182
  return localTranscript;
154
183
  }
155
184
  /** 执行一次 LLM 调用 */
@@ -161,6 +190,10 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
161
190
  let tokenCount = 0;
162
191
  let firstTokenTime = null;
163
192
  const thinkingId = uuid();
193
+ logInfo('llm.iteration.requested', {
194
+ transcriptLength: transcript.length,
195
+ toolCount: tools.length,
196
+ });
164
197
  callbacks.onMessage({
165
198
  id: thinkingId,
166
199
  type: 'thinking',
@@ -268,6 +301,15 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
268
301
  });
269
302
  }
270
303
  const effectiveText = accumulatedText || (accumulatedThinking && toolCalls.length === 0 ? accumulatedThinking : '');
304
+ logInfo('llm.iteration.completed', {
305
+ duration,
306
+ tokenCount,
307
+ firstTokenLatency,
308
+ tokensPerSecond,
309
+ textLength: effectiveText.length,
310
+ thinkingLength: accumulatedThinking.length,
311
+ toolCallCount: toolCalls.length,
312
+ });
271
313
  return { text: effectiveText, toolCalls, duration, tokenCount, firstTokenLatency, tokensPerSecond };
272
314
  }
273
315
  // ===== 并行执行判断 =====
@@ -358,17 +400,27 @@ export async function runToolDirect(tc, abortSignal) {
358
400
  if (!tool)
359
401
  return `错误: 未知工具 ${tc.name}`;
360
402
  try {
403
+ logInfo('tool.direct.start', { toolName: tc.name, toolArgs: tc.input });
361
404
  const result = await tool.execute(tc.input, abortSignal);
362
405
  const { sanitizeOutput } = await import('./safeguard.js');
406
+ logInfo('tool.direct.done', {
407
+ toolName: tc.name,
408
+ resultLength: String(result).length,
409
+ });
363
410
  return sanitizeOutput(result);
364
411
  }
365
412
  catch (err) {
413
+ logError('tool.direct.failed', err, { toolName: tc.name, toolArgs: tc.input });
366
414
  return `错误: ${err.message || '工具执行失败'}`;
367
415
  }
368
416
  }
369
417
  /** 并行执行多个工具,每个工具在独立 Worker 线程中运行,实时更新 UI */
370
418
  async function executeToolsInParallel(calls, callbacks, abortSignal) {
371
419
  const groupId = uuid();
420
+ logInfo('tool.parallel_group.start', {
421
+ groupId,
422
+ toolNames: calls.map((call) => call.name),
423
+ });
372
424
  // 为每个工具预先创建 pending 消息节点(TUI 立即渲染占位)
373
425
  const msgIds = calls.map((tc) => {
374
426
  const msgId = uuid();
@@ -433,11 +485,18 @@ async function executeToolsInParallel(calls, callbacks, abortSignal) {
433
485
  }
434
486
  catch (err) {
435
487
  const errMsg = err.message || '工具执行失败';
488
+ logError('tool.parallel.failed', err, { groupId, toolName: tc.name, toolArgs: tc.input });
436
489
  callbacks.onUpdateMessage(msgId, { status: 'error', content: errMsg, toolResult: errMsg });
437
490
  return { tc, content: `错误: ${errMsg}`, isError: false };
438
491
  }
439
492
  });
440
- return Promise.all(tasks);
493
+ const results = await Promise.all(tasks);
494
+ logInfo('tool.parallel_group.done', {
495
+ groupId,
496
+ toolNames: calls.map((call) => call.name),
497
+ errorCount: results.filter((item) => item.isError).length,
498
+ });
499
+ return results;
441
500
  }
442
501
  /** 执行工具并返回结果 */
443
502
  async function executeTool(tc, callbacks, abortSignal) {
@@ -462,6 +521,11 @@ async function executeTool(tc, callbacks, abortSignal) {
462
521
  toolName: tc.name,
463
522
  toolArgs: tc.input,
464
523
  });
524
+ logInfo('tool.execute.start', {
525
+ toolName: tc.name,
526
+ toolArgs: tc.input,
527
+ toolExecId,
528
+ });
465
529
  // ===== 安全围栏:Bash 命令拦截 + 交互式确认 =====
466
530
  if (tc.name === 'Bash' && tc.input.command) {
467
531
  const command = tc.input.command;
@@ -470,6 +534,7 @@ async function executeTool(tc, callbacks, abortSignal) {
470
534
  if (!check.canOverride) {
471
535
  // critical 级别:直接禁止
472
536
  const errMsg = `${check.reason}\n🚫 该命令已被永久禁止,无法通过授权绕过。\n命令: ${command}`;
537
+ logWarn('tool.execute.blocked', { toolName: tc.name, command, reason: check.reason });
473
538
  callbacks.onUpdateMessage(toolExecId, { status: 'error', content: errMsg, toolResult: errMsg });
474
539
  return { content: `错误: ${errMsg}`, isError: true };
475
540
  }
@@ -480,6 +545,7 @@ async function executeTool(tc, callbacks, abortSignal) {
480
545
  const userChoice = await callbacks.onConfirmDangerousCommand(command, reason, ruleName);
481
546
  if (userChoice === 'cancel') {
482
547
  const cancelMsg = `⛔ 用户取消执行危险命令: ${command}`;
548
+ logWarn('tool.execute.cancelled_by_user', { toolName: tc.name, command, reason });
483
549
  callbacks.onUpdateMessage(toolExecId, { status: 'error', content: cancelMsg, toolResult: cancelMsg });
484
550
  return { content: cancelMsg, isError: true };
485
551
  }
@@ -502,6 +568,7 @@ async function executeTool(tc, callbacks, abortSignal) {
502
568
  const tool = findTool(tc.name);
503
569
  if (!tool) {
504
570
  const errMsg = `未知工具: ${tc.name}`;
571
+ logWarn('tool.execute.unknown', { toolName: tc.name });
505
572
  callbacks.onUpdateMessage(toolExecId, { status: 'error', content: errMsg });
506
573
  return { content: errMsg, isError: true };
507
574
  }
@@ -539,10 +606,22 @@ async function executeTool(tc, callbacks, abortSignal) {
539
606
  duration: Date.now() - start,
540
607
  ...(wasAborted ? { abortHint: '命令已中断(ESC)' } : {}),
541
608
  });
609
+ logInfo('tool.execute.done', {
610
+ toolName: tc.name,
611
+ toolExecId,
612
+ duration: Date.now() - start,
613
+ aborted: Boolean(wasAborted),
614
+ resultLength: safeResult.length,
615
+ });
542
616
  return { content: safeResult, isError: false };
543
617
  }
544
618
  catch (err) {
545
619
  const errMsg = err.message || '工具执行失败';
620
+ logError('tool.execute.failed', err, {
621
+ toolName: tc.name,
622
+ toolArgs: tc.input,
623
+ toolExecId,
624
+ });
546
625
  callbacks.onUpdateMessage(toolExecId, {
547
626
  status: 'error',
548
627
  content: errMsg,
@@ -10,8 +10,10 @@ import { MockService } from '../services/api/mock.js';
10
10
  import { loadConfig, getActiveModel } from '../config/loader.js';
11
11
  import { pendingBusRequests } from './workerBusProxy.js';
12
12
  import { pendingSpawnRequests } from './spawnRegistry.js';
13
+ import { logError, logInfo, logWarn } from './logger.js';
13
14
  if (!parentPort)
14
15
  throw new Error('queryWorker must run inside worker_threads');
16
+ logInfo('query_worker.ready');
15
17
  // ===== 初始化 LLM 服务 =====
16
18
  const config = loadConfig();
17
19
  const activeModel = getActiveModel(config);
@@ -30,6 +32,7 @@ const pendingConfirms = new Map();
30
32
  parentPort.on('message', async (msg) => {
31
33
  if (msg.type === 'abort') {
32
34
  abortSignal.aborted = true;
35
+ logWarn('query_worker.abort_received');
33
36
  return;
34
37
  }
35
38
  if (msg.type === 'danger_confirm_result') {
@@ -73,6 +76,10 @@ parentPort.on('message', async (msg) => {
73
76
  }
74
77
  if (msg.type === 'run') {
75
78
  abortSignal.aborted = false;
79
+ logInfo('query_worker.run.start', {
80
+ inputLength: msg.userInput.length,
81
+ transcriptLength: msg.transcript.length,
82
+ });
76
83
  const send = (out) => parentPort.postMessage(out);
77
84
  const callbacks = {
78
85
  onMessage: (m) => send({ type: 'message', msg: m }),
@@ -92,9 +99,13 @@ parentPort.on('message', async (msg) => {
92
99
  };
93
100
  try {
94
101
  const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal);
102
+ logInfo('query_worker.run.done', {
103
+ transcriptLength: newTranscript.length,
104
+ });
95
105
  send({ type: 'done', transcript: newTranscript });
96
106
  }
97
107
  catch (err) {
108
+ logError('query_worker.run.failed', err);
98
109
  send({ type: 'error', message: err.message ?? '未知错误' });
99
110
  }
100
111
  }
@@ -15,8 +15,10 @@ import { LLMServiceImpl, fromModelConfig } from '../services/api/llm.js';
15
15
  import { MockService } from '../services/api/mock.js';
16
16
  import { loadConfig, getActiveModel } from '../config/loader.js';
17
17
  import { pendingBusRequests } from './workerBusProxy.js';
18
+ import { logError, logInfo, logWarn } from './logger.js';
18
19
  if (!parentPort)
19
20
  throw new Error('subAgentWorker must run inside worker_threads');
21
+ logInfo('subagent_worker.ready');
20
22
  // ===== 初始化 LLM 服务 =====
21
23
  const config = loadConfig();
22
24
  const activeModel = getActiveModel(config);
@@ -43,6 +45,7 @@ const pendingConfirms = new Map();
43
45
  parentPort.on('message', async (msg) => {
44
46
  if (msg.type === 'abort') {
45
47
  abortSignal.aborted = true;
48
+ logWarn('subagent_worker.abort_received');
46
49
  return;
47
50
  }
48
51
  if (msg.type === 'danger_confirm_result') {
@@ -79,6 +82,12 @@ parentPort.on('message', async (msg) => {
79
82
  abortSignal.aborted = false;
80
83
  const { task } = msg;
81
84
  const { taskId, instruction, allowedTools, contextTranscript, role, systemPrompt } = task;
85
+ logInfo('subagent_worker.run.start', {
86
+ taskId,
87
+ inputLength: instruction.length,
88
+ allowedTools,
89
+ transcriptLength: contextTranscript?.length ?? 0,
90
+ });
82
91
  const send = (out) => parentPort.postMessage(out);
83
92
  const service = buildSubAgentService(role, systemPrompt);
84
93
  const tools = getAllTools().filter((t) => !allowedTools || allowedTools.length === 0 || allowedTools.includes(t.name));
@@ -98,9 +107,14 @@ parentPort.on('message', async (msg) => {
98
107
  };
99
108
  try {
100
109
  const newTranscript = await executeQuery(instruction, contextTranscript ?? [], tools, service, callbacks, abortSignal);
110
+ logInfo('subagent_worker.run.done', {
111
+ taskId,
112
+ transcriptLength: newTranscript.length,
113
+ });
101
114
  send({ type: 'done', taskId, transcript: newTranscript });
102
115
  }
103
116
  catch (err) {
117
+ logError('subagent_worker.run.failed', err, { taskId });
104
118
  send({ type: 'error', taskId, message: err.message ?? '未知错误' });
105
119
  }
106
120
  }
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from 'ink';
3
+ import { logInfo } from './core/logger.js';
3
4
  import REPL from './screens/repl.js';
4
5
  export function startJarvis() {
6
+ logInfo('app.render.start');
5
7
  render(_jsx(REPL, {}), { exitOnCtrlC: false });
6
8
  }
@@ -21,6 +21,7 @@ import { QueryEngine } from '../core/QueryEngine.js';
21
21
  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
+ import { logError, logInfo, logWarn } from '../core/logger.js';
24
25
  export default function REPL() {
25
26
  const { exit } = useApp();
26
27
  const width = useTerminalWidth();
@@ -45,6 +46,7 @@ export default function REPL() {
45
46
  const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, syncTokenDisplay, resetTokens, } = useTokenDisplay();
46
47
  // ===== 新会话逻辑 =====
47
48
  const handleNewSession = useCallback(() => {
49
+ logInfo('ui.new_session');
48
50
  if (engineRef.current) {
49
51
  engineRef.current.reset();
50
52
  sessionRef.current = engineRef.current.getSession();
@@ -74,8 +76,13 @@ export default function REPL() {
74
76
  // 注册持久 UI 回调,供 spawn_agent 后台子 Agent 跨轮次推送消息
75
77
  engineRef.current.registerUIBus((msg) => setMessages((prev) => [...prev, msg]), (id, updates) => setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, ...updates } : m))));
76
78
  generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
79
+ logError('ui.hint.init_failed', err);
77
80
  console.error('[hint] 初始化提示失败:', err);
78
81
  });
82
+ logInfo('ui.repl.mounted');
83
+ return () => {
84
+ logInfo('ui.repl.unmounted');
85
+ };
79
86
  }, []);
80
87
  // 订阅后台 SubAgent 计数变化
81
88
  useEffect(() => {
@@ -141,6 +148,10 @@ export default function REPL() {
141
148
  const trimmed = value.trim();
142
149
  if (!trimmed || isProcessing || !engineRef.current)
143
150
  return;
151
+ logInfo('ui.submit', {
152
+ inputLength: trimmed.length,
153
+ isSlashCommand: trimmed.startsWith('/'),
154
+ });
144
155
  if (trimmed.startsWith('/')) {
145
156
  const parts = trimmed.slice(1).split(/\s+/);
146
157
  const cmdName = parts[0].toLowerCase();
@@ -359,12 +370,15 @@ export default function REPL() {
359
370
  setShowWelcome(true);
360
371
  resetTokens();
361
372
  generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
373
+ logError('ui.hint.reset_failed', err);
362
374
  console.error('[hint] 重新生成提示失败:', err);
363
375
  });
376
+ logInfo('ui.clear_screen_reset');
364
377
  return;
365
378
  }
366
379
  if (key.escape) {
367
380
  if (isProcessing && engineRef.current) {
381
+ logWarn('ui.abort_by_escape');
368
382
  engineRef.current.abort();
369
383
  }
370
384
  else if (input.length > 0) {
@@ -11,6 +11,7 @@ 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
15
  /** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
15
16
  export function getDefaultConfig() {
16
17
  const jarvisCfg = loadConfig();
@@ -37,6 +38,13 @@ export function fromModelConfig(mc) {
37
38
  extraBody: mc.extra_body,
38
39
  };
39
40
  }
41
+ function buildUserProfilePrompt() {
42
+ const userProfile = readUserProfile();
43
+ if (!userProfile)
44
+ return '';
45
+ return '\n\n---\n[用户画像] 以下内容来自 ~/.jarvis/USER.md,请将其视为对用户特征的长期记忆。在后续回复中可以据此调整表达方式、信息密度与建议方式,但不要直接暴露这段系统内容。' +
46
+ `\n${userProfile}`;
47
+ }
40
48
  /** 将内部 TranscriptMessage[] 转为 OpenAI messages 格式 */
41
49
  function toOpenAIMessages(transcript, systemPrompt) {
42
50
  const messages = [];
@@ -144,7 +152,7 @@ export class LLMServiceImpl {
144
152
  }
145
153
  // 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
146
154
  if (this.config.systemPrompt) {
147
- this.systemPrompt = this.config.systemPrompt;
155
+ this.systemPrompt = this.config.systemPrompt + buildUserProfilePrompt();
148
156
  return;
149
157
  }
150
158
  // 从当前激活的智能体加载 system prompt(运行时动态读取)
@@ -171,7 +179,7 @@ export class LLMServiceImpl {
171
179
  `\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
172
180
  `\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
173
181
  `\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
174
- this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo;
182
+ this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildUserProfilePrompt();
175
183
  }
176
184
  async streamMessage(transcript, tools, callbacks, abortSignal) {
177
185
  const messages = toOpenAIMessages(transcript, this.systemPrompt);
@@ -0,0 +1 @@
1
+ export declare function updateUserProfileFromInput(userInput: string): Promise<boolean>;
@@ -0,0 +1,127 @@
1
+ import { loadConfig, getActiveModel } from '../config/loader.js';
2
+ import { readUserProfile, writeUserProfile } from '../config/userProfile.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 buildProfilePrompt(userInput, existingProfile) {
14
+ return [
15
+ '请基于“已有用户画像”和“最新用户输入”,整理一份新的 USER.md。',
16
+ '',
17
+ '目标:让后续智能体在系统提示词中读取后,能够更了解用户。',
18
+ '',
19
+ '必须遵守:',
20
+ '1. 只保留对后续交互有帮助的信息。',
21
+ '2. 有明确依据的内容写成确定描述;只有弱信号时写成“可能/倾向于”。',
22
+ '3. 没有依据时明确写“未知”,不要编造职业、年龄、经历。',
23
+ '4. 输出必须是中文 Markdown,不要输出代码块围栏,不要解释你的推理过程。',
24
+ '5. 画像应简洁稳定,避免复述用户原话。',
25
+ '',
26
+ '请严格按下面结构输出:',
27
+ '# 用户画像',
28
+ '## 基本信息',
29
+ '- 职业:',
30
+ '- 年龄阶段:',
31
+ '- 所在地区:',
32
+ '- 语言偏好:',
33
+ '',
34
+ '## 思维与沟通',
35
+ '- 思维习惯:',
36
+ '- 沟通风格:',
37
+ '- 决策偏好:',
38
+ '',
39
+ '## 能力与背景',
40
+ '- 技术背景:',
41
+ '- 专业领域:',
42
+ '- 熟悉工具:',
43
+ '',
44
+ '## 当前关注点',
45
+ '- 长期目标:',
46
+ '- 近期任务倾向:',
47
+ '- 约束与偏好:',
48
+ '',
49
+ '## 交互建议',
50
+ '- 回答策略:',
51
+ '- 需要避免:',
52
+ '',
53
+ '## 置信说明',
54
+ '- 高置信信息:',
55
+ '- 低置信推断:',
56
+ '- 明显未知:',
57
+ '',
58
+ '---',
59
+ '已有用户画像:',
60
+ existingProfile || '(暂无)',
61
+ '',
62
+ '最新用户输入:',
63
+ userInput,
64
+ ].join('\n');
65
+ }
66
+ export async function updateUserProfileFromInput(userInput) {
67
+ const normalizedInput = userInput.trim();
68
+ if (!normalizedInput)
69
+ return false;
70
+ const config = loadConfig();
71
+ const activeModel = getActiveModel(config);
72
+ if (!activeModel) {
73
+ logWarn('user_profile.skip.no_active_model');
74
+ return false;
75
+ }
76
+ const prompt = buildProfilePrompt(normalizedInput, readUserProfile());
77
+ const body = {
78
+ model: activeModel.model,
79
+ messages: [
80
+ {
81
+ role: 'system',
82
+ content: '你是一个严谨的用户画像整理助手,负责维护 ~/.jarvis/USER.md。输出必须是可直接写入文件的 Markdown 正文。',
83
+ },
84
+ {
85
+ role: 'user',
86
+ content: prompt,
87
+ },
88
+ ],
89
+ max_tokens: Math.min(activeModel.max_tokens ?? 4096, 1200),
90
+ temperature: 0.2,
91
+ stream: false,
92
+ };
93
+ if (activeModel.extra_body) {
94
+ Object.assign(body, activeModel.extra_body);
95
+ }
96
+ try {
97
+ const response = await fetch(activeModel.api_url, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ Authorization: `Bearer ${activeModel.api_key}`,
102
+ },
103
+ body: JSON.stringify(body),
104
+ });
105
+ if (!response.ok) {
106
+ const errorText = await response.text().catch(() => '');
107
+ throw new Error(`API 错误 ${response.status}: ${errorText.slice(0, 300)}`);
108
+ }
109
+ const data = await response.json();
110
+ const content = extractMessageText(data.choices?.[0]?.message?.content).trim();
111
+ if (!content) {
112
+ throw new Error('用户画像生成结果为空');
113
+ }
114
+ writeUserProfile(content);
115
+ logInfo('user_profile.updated', {
116
+ inputLength: normalizedInput.length,
117
+ outputLength: content.length,
118
+ });
119
+ return true;
120
+ }
121
+ catch (error) {
122
+ logError('user_profile.update_failed', error, {
123
+ inputLength: normalizedInput.length,
124
+ });
125
+ return false;
126
+ }
127
+ }
@@ -1,10 +1,12 @@
1
1
  import { exec } from 'child_process';
2
2
  import { sanitizeOutput } from '../core/safeguard.js';
3
+ import { logError, logInfo, logWarn } from '../core/logger.js';
3
4
  /**
4
5
  * 异步执行命令,支持通过 abortSignal 中断子进程
5
6
  */
6
7
  function execAsync(command, options, abortSignal) {
7
8
  return new Promise((resolve, reject) => {
9
+ logInfo('bash.exec.start', { command });
8
10
  const child = exec(command, options, (error, stdout, stderr) => {
9
11
  // 清理轮询
10
12
  if (pollTimer !== null)
@@ -12,6 +14,10 @@ function execAsync(command, options, abortSignal) {
12
14
  // 被中断时直接返回已有输出,不视为错误
13
15
  if (abortSignal?.aborted) {
14
16
  const partial = sanitizeOutput(String(stdout ?? '').trim());
17
+ logWarn('bash.exec.aborted', {
18
+ command,
19
+ stdoutLength: String(stdout ?? '').length,
20
+ });
15
21
  resolve(partial ? `(命令被中断)\n${partial}` : '(命令被中断)');
16
22
  return;
17
23
  }
@@ -25,9 +31,18 @@ function execAsync(command, options, abortSignal) {
25
31
  parts.push(`[exit code] ${error.code}`);
26
32
  if (parts.length === 0)
27
33
  parts.push(error.message);
34
+ logError('bash.exec.failed', error, {
35
+ command,
36
+ stdoutLength: String(stdout ?? '').length,
37
+ stderrLength: String(stderr ?? '').length,
38
+ });
28
39
  reject(new Error(`命令执行失败:\n${parts.join('\n')}`));
29
40
  return;
30
41
  }
42
+ logInfo('bash.exec.done', {
43
+ command,
44
+ stdoutLength: String(stdout ?? '').length,
45
+ });
31
46
  resolve(sanitizeOutput(String(stdout).trim()) || '(命令执行完成,无输出)');
32
47
  });
33
48
  // 轮询 abortSignal,检测到中断时 kill 子进程
@@ -38,6 +53,7 @@ function execAsync(command, options, abortSignal) {
38
53
  if (pollTimer !== null)
39
54
  clearInterval(pollTimer);
40
55
  pollTimer = null;
56
+ logWarn('bash.exec.kill_requested', { command, pid: child.pid });
41
57
  // 先尝试 SIGTERM,给进程优雅退出的机会
42
58
  try {
43
59
  child.kill('SIGTERM');
@@ -15,6 +15,7 @@ import { agentUIBus } from '../core/AgentRegistry.js';
15
15
  import { agentMessageBus } from '../core/AgentMessageBus.js';
16
16
  import { getBus } from '../core/busAccess.js';
17
17
  import { pendingSpawnRequests, incrementActiveAgents, decrementActiveAgents } from '../core/spawnRegistry.js';
18
+ import { logError, logInfo } from '../core/logger.js';
18
19
  /** 运行中的 task_id 集合(主线程侧,防止重复启动) */
19
20
  const runningAgents = new Set();
20
21
  function resolveAgentLabel(taskId, role) {
@@ -31,12 +32,18 @@ function resolveAgentLabel(taskId, role) {
31
32
  */
32
33
  export function spawnSubAgentInMainThread(taskId, instruction, agentLabel, allowedTools) {
33
34
  if (runningAgents.has(taskId)) {
35
+ logInfo('spawn_agent.duplicate', { taskId, agentLabel });
34
36
  return `子 Agent [${taskId}] 已在运行中,请使用 send_to_agent 向其发送消息,或使用不同的 task_id 启动新实例。`;
35
37
  }
36
38
  const replyChannel = `agent-reply:${taskId}`;
37
39
  const bridge = new SubAgentBridge();
38
40
  runningAgents.add(taskId);
39
41
  incrementActiveAgents();
42
+ logInfo('spawn_agent.started', {
43
+ taskId,
44
+ agentLabel,
45
+ allowedTools,
46
+ });
40
47
  bridge.run({ taskId, instruction, allowedTools }, {
41
48
  onMessage: (_tid, msg) => {
42
49
  const tagged = { ...msg, subAgentId: agentLabel };
@@ -51,10 +58,20 @@ export function spawnSubAgentInMainThread(taskId, instruction, agentLabel, allow
51
58
  }).then((result) => {
52
59
  runningAgents.delete(taskId);
53
60
  decrementActiveAgents();
61
+ logInfo('spawn_agent.done', {
62
+ taskId,
63
+ agentLabel,
64
+ status: result.status,
65
+ outputLength: result.output.length,
66
+ });
54
67
  agentMessageBus.publish(taskId, replyChannel, `[AGENT_DONE] ${result.output || '子 Agent 已完成'}`);
55
68
  }).catch((err) => {
56
69
  runningAgents.delete(taskId);
57
70
  decrementActiveAgents();
71
+ logError('spawn_agent.failed', err, {
72
+ taskId,
73
+ agentLabel,
74
+ });
58
75
  agentMessageBus.publish(taskId, replyChannel, `[AGENT_ERROR] ${err.message}`);
59
76
  });
60
77
  const inboxChannel = `agent-inbox:${taskId}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code4bug/jarvis-agent",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "基于 React + TypeScript + Ink 构建的命令行智能体交互界面",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",