claude-coder 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -214
- package/bin/cli.js +155 -155
- package/package.json +55 -55
- package/recipes/_shared/roles/developer.md +11 -11
- package/recipes/_shared/roles/product.md +12 -12
- package/recipes/_shared/roles/tester.md +12 -12
- package/recipes/_shared/test/report-format.md +86 -86
- package/recipes/backend/base.md +27 -27
- package/recipes/backend/components/auth.md +18 -18
- package/recipes/backend/components/crud-api.md +18 -18
- package/recipes/backend/components/file-service.md +15 -15
- package/recipes/backend/manifest.json +20 -20
- package/recipes/backend/test/api-test.md +25 -25
- package/recipes/console/base.md +37 -37
- package/recipes/console/components/modal-form.md +20 -20
- package/recipes/console/components/pagination.md +17 -17
- package/recipes/console/components/search.md +17 -17
- package/recipes/console/components/table-list.md +18 -18
- package/recipes/console/components/tabs.md +14 -14
- package/recipes/console/components/tree.md +15 -15
- package/recipes/console/components/upload.md +15 -15
- package/recipes/console/manifest.json +24 -24
- package/recipes/console/test/crud-e2e.md +47 -47
- package/recipes/h5/base.md +26 -26
- package/recipes/h5/components/animation.md +11 -11
- package/recipes/h5/components/countdown.md +11 -11
- package/recipes/h5/components/share.md +11 -11
- package/recipes/h5/components/swiper.md +11 -11
- package/recipes/h5/manifest.json +21 -21
- package/recipes/h5/test/h5-e2e.md +20 -20
- package/src/commands/auth.js +362 -362
- package/src/commands/setup-modules/helpers.js +100 -100
- package/src/commands/setup-modules/index.js +25 -25
- package/src/commands/setup-modules/mcp.js +115 -115
- package/src/commands/setup-modules/provider.js +260 -260
- package/src/commands/setup-modules/safety.js +47 -47
- package/src/commands/setup-modules/simplify.js +52 -52
- package/src/commands/setup.js +172 -172
- package/src/common/assets.js +245 -245
- package/src/common/config.js +125 -125
- package/src/common/constants.js +55 -55
- package/src/common/indicator.js +260 -260
- package/src/common/interaction.js +170 -170
- package/src/common/logging.js +77 -77
- package/src/common/sdk.js +50 -50
- package/src/common/tasks.js +88 -88
- package/src/common/utils.js +213 -213
- package/src/core/coding.js +33 -33
- package/src/core/go.js +264 -264
- package/src/core/hooks.js +500 -500
- package/src/core/init.js +166 -165
- package/src/core/plan.js +188 -187
- package/src/core/prompts.js +247 -247
- package/src/core/repair.js +36 -36
- package/src/core/runner.js +458 -458
- package/src/core/scan.js +93 -93
- package/src/core/session.js +271 -271
- package/src/core/simplify.js +74 -74
- package/src/core/state.js +105 -105
- package/src/index.js +76 -76
- package/templates/bash-process.md +12 -12
- package/templates/codingSystem.md +65 -65
- package/templates/codingUser.md +17 -17
- package/templates/coreProtocol.md +29 -29
- package/templates/goSystem.md +130 -130
- package/templates/guidance.json +72 -72
- package/templates/planSystem.md +78 -78
- package/templates/planUser.md +8 -8
- package/templates/requirements.example.md +57 -57
- package/templates/scanSystem.md +120 -120
- package/templates/scanUser.md +10 -10
- package/templates/test_rule.md +194 -194
- package/templates/web-testing.md +17 -17
- package/types/index.d.ts +217 -217
package/src/core/session.js
CHANGED
|
@@ -1,271 +1,271 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { buildEnvVars, log } = require('../common/config');
|
|
6
|
-
const { Indicator } = require('../common/indicator');
|
|
7
|
-
const { logMessage: baseLogMessage, extractResult, writeSessionSeparator } = require('../common/logging');
|
|
8
|
-
const { createHooks } = require('./hooks');
|
|
9
|
-
const { assets } = require('../common/assets');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {Object} SessionRunOptions
|
|
13
|
-
* @property {string} logFileName - 日志文件名
|
|
14
|
-
* @property {import('fs').WriteStream} [logStream] - 外部日志流(与 logFileName 二选一)
|
|
15
|
-
* @property {number} [sessionNum=0] - 会话编号
|
|
16
|
-
* @property {string} [label=''] - 会话标签
|
|
17
|
-
* @property {(session: Session) => Promise<Object>} execute - 执行回调,接收 session 实例
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @typedef {Object} QueryResult
|
|
22
|
-
* @property {Array<Object>} messages - 所有 SDK 消息
|
|
23
|
-
* @property {boolean} success - 是否成功完成
|
|
24
|
-
* @property {string|null} subtype - 结果子类型
|
|
25
|
-
* @property {number|null} cost - 美元费用
|
|
26
|
-
* @property {Object|null} usage - token 用量 { input_tokens, output_tokens }
|
|
27
|
-
* @property {number|null} turns - 对话轮次
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {Object} RunQueryOpts
|
|
32
|
-
* @property {(message: Object, messages: Array<Object>) => void|'break'} [onMessage] - 每条消息的回调,返回 'break' 中断
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* SDK 会话管理类。通过 Session.run() 创建和管理一次完整的 AI 会话生命周期。
|
|
37
|
-
*
|
|
38
|
-
* 使用方式:
|
|
39
|
-
* ```js
|
|
40
|
-
* const result = await Session.run('coding', config, {
|
|
41
|
-
* logFileName: 'coding.log',
|
|
42
|
-
* async execute(session) {
|
|
43
|
-
* const queryOpts = session.buildQueryOptions();
|
|
44
|
-
* const { messages, success } = await session.runQuery(prompt, queryOpts);
|
|
45
|
-
* return { success };
|
|
46
|
-
* },
|
|
47
|
-
* });
|
|
48
|
-
* ```
|
|
49
|
-
*/
|
|
50
|
-
class Session {
|
|
51
|
-
/** @type {Object|null} SDK 单例 */
|
|
52
|
-
static _sdk = null;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 确保 SDK 已加载(懒加载单例)
|
|
56
|
-
* @param {Object} config - 项目配置
|
|
57
|
-
* @returns {Promise<Object>} SDK 实例
|
|
58
|
-
*/
|
|
59
|
-
static async ensureSDK(config) {
|
|
60
|
-
if (!Session._sdk) {
|
|
61
|
-
Object.assign(process.env, buildEnvVars(config));
|
|
62
|
-
const { loadSDK } = require('../common/sdk');
|
|
63
|
-
Session._sdk = await loadSDK();
|
|
64
|
-
}
|
|
65
|
-
return Session._sdk;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 创建 Session 实例并执行回调,自动管理生命周期(日志、hooks、indicator)
|
|
70
|
-
* @param {string} type - 会话类型(coding | plan | scan | go | simplify | repair 等)
|
|
71
|
-
* @param {Object} config - 项目配置
|
|
72
|
-
* @param {SessionRunOptions} options - 运行选项
|
|
73
|
-
* @returns {Promise<Object>} 包含 exitCode、logFile、stalled 以及 execute 返回值
|
|
74
|
-
*/
|
|
75
|
-
static async run(type, config, { logFileName, logStream, sessionNum = 0, label = '', execute }) {
|
|
76
|
-
await Session.ensureSDK(config);
|
|
77
|
-
const session = new Session(type, config, { logFileName, logStream, sessionNum, label });
|
|
78
|
-
try {
|
|
79
|
-
const result = await execute(session);
|
|
80
|
-
session.finish();
|
|
81
|
-
return {
|
|
82
|
-
exitCode: session.isStalled() ? 2 : 0,
|
|
83
|
-
logFile: session.logFile,
|
|
84
|
-
stalled: session.isStalled(),
|
|
85
|
-
...result,
|
|
86
|
-
};
|
|
87
|
-
} catch (err) {
|
|
88
|
-
session.finish();
|
|
89
|
-
throw err;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @param {string} type - 会话类型
|
|
95
|
-
* @param {Object} config - 项目配置
|
|
96
|
-
* @param {Object} options
|
|
97
|
-
* @param {string} options.logFileName - 日志文件名
|
|
98
|
-
* @param {import('fs').WriteStream} [options.logStream] - 外部日志流
|
|
99
|
-
* @param {number} [options.sessionNum=0]
|
|
100
|
-
* @param {string} [options.label='']
|
|
101
|
-
*/
|
|
102
|
-
constructor(type, config, { logFileName, logStream, sessionNum = 0, label = '' }) {
|
|
103
|
-
this.config = config;
|
|
104
|
-
this.type = type;
|
|
105
|
-
this.indicator = new Indicator();
|
|
106
|
-
/** @type {import('fs').WriteStream|null} */
|
|
107
|
-
this.logStream = null;
|
|
108
|
-
/** @type {string|null} */
|
|
109
|
-
this.logFile = null;
|
|
110
|
-
/** @type {Object|null} */
|
|
111
|
-
this.hooks = null;
|
|
112
|
-
/** @type {Function|null} */
|
|
113
|
-
this.cleanup = null;
|
|
114
|
-
this._isStalled = () => false;
|
|
115
|
-
this.abortController = new AbortController();
|
|
116
|
-
|
|
117
|
-
this._initLogging(logFileName, logStream);
|
|
118
|
-
writeSessionSeparator(this.logStream, sessionNum, label);
|
|
119
|
-
const stallTimeoutMin = this._initHooks(type);
|
|
120
|
-
this._startIndicator(sessionNum, stallTimeoutMin);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 构建 SDK query 选项,自动附加 hooks、abortController、权限模式
|
|
125
|
-
* @param {Object} [overrides={}] - 覆盖选项(permissionMode, projectRoot, model 等)
|
|
126
|
-
* @returns {Object} SDK query options
|
|
127
|
-
*/
|
|
128
|
-
buildQueryOptions(overrides = {}) {
|
|
129
|
-
const mode = overrides.permissionMode || 'bypassPermissions';
|
|
130
|
-
const base = {
|
|
131
|
-
permissionMode: mode,
|
|
132
|
-
cwd: overrides.projectRoot || assets.projectRoot,
|
|
133
|
-
env: buildEnvVars(this.config),
|
|
134
|
-
settingSources: ['project'],
|
|
135
|
-
hooks: this.hooks,
|
|
136
|
-
abortController: this.abortController,
|
|
137
|
-
};
|
|
138
|
-
if (mode === 'bypassPermissions') {
|
|
139
|
-
base.allowDangerouslySkipPermissions = true;
|
|
140
|
-
}
|
|
141
|
-
if (this.config.maxTurns > 0) base.maxTurns = this.config.maxTurns;
|
|
142
|
-
if (overrides.model) base.model = overrides.model;
|
|
143
|
-
else if (this.config.model) base.model = this.config.model;
|
|
144
|
-
return base;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* 执行一次 SDK 查询,遍历消息流并收集结果
|
|
149
|
-
* @param {string} prompt - 用户提示
|
|
150
|
-
* @param {Object} queryOpts - SDK query 选项(通常来自 buildQueryOptions)
|
|
151
|
-
* @param {RunQueryOpts} [opts={}] - 额外选项(onMessage 回调)
|
|
152
|
-
* @returns {Promise<QueryResult>}
|
|
153
|
-
*/
|
|
154
|
-
async runQuery(prompt, queryOpts, opts = {}) {
|
|
155
|
-
if (this.logStream?.writable) {
|
|
156
|
-
const sep = '-'.repeat(40);
|
|
157
|
-
if (queryOpts.systemPrompt) {
|
|
158
|
-
this.logStream.write(`\n${sep}\n[SYSTEM_PROMPT]\n${sep}\n${queryOpts.systemPrompt}\n`);
|
|
159
|
-
}
|
|
160
|
-
this.logStream.write(`\n${sep}\n[USER_PROMPT]\n${sep}\n${prompt}\n${sep}\n\n`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const sdk = Session._sdk;
|
|
164
|
-
const messages = [];
|
|
165
|
-
const querySession = sdk.query({ prompt, options: queryOpts });
|
|
166
|
-
|
|
167
|
-
for await (const message of querySession) {
|
|
168
|
-
if (this._isStalled()) {
|
|
169
|
-
log('warn', '停顿超时,中断消息循环');
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
messages.push(message);
|
|
173
|
-
this._logMessage(message);
|
|
174
|
-
|
|
175
|
-
if (opts.onMessage) {
|
|
176
|
-
const action = opts.onMessage(message, messages);
|
|
177
|
-
if (action === 'break') break;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const sdkResult = extractResult(messages);
|
|
182
|
-
const cost = sdkResult?.total_cost_usd || null;
|
|
183
|
-
const usage = sdkResult?.usage || null;
|
|
184
|
-
const turns = sdkResult?.num_turns || null;
|
|
185
|
-
|
|
186
|
-
if (cost != null || turns != null) {
|
|
187
|
-
const parts = [];
|
|
188
|
-
if (turns != null) parts.push(`turns: ${turns}`);
|
|
189
|
-
if (cost != null) parts.push(`cost: $${cost}`);
|
|
190
|
-
if (usage) {
|
|
191
|
-
const inp = usage.input_tokens || 0;
|
|
192
|
-
const out = usage.output_tokens || 0;
|
|
193
|
-
parts.push(`tokens: ${inp}+${out}`);
|
|
194
|
-
}
|
|
195
|
-
const summary = parts.join(', ');
|
|
196
|
-
console.log('----- SESSION END -----');
|
|
197
|
-
log('info', `session 统计: ${summary}`);
|
|
198
|
-
if (this.logStream?.writable) {
|
|
199
|
-
this.logStream.write(`[SESSION_STATS] ${summary}\n`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
messages,
|
|
205
|
-
success: sdkResult?.subtype === 'success',
|
|
206
|
-
subtype: sdkResult?.subtype || null,
|
|
207
|
-
cost, usage, turns,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** 检查会话是否因停顿超时 */
|
|
212
|
-
isStalled() {
|
|
213
|
-
return this._isStalled();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** 结束会话:清理 hooks、关闭日志流、停止 indicator */
|
|
217
|
-
finish() {
|
|
218
|
-
if (this.cleanup) this.cleanup();
|
|
219
|
-
if (this.logStream && !this._externalLogStream) this.logStream.end();
|
|
220
|
-
this.indicator.stop();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ─── Private ────────────────────────────────────────────
|
|
224
|
-
|
|
225
|
-
_initLogging(logFileName, externalLogStream) {
|
|
226
|
-
if (externalLogStream) {
|
|
227
|
-
this.logStream = externalLogStream;
|
|
228
|
-
this._externalLogStream = true;
|
|
229
|
-
} else {
|
|
230
|
-
const logsDir = assets.dir('logs');
|
|
231
|
-
this.logFile = path.join(logsDir, logFileName);
|
|
232
|
-
this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
233
|
-
this._externalLogStream = false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
_initHooks(hookType) {
|
|
238
|
-
const stallTimeoutMs = this.config.stallTimeout * 1000;
|
|
239
|
-
const result = createHooks(hookType, this.indicator, this.logStream, {
|
|
240
|
-
stallTimeoutMs,
|
|
241
|
-
abortController: this.abortController,
|
|
242
|
-
editThreshold: this.config.editThreshold,
|
|
243
|
-
});
|
|
244
|
-
this.hooks = result.hooks;
|
|
245
|
-
this.cleanup = result.cleanup;
|
|
246
|
-
this._isStalled = result.isStalled;
|
|
247
|
-
return Math.floor(stallTimeoutMs / 60000);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
_startIndicator(sessionNum, stallTimeoutMin) {
|
|
251
|
-
this.indicator.start(sessionNum, stallTimeoutMin, assets.projectRoot);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
_logMessage(message) {
|
|
255
|
-
const hasText = message.type === 'assistant'
|
|
256
|
-
&& message.message?.content?.some(b => b.type === 'text' && b.text);
|
|
257
|
-
|
|
258
|
-
if (hasText && this.indicator) {
|
|
259
|
-
this.indicator.pauseRendering();
|
|
260
|
-
process.stderr.write('\r\x1b[K');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
baseLogMessage(message, this.logStream, this.indicator);
|
|
264
|
-
|
|
265
|
-
if (hasText && this.indicator) {
|
|
266
|
-
this.indicator.resumeRendering();
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
module.exports = { Session };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { buildEnvVars, log } = require('../common/config');
|
|
6
|
+
const { Indicator } = require('../common/indicator');
|
|
7
|
+
const { logMessage: baseLogMessage, extractResult, writeSessionSeparator } = require('../common/logging');
|
|
8
|
+
const { createHooks } = require('./hooks');
|
|
9
|
+
const { assets } = require('../common/assets');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} SessionRunOptions
|
|
13
|
+
* @property {string} logFileName - 日志文件名
|
|
14
|
+
* @property {import('fs').WriteStream} [logStream] - 外部日志流(与 logFileName 二选一)
|
|
15
|
+
* @property {number} [sessionNum=0] - 会话编号
|
|
16
|
+
* @property {string} [label=''] - 会话标签
|
|
17
|
+
* @property {(session: Session) => Promise<Object>} execute - 执行回调,接收 session 实例
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} QueryResult
|
|
22
|
+
* @property {Array<Object>} messages - 所有 SDK 消息
|
|
23
|
+
* @property {boolean} success - 是否成功完成
|
|
24
|
+
* @property {string|null} subtype - 结果子类型
|
|
25
|
+
* @property {number|null} cost - 美元费用
|
|
26
|
+
* @property {Object|null} usage - token 用量 { input_tokens, output_tokens }
|
|
27
|
+
* @property {number|null} turns - 对话轮次
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {Object} RunQueryOpts
|
|
32
|
+
* @property {(message: Object, messages: Array<Object>) => void|'break'} [onMessage] - 每条消息的回调,返回 'break' 中断
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* SDK 会话管理类。通过 Session.run() 创建和管理一次完整的 AI 会话生命周期。
|
|
37
|
+
*
|
|
38
|
+
* 使用方式:
|
|
39
|
+
* ```js
|
|
40
|
+
* const result = await Session.run('coding', config, {
|
|
41
|
+
* logFileName: 'coding.log',
|
|
42
|
+
* async execute(session) {
|
|
43
|
+
* const queryOpts = session.buildQueryOptions();
|
|
44
|
+
* const { messages, success } = await session.runQuery(prompt, queryOpts);
|
|
45
|
+
* return { success };
|
|
46
|
+
* },
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
class Session {
|
|
51
|
+
/** @type {Object|null} SDK 单例 */
|
|
52
|
+
static _sdk = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 确保 SDK 已加载(懒加载单例)
|
|
56
|
+
* @param {Object} config - 项目配置
|
|
57
|
+
* @returns {Promise<Object>} SDK 实例
|
|
58
|
+
*/
|
|
59
|
+
static async ensureSDK(config) {
|
|
60
|
+
if (!Session._sdk) {
|
|
61
|
+
Object.assign(process.env, buildEnvVars(config));
|
|
62
|
+
const { loadSDK } = require('../common/sdk');
|
|
63
|
+
Session._sdk = await loadSDK();
|
|
64
|
+
}
|
|
65
|
+
return Session._sdk;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 创建 Session 实例并执行回调,自动管理生命周期(日志、hooks、indicator)
|
|
70
|
+
* @param {string} type - 会话类型(coding | plan | scan | go | simplify | repair 等)
|
|
71
|
+
* @param {Object} config - 项目配置
|
|
72
|
+
* @param {SessionRunOptions} options - 运行选项
|
|
73
|
+
* @returns {Promise<Object>} 包含 exitCode、logFile、stalled 以及 execute 返回值
|
|
74
|
+
*/
|
|
75
|
+
static async run(type, config, { logFileName, logStream, sessionNum = 0, label = '', execute }) {
|
|
76
|
+
await Session.ensureSDK(config);
|
|
77
|
+
const session = new Session(type, config, { logFileName, logStream, sessionNum, label });
|
|
78
|
+
try {
|
|
79
|
+
const result = await execute(session);
|
|
80
|
+
session.finish();
|
|
81
|
+
return {
|
|
82
|
+
exitCode: session.isStalled() ? 2 : 0,
|
|
83
|
+
logFile: session.logFile,
|
|
84
|
+
stalled: session.isStalled(),
|
|
85
|
+
...result,
|
|
86
|
+
};
|
|
87
|
+
} catch (err) {
|
|
88
|
+
session.finish();
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {string} type - 会话类型
|
|
95
|
+
* @param {Object} config - 项目配置
|
|
96
|
+
* @param {Object} options
|
|
97
|
+
* @param {string} options.logFileName - 日志文件名
|
|
98
|
+
* @param {import('fs').WriteStream} [options.logStream] - 外部日志流
|
|
99
|
+
* @param {number} [options.sessionNum=0]
|
|
100
|
+
* @param {string} [options.label='']
|
|
101
|
+
*/
|
|
102
|
+
constructor(type, config, { logFileName, logStream, sessionNum = 0, label = '' }) {
|
|
103
|
+
this.config = config;
|
|
104
|
+
this.type = type;
|
|
105
|
+
this.indicator = new Indicator();
|
|
106
|
+
/** @type {import('fs').WriteStream|null} */
|
|
107
|
+
this.logStream = null;
|
|
108
|
+
/** @type {string|null} */
|
|
109
|
+
this.logFile = null;
|
|
110
|
+
/** @type {Object|null} */
|
|
111
|
+
this.hooks = null;
|
|
112
|
+
/** @type {Function|null} */
|
|
113
|
+
this.cleanup = null;
|
|
114
|
+
this._isStalled = () => false;
|
|
115
|
+
this.abortController = new AbortController();
|
|
116
|
+
|
|
117
|
+
this._initLogging(logFileName, logStream);
|
|
118
|
+
writeSessionSeparator(this.logStream, sessionNum, label);
|
|
119
|
+
const stallTimeoutMin = this._initHooks(type);
|
|
120
|
+
this._startIndicator(sessionNum, stallTimeoutMin);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 构建 SDK query 选项,自动附加 hooks、abortController、权限模式
|
|
125
|
+
* @param {Object} [overrides={}] - 覆盖选项(permissionMode, projectRoot, model 等)
|
|
126
|
+
* @returns {Object} SDK query options
|
|
127
|
+
*/
|
|
128
|
+
buildQueryOptions(overrides = {}) {
|
|
129
|
+
const mode = overrides.permissionMode || 'bypassPermissions';
|
|
130
|
+
const base = {
|
|
131
|
+
permissionMode: mode,
|
|
132
|
+
cwd: overrides.projectRoot || assets.projectRoot,
|
|
133
|
+
env: buildEnvVars(this.config),
|
|
134
|
+
settingSources: ['project'],
|
|
135
|
+
hooks: this.hooks,
|
|
136
|
+
abortController: this.abortController,
|
|
137
|
+
};
|
|
138
|
+
if (mode === 'bypassPermissions') {
|
|
139
|
+
base.allowDangerouslySkipPermissions = true;
|
|
140
|
+
}
|
|
141
|
+
if (this.config.maxTurns > 0) base.maxTurns = this.config.maxTurns;
|
|
142
|
+
if (overrides.model) base.model = overrides.model;
|
|
143
|
+
else if (this.config.model) base.model = this.config.model;
|
|
144
|
+
return base;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 执行一次 SDK 查询,遍历消息流并收集结果
|
|
149
|
+
* @param {string} prompt - 用户提示
|
|
150
|
+
* @param {Object} queryOpts - SDK query 选项(通常来自 buildQueryOptions)
|
|
151
|
+
* @param {RunQueryOpts} [opts={}] - 额外选项(onMessage 回调)
|
|
152
|
+
* @returns {Promise<QueryResult>}
|
|
153
|
+
*/
|
|
154
|
+
async runQuery(prompt, queryOpts, opts = {}) {
|
|
155
|
+
if (this.logStream?.writable) {
|
|
156
|
+
const sep = '-'.repeat(40);
|
|
157
|
+
if (queryOpts.systemPrompt) {
|
|
158
|
+
this.logStream.write(`\n${sep}\n[SYSTEM_PROMPT]\n${sep}\n${queryOpts.systemPrompt}\n`);
|
|
159
|
+
}
|
|
160
|
+
this.logStream.write(`\n${sep}\n[USER_PROMPT]\n${sep}\n${prompt}\n${sep}\n\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const sdk = Session._sdk;
|
|
164
|
+
const messages = [];
|
|
165
|
+
const querySession = sdk.query({ prompt, options: queryOpts });
|
|
166
|
+
|
|
167
|
+
for await (const message of querySession) {
|
|
168
|
+
if (this._isStalled()) {
|
|
169
|
+
log('warn', '停顿超时,中断消息循环');
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
messages.push(message);
|
|
173
|
+
this._logMessage(message);
|
|
174
|
+
|
|
175
|
+
if (opts.onMessage) {
|
|
176
|
+
const action = opts.onMessage(message, messages);
|
|
177
|
+
if (action === 'break') break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const sdkResult = extractResult(messages);
|
|
182
|
+
const cost = sdkResult?.total_cost_usd || null;
|
|
183
|
+
const usage = sdkResult?.usage || null;
|
|
184
|
+
const turns = sdkResult?.num_turns || null;
|
|
185
|
+
|
|
186
|
+
if (cost != null || turns != null) {
|
|
187
|
+
const parts = [];
|
|
188
|
+
if (turns != null) parts.push(`turns: ${turns}`);
|
|
189
|
+
if (cost != null) parts.push(`cost: $${cost}`);
|
|
190
|
+
if (usage) {
|
|
191
|
+
const inp = usage.input_tokens || 0;
|
|
192
|
+
const out = usage.output_tokens || 0;
|
|
193
|
+
parts.push(`tokens: ${inp}+${out}`);
|
|
194
|
+
}
|
|
195
|
+
const summary = parts.join(', ');
|
|
196
|
+
console.log('----- SESSION END -----');
|
|
197
|
+
log('info', `session 统计: ${summary}`);
|
|
198
|
+
if (this.logStream?.writable) {
|
|
199
|
+
this.logStream.write(`[SESSION_STATS] ${summary}\n`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
messages,
|
|
205
|
+
success: sdkResult?.subtype === 'success',
|
|
206
|
+
subtype: sdkResult?.subtype || null,
|
|
207
|
+
cost, usage, turns,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** 检查会话是否因停顿超时 */
|
|
212
|
+
isStalled() {
|
|
213
|
+
return this._isStalled();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 结束会话:清理 hooks、关闭日志流、停止 indicator */
|
|
217
|
+
finish() {
|
|
218
|
+
if (this.cleanup) this.cleanup();
|
|
219
|
+
if (this.logStream && !this._externalLogStream) this.logStream.end();
|
|
220
|
+
this.indicator.stop();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Private ────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
_initLogging(logFileName, externalLogStream) {
|
|
226
|
+
if (externalLogStream) {
|
|
227
|
+
this.logStream = externalLogStream;
|
|
228
|
+
this._externalLogStream = true;
|
|
229
|
+
} else {
|
|
230
|
+
const logsDir = assets.dir('logs');
|
|
231
|
+
this.logFile = path.join(logsDir, logFileName);
|
|
232
|
+
this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
233
|
+
this._externalLogStream = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_initHooks(hookType) {
|
|
238
|
+
const stallTimeoutMs = this.config.stallTimeout * 1000;
|
|
239
|
+
const result = createHooks(hookType, this.indicator, this.logStream, {
|
|
240
|
+
stallTimeoutMs,
|
|
241
|
+
abortController: this.abortController,
|
|
242
|
+
editThreshold: this.config.editThreshold,
|
|
243
|
+
});
|
|
244
|
+
this.hooks = result.hooks;
|
|
245
|
+
this.cleanup = result.cleanup;
|
|
246
|
+
this._isStalled = result.isStalled;
|
|
247
|
+
return Math.floor(stallTimeoutMs / 60000);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_startIndicator(sessionNum, stallTimeoutMin) {
|
|
251
|
+
this.indicator.start(sessionNum, stallTimeoutMin, assets.projectRoot);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_logMessage(message) {
|
|
255
|
+
const hasText = message.type === 'assistant'
|
|
256
|
+
&& message.message?.content?.some(b => b.type === 'text' && b.text);
|
|
257
|
+
|
|
258
|
+
if (hasText && this.indicator) {
|
|
259
|
+
this.indicator.pauseRendering();
|
|
260
|
+
process.stderr.write('\r\x1b[K');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
baseLogMessage(message, this.logStream, this.indicator);
|
|
264
|
+
|
|
265
|
+
if (hasText && this.indicator) {
|
|
266
|
+
this.indicator.resumeRendering();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = { Session };
|