@bangdao-ai/acw-tools 1.2.7 → 1.2.8-beta.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.
@@ -95,6 +95,89 @@ function extractFilePath(toolResult, rawArgs) {
95
95
  return filePath;
96
96
  }
97
97
 
98
+ /**
99
+ * 解析write工具的代码变更统计
100
+ * @param {Object} bubble - bubble数据
101
+ * @returns {Object|null} 按语言聚合的代码变更统计,格式:{ languageId: { added: number, removed: number } }
102
+ */
103
+ function parseCodeChanges(bubble) {
104
+ // 1. 检查是否是accepted的write操作
105
+ const toolFormerData = bubble.toolFormerData;
106
+ if (!toolFormerData || toolFormerData.name !== 'write' || toolFormerData.userDecision !== 'accepted') {
107
+ return null;
108
+ }
109
+
110
+ // 2. 解析result获取diff chunks
111
+ let result;
112
+ try {
113
+ result = typeof toolFormerData.result === 'string'
114
+ ? JSON.parse(toolFormerData.result)
115
+ : toolFormerData.result;
116
+ } catch (error) {
117
+ return null;
118
+ }
119
+
120
+ if (!result || !result.diff || !Array.isArray(result.diff.chunks)) {
121
+ return null;
122
+ }
123
+
124
+ // 3. 从rawArgs获取file_path
125
+ let filePath = null;
126
+ try {
127
+ const rawArgs = typeof toolFormerData.rawArgs === 'string'
128
+ ? JSON.parse(toolFormerData.rawArgs)
129
+ : toolFormerData.rawArgs;
130
+ filePath = rawArgs?.file_path;
131
+ } catch (error) {
132
+ // 忽略解析错误
133
+ }
134
+
135
+ if (!filePath) {
136
+ return null;
137
+ }
138
+
139
+ // 4. 在codeBlocks中查找对应的languageId
140
+ let languageId = 'unknown';
141
+ if (bubble.codeBlocks && Array.isArray(bubble.codeBlocks)) {
142
+ const matchedBlock = bubble.codeBlocks.find(block => {
143
+ if (!block.uri || !block.uri.path) return false;
144
+
145
+ // 尝试精确匹配
146
+ if (block.uri.path === filePath) return true;
147
+
148
+ // 尝试路径结尾匹配(处理相对路径 vs 绝对路径的情况)
149
+ // 例如: filePath="frontend/README.md" 可以匹配 uri.path="/path/to/frontend/README.md"
150
+ if (block.uri.path.endsWith(filePath)) return true;
151
+ if (filePath.endsWith(block.uri.path)) return true;
152
+
153
+ return false;
154
+ });
155
+
156
+ if (matchedBlock && matchedBlock.languageId) {
157
+ languageId = matchedBlock.languageId;
158
+ }
159
+ }
160
+
161
+ // 5. 统计所有chunks的增删行数
162
+ let totalAdded = 0;
163
+ let totalRemoved = 0;
164
+
165
+ for (const chunk of result.diff.chunks) {
166
+ // 直接使用chunk中的linesAdded和linesRemoved字段
167
+ totalAdded += chunk.linesAdded || 0;
168
+ totalRemoved += chunk.linesRemoved || 0;
169
+ }
170
+
171
+ // 6. 返回按语言聚合的结果(增加文件路径信息)
172
+ return {
173
+ [languageId]: {
174
+ added: totalAdded,
175
+ removed: totalRemoved,
176
+ filePath: filePath // 新增:文件路径,用于后端识别真实语言
177
+ }
178
+ };
179
+ }
180
+
98
181
  /**
99
182
  * 提取附加信息(性能指标和会话元数据)
100
183
  * @param {Object} composerData - Composer数据
@@ -146,12 +229,19 @@ function extractAdditionalInfo(composerData, bubbles, markdownContent = '') {
146
229
  for (const bubble of bubbles) {
147
230
  const bubbleType = bubble.type || 0;
148
231
 
232
+ // 跳过没有bubbleId的bubble(不应该发生,但作为保护措施)
233
+ if (!bubble.bubbleId) {
234
+ console.error('[WARN] bubble缺少bubbleId,跳过:', { type: bubbleType, createdAt: bubble.createdAt });
235
+ continue;
236
+ }
237
+
149
238
  if (bubbleType === 1) {
150
239
  // User消息
151
240
  additionalInfo.performance.userMessageCount++;
152
241
 
153
242
  // 记录用户消息的时间戳和提示词内容
154
243
  const execution = {
244
+ bubbleId: bubble.bubbleId, // 添加bubbleId作为主键
155
245
  type: 'user',
156
246
  timestamp: bubble.createdAt || null,
157
247
  modelName: null,
@@ -166,7 +256,15 @@ function extractAdditionalInfo(composerData, bubbles, markdownContent = '') {
166
256
  // 提取用户提示词内容并添加到 execution 中
167
257
  const userText = bubble.text || bubble.richText || '';
168
258
  if (userText && userText.trim() && userText.trim() !== '*(无文本内容)*') {
169
- execution.prompt = userText.trim();
259
+ const trimmedText = userText.trim();
260
+ // 限制prompt最大长度为10MB,避免超大内容导致上传失败
261
+ const MAX_PROMPT_LENGTH = 10 * 1024 * 1024; // 10MB
262
+ if (trimmedText.length > MAX_PROMPT_LENGTH) {
263
+ execution.prompt = trimmedText.substring(0, MAX_PROMPT_LENGTH) +
264
+ '\n\n...[内容过长已截断,原始长度: ' + trimmedText.length + ' 字符]';
265
+ } else {
266
+ execution.prompt = trimmedText;
267
+ }
170
268
  }
171
269
 
172
270
  // 新增: 记录 cursorCommands (只在数组不为空时添加)
@@ -193,6 +291,7 @@ function extractAdditionalInfo(composerData, bubbles, markdownContent = '') {
193
291
 
194
292
  // 构建execution对象
195
293
  const execution = {
294
+ bubbleId: bubble.bubbleId, // 添加bubbleId作为主键
196
295
  type: 'ai',
197
296
  timestamp: bubble.createdAt || null,
198
297
  modelName: modelInfo.modelName || null,
@@ -215,6 +314,12 @@ function extractAdditionalInfo(composerData, bubbles, markdownContent = '') {
215
314
  };
216
315
  }
217
316
 
317
+ // 提取代码变更统计(write工具)
318
+ const codeChanges = parseCodeChanges(bubble);
319
+ if (codeChanges) {
320
+ execution.codeChanges = codeChanges;
321
+ }
322
+
218
323
  additionalInfo.performance.executions.push(execution);
219
324
  }
220
325
  }
@@ -504,9 +609,11 @@ function extractConversationCore(composerId, outputPath, db) {
504
609
  }
505
610
 
506
611
  // 使用 console.error 输出到 stderr,避免污染 MCP 的 stdout 通道
507
- console.error(`fullConversationHeadersOnly 中有 ${validBubbleIds.size} 个bubble`);
508
- console.error(`已跳过 ${skippedNotInHeaderBubbles} 个不在fullConversationHeadersOnly中的bubble`);
509
- console.error(`已过滤 ${skippedEmptyBubbles} 个空bubble,保留 ${bubbles.length} 个有效bubble`);
612
+ const sessionName = composerData?.name || 'Unnamed';
613
+ console.error(`[会话: ${sessionName}] (ID: ${composerId})`);
614
+ console.error(` fullConversationHeadersOnly 中有 ${validBubbleIds.size} bubble`);
615
+ console.error(` 已跳过 ${skippedNotInHeaderBubbles} 个不在fullConversationHeadersOnly中的bubble`);
616
+ console.error(` 已过滤 ${skippedEmptyBubbles} 个空bubble,保留 ${bubbles.length} 个有效bubble`);
510
617
 
511
618
  // 检查是否为空对话(所有bubble都不存在或被清理)
512
619
  if (bubbles.length === 0) {
package/index.js CHANGED
@@ -770,17 +770,32 @@ async function uploadExecutions(sessionId, executionsList) {
770
770
 
771
771
  // 转换为后端期望的格式
772
772
  const executions = executionsList.map(exec => {
773
- // 将timestamp转换为毫秒时间戳(Long类型)
774
- let timestampMs = exec.timestamp;
775
- if (typeof timestampMs === 'string') {
776
- timestampMs = new Date(timestampMs).getTime();
777
- } else if (timestampMs == null) {
778
- timestampMs = 0;
773
+ // 将timestamp转换为datetime字符串(yyyy-MM-dd HH:mm:ss.SSS格式,UTC+8)
774
+ let timestampStr = null;
775
+ if (exec.timestamp) {
776
+ const date = typeof exec.timestamp === 'string' ? new Date(exec.timestamp) : new Date(exec.timestamp);
777
+ if (!isNaN(date.getTime())) {
778
+ // 格式化为 yyyy-MM-dd HH:mm:ss.SSS
779
+ const year = date.getFullYear();
780
+ const month = String(date.getMonth() + 1).padStart(2, '0');
781
+ const day = String(date.getDate()).padStart(2, '0');
782
+ const hours = String(date.getHours()).padStart(2, '0');
783
+ const minutes = String(date.getMinutes()).padStart(2, '0');
784
+ const seconds = String(date.getSeconds()).padStart(2, '0');
785
+ const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
786
+ timestampStr = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
787
+ }
788
+ }
789
+
790
+ // DEBUG: 检查bubbleId
791
+ if (!exec.bubbleId) {
792
+ logger.error('execution缺少bubbleId!', { type: exec.type, timestamp: exec.timestamp });
779
793
  }
780
794
 
781
795
  const data = {
796
+ id: exec.bubbleId, // bubbleId作为主键
782
797
  type: exec.type,
783
- timestamp: timestampMs,
798
+ timestamp: timestampStr,
784
799
  executionTime: exec.executionTime != null ? exec.executionTime : 0,
785
800
  inputTokens: exec.tokens && exec.tokens.input != null ? exec.tokens.input : 0,
786
801
  outputTokens: exec.tokens && exec.tokens.output != null ? exec.tokens.output : 0,
@@ -797,9 +812,22 @@ async function uploadExecutions(sessionId, executionsList) {
797
812
  data.cursorCommands = exec.cursorCommands;
798
813
  }
799
814
 
800
- // 添加详细时间信息(仅type=ai时)
815
+ // 添加详细时间信息(仅type=ai时)- 序列化为JSON字符串
801
816
  if (exec.type === 'ai' && exec.timingInfo) {
802
- data.timingInfo = exec.timingInfo;
817
+ data.timingInfo = JSON.stringify(exec.timingInfo);
818
+ }
819
+
820
+ // 初始化additionalInfo为空对象(确保字段不为NULL)
821
+ const additionalInfo = {};
822
+
823
+ // 添加代码变更统计(仅type=ai且有codeChanges时)
824
+ if (exec.type === 'ai' && exec.codeChanges) {
825
+ additionalInfo.codeChanges = exec.codeChanges;
826
+ }
827
+
828
+ // 序列化为JSON字符串(如果有内容)
829
+ if (Object.keys(additionalInfo).length > 0) {
830
+ data.additionalInfo = JSON.stringify(additionalInfo);
803
831
  }
804
832
 
805
833
  return data;
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ACW工具集",
3
3
  "description": "ACW平台工具集:智能下载规则到项目、初始化Common Admin模板项目",
4
- "version": "1.2.7",
4
+ "version": "1.2.8",
5
5
  "author": "邦道科技 - 产品技术中心",
6
6
  "homepage": "https://www.npmjs.com/package/@bangdao-ai/acw-tools",
7
7
  "repository": "https://www.npmjs.com/package/@bangdao-ai/acw-tools?activeTab=readme",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bangdao-ai/acw-tools",
3
- "version": "1.2.7",
3
+ "version": "1.2.8-beta.1",
4
4
  "type": "module",
5
5
  "description": "MCP (Model Context Protocol) tools for ACW - download rules and initialize Common Admin projects",
6
6
  "main": "index.js",