cc-viewer 0.1.6 → 0.2.0
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 +17 -19
- package/cli.js +136 -24
- package/i18n.js +85 -0
- package/lib/assets/index-BX8W4MxH.js +551 -0
- package/lib/index.html +1 -1
- package/lib/server.js +4 -3
- package/locales/ar.json +62 -0
- package/locales/da.json +62 -0
- package/locales/de.json +62 -0
- package/locales/en.json +62 -0
- package/locales/es.json +62 -0
- package/locales/fr.json +62 -0
- package/locales/it.json +62 -0
- package/locales/ja.json +62 -0
- package/locales/ko.json +62 -0
- package/locales/no.json +62 -0
- package/locales/pl.json +62 -0
- package/locales/pt-BR.json +62 -0
- package/locales/ru.json +62 -0
- package/locales/th.json +62 -0
- package/locales/tr.json +62 -0
- package/locales/uk.json +62 -0
- package/locales/zh-TW.json +62 -0
- package/locales/zh.json +62 -0
- package/package.json +4 -2
- package/lib/assets/index-B_2FZecC.js +0 -443
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# CC-Viewer
|
|
2
2
|
|
|
3
|
-
Claude Code
|
|
3
|
+
Claude Code 请求监控系统,实时捕获并可视化展示 Claude Code 的所有 API 请求与响应。方便开发者监控自己的 Context,以便于 Vibe Coding 过程中回顾和排查问题。
|
|
4
|
+
|
|
5
|
+
[English](./docs/README.en.md) | [繁體中文](./docs/README.zh-TW.md) | [한국어](./docs/README.ko.md) | [日本語](./docs/README.ja.md) | [Deutsch](./docs/README.de.md) | [Español](./docs/README.es.md) | [Français](./docs/README.fr.md) | [Italiano](./docs/README.it.md) | [Dansk](./docs/README.da.md) | [Polski](./docs/README.pl.md) | [Русский](./docs/README.ru.md) | [العربية](./docs/README.ar.md) | [Norsk](./docs/README.no.md) | [Português (Brasil)](./docs/README.pt-BR.md) | [ไทย](./docs/README.th.md) | [Türkçe](./docs/README.tr.md) | [Українська](./docs/README.uk.md)
|
|
4
6
|
|
|
5
7
|
## 使用方法
|
|
6
8
|
|
|
@@ -14,7 +16,17 @@ npm install -g cc-viewer
|
|
|
14
16
|
ccv
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
该命令会自动将监控脚本注入到本地安装的 Claude Code
|
|
19
|
+
该命令会自动将监控脚本注入到本地安装的 Claude Code 中,并在 shell 配置文件(`~/.zshrc` 或 `~/.bashrc`)中添加自动重注入 hook。之后正常使用 Claude Code,打开浏览器访问 `http://localhost:7008` 即可查看监控界面。
|
|
20
|
+
|
|
21
|
+
Claude Code 更新后无需手动操作,下次运行 `claude` 时会自动检测并重新注入。
|
|
22
|
+
|
|
23
|
+
### 卸载
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ccv --uninstall
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
一键清理 cli.js 中的注入代码和 shell 配置文件中的 hook。
|
|
18
30
|
|
|
19
31
|
## 功能
|
|
20
32
|
|
|
@@ -60,25 +72,11 @@ Header 区域的「Token 消耗统计」悬浮面板:
|
|
|
60
72
|
- 当前日志另存为:下载当前监控的 JSONL 日志文件
|
|
61
73
|
- 导出用户 Prompt:提取并展示所有用户输入,支持 system-reminder 折叠查看
|
|
62
74
|
|
|
63
|
-
###
|
|
64
|
-
|
|
65
|
-
| 消息类型 | 识别方式 | 展示 |
|
|
66
|
-
|---------|---------|------|
|
|
67
|
-
| 用户输入 | `role: "user"` + 非系统标签文本 | 蓝色气泡 |
|
|
68
|
-
| 用户选择 | `[SUGGESTION MODE:]` + `tool_result` | 问答卡片 |
|
|
69
|
-
| Agent 文本回复 | `role: "assistant"` + `type: "text"` | Markdown 渲染 |
|
|
70
|
-
| Agent 工具调用 | `role: "assistant"` + `type: "tool_use"` | 工具调用卡片 + 内联结果 |
|
|
71
|
-
| Agent 思考 | `role: "assistant"` + `type: "thinking"` | 可折叠思考块 |
|
|
72
|
-
|
|
73
|
-
对于 `Task` 类型的工具调用,会从 `input` 中提取 `subagent_type` 和 `description` 来标识具体的 Sub Agent。
|
|
75
|
+
### 多语言支持
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
CC-Viewer 支持 18 种语言,根据系统语言环境自动切换:
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
- Markdown 渲染:[marked](https://github.com/markedjs/marked)
|
|
79
|
-
- 后端:Node.js 原生 HTTP 服务 + SSE 实时推送
|
|
80
|
-
- 请求拦截:通过 `globalThis.fetch` 拦截,支持流式响应组装
|
|
81
|
-
- 构建:Vite
|
|
79
|
+
简体中文 | English | 繁體中文 | 한국어 | Deutsch | Español | Français | Italiano | Dansk | 日本語 | Polski | Русский | العربية | Norsk | Português (Brasil) | ไทย | Türkçe | Українська
|
|
82
80
|
|
|
83
81
|
## License
|
|
84
82
|
|
package/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { t } from './i18n.js';
|
|
6
8
|
|
|
7
9
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
8
10
|
|
|
@@ -12,7 +14,120 @@ const INJECT_IMPORT = "import '../../cc-viewer/interceptor.js';";
|
|
|
12
14
|
const INJECT_BLOCK = `${INJECT_START}\n${INJECT_IMPORT}\n${INJECT_END}`;
|
|
13
15
|
const SHOW_ALL_FILE = '/tmp/cc-viewer-show-all';
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
const SHELL_HOOK_START = '# >>> CC-Viewer Auto-Inject >>>';
|
|
18
|
+
const SHELL_HOOK_END = '# <<< CC-Viewer Auto-Inject <<<';
|
|
19
|
+
|
|
20
|
+
const cliPath = resolve(__dirname, '../@anthropic-ai/claude-code/cli.js');
|
|
21
|
+
|
|
22
|
+
function getShellConfigPath() {
|
|
23
|
+
const shell = process.env.SHELL || '';
|
|
24
|
+
if (shell.includes('zsh')) return resolve(homedir(), '.zshrc');
|
|
25
|
+
if (shell.includes('bash')) {
|
|
26
|
+
const bashProfile = resolve(homedir(), '.bash_profile');
|
|
27
|
+
if (process.platform === 'darwin' && existsSync(bashProfile)) return bashProfile;
|
|
28
|
+
return resolve(homedir(), '.bashrc');
|
|
29
|
+
}
|
|
30
|
+
return resolve(homedir(), '.zshrc');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildShellHook() {
|
|
34
|
+
return `${SHELL_HOOK_START}
|
|
35
|
+
claude() {
|
|
36
|
+
local cli_js="$HOME/.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js"
|
|
37
|
+
if [ -f "$cli_js" ] && ! grep -q "CC Viewer" "$cli_js" 2>/dev/null; then
|
|
38
|
+
ccv 2>/dev/null
|
|
39
|
+
fi
|
|
40
|
+
command claude "$@"
|
|
41
|
+
}
|
|
42
|
+
${SHELL_HOOK_END}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function installShellHook() {
|
|
46
|
+
const configPath = getShellConfigPath();
|
|
47
|
+
try {
|
|
48
|
+
const content = existsSync(configPath) ? readFileSync(configPath, 'utf-8') : '';
|
|
49
|
+
if (content.includes(SHELL_HOOK_START)) {
|
|
50
|
+
return { path: configPath, status: 'exists' };
|
|
51
|
+
}
|
|
52
|
+
const hook = buildShellHook();
|
|
53
|
+
const newContent = content.endsWith('\n') ? content + '\n' + hook + '\n' : content + '\n\n' + hook + '\n';
|
|
54
|
+
writeFileSync(configPath, newContent);
|
|
55
|
+
return { path: configPath, status: 'installed' };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { path: configPath, status: 'error', error: err.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function removeShellHook() {
|
|
62
|
+
const configPath = getShellConfigPath();
|
|
63
|
+
try {
|
|
64
|
+
if (!existsSync(configPath)) return { path: configPath, status: 'not_found' };
|
|
65
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
66
|
+
if (!content.includes(SHELL_HOOK_START)) return { path: configPath, status: 'clean' };
|
|
67
|
+
const regex = new RegExp(`\\n?${SHELL_HOOK_START}[\\s\\S]*?${SHELL_HOOK_END}\\n?`, 'g');
|
|
68
|
+
const newContent = content.replace(regex, '\n');
|
|
69
|
+
writeFileSync(configPath, newContent);
|
|
70
|
+
return { path: configPath, status: 'removed' };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { path: configPath, status: 'error', error: err.message };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function injectCliJs() {
|
|
77
|
+
const content = readFileSync(cliPath, 'utf-8');
|
|
78
|
+
if (content.includes(INJECT_START)) {
|
|
79
|
+
return 'exists';
|
|
80
|
+
}
|
|
81
|
+
const lines = content.split('\n');
|
|
82
|
+
lines.splice(2, 0, INJECT_BLOCK);
|
|
83
|
+
writeFileSync(cliPath, lines.join('\n'));
|
|
84
|
+
return 'injected';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function removeCliJsInjection() {
|
|
88
|
+
try {
|
|
89
|
+
if (!existsSync(cliPath)) return 'not_found';
|
|
90
|
+
const content = readFileSync(cliPath, 'utf-8');
|
|
91
|
+
if (!content.includes(INJECT_START)) return 'clean';
|
|
92
|
+
const regex = new RegExp(`${INJECT_START}\\n${INJECT_IMPORT}\\n${INJECT_END}\\n?`, 'g');
|
|
93
|
+
writeFileSync(cliPath, content.replace(regex, ''));
|
|
94
|
+
return 'removed';
|
|
95
|
+
} catch {
|
|
96
|
+
return 'error';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// === 主逻辑 ===
|
|
101
|
+
|
|
102
|
+
const isUninstall = process.argv.includes('--uninstall');
|
|
103
|
+
|
|
104
|
+
if (isUninstall) {
|
|
105
|
+
const cliResult = removeCliJsInjection();
|
|
106
|
+
const shellResult = removeShellHook();
|
|
107
|
+
|
|
108
|
+
if (cliResult === 'removed' || cliResult === 'clean') {
|
|
109
|
+
console.log(t('cli.uninstall.cliCleaned'));
|
|
110
|
+
} else if (cliResult === 'not_found') {
|
|
111
|
+
console.log(t('cli.uninstall.cliNotFound'));
|
|
112
|
+
} else {
|
|
113
|
+
console.log(t('cli.uninstall.cliFail'));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (shellResult.status === 'removed') {
|
|
117
|
+
console.log(t('cli.uninstall.hookRemoved', { path: shellResult.path }));
|
|
118
|
+
} else if (shellResult.status === 'clean' || shellResult.status === 'not_found') {
|
|
119
|
+
console.log(t('cli.uninstall.hookClean', { path: shellResult.path }));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(t('cli.uninstall.hookFail', { error: shellResult.error }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try { unlinkSync(SHOW_ALL_FILE); } catch {}
|
|
125
|
+
|
|
126
|
+
console.log(t('cli.uninstall.done'));
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 正常安装流程
|
|
16
131
|
const showAll = process.argv.includes('--all');
|
|
17
132
|
if (showAll) {
|
|
18
133
|
try { writeFileSync(SHOW_ALL_FILE, '1'); } catch {}
|
|
@@ -20,34 +135,31 @@ if (showAll) {
|
|
|
20
135
|
try { unlinkSync(SHOW_ALL_FILE); } catch {}
|
|
21
136
|
}
|
|
22
137
|
|
|
23
|
-
const INJECT_START = '// >>> Start CC Viewer Web Service >>>';
|
|
24
|
-
const INJECT_END = '// <<< Start CC Viewer Web Service <<<';
|
|
25
|
-
const INJECT_IMPORT = "import '../../cc-viewer/interceptor.js';";
|
|
26
|
-
const INJECT_BLOCK = `${INJECT_START}\n${INJECT_IMPORT}\n${INJECT_END}`;
|
|
27
|
-
|
|
28
|
-
// Claude Code cli.js 的路径
|
|
29
|
-
const cliPath = resolve(__dirname, '../@anthropic-ai/claude-code/cli.js');
|
|
30
|
-
|
|
31
138
|
try {
|
|
32
|
-
const
|
|
139
|
+
const cliResult = injectCliJs();
|
|
140
|
+
if (cliResult === 'exists') {
|
|
141
|
+
console.log(t('cli.inject.exists'));
|
|
142
|
+
} else {
|
|
143
|
+
console.log(t('cli.inject.success'));
|
|
144
|
+
}
|
|
33
145
|
|
|
34
|
-
|
|
35
|
-
|
|
146
|
+
const shellResult = installShellHook();
|
|
147
|
+
if (shellResult.status === 'installed') {
|
|
148
|
+
console.log(t('cli.hook.installed', { path: shellResult.path }));
|
|
149
|
+
} else if (shellResult.status === 'exists') {
|
|
150
|
+
console.log(t('cli.hook.exists', { path: shellResult.path }));
|
|
36
151
|
} else {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
console.log(`直接运行 claude 即可,启动时会显示 cc-viewer 的实际运行地址`);
|
|
44
|
-
console.log(`如果后续功能失效,请重新执行一次 ccviewer 命令即可`);
|
|
152
|
+
console.log(t('cli.hook.fail', { error: shellResult.error }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(t('cli.usage.hint'));
|
|
156
|
+
console.log(t('cli.usage.uninstallHint'));
|
|
45
157
|
} catch (err) {
|
|
46
158
|
if (err.code === 'ENOENT') {
|
|
47
|
-
console.error('
|
|
48
|
-
console.error('
|
|
159
|
+
console.error(t('cli.inject.notFound', { path: cliPath }));
|
|
160
|
+
console.error(t('cli.inject.notFoundHint'));
|
|
49
161
|
} else {
|
|
50
|
-
console.error('
|
|
162
|
+
console.error(t('cli.inject.fail', { error: err.message }));
|
|
51
163
|
}
|
|
52
164
|
process.exit(1);
|
|
53
165
|
}
|
package/i18n.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
const SUPPORTED_LANGS = [
|
|
8
|
+
'zh', 'en', 'zh-TW', 'ko', 'de', 'es', 'fr', 'it', 'da', 'ja',
|
|
9
|
+
'pl', 'ru', 'ar', 'no', 'pt-BR', 'th', 'tr', 'uk',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function loadLocale(lang) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(join(__dirname, 'locales', `${lang}.json`), 'utf-8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const locales = {};
|
|
21
|
+
for (const lang of SUPPORTED_LANGS) {
|
|
22
|
+
locales[lang] = loadLocale(lang);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let currentLang = 'zh';
|
|
26
|
+
|
|
27
|
+
// 语言代码 → locale key 映射
|
|
28
|
+
const LANG_MAP = {
|
|
29
|
+
zh: 'zh', 'zh-cn': 'zh', 'zh-hans': 'zh',
|
|
30
|
+
'zh-tw': 'zh-TW', 'zh-hk': 'zh-TW', 'zh-hant': 'zh-TW',
|
|
31
|
+
en: 'en',
|
|
32
|
+
ko: 'ko',
|
|
33
|
+
de: 'de',
|
|
34
|
+
es: 'es',
|
|
35
|
+
fr: 'fr',
|
|
36
|
+
it: 'it',
|
|
37
|
+
da: 'da',
|
|
38
|
+
ja: 'ja',
|
|
39
|
+
pl: 'pl',
|
|
40
|
+
ru: 'ru',
|
|
41
|
+
ar: 'ar',
|
|
42
|
+
no: 'no', nb: 'no', nn: 'no',
|
|
43
|
+
pt: 'pt-BR', 'pt-br': 'pt-BR',
|
|
44
|
+
th: 'th',
|
|
45
|
+
tr: 'tr',
|
|
46
|
+
uk: 'uk',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function resolveLocale(raw) {
|
|
50
|
+
if (!raw) return 'en';
|
|
51
|
+
// 去掉 .UTF-8 等后缀,统一小写
|
|
52
|
+
const cleaned = raw.split('.')[0].replace(/_/g, '-').toLowerCase();
|
|
53
|
+
// 精确匹配
|
|
54
|
+
if (LANG_MAP[cleaned]) return LANG_MAP[cleaned];
|
|
55
|
+
// 取主语言
|
|
56
|
+
const primary = cleaned.split('-')[0];
|
|
57
|
+
if (LANG_MAP[primary]) return LANG_MAP[primary];
|
|
58
|
+
return 'en';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function detectLanguage() {
|
|
62
|
+
const lang = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || '';
|
|
63
|
+
return resolveLocale(lang);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function setLang(lang) {
|
|
67
|
+
currentLang = locales[lang] ? lang : 'en';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getLang() {
|
|
71
|
+
return currentLang;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function t(key, params) {
|
|
75
|
+
let text = locales[currentLang]?.[key] || locales['en'][key] || key;
|
|
76
|
+
if (params) {
|
|
77
|
+
for (const [k, v] of Object.entries(params)) {
|
|
78
|
+
text = text.replaceAll(`{${k}}`, v);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return text;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 自动检测并设置语言
|
|
85
|
+
setLang(detectLanguage());
|