cc-team-viewer 1.5.6 → 1.5.8
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/cli/cli.js +5 -0
- package/package.json +4 -2
- package/proxy/interceptor.js +21 -19
- package/src/utils/contentFilter.js +153 -0
- package/src/utils/sessionBuilder.js +102 -0
package/cli/cli.js
CHANGED
|
@@ -135,6 +135,9 @@ function removeShellHook() {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function injectCliJs() {
|
|
138
|
+
if (!existsSync(cliPath)) {
|
|
139
|
+
return 'skipped'; // 原生二进制安装,无 cli.js 可注入
|
|
140
|
+
}
|
|
138
141
|
const content = readFileSync(cliPath, 'utf-8');
|
|
139
142
|
if (content.includes(INJECT_START)) {
|
|
140
143
|
// 已经是自己的注入
|
|
@@ -182,6 +185,8 @@ async function runProxyCommand(args) {
|
|
|
182
185
|
const injectResult = injectCliJs();
|
|
183
186
|
if (injectResult === 'injected') {
|
|
184
187
|
console.log('[cctv] 已重新注入 interceptor 到 cli.js');
|
|
188
|
+
} else if (injectResult === 'skipped') {
|
|
189
|
+
console.log('[cctv] 原生安装模式,跳过 cli.js 注入');
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
// Dynamic import to avoid side effects when just installing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-team-viewer",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.8",
|
|
4
4
|
"description": "Claude Code Logger visualization management tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server/server.js",
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
"src/vite.config.js",
|
|
49
49
|
"src/i18n/",
|
|
50
50
|
"src/utils/apiUrl.js",
|
|
51
|
-
"src/utils/
|
|
51
|
+
"src/utils/contentFilter.js",
|
|
52
|
+
"src/utils/env.js",
|
|
53
|
+
"src/utils/sessionBuilder.js"
|
|
52
54
|
],
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@codemirror/lang-cpp": "^6.0.3",
|
package/proxy/interceptor.js
CHANGED
|
@@ -81,7 +81,7 @@ function setupTeamConfigWatcher() {
|
|
|
81
81
|
if (teamsDirWatcher) return; // 已经启动了监听器
|
|
82
82
|
const teamsDir = join(homedir(), '.claude', 'teams');
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
if (!existsSync(teamsDir)) return; // 目录不存在时跳过,避免 ENOENT
|
|
85
85
|
try {
|
|
86
86
|
teamsDirWatcher = watch(teamsDir, { recursive: true }, (eventType, filename) => {
|
|
87
87
|
// 监听所有 config.json 文件的变化
|
|
@@ -660,7 +660,7 @@ export function setupInterceptor() {
|
|
|
660
660
|
const tryStartProxy = async () => {
|
|
661
661
|
const START = 7008, END = 7020;
|
|
662
662
|
for (let port = START; port <= END; port++) {
|
|
663
|
-
if (await checkProxyPort(port)) return; // proxy
|
|
663
|
+
if (await checkProxyPort(port)) return; // proxy 已在运行��跳过
|
|
664
664
|
}
|
|
665
665
|
// 没有找到 proxy,启动一个(不会阻塞 Claude Code)
|
|
666
666
|
_proxyStarted = true;
|
|
@@ -670,27 +670,29 @@ export function setupInterceptor() {
|
|
|
670
670
|
stdio: 'ignore',
|
|
671
671
|
env: { ...process.env, CCV_DETACHED: '1' }
|
|
672
672
|
}).unref();
|
|
673
|
+
};
|
|
673
674
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
675
|
+
tryStartProxy().catch(() => {});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 即使 proxy 已在运行(isLocalProxy),也打印端口信息
|
|
679
|
+
if (!_proxyStarted && !isProxyMode) {
|
|
680
|
+
const printExistingProxy = async () => {
|
|
681
|
+
for (let i = 0; i < 10; i++) {
|
|
682
|
+
await new Promise(r => setTimeout(r, 500));
|
|
683
|
+
for (let p = 7008; p <= 7020; p++) {
|
|
684
|
+
const ok = await new Promise(res => {
|
|
685
|
+
const c = connect(p, '127.0.0.1', () => { c.end(); res(true); });
|
|
686
|
+
c.on('error', () => res(false));
|
|
687
|
+
});
|
|
688
|
+
if (ok) {
|
|
689
|
+
process.stderr.write(`\n[cctv] 监控面板: http://127.0.0.1:${p}\n`);
|
|
690
|
+
return;
|
|
687
691
|
}
|
|
688
692
|
}
|
|
689
|
-
}
|
|
690
|
-
waitAndPrint().catch(() => {});
|
|
693
|
+
}
|
|
691
694
|
};
|
|
692
|
-
|
|
693
|
-
tryStartProxy().catch(() => {});
|
|
695
|
+
printExistingProxy().catch(() => {});
|
|
694
696
|
}
|
|
695
697
|
|
|
696
698
|
// 注册退出处理器
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// 内容分类与过滤规则
|
|
2
|
+
// ChatView(对话模式)和 AppHeader(用户 Prompt 弹窗)共用此模块,确保过滤逻辑一致。
|
|
3
|
+
// MainAgent 判断也收敛于此,供全局统一调用。
|
|
4
|
+
|
|
5
|
+
// ============== 请求体辅助 ==============
|
|
6
|
+
|
|
7
|
+
const SUBAGENT_SYSTEM_RE = /command execution specialist|file search specialist|planning specialist|general-purpose agent/i;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 从请求中提取团队成员名称
|
|
11
|
+
* 优先使用 request.teamMember 字段(由 interceptor 填充),若无则从 system prompt 解析
|
|
12
|
+
*/
|
|
13
|
+
export function extractTeamMember(req) {
|
|
14
|
+
if (!req) return null;
|
|
15
|
+
|
|
16
|
+
// 1. 优先使用 interceptor 填充的 teamMember 字段
|
|
17
|
+
if (req.teamMember) return req.teamMember;
|
|
18
|
+
|
|
19
|
+
// 2. 从 system prompt 解析(旧日志兼容)
|
|
20
|
+
const body = req.body || {};
|
|
21
|
+
const sysText = getSystemText(body);
|
|
22
|
+
|
|
23
|
+
// 检查是否为团队成员(general-purpose agent 或 specialist)
|
|
24
|
+
if (/general-purpose agent/i.test(sysText)) return 'general-purpose';
|
|
25
|
+
if (/command execution specialist/i.test(sysText)) return 'command-execution';
|
|
26
|
+
if (/file search specialist/i.test(sysText)) return 'file-search';
|
|
27
|
+
if (/planning specialist/i.test(sysText)) return 'planning';
|
|
28
|
+
|
|
29
|
+
// 尝试匹配 "You are xxx agent" 模式
|
|
30
|
+
const match = sysText.match(/You are (?:a |the )?([-\w]+)(?:\s+agent| specialist| explorer| developer)?/i);
|
|
31
|
+
if (match) {
|
|
32
|
+
const member = match[1].toLowerCase().replace(/[-\s]+/g, '-');
|
|
33
|
+
// 排除常见的非成员名
|
|
34
|
+
if (!['claude', 'code', 'anthropic', 'interactive', 'ai', 'general', 'purpose'].includes(member)) {
|
|
35
|
+
return member;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 提取请求体中的 system prompt 文本
|
|
44
|
+
*/
|
|
45
|
+
export function getSystemText(body) {
|
|
46
|
+
const system = body?.system;
|
|
47
|
+
if (typeof system === 'string') return system;
|
|
48
|
+
if (Array.isArray(system)) {
|
|
49
|
+
return system.map(s => (s && s.text) || '').join('');
|
|
50
|
+
}
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 判断请求是否为 MainAgent 请求。
|
|
56
|
+
* 包含 interceptor 标记校验 + 旧日志回退检测,全局唯一入口。
|
|
57
|
+
*/
|
|
58
|
+
export function isMainAgent(req) {
|
|
59
|
+
if (!req) return false;
|
|
60
|
+
|
|
61
|
+
if (req.mainAgent) {
|
|
62
|
+
// 排除被误标记的 SubAgent(旧日志兼容)
|
|
63
|
+
const sysText = getSystemText(req.body || {});
|
|
64
|
+
if (SUBAGENT_SYSTEM_RE.test(sysText)) return false;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 回退检测:旧版 interceptor 未标记 mainAgent,但实际符合 MainAgent 特征
|
|
69
|
+
const body = req.body || {};
|
|
70
|
+
const tools = body.tools;
|
|
71
|
+
if (body.system && Array.isArray(tools) && tools.length > 10) {
|
|
72
|
+
const hasEdit = tools.some(t => t.name === 'Edit');
|
|
73
|
+
const hasBash = tools.some(t => t.name === 'Bash');
|
|
74
|
+
const hasTaskOrAgent = tools.some(t => t.name === 'Task' || t.name === 'Agent');
|
|
75
|
+
if (hasEdit && hasBash && hasTaskOrAgent) {
|
|
76
|
+
const sysText = getSystemText(body);
|
|
77
|
+
if (sysText.includes('You are Claude Code') && !SUBAGENT_SYSTEM_RE.test(sysText)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============== 文本内容过滤 ==============
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 判断文本是否为 Skill 加载内容
|
|
90
|
+
*/
|
|
91
|
+
export function isSkillText(text) {
|
|
92
|
+
if (!text) return false;
|
|
93
|
+
return /^Base directory for this skill:/i.test(text.trim());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 判断文本是否为系统注入文本(不应作为用户消息展示)
|
|
98
|
+
*/
|
|
99
|
+
export function isSystemText(text) {
|
|
100
|
+
if (!text) return true;
|
|
101
|
+
const trimmed = text.trim();
|
|
102
|
+
if (!trimmed) return true;
|
|
103
|
+
if (/^<[a-zA-Z_][\w-]*[\s>]/i.test(trimmed)) return true;
|
|
104
|
+
if (/^\[SUGGESTION MODE:/i.test(trimmed)) return true;
|
|
105
|
+
// Claude Code 输出截断时注入的系统消息
|
|
106
|
+
if (/^Your response was cut off because it exceeded the output token limit/i.test(trimmed)) return true;
|
|
107
|
+
// Skill 加载的文档内容
|
|
108
|
+
if (/^Base directory for this skill:/i.test(trimmed)) return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 从 user message 的 content 数组中分类提取各类文本块。
|
|
114
|
+
* @param {Array} content — message.content 数组
|
|
115
|
+
* @returns {{ commands: string[], textBlocks: Array, skillBlocks: Array }}
|
|
116
|
+
* commands — 提取到的 slash command 名称(如 "/clear")
|
|
117
|
+
* textBlocks — 过滤后的普通用户文本块(不含系统文本、command 块、skill 块)
|
|
118
|
+
* skillBlocks — skill 加载的文本块
|
|
119
|
+
*/
|
|
120
|
+
export function classifyUserContent(content) {
|
|
121
|
+
if (!Array.isArray(content)) return { commands: [], textBlocks: [], skillBlocks: [] };
|
|
122
|
+
|
|
123
|
+
const hasCommand = content.some(b => b.type === 'text' && /<command-message>/i.test(b.text || ''));
|
|
124
|
+
|
|
125
|
+
// 提取 slash command 名称
|
|
126
|
+
const commands = [];
|
|
127
|
+
if (hasCommand) {
|
|
128
|
+
for (const b of content) {
|
|
129
|
+
if (b.type !== 'text') continue;
|
|
130
|
+
const m = (b.text || '').match(/<command-name>\s*([^<]*)<\/command-name>/i);
|
|
131
|
+
if (m) {
|
|
132
|
+
const cmd = m[1].trim();
|
|
133
|
+
commands.push(cmd.startsWith('/') ? cmd : `/${cmd}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 过滤出非系统文本块
|
|
139
|
+
let textBlocks = content.filter(b => b.type === 'text' && !isSystemText(b.text));
|
|
140
|
+
|
|
141
|
+
// 过滤掉 command 相关块
|
|
142
|
+
if (hasCommand) {
|
|
143
|
+
textBlocks = textBlocks.filter(b => !/<command-message>/i.test(b.text || ''));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 分离 skill 块
|
|
147
|
+
const skillBlocks = textBlocks.filter(b => isSkillText(b.text));
|
|
148
|
+
if (skillBlocks.length > 0) {
|
|
149
|
+
textBlocks = textBlocks.filter(b => !isSkillText(b.text));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { commands, textBlocks, skillBlocks };
|
|
153
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话构建工具
|
|
3
|
+
* 负责从请求列表中构建 MainAgent 会话
|
|
4
|
+
*/
|
|
5
|
+
import { isMainAgent } from './contentFilter.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 合并 MainAgent sessions
|
|
9
|
+
* 通过 metadata.user_id 判断 session 归属
|
|
10
|
+
*/
|
|
11
|
+
export function mergeMainAgentSessions(prevSessions, entry) {
|
|
12
|
+
const newMessages = entry.body.messages;
|
|
13
|
+
const newResponse = entry.response;
|
|
14
|
+
const userId = entry.body.metadata?.user_id || null;
|
|
15
|
+
const entryTimestamp = entry.timestamp || null;
|
|
16
|
+
const teamMember = entry.teamMember || null;
|
|
17
|
+
|
|
18
|
+
if (prevSessions.length === 0) {
|
|
19
|
+
return [{ userId, messages: newMessages, response: newResponse, entryTimestamp, teamMember }];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lastSession = prevSessions[prevSessions.length - 1];
|
|
23
|
+
const prevMsgCount = lastSession.messages ? lastSession.messages.length : 0;
|
|
24
|
+
|
|
25
|
+
// 消息数量大幅缩减视为新对话
|
|
26
|
+
const isNewConversation = prevMsgCount > 0 &&
|
|
27
|
+
newMessages.length < prevMsgCount * 0.5 &&
|
|
28
|
+
(prevMsgCount - newMessages.length) > 4;
|
|
29
|
+
|
|
30
|
+
if (userId && userId === lastSession.userId && !isNewConversation) {
|
|
31
|
+
// 更新当前 session
|
|
32
|
+
const updated = [...prevSessions];
|
|
33
|
+
updated[updated.length - 1] = {
|
|
34
|
+
...lastSession,
|
|
35
|
+
messages: newMessages,
|
|
36
|
+
response: newResponse,
|
|
37
|
+
entryTimestamp,
|
|
38
|
+
teamMember: teamMember || lastSession.teamMember,
|
|
39
|
+
};
|
|
40
|
+
return updated;
|
|
41
|
+
} else {
|
|
42
|
+
// 新开一段
|
|
43
|
+
return [...prevSessions, { userId, messages: newMessages, response: newResponse, entryTimestamp, teamMember }];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 从批量 entries 构建 sessions
|
|
49
|
+
*/
|
|
50
|
+
export function buildSessionsFromEntries(entries, includeAllRequests = false) {
|
|
51
|
+
let sessions = [];
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const shouldInclude = includeAllRequests
|
|
54
|
+
? entry.body && Array.isArray(entry.body.messages)
|
|
55
|
+
: isMainAgent(entry) && entry.body && Array.isArray(entry.body.messages);
|
|
56
|
+
|
|
57
|
+
if (shouldInclude) {
|
|
58
|
+
sessions = mergeMainAgentSessions(sessions, entry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return sessions;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 前置处理:遍历所有 MainAgent entries,给每条消息注入 _timestamp
|
|
66
|
+
*/
|
|
67
|
+
export function assignMessageTimestamps(entries) {
|
|
68
|
+
let timestamps = [];
|
|
69
|
+
let prevUserId = null;
|
|
70
|
+
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!isMainAgent(entry) || !entry.body || !Array.isArray(entry.body.messages)) continue;
|
|
73
|
+
|
|
74
|
+
const messages = entry.body.messages;
|
|
75
|
+
const count = messages.length;
|
|
76
|
+
const userId = entry.body.metadata?.user_id || null;
|
|
77
|
+
const timestamp = entry.timestamp || new Date().toISOString();
|
|
78
|
+
|
|
79
|
+
// 检测 session 切换
|
|
80
|
+
const prevCount = timestamps.length;
|
|
81
|
+
const isNewSession = prevCount > 0 && (
|
|
82
|
+
(count < prevCount * 0.5 && (prevCount - count) > 4) ||
|
|
83
|
+
(prevUserId && userId && userId !== prevUserId)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (isNewSession) {
|
|
87
|
+
timestamps = [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 新增的消息用当前 entry.timestamp
|
|
91
|
+
for (let i = timestamps.length; i < count; i++) {
|
|
92
|
+
timestamps.push(timestamp);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 把累积的时间戳写入当前 entry 的所有消息
|
|
96
|
+
for (let i = 0; i < count; i++) {
|
|
97
|
+
messages[i]._timestamp = timestamps[i];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
prevUserId = userId;
|
|
101
|
+
}
|
|
102
|
+
}
|