feat: 提交2025-11-19日代码,完成cc历史消息读取功能

This commit is contained in:
zhukunpenglinyutong
2025-11-20 12:58:09 +08:00
parent b8119e413c
commit 57117efc9a
29 changed files with 5889 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Kotlin ###
.kotlin
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
+5
View File
@@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
+10
View File
@@ -0,0 +1,10 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+51
View File
@@ -0,0 +1,51 @@
# IDEA版 Claude Code GUI 插件
本项目主要解决在IDEA中使用Claude Code 没有 GUI操作窗口的场景
目前在实验阶段,成品尚未完成,代码会按天更新进度
### 目前进度
2025年11月19日 实现历史记录读取功能
安装包:[idea-claude-code-gui-0.0.1.zip](https://claudecodecn-1253302184.cos.ap-beijing.myqcloud.com/idea/v0.0.1/idea-claude-code-gui-0.0.1.zip)
<img width="400" alt="Image" src="https://claudecodecn-1253302184.cos.ap-beijing.myqcloud.com/idea/v0.0.1/1.png" />
2025年11月20日 攻关JAVA 与 @anthropic-ai/claude-agent-sdk 交互问题
### 构建插件
```sh
./gradlew build
# 生成的插件包会在 build/distributions/ 目录下
```
### 开发环境
```
IntelliJ IDEA 2025.2.4 (Ultimate Edition)
Build #IU-252.27397.103, built on October 23, 2025
Source revision: 9b31ba2c05b47
Runtime version: 21.0.8+9-b1038.73 aarch64 (JCEF 122.1.9)
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
Toolkit: sun.lwawt.macosx.LWCToolkit
macOS 15.3.1
GC: G1 Young Generation, G1 Concurrent GC, G1 Old Generation
Memory: 2048M
Cores: 12
Metal Rendering is ON
Registry:
ide.experimental.ui=true
llm.selector.config.refresh.interval=10
llm.rules.refresh.interval=10
Non-Bundled Plugins:
com.luomacode.ChatMoss (7.1.2)
com.anthropic.code.plugin (0.1.12-beta)
com.intellij.ml.llm (252.27397.144)
com.example.claudeagent (1.0-SNAPSHOT)
Kotlin: 252.27397.103-IJ
```
+58
View File
@@ -0,0 +1,58 @@
plugins {
id 'java'
id 'org.jetbrains.intellij' version '1.17.0'
}
group 'com.github.idea-claude-code-gui'
version '0.0.1'
sourceCompatibility = 17
targetCompatibility = 17
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains:annotations:24.0.0'
implementation 'com.google.code.gson:gson:2.10.1'
}
// 配置 IntelliJ Platform 插件
intellij {
version = '2023.3.2' // 目标 IDEA 版本
type = 'IC' // IC = IntelliJ IDEA Community Edition
// 下载源码和 JavaDoc
downloadSources = true
// 插件依赖(如果需要)
plugins = []
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
patchPluginXml {
sinceBuild = '233'
untilBuild = '252.*' // 更新为支持2024.2版本
changeNotes = """
<ul>
<li>初始版本发布</li>
<li>添加 Vue.js 视图支持</li>
<li>实现右侧工具栏窗口</li>
<li>更新支持IDEA 2024.2版本</li>
</ul>
"""
}
runIde {
// 启用 JCEF 支持
jvmArgs '-Djcef.sandbox.enable=false'
}
buildSearchableOptions {
enabled = false
}
+21
View File
@@ -0,0 +1,21 @@
plugins {
kotlin("jvm") version "2.2.20"
}
group = "org.example"
version = "0.0.1"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}
+533
View File
@@ -0,0 +1,533 @@
# Claude Code 本地历史消息记录读取原理
---
## 概述
Claude Code 是 Cursor 编辑器内置的 AI 助手,它会将所有的对话历史记录以 **JSONL 格式**(JSON Lines)存储在本地文件系统中。通过直接读取这些文件,我们可以实现历史记录的查看、搜索和统计功能。
### 核心特点
- **本地存储**:所有数据存储在用户主目录的 `.claude` 文件夹
- **JSONL 格式**:每行一个 JSON 对象,易于追加和解析
- **项目隔离**:每个项目的会话独立存储
- **无需 API**:直接读取文件系统,无需网络请求
---
## 数据存储位置
### 1. 主目录结构
```
~/.claude/
├── history.jsonl # 全局历史记录索引(已废弃,主要用旧版)
└── projects/ # 项目会话目录
├── {sanitized-path-1}/ # 项目1的目录(路径被转义)
│ ├── {session-id-1}.jsonl # 会话1
│ ├── {session-id-2}.jsonl # 会话2
│ └── ...
├── {sanitized-path-2}/ # 项目2的目录
│ └── ...
└── ...
```
### 2. 路径转义规则
项目路径会被转义为文件系统安全的名称:
```java
// 将所有非字母数字字符替换为 -
String sanitizedPath = projectPath.replaceAll("[^a-zA-Z0-9]", "-");
```
**示例:**
```
原始路径: /Users/john/Desktop/my-project
转义后: -Users-john-Desktop-my-project
```
---
## 数据结构分析
### 1. history.jsonl(历史索引文件)
每行是一个 JSON 对象,记录了单条历史消息:
```json
{
"display": "用户的消息内容",
"pastedContents": {},
"timestamp": 1700000000000,
"project": "/path/to/project",
"sessionId": "session-uuid-xxxx"
}
```
**字段说明:**
- `display`: 显示的消息内容
- `pastedContents`: 粘贴的内容(如代码片段)
- `timestamp`: Unix 时间戳(毫秒)
- `project`: 项目路径
- `sessionId`: 会话ID
### 2. 会话文件 (.jsonl)
每个会话文件包含该会话的所有消息,每行一个消息对象:
```json
{
"uuid": "msg-uuid-xxxx",
"sessionId": "session-uuid-xxxx",
"parentUuid": "parent-msg-uuid",
"timestamp": "2025-11-18T20:16:42.310Z",
"type": "user",
"message": {
"role": "user",
"content": "这是用户的消息"
},
"isMeta": false,
"isSidechain": false,
"cwd": "/path/to/project"
}
```
**字段说明:**
- `uuid`: 消息唯一标识
- `sessionId`: 所属会话ID
- `parentUuid`: 父消息ID(用于构建对话树)
- `timestamp`: ISO 8601 格式时间戳
- `type`: 消息类型(`user``assistant`
- `message.content`: 消息内容(可能是字符串或数组)
- `isMeta`: 是否为元消息(系统消息)
- `isSidechain`: 是否为侧链消息
---
## 读取原理
### 核心流程图
```
┌─────────────────┐
│ 获取项目路径 │
└────────┬────────┘
┌─────────────────┐
│ 路径转义处理 │ projectPath.replaceAll("[^a-zA-Z0-9]", "-")
└────────┬────────┘
┌─────────────────┐
│ 定位项目目录 │ ~/.claude/projects/{sanitized-path}/
└────────┬────────┘
┌─────────────────┐
│ 遍历.jsonl文件 │ 读取目录下所有 *.jsonl 文件
└────────┬────────┘
┌─────────────────┐
│ 解析JSONL格式 │ 逐行读取,每行解析为JSON对象
└────────┬────────┘
┌─────────────────┐
│ 提取会话信息 │ 生成会话摘要、统计消息数
└────────┬────────┘
┌─────────────────┐
│ 过滤无效会话 │ 排除 Warmup、agent-xxx 等
└────────┬────────┘
┌─────────────────┐
│ 按时间排序 │ 最新的会话在前
└────────┬────────┘
┌─────────────────┐
│ 返回结果数据 │ JSON格式返回给前端
└─────────────────┘
```
---
## 技术实现细节
### 1. JSONL 文件读取
```java
// 使用 BufferedReader 逐行读取
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().isEmpty()) continue;
try {
// 使用 Gson 解析每一行
ConversationMessage msg = gson.fromJson(line, ConversationMessage.class);
if (msg != null) {
messages.add(msg);
}
} catch (Exception e) {
// 跳过解析失败的行
}
}
}
```
**关键点:**
- JSONL 格式每行是独立的 JSON,便于流式处理
- 使用 Gson 库进行 JSON 解析
- 错误容忍:单行解析失败不影响整体
### 2. 会话摘要生成
从会话消息中提取第一条用户消息作为摘要:
```java
private String generateSummary(List<ConversationMessage> messages) {
for (ConversationMessage msg : messages) {
if ("user".equals(msg.type) &&
(msg.isMeta == null || !msg.isMeta) &&
msg.message != null &&
msg.message.content != null) {
String text = extractTextFromContent(msg.message.content);
if (text != null && !text.isEmpty()) {
// 去除换行符并截断
text = text.replace("\n", " ").trim();
if (text.length() > 45) {
text = text.substring(0, 45) + "...";
}
return text;
}
}
}
return null;
}
```
**策略:**
- 查找第一条非 meta 的用户消息
- 提取文本内容(content 可能是字符串或数组)
- 截断到 45 字符,添加省略号
### 3. 时间戳处理
支持多种时间戳格式:
```java
private long parseTimestamp(String timestamp) {
try {
// ISO 8601 格式: "2025-11-18T20:16:42.310Z"
java.time.Instant instant = java.time.Instant.parse(timestamp);
return instant.toEpochMilli();
} catch (Exception e) {
return 0;
}
}
```
---
## 会话过滤机制
为了提供更好的用户体验,需要过滤掉无效会话:
### 过滤规则
```java
private boolean isValidSession(String sessionId, String summary, int messageCount) {
// 1. 过滤 agent-xxx 格式的会话(都是 Warmup
if (sessionId != null && sessionId.startsWith("agent-")) {
return false;
}
// 2. 过滤摘要为空的会话
if (summary == null || summary.isEmpty()) {
return false;
}
// 3. 过滤 "Warmup" 或 "No prompt" 会话
String lowerSummary = summary.toLowerCase();
if (lowerSummary.equals("warmup") ||
lowerSummary.equals("no prompt") ||
lowerSummary.startsWith("warmup") ||
lowerSummary.startsWith("no prompt")) {
return false;
}
// 4. 过滤消息数太少的会话(少于2条)
if (messageCount < 2) {
return false;
}
return true;
}
```
### 过滤原因
| 类型 | 原因 | 示例 |
|------|------|------|
| `agent-xxx` | 系统内部会话 | `agent-warmup-12345` |
| 空摘要 | 无实际内容 | 只有系统消息 |
| "Warmup" | 预热会话 | 系统启动时的测试 |
| "No prompt" | 空提示 | 用户未输入内容 |
| 消息数 < 2 | 不完整对话 | 只有一条消息 |
---
## 实际应用场景
### 1. IntelliJ IDEA 插件集成
**实现类:** `VueHelloToolWindowFactorySimple.java`
```java
public VueHelloToolWindow(String projectPath) {
this.projectPath = projectPath;
this.historyReader = new ClaudeHistoryReader();
// 获取当前项目的历史数据
String jsonData = historyReader.getProjectDataAsJson(projectPath);
// 使用 JCEF 浏览器组件渲染 HTML
JBCefBrowser browser = new JBCefBrowser();
String htmlContent = generateHtmlWithData(jsonData);
browser.loadHTML(htmlContent);
}
```
**功能:**
- 在 IDE 侧边栏显示当前项目的 Claude 历史
- 实时加载,无需刷新
- Vue.js 渲染,交互流畅
### 2. Web 端历史查看器
**文件:** `claude-real-history.html`
**特点:**
- 完整的 Web UI,使用 Vue 3
- 支持搜索、过滤、导出
- 需要后端 APINode.js 服务)
### 3. 命令行工具
```java
public static void main(String[] args) {
ClaudeHistoryReader reader = new ClaudeHistoryReader();
// 读取历史
List<HistoryEntry> history = reader.readHistory();
System.out.println("历史记录条数: " + history.size());
// 获取项目列表
List<ProjectInfo> projects = reader.getProjects(history);
System.out.println("项目数: " + projects.size());
// 输出 JSON
System.out.println(reader.getAllDataAsJson());
}
```
---
## 注意事项
### ⚠️ 安全性
1. **隐私保护**:历史记录可能包含敏感信息(代码、密钥等)
2. **权限控制**:确保只有授权用户能访问
3. **数据加密**:考虑对敏感数据进行加密存储
### ⚠️ 兼容性
1. **路径差异**
- macOS/Linux: `~/.claude/`
- Windows: `%USERPROFILE%\.claude\`
2. **格式变化**Claude Code 可能更新数据格式
- 使用错误容忍的解析方式
- 版本检测机制
3. **文件锁定**
- Claude Code 可能正在写入文件
- 使用只读模式打开
- 实现重试机制
### ⚠️ 性能优化
1. **大文件处理**
```java
// 限制返回的消息数量
history.size() > 200 ? history.subList(0, 200) : history
```
2. **异步读取**
```java
// 使用 Java 8 Stream API 并行处理
Files.list(projectDir)
.parallel()
.filter(path -> path.toString().endsWith(".jsonl"))
.forEach(path -> processFile(path));
```
3. **缓存机制**
- 缓存已读取的数据
- 监听文件变化,增量更新
---
## 数据流示意图
```
┌──────────────────────────────────────────────────────────┐
│ Cursor 编辑器 │
│ │
│ ┌─────────────┐ │
│ │ Claude AI │ 写入历史记录 │
│ │ Assistant │────────┐ │
│ └─────────────┘ │ │
└─────────────────────────┼─────────────────────────────────┘
┌───────────────────────┐
│ ~/.claude/projects/ │
│ {project}/ │
│ ├── session1.jsonl │
│ ├── session2.jsonl │
│ └── ... │
└───────────────────────┘
│ 读取
┌───────────────────────┐
│ ClaudeHistoryReader │
│ Java 读取器 │
│ - 解析 JSONL │
│ - 过滤会话 │
│ - 生成摘要 │
└───────────────────────┘
│ JSON API
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ IDEA 插件 │ │ Web 界面 │
│ (JCEF Browser) │ │ (Vue.js) │
│ - 嵌入式展示 │ │ - 全功能查看器 │
└──────────────────┘ └──────────────────┘
```
---
## 示例代码
### 完整读取流程
```java
// 1. 创建读取器
ClaudeHistoryReader reader = new ClaudeHistoryReader();
// 2. 读取指定项目的会话列表
String projectPath = "/Users/john/Desktop/my-project";
List<SessionInfo> sessions = reader.readProjectSessions(projectPath);
// 3. 输出会话信息
for (SessionInfo session : sessions) {
System.out.println("会话ID: " + session.sessionId);
System.out.println("标题: " + session.title);
System.out.println("消息数: " + session.messageCount);
System.out.println("时间: " + new Date(session.lastTimestamp));
System.out.println("---");
}
// 4. 获取 JSON 格式数据(用于前端显示)
String jsonData = reader.getProjectDataAsJson(projectPath);
System.out.println(jsonData);
```
### 输出示例
```json
{
"success": true,
"sessions": [
{
"sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "实现用户登录功能",
"messageCount": 15,
"lastTimestamp": 1700000000000,
"firstTimestamp": 1699999000000
},
{
"sessionId": "b2c3d4e5-f6g7-8901-bcde-f12345678901",
"title": "修复数据库连接问题",
"messageCount": 8,
"lastTimestamp": 1699998000000,
"firstTimestamp": 1699997000000
}
],
"currentProject": "/Users/john/Desktop/my-project",
"total": 23,
"sessionCount": 2
}
```
---
## 技术栈总结
| 组件 | 技术 | 用途 |
|------|------|------|
| 数据读取 | Java NIO | 文件系统操作 |
| JSON 解析 | Gson | JSON 序列化/反序列化 |
| UI 渲染 | JCEF (Chromium) | 嵌入式浏览器 |
| 前端框架 | Vue.js 3 | 响应式 UI |
| HTTP 客户端 | Axios | API 请求(Web版) |
| 数据格式 | JSONL | 行式 JSON 存储 |
---
## 扩展功能建议
### 🚀 可实现的功能
1. **全文搜索**:基于 Apache Lucene 实现
2. **数据统计**:消息数量、使用频率、时间分布
3. **导出功能**:导出为 Markdown、PDF
4. **会话恢复**:点击历史会话,在 Cursor 中恢复
5. **智能分类**:基于内容自动分类(Bug修复、功能开发等)
6. **数据同步**:跨设备同步历史记录
---
## 参考资料
- [JSONL 格式规范](http://jsonlines.org/)
- [Gson 用户指南](https://github.com/google/gson/blob/master/UserGuide.md)
- [Java NIO 文件操作](https://docs.oracle.com/javase/tutorial/essential/io/fileio.html)
- [IntelliJ Platform SDK](https://plugins.jetbrains.com/docs/intellij/welcome.html)
---
## 版本历史
| 版本 | 日期 | 变更说明 |
|------|------|----------|
| 1.0.0 | 2025-11-19 | 初始版本 |
---
**最后更新:** 2025年11月19日
+1
View File
@@ -0,0 +1 @@
kotlin.code.style=official
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#Wed Nov 19 01:08:29 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+234
View File
@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'idea-claude-code-gui'
+4
View File
@@ -0,0 +1,4 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "idea-claude-code-gui"
@@ -0,0 +1,259 @@
package com.github.claudecodegui;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.intellij.ui.jcef.JBCefBrowser;
import com.intellij.ui.jcef.JBCefJSQuery;
import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.handler.CefLoadHandlerAdapter;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* CC-GUI 工具窗口工厂类(完整版,包含 JavaScript 桥接)
*/
public class CCGuiToolWindowFactory implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
// 创建工具窗口内容
CCGuiToolWindow ccGuiToolWindow = new CCGuiToolWindow();
ContentFactory contentFactory = ContentFactory.getInstance();
Content content = contentFactory.createContent(
ccGuiToolWindow.getContent(),
"CC-GUI",
false
);
toolWindow.getContentManager().addContent(content);
}
/**
* CC-GUI 工具窗口内容类
*/
private static class CCGuiToolWindow {
private JPanel mainPanel;
public CCGuiToolWindow() {
createUIComponents();
}
private void createUIComponents() {
mainPanel = new JPanel(new BorderLayout());
try {
// 使用 JCEF (Java Chromium Embedded Framework) 显示 HTML 内容
JBCefBrowser browser = new JBCefBrowser();
// 读取 HTML 文件内容
String htmlContent = loadHtmlContent();
// 加载 HTML 内容
browser.loadHTML(htmlContent);
// 添加浏览器组件到面板
mainPanel.add(browser.getComponent(), BorderLayout.CENTER);
// 可选:添加 Java-JavaScript 交互桥接
setupJavaScriptBridge(browser);
} catch (Exception e) {
// 如果 JCEF 不可用,显示备用内容
JLabel label = new JLabel("CC-GUI - Claude Code GUI", SwingConstants.CENTER);
label.setFont(new Font("Microsoft YaHei", Font.BOLD, 24));
mainPanel.add(label, BorderLayout.CENTER);
// 显示错误信息
JTextArea errorArea = new JTextArea("注意:JCEF 组件未能加载\n" + e.getMessage());
errorArea.setEditable(false);
JScrollPane scrollPane = new JScrollPane(errorArea);
scrollPane.setPreferredSize(new Dimension(400, 100));
mainPanel.add(scrollPane, BorderLayout.SOUTH);
}
}
/**
* 加载 HTML 内容
*/
private String loadHtmlContent() {
try {
// 从资源文件中读取 HTML
InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("html/index.html");
if (inputStream != null) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 如果无法加载文件,返回默认 HTML
return getDefaultHtml();
}
/**
* 获取默认的 HTML 内容
*/
private String getDefaultHtml() {
return """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
#app {
text-align: center;
color: white;
}
h1 {
font-size: 48px;
margin-bottom: 20px;
}
.button {
padding: 10px 20px;
font-size: 16px;
background: white;
color: #764ba2;
border: none;
border-radius: 20px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="app">
<h1>{{ message }}</h1>
<button class="button" @click="count++">
点击次数: {{ count }}
</button>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
message: 'CC-GUI',
count: 0
}
}
}).mount('#app');
</script>
</body>
</html>
""";
}
/**
* 设置 JavaScript 桥接
* 允许 JavaScript 与 Java 代码交互
*/
private void setupJavaScriptBridge(JBCefBrowser browser) {
// 创建 Claude 历史记录读取器
ClaudeHistoryReader historyReader = new ClaudeHistoryReader();
// 创建 JS 查询对象
JBCefJSQuery jsQuery = JBCefJSQuery.create(browser);
// 注册回调处理器
jsQuery.addHandler((request) -> {
try {
// 解析请求
String[] parts = request.split("\\|", 2);
String endpoint = parts[0];
Map<String, String> params = new HashMap<>();
if (parts.length > 1) {
// 解析参数
String[] paramPairs = parts[1].split("&");
for (String pair : paramPairs) {
String[] keyValue = pair.split("=", 2);
if (keyValue.length == 2) {
params.put(keyValue[0], keyValue[1]);
}
}
}
// 调用历史记录读取器处理请求
String response = historyReader.handleApiRequest(endpoint, params);
return new JBCefJSQuery.Response(response);
} catch (Exception e) {
return new JBCefJSQuery.Response(null, 0, e.getMessage());
}
});
// 页面加载完成后注入JavaScript代码
browser.getJBCefClient().addLoadHandler(new CefLoadHandlerAdapter() {
@Override
public void onLoadEnd(CefBrowser cefBrowser, CefFrame frame, int httpStatusCode) {
// 注入全局 API 对象
String jsCode = jsQuery.inject("request",
"function(response) {" +
" return response;" +
"}",
"function(error_code, error_msg) {" +
" console.error('Error:', error_code, error_msg);" +
" return null;" +
"}"
);
String injection =
"window.ClaudeAPI = {" +
" fetchHistory: function(callback) {" +
" " + jsCode + "('/history', function(response) {" +
" try { callback(JSON.parse(response)); } catch(e) { console.error(e); }" +
" });" +
" }," +
" fetchStats: function(callback) {" +
" " + jsCode + "('/stats', function(response) {" +
" try { callback(JSON.parse(response)); } catch(e) { console.error(e); }" +
" });" +
" }," +
" search: function(query, callback) {" +
" " + jsCode + "('/search|q=' + encodeURIComponent(query), function(response) {" +
" try { callback(JSON.parse(response)); } catch(e) { console.error(e); }" +
" });" +
" }," +
" fetchProject: function(path, callback) {" +
" " + jsCode + "('/project|path=' + encodeURIComponent(path), function(response) {" +
" try { callback(JSON.parse(response)); } catch(e) { console.error(e); }" +
" });" +
" }" +
"};" +
"console.log('ClaudeAPI injected successfully');";
cefBrowser.executeJavaScript(injection, "", 0);
}
}, browser.getCefBrowser());
}
public JPanel getContent() {
return mainPanel;
}
}
}
@@ -0,0 +1,323 @@
package com.github.claudecodegui;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.intellij.ui.jcef.JBCefBrowser;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
/**
* CC-GUI 工具窗口工厂类(简化版)
*/
public class CCGuiToolWindowFactorySimple implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
String projectPath = project.getBasePath();
CCGuiToolWindow ccGuiToolWindow = new CCGuiToolWindow(projectPath);
ContentFactory contentFactory = ContentFactory.getInstance();
Content content = contentFactory.createContent(
ccGuiToolWindow.getContent(),
"Claude History",
false
);
toolWindow.getContentManager().addContent(content);
}
private static class CCGuiToolWindow {
private JPanel mainPanel;
private ClaudeHistoryReader historyReader;
private String projectPath;
public CCGuiToolWindow(String projectPath) {
this.projectPath = projectPath;
this.historyReader = new ClaudeHistoryReader();
createUIComponents();
}
private void createUIComponents() {
mainPanel = new JPanel(new BorderLayout());
try {
JBCefBrowser browser = new JBCefBrowser();
// 获取当前项目的数据
String jsonData = historyReader.getProjectDataAsJson(projectPath);
// 生成HTML
String htmlContent = generateHtmlWithData(jsonData);
// 加载HTML
browser.loadHTML(htmlContent);
mainPanel.add(browser.getComponent(), BorderLayout.CENTER);
} catch (Exception e) {
// 备用显示
JTextArea textArea = new JTextArea();
textArea.setEditable(false);
textArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
try {
String jsonData = historyReader.getProjectDataAsJson(projectPath);
textArea.setText("Claude历史数据 (JSON格式):\n\n" + jsonData);
} catch (Exception ex) {
textArea.setText("无法加载数据: " + ex.getMessage());
}
mainPanel.add(new JScrollPane(textArea), BorderLayout.CENTER);
}
}
private String generateHtmlWithData(String jsonData) {
// 正确的转义顺序很重要!
String escapedJson = jsonData
.replace("\\", "\\\\") // 先转义反斜杠
.replace("\"", "\\\"") // 再转义双引号
.replace("'", "\\'") // 转义单引号
.replace("\n", "\\n") // 转义换行
.replace("\r", "\\r"); // 转义回车
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n");
html.append("<html>\n");
html.append("<head>\n");
html.append("<meta charset=\"UTF-8\">\n");
html.append("<script src=\"https://unpkg.com/vue@3/dist/vue.global.js\"></script>\n");
html.append("<style>\n");
html.append(":root {\n");
html.append(" --bg-color: #1e1e1e;\n");
html.append(" --card-bg: #252526;\n");
html.append(" --text-primary: #cccccc;\n");
html.append(" --text-secondary: #858585;\n");
html.append(" --accent-color: #4a90e2;\n");
html.append(" --border-color: #3e3e42;\n");
html.append(" --user-msg-bg: #2d2d2d;\n");
html.append(" --ai-msg-bg: #252526;\n");
html.append("}\n");
html.append("body {\n");
html.append(" font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\n");
html.append(" background: var(--bg-color);\n");
html.append(" color: var(--text-primary);\n");
html.append(" margin: 0;\n");
html.append(" padding: 0;\n");
html.append(" height: 100vh;\n");
html.append(" display: flex;\n");
html.append(" flex-direction: column;\n");
html.append("}\n");
html.append(".header {\n");
html.append(" padding: 16px;\n");
html.append(" background: var(--card-bg);\n");
html.append(" border-bottom: 1px solid var(--border-color);\n");
html.append(" position: sticky;\n");
html.append(" top: 0;\n");
html.append(" z-index: 100;\n");
html.append("}\n");
html.append("h1 {\n");
html.append(" font-size: 18px;\n");
html.append(" margin: 0 0 8px 0;\n");
html.append(" color: var(--text-primary);\n");
html.append(" display: flex;\n");
html.append(" align-items: center;\n");
html.append(" gap: 8px;\n");
html.append("}\n");
html.append(".project-path {\n");
html.append(" font-size: 12px;\n");
html.append(" color: var(--text-secondary);\n");
html.append(" word-break: break-all;\n");
html.append("}\n");
html.append(".stats {\n");
html.append(" display: flex;\n");
html.append(" gap: 16px;\n");
html.append(" font-size: 12px;\n");
html.append(" color: var(--text-secondary);\n");
html.append(" margin-top: 8px;\n");
html.append("}\n");
html.append(".message-list {\n");
html.append(" flex: 1;\n");
html.append(" overflow-y: auto;\n");
html.append(" padding: 16px;\n");
html.append("}\n");
html.append(".message-item {\n");
html.append(" background: var(--card-bg);\n");
html.append(" border: 1px solid #3e3e42;\n");
html.append(" border-radius: 8px;\n");
html.append(" padding: 16px;\n");
html.append(" margin-bottom: 12px;\n");
html.append(" transition: background-color 0.2s;\n");
html.append(" cursor: pointer;\n");
html.append("}\n");
html.append(".message-item:hover {\n");
html.append(" background: #2d2d2d;\n");
html.append("}\n");
html.append(".message-header {\n");
html.append(" display: flex;\n");
html.append(" justify-content: space-between;\n");
html.append(" margin-bottom: 24px;\n");
html.append("}\n");
html.append(".message-title {\n");
html.append(" font-size: 15px;\n");
html.append(" font-weight: 600;\n");
html.append(" color: #e0e0e0;\n");
html.append(" white-space: nowrap;\n");
html.append(" overflow: hidden;\n");
html.append(" text-overflow: ellipsis;\n");
html.append(" margin-right: 16px;\n");
html.append(" flex: 1;\n");
html.append("}\n");
html.append(".message-time {\n");
html.append(" font-size: 13px;\n");
html.append(" color: #858585;\n");
html.append(" white-space: nowrap;\n");
html.append("}\n");
html.append(".message-footer {\n");
html.append(" display: flex;\n");
html.append(" justify-content: space-between;\n");
html.append(" align-items: center;\n");
html.append(" font-size: 13px;\n");
html.append(" color: #858585;\n");
html.append("}\n");
html.append(".message-id {\n");
html.append(" font-family: monospace;\n");
html.append(" color: #666;\n");
html.append("}\n");
html.append(".empty-state {\n");
html.append(" display: flex;\n");
html.append(" flex-direction: column;\n");
html.append(" align-items: center;\n");
html.append(" justify-content: center;\n");
html.append(" height: 100%;\n");
html.append(" color: var(--text-secondary);\n");
html.append(" text-align: center;\n");
html.append("}\n");
html.append("::-webkit-scrollbar {\n");
html.append(" width: 8px;\n");
html.append("}\n");
html.append("::-webkit-scrollbar-track {\n");
html.append(" background: var(--bg-color);\n");
html.append("}\n");
html.append("::-webkit-scrollbar-thumb {\n");
html.append(" background: #424242;\n");
html.append(" border-radius: 4px;\n");
html.append("}\n");
html.append("::-webkit-scrollbar-thumb:hover {\n");
html.append(" background: #4f4f4f;\n");
html.append("}\n");
html.append("</style>\n");
html.append("</head>\n");
html.append("<body>\n");
html.append("<div id=\"app\">\n");
// 头部区域
html.append(" <div class=\"header\">\n");
html.append(" <h1>🤖 Claude 项目历史</h1>\n");
html.append(" <div class=\"project-path\" v-if=\"data && data.currentProject\">\n");
html.append(" {{ data.currentProject }}\n");
html.append(" </div>\n");
html.append(" <div class=\"stats\" v-if=\"data && data.success\">\n");
html.append(" <span>📝 {{ data.sessions ? data.sessions.length : 0 }} 个会话</span>\n");
html.append(" <span>💬 {{ data.total || 0 }} 条消息</span>\n");
html.append(" </div>\n");
html.append(" </div>\n");
// 内容区域
html.append(" <div class=\"message-list\" v-if=\"data && data.sessions && data.sessions.length > 0\">\n");
html.append(" <div v-for=\"session in data.sessions\" :key=\"session.sessionId\" class=\"message-item\">\n");
html.append(" <div class=\"message-header\">\n");
html.append(" <div class=\"message-title\">{{ session.title }}</div>\n");
html.append(" <div class=\"message-time\">{{ timeAgo(session.lastTimestamp) }}</div>\n");
html.append(" </div>\n");
html.append(" <div class=\"message-footer\">\n");
html.append(" <span>{{ session.messageCount }} 条消息</span>\n");
html.append(" <span class=\"message-id\">{{ session.sessionId.substring(0, 8) }}</span>\n");
html.append(" </div>\n");
html.append(" </div>\n");
html.append(" </div>\n");
// 空状态
html.append(" <div class=\"empty-state\" v-else-if=\"data && data.success\">\n");
html.append(" <h3>暂无历史会话</h3>\n");
html.append(" <p>当前项目下没有找到 Claude 会话记录</p>\n");
html.append(" </div>\n");
// 错误状态
html.append(" <div v-else class=\"empty-state\">\n");
html.append(" <h3>⚠️ 加载失败</h3>\n");
html.append(" <p>{{ error || (data && data.error) || '未知错误' }}</p>\n");
html.append(" </div>\n");
html.append("</div>\n");
html.append("<script>\n");
html.append("console.log('Starting Vue initialization...');\n");
html.append("console.log('Vue available:', typeof Vue !== 'undefined');\n");
html.append("if (typeof Vue === 'undefined') {\n");
html.append(" console.error('Vue is not loaded!');\n");
html.append(" document.getElementById('app').innerHTML = '<div style=\"color:red;padding:20px;\">错误:Vue.js 未加载</div>';\n");
html.append("} else {\n");
html.append(" const { createApp } = Vue;\n");
html.append(" const claudeDataStr = '").append(escapedJson).append("';\n");
html.append(" console.log('Data string length:', claudeDataStr.length);\n");
html.append(" let claudeData = null;\n");
html.append(" try {\n");
html.append(" claudeData = JSON.parse(claudeDataStr);\n");
html.append(" console.log('Parsed data:', claudeData);\n");
html.append(" } catch(e) {\n");
html.append(" console.error('Failed to parse data:', e);\n");
html.append(" console.error('Data string:', claudeDataStr.substring(0, 200));\n");
html.append(" }\n");
html.append(" \n");
html.append(" const app = createApp({\n");
html.append(" data() {\n");
html.append(" return {\n");
html.append(" data: claudeData,\n");
html.append(" error: claudeData ? null : 'Failed to parse data'\n");
html.append(" }\n");
html.append(" },\n");
html.append(" methods: {\n");
html.append(" formatTime(timestamp) {\n");
html.append(" if (!timestamp) return '';\n");
html.append(" const date = new Date(timestamp);\n");
html.append(" return date.toLocaleString();\n");
html.append(" },\n");
html.append(" timeAgo(timestamp) {\n");
html.append(" if (!timestamp) return '';\n");
html.append(" const seconds = Math.floor((new Date() - new Date(timestamp)) / 1000);\n");
html.append(" let interval = seconds / 31536000;\n");
html.append(" if (interval > 1) return Math.floor(interval) + ' 年前';\n");
html.append(" interval = seconds / 2592000;\n");
html.append(" if (interval > 1) return Math.floor(interval) + ' 个月前';\n");
html.append(" interval = seconds / 86400;\n");
html.append(" if (interval > 1) return Math.floor(interval) + ' 天前';\n");
html.append(" interval = seconds / 3600;\n");
html.append(" if (interval > 1) return Math.floor(interval) + ' 小时前';\n");
html.append(" interval = seconds / 60;\n");
html.append(" if (interval > 1) return Math.floor(interval) + ' 分钟前';\n");
html.append(" return Math.floor(seconds) + ' 秒前';\n");
html.append(" }\n");
html.append(" },\n");
html.append(" mounted() {\n");
html.append(" console.log('Vue app mounted, data:', this.data);\n");
html.append(" }\n");
html.append(" });\n");
html.append(" \n");
html.append(" app.mount('#app');\n");
html.append(" console.log('Vue app mounted successfully');\n");
html.append("}\n");
html.append("</script>\n");
html.append("</body>\n");
html.append("</html>");
return html.toString();
}
public JPanel getContent() {
return mainPanel;
}
}
}
@@ -0,0 +1,603 @@
package com.github.claudecodegui;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
/**
* Claude本地历史记录读取器
* 直接从本地文件系统读取Claude的历史数据
*/
public class ClaudeHistoryReader {
private static final String HOME_DIR = System.getProperty("user.home");
private static final Path CLAUDE_DIR = Paths.get(HOME_DIR, ".claude");
private static final Path HISTORY_FILE = CLAUDE_DIR.resolve("history.jsonl");
private static final Path PROJECTS_DIR = CLAUDE_DIR.resolve("projects");
private final Gson gson = new Gson();
/**
* 历史记录条目
*/
public static class HistoryEntry {
public String display;
public Map<String, Object> pastedContents;
public long timestamp;
public String project;
public String sessionId;
public HistoryEntry() {
this.pastedContents = new HashMap<>();
}
}
/**
* 项目信息
*/
public static class ProjectInfo {
public String path;
public String name;
public int count;
public long lastAccess;
public List<HistoryEntry> messages;
public ProjectInfo(String path) {
this.path = path;
this.name = path != null ? Paths.get(path).getFileName().toString() : "Root";
if (this.name.isEmpty()) {
this.name = "Root";
}
this.count = 0;
this.lastAccess = 0;
this.messages = new ArrayList<>();
}
}
/**
* 会话消息(从 projects 目录读取)
*/
public static class ConversationMessage {
public String uuid;
public String sessionId;
public String parentUuid;
public String timestamp;
public String type;
public Message message;
public Boolean isMeta;
public Boolean isSidechain;
public String cwd;
public static class Message {
public String role;
public Object content; // 可能是 String 或 Array
}
}
/**
* 从 projects 目录读取项目的所有会话
*/
public List<SessionInfo> readProjectSessions(String projectPath) throws IOException {
List<SessionInfo> sessions = new ArrayList<>();
if (projectPath == null || projectPath.isEmpty()) {
return sessions;
}
// 转换项目路径为安全的目录名(与 VSCode 扩展逻辑一致)
String sanitizedPath = projectPath.replaceAll("[^a-zA-Z0-9]", "-");
Path projectDir = PROJECTS_DIR.resolve(sanitizedPath);
if (!Files.exists(projectDir) || !Files.isDirectory(projectDir)) {
return sessions;
}
// 读取项目目录下所有 .jsonl 文件
Map<String, List<ConversationMessage>> sessionMessagesMap = new HashMap<>();
Files.list(projectDir)
.filter(path -> path.toString().endsWith(".jsonl"))
.filter(path -> {
try {
return Files.size(path) > 0;
} catch (IOException e) {
return false;
}
})
.forEach(path -> {
try (BufferedReader reader = Files.newBufferedReader(path)) {
// 从文件名提取 sessionId
String fileName = path.getFileName().toString();
String sessionId = fileName.substring(0, fileName.lastIndexOf(".jsonl"));
List<ConversationMessage> messages = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().isEmpty()) continue;
try {
ConversationMessage msg = gson.fromJson(line, ConversationMessage.class);
if (msg != null) {
messages.add(msg);
}
} catch (Exception e) {
// 跳过解析失败的行
}
}
if (!messages.isEmpty()) {
sessionMessagesMap.put(sessionId, messages);
}
} catch (Exception e) {
System.err.println("读取对话文件失败: " + path + " - " + e.getMessage());
}
});
// 为每个会话生成 SessionInfo
for (Map.Entry<String, List<ConversationMessage>> entry : sessionMessagesMap.entrySet()) {
String sessionId = entry.getKey();
List<ConversationMessage> messages = entry.getValue();
if (messages.isEmpty()) continue;
// 生成摘要:找到第一条非 meta 的用户消息
String summary = generateSummary(messages);
// 获取最后一条消息的时间戳
long lastTimestamp = 0;
for (ConversationMessage msg : messages) {
if (msg.timestamp != null) {
try {
long ts = parseTimestamp(msg.timestamp);
if (ts > lastTimestamp) {
lastTimestamp = ts;
}
} catch (Exception e) {
// 忽略无效的时间戳
}
}
}
// 过滤无效会话
if (!isValidSession(sessionId, summary, messages.size())) {
continue;
}
SessionInfo session = new SessionInfo();
session.sessionId = sessionId;
session.title = summary;
session.messageCount = messages.size();
session.lastTimestamp = lastTimestamp;
session.firstTimestamp = lastTimestamp; // 简化处理
sessions.add(session);
}
// 按最后更新时间倒序排序
sessions.sort((a, b) -> Long.compare(b.lastTimestamp, a.lastTimestamp));
return sessions;
}
/**
* 生成会话摘要
*/
private String generateSummary(List<ConversationMessage> messages) {
for (ConversationMessage msg : messages) {
if ("user".equals(msg.type) &&
(msg.isMeta == null || !msg.isMeta) &&
msg.message != null &&
msg.message.content != null) {
String text = extractTextFromContent(msg.message.content);
if (text != null && !text.isEmpty()) {
// 去除换行符并截断
text = text.replace("\n", " ").trim();
if (text.length() > 45) {
text = text.substring(0, 45) + "...";
}
return text;
}
}
}
return null; // 返回 null 表示没有有效内容
}
/**
* 判断会话是否有效(过滤掉 Warmup、No prompt 等无效会话)
*/
private boolean isValidSession(String sessionId, String summary, int messageCount) {
// 过滤 agent-xxx 格式的会话(都是 Warmup
if (sessionId != null && sessionId.startsWith("agent-")) {
return false;
}
// 过滤摘要为空或无效的会话
if (summary == null || summary.isEmpty()) {
return false;
}
// 过滤只有 "Warmup" 或 "No prompt" 的会话
String lowerSummary = summary.toLowerCase();
if (lowerSummary.equals("warmup") ||
lowerSummary.equals("no prompt") ||
lowerSummary.startsWith("warmup") ||
lowerSummary.startsWith("no prompt")) {
return false;
}
// 过滤消息数太少的会话(少于2条消息通常没什么内容)
if (messageCount < 2) {
return false;
}
return true;
}
/**
* 从 content 提取文本
*/
private String extractTextFromContent(Object content) {
if (content instanceof String) {
return (String) content;
} else if (content instanceof List) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> contentList = (List<Map<String, Object>>) content;
// 从后向前查找最后一个 text 类型的项
for (int i = contentList.size() - 1; i >= 0; i--) {
Map<String, Object> item = contentList.get(i);
if ("text".equals(item.get("type"))) {
Object text = item.get("text");
if (text instanceof String) {
return (String) text;
}
}
}
}
return null;
}
/**
* 解析时间戳(支持 ISO 8601 格式)
*/
private long parseTimestamp(String timestamp) {
try {
// ISO 8601 格式如 "2025-11-18T20:16:42.310Z"
java.time.Instant instant = java.time.Instant.parse(timestamp);
return instant.toEpochMilli();
} catch (Exception e) {
return 0;
}
}
/**
* 会话信息
*/
public static class SessionInfo {
public String sessionId;
public String title;
public int messageCount;
public long lastTimestamp;
public long firstTimestamp;
}
/**
* 统计信息
*/
public static class Statistics {
public int totalMessages;
public int totalProjects;
public HistoryEntry firstMessage;
public HistoryEntry lastMessage;
public Map<String, Integer> messagesByDay;
public Statistics() {
this.messagesByDay = new HashMap<>();
}
}
/**
* API响应
*/
public static class ApiResponse {
public boolean success;
public String error;
public Object data;
public static ApiResponse success(Object data) {
ApiResponse response = new ApiResponse();
response.success = true;
response.data = data;
return response;
}
public static ApiResponse error(String message) {
ApiResponse response = new ApiResponse();
response.success = false;
response.error = message;
return response;
}
}
/**
* 读取所有历史记录
*/
public List<HistoryEntry> readHistory() throws IOException {
List<HistoryEntry> history = new ArrayList<>();
if (!Files.exists(HISTORY_FILE)) {
return history;
}
try (BufferedReader reader = Files.newBufferedReader(HISTORY_FILE)) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.trim().isEmpty()) {
try {
HistoryEntry entry = gson.fromJson(line, HistoryEntry.class);
if (entry != null) {
history.add(entry);
}
} catch (Exception e) {
System.err.println("解析行失败: " + e.getMessage());
}
}
}
}
// 按时间戳排序(最新的在前)
history.sort((a, b) -> Long.compare(b.timestamp, a.timestamp));
return history;
}
/**
* 获取项目列表
*/
public List<ProjectInfo> getProjects(List<HistoryEntry> history) {
Map<String, ProjectInfo> projectsMap = new HashMap<>();
for (HistoryEntry entry : history) {
if (entry.project != null) {
ProjectInfo project = projectsMap.computeIfAbsent(
entry.project,
ProjectInfo::new
);
project.count++;
project.messages.add(entry);
if (entry.timestamp > project.lastAccess) {
project.lastAccess = entry.timestamp;
}
}
}
return projectsMap.values().stream()
.sorted((a, b) -> Long.compare(b.lastAccess, a.lastAccess))
.collect(Collectors.toList());
}
/**
* 获取统计信息
*/
public Statistics getStatistics(List<HistoryEntry> history) {
Statistics stats = new Statistics();
stats.totalMessages = history.size();
if (!history.isEmpty()) {
// 获取第一条和最后一条消息
List<HistoryEntry> sorted = new ArrayList<>(history);
sorted.sort(Comparator.comparingLong(e -> e.timestamp));
stats.firstMessage = sorted.get(0);
stats.lastMessage = sorted.get(sorted.size() - 1);
// 统计项目数
Set<String> projects = history.stream()
.map(e -> e.project)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
stats.totalProjects = projects.size();
// 按天统计消息
for (HistoryEntry entry : history) {
if (entry.timestamp > 0) {
Date date = new Date(entry.timestamp);
String dateStr = String.format("%tF", date);
stats.messagesByDay.merge(dateStr, 1, Integer::sum);
}
}
}
return stats;
}
/**
* 搜索历史记录
*/
public List<HistoryEntry> searchHistory(List<HistoryEntry> history, String query) {
if (query == null || query.trim().isEmpty()) {
return history;
}
String lowerQuery = query.toLowerCase();
return history.stream()
.filter(entry -> {
String display = entry.display != null ? entry.display.toLowerCase() : "";
return display.contains(lowerQuery);
})
.limit(100)
.collect(Collectors.toList());
}
/**
* 读取项目详情
*/
public Map<String, Object> getProjectDetails(String projectPath) {
Map<String, Object> details = new HashMap<>();
details.put("path", projectPath);
details.put("exists", false);
details.put("conversations", new ArrayList<>());
if (projectPath == null || projectPath.isEmpty()) {
return details;
}
// 将路径转换为文件系统安全的名称
String sanitizedPath = projectPath.replace("/", "-");
Path projectDir = PROJECTS_DIR.resolve(sanitizedPath);
if (Files.exists(projectDir) && Files.isDirectory(projectDir)) {
details.put("exists", true);
try {
List<Map<String, Object>> conversations = new ArrayList<>();
// 读取项目目录中的对话文件
Files.list(projectDir)
.filter(Files::isDirectory)
.forEach(subDir -> {
Path convFile = subDir.resolve("conversation.json");
if (Files.exists(convFile)) {
try {
String content = new String(Files.readAllBytes(convFile));
Map<String, Object> convData = new HashMap<>();
convData.put("id", subDir.getFileName().toString());
convData.put("data", JsonParser.parseString(content));
convData.put("timestamp", Files.getLastModifiedTime(convFile).toMillis());
conversations.add(convData);
} catch (Exception e) {
System.err.println("读取对话文件失败: " + e.getMessage());
}
}
});
details.put("conversations", conversations);
} catch (IOException e) {
System.err.println("读取项目详情失败: " + e.getMessage());
}
}
return details;
}
/**
* 获取指定项目的历史记录JSON字符串
*/
public String getProjectDataAsJson(String projectPath) {
try {
// 从 projects 目录读取会话列表
List<SessionInfo> sessions = readProjectSessions(projectPath);
// 计算总消息数
int totalMessages = sessions.stream()
.mapToInt(s -> s.messageCount)
.sum();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("sessions", sessions);
result.put("currentProject", projectPath);
result.put("total", totalMessages);
result.put("sessionCount", sessions.size());
return gson.toJson(result);
} catch (Exception e) {
return gson.toJson(ApiResponse.error("读取项目数据失败: " + e.getMessage()));
}
}
/**
* 获取所有数据的JSON字符串
*/
public String getAllDataAsJson() {
try {
List<HistoryEntry> history = readHistory();
List<ProjectInfo> projects = getProjects(history);
Statistics stats = getStatistics(history);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("history", history.size() > 200 ? history.subList(0, 200) : history);
result.put("projects", projects);
result.put("stats", stats);
result.put("total", history.size());
return gson.toJson(result);
} catch (Exception e) {
return gson.toJson(ApiResponse.error("读取数据失败: " + e.getMessage()));
}
}
/**
* 处理API请求
*/
public String handleApiRequest(String endpoint, Map<String, String> params) {
try {
switch (endpoint) {
case "/history":
return getAllDataAsJson();
case "/stats":
List<HistoryEntry> historyForStats = readHistory();
Statistics stats = getStatistics(historyForStats);
return gson.toJson(ApiResponse.success(stats));
case "/search":
String query = params.get("q");
List<HistoryEntry> historyForSearch = readHistory();
List<HistoryEntry> searchResults = searchHistory(historyForSearch, query);
Map<String, Object> searchResponse = new HashMap<>();
searchResponse.put("query", query);
searchResponse.put("count", searchResults.size());
searchResponse.put("results", searchResults);
return gson.toJson(ApiResponse.success(searchResponse));
case "/project":
String projectPath = params.get("path");
Map<String, Object> projectDetails = getProjectDetails(projectPath);
return gson.toJson(ApiResponse.success(projectDetails));
default:
return gson.toJson(ApiResponse.error("Unknown endpoint: " + endpoint));
}
} catch (Exception e) {
return gson.toJson(ApiResponse.error("处理请求失败: " + e.getMessage()));
}
}
/**
* 主方法用于测试
*/
public static void main(String[] args) {
ClaudeHistoryReader reader = new ClaudeHistoryReader();
try {
// 测试读取历史
List<HistoryEntry> history = reader.readHistory();
System.out.println("历史记录条数: " + history.size());
// 测试获取项目
List<ProjectInfo> projects = reader.getProjects(history);
System.out.println("项目数: " + projects.size());
// 测试获取统计
Statistics stats = reader.getStatistics(history);
System.out.println("总消息数: " + stats.totalMessages);
System.out.println("总项目数: " + stats.totalProjects);
// 输出JSON
System.out.println("\nJSON输出:");
System.out.println(reader.getAllDataAsJson());
} catch (Exception e) {
e.printStackTrace();
}
}
}
+16
View File
@@ -0,0 +1,16 @@
package org.example
//TIP 要<b>运行</b>代码,请按 <shortcut actionId="Run"/> 或
// 点击装订区域中的 <icon src="AllIcons.Actions.Execute"/> 图标。
fun main() {
val name = "Kotlin"
//TIP 当文本光标位于高亮显示的文本处时按 <shortcut actionId="ShowIntentionActions"/>
// 查看 IntelliJ IDEA 建议如何修正。
println("Hello, " + name + "!")
for (i in 1..5) {
//TIP 按 <shortcut actionId="Debug"/> 开始调试代码。我们已经设置了一个 <icon src="AllIcons.Debugger.Db_set_breakpoint"/> 断点
// 但您始终可以通过按 <shortcut actionId="ToggleLineBreakpoint"/> 添加更多断点。
println("i = $i")
}
}
+16
View File
@@ -0,0 +1,16 @@
<idea-plugin>
<id>com.github.idea-claude-code-gui</id>
<name>Claude Code GUI</name>
<vendor>Your Name</vendor>
<description>IntelliJ IDEA plugin for viewing Claude Code history with an intuitive GUI</description>
<depends>com.intellij.modules.platform</depends>
<extensions defaultExtensionNs="com.intellij">
<!-- 注册右侧工具窗口 -->
<toolWindow id="CC-GUI"
anchor="right"
factoryClass="com.github.claudecodegui.CCGuiToolWindowFactorySimple"
icon="/icons/cc-gui-icon.svg"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,638 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude History - Java Backend</title>
<!-- 引入 Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
background: #1a1a2e;
color: #eee;
height: 100vh;
overflow: hidden;
}
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.header h1 {
font-size: 24px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.stats-bar {
display: flex;
gap: 30px;
margin-top: 10px;
font-size: 14px;
opacity: 0.95;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.stat-value {
font-weight: bold;
color: #ffd700;
}
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 300px;
background: #16213e;
border-right: 1px solid #2a2a4e;
display: flex;
flex-direction: column;
}
.search-section {
padding: 15px;
border-bottom: 1px solid #2a2a4e;
}
.search-input {
width: 100%;
padding: 10px;
background: #0f3460;
border: 1px solid #2a2a4e;
border-radius: 6px;
color: white;
font-size: 14px;
}
.project-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.project-item {
background: #0f3460;
border: 1px solid #2a2a4e;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s;
}
.project-item:hover {
background: #1e5f8e;
transform: translateX(3px);
}
.project-item.active {
background: #667eea;
border-color: #764ba2;
}
.project-name {
font-weight: bold;
margin-bottom: 5px;
}
.project-path {
font-size: 11px;
color: #aaa;
margin-bottom: 8px;
word-break: break-all;
}
.project-stats {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #ccc;
}
.message-area {
flex: 1;
background: #0f3460;
display: flex;
flex-direction: column;
}
.message-header {
padding: 15px;
background: #16213e;
border-bottom: 1px solid #2a2a4e;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.message-item {
background: #16213e;
border: 1px solid #2a2a4e;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
animation: slideIn 0.3s ease;
}
.message-content {
margin-bottom: 8px;
line-height: 1.5;
}
.message-meta {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #888;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #2a2a4e;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.empty-icon {
font-size: 48px;
margin-bottom: 10px;
}
.error-message {
background: #e74c3c;
color: white;
padding: 10px;
border-radius: 6px;
margin: 10px;
}
.btn {
padding: 8px 16px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
margin: 5px;
}
.btn:hover {
background: #764ba2;
}
.connection-status {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
font-size: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4caf50;
animation: pulse 2s infinite;
}
.status-dot.error {
background: #f44336;
animation: none;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #0f3460;
}
::-webkit-scrollbar-thumb {
background: #2a2a4e;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #667eea;
}
</style>
</head>
<body>
<div id="app">
<div class="header">
<h1>
🤖 Claude 本地历史记录 (Java Backend)
<div class="connection-status">
<span class="status-dot" :class="{ error: !connected }"></span>
{{ connectionStatus }}
</div>
</h1>
<div class="stats-bar">
<div class="stat-item">
<span>📝</span>
<span>总消息:</span>
<span class="stat-value">{{ stats.totalMessages }}</span>
</div>
<div class="stat-item">
<span>📁</span>
<span>项目数:</span>
<span class="stat-value">{{ stats.totalProjects }}</span>
</div>
<div class="stat-item">
<span>📅</span>
<span>最后活动:</span>
<span class="stat-value">{{ lastActivityTime }}</span>
</div>
</div>
</div>
<div class="main-content">
<div class="sidebar">
<div class="search-section">
<input
type="text"
class="search-input"
v-model="searchQuery"
placeholder="搜索消息..."
@input="performSearch"
>
</div>
<div class="project-list">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="projects.length === 0" class="empty-state">
<div class="empty-icon">📭</div>
<p>暂无项目</p>
</div>
<div
v-else
v-for="project in projects"
:key="project.path"
:class="['project-item', { active: selectedProject === project }]"
@click="selectProject(project)"
>
<div class="project-name">📁 {{ project.name }}</div>
<div class="project-path">{{ project.path }}</div>
<div class="project-stats">
<span>💬 {{ project.count }}</span>
<span>{{ formatDate(project.lastAccess) }}</span>
</div>
</div>
</div>
</div>
<div class="message-area">
<div class="message-header">
<h3>{{ selectedProject ? selectedProject.name + ' 的消息' : '最近消息' }}</h3>
<div style="margin-top: 5px; font-size: 12px; color: #888;">
显示 {{ displayMessages.length }} / {{ totalMessages }} 条
</div>
</div>
<div class="message-list">
<div v-if="displayMessages.length === 0" class="empty-state">
<div class="empty-icon">💭</div>
<p>暂无消息</p>
</div>
<div
v-for="(msg, index) in displayMessages"
:key="index"
class="message-item"
>
<div class="message-content">{{ msg.display }}</div>
<div class="message-meta">
<span>{{ getProjectName(msg.project) }}</span>
<span>{{ formatDateTime(msg.timestamp) }}</span>
</div>
</div>
<div style="text-align: center; padding: 20px;">
<button
class="btn"
@click="loadMore"
v-if="hasMore"
>
加载更多
</button>
</div>
</div>
<div style="padding: 10px; background: #16213e; border-top: 1px solid #2a2a4e;">
<button class="btn" @click="refreshData">🔄 刷新</button>
<button class="btn" @click="exportData">📥 导出</button>
<button class="btn" @click="clearSelection">❌ 清除选择</button>
</div>
</div>
</div>
<div v-if="error" class="error-message">
⚠️ {{ error }}
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
// 数据
history: [],
projects: [],
stats: {
totalMessages: 0,
totalProjects: 0,
firstMessage: null,
lastMessage: null
},
// UI 状态
loading: false,
error: null,
connected: false,
connectionStatus: '检查中...',
searchQuery: '',
selectedProject: null,
// 显示相关
displayMessages: [],
displayLimit: 50,
totalMessages: 0
}
},
computed: {
lastActivityTime() {
if (this.stats.lastMessage) {
return this.formatDate(this.stats.lastMessage.timestamp);
}
return '无';
},
hasMore() {
return this.displayMessages.length < this.totalMessages;
}
},
methods: {
// 使用 Java 后端获取数据
async fetchHistory() {
this.loading = true;
this.error = null;
try {
// 检查 ClaudeAPI 是否可用
if (typeof window.ClaudeAPI === 'undefined') {
// 等待 API 注入
await new Promise(resolve => setTimeout(resolve, 1000));
if (typeof window.ClaudeAPI === 'undefined') {
throw new Error('ClaudeAPI 未初始化,请确保插件正确加载');
}
}
// 使用 Java 后端 API
window.ClaudeAPI.fetchHistory((response) => {
if (response.success) {
this.history = response.history || [];
this.projects = response.projects || [];
this.stats = response.stats || this.stats;
this.totalMessages = response.total || this.history.length;
this.displayMessages = this.history.slice(0, this.displayLimit);
this.connected = true;
this.connectionStatus = 'Java Backend';
this.loading = false;
} else {
throw new Error(response.error || '获取数据失败');
}
});
} catch (error) {
console.error('Error:', error);
this.error = error.message;
this.connected = false;
this.connectionStatus = '连接失败';
this.loading = false;
// 使用模拟数据
this.useMockData();
}
},
// 搜索
performSearch() {
if (!this.searchQuery) {
this.displayMessages = this.history.slice(0, this.displayLimit);
return;
}
if (window.ClaudeAPI && window.ClaudeAPI.search) {
window.ClaudeAPI.search(this.searchQuery, (response) => {
if (response.success) {
this.displayMessages = response.data.results || [];
}
});
} else {
// 本地搜索
const query = this.searchQuery.toLowerCase();
this.displayMessages = this.history.filter(msg =>
msg.display && msg.display.toLowerCase().includes(query)
);
}
},
// 选择项目
selectProject(project) {
this.selectedProject = project;
if (project && project.messages) {
this.displayMessages = project.messages.slice(0, this.displayLimit);
this.totalMessages = project.messages.length;
} else {
this.displayMessages = this.history.slice(0, this.displayLimit);
this.totalMessages = this.history.length;
}
},
// 清除选择
clearSelection() {
this.selectedProject = null;
this.displayMessages = this.history.slice(0, this.displayLimit);
this.totalMessages = this.history.length;
},
// 加载更多
loadMore() {
this.displayLimit += 50;
if (this.selectedProject && this.selectedProject.messages) {
this.displayMessages = this.selectedProject.messages.slice(0, this.displayLimit);
} else {
this.displayMessages = this.history.slice(0, this.displayLimit);
}
},
// 刷新数据
refreshData() {
this.displayLimit = 50;
this.fetchHistory();
},
// 导出数据
exportData() {
const dataStr = JSON.stringify({
history: this.history,
projects: this.projects,
stats: this.stats,
exportDate: new Date().toISOString()
}, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-history-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
},
// 使用模拟数据
useMockData() {
this.history = [
{ display: '示例消息 1', timestamp: Date.now(), project: '/example/project1' },
{ display: '示例消息 2', timestamp: Date.now() - 3600000, project: '/example/project2' },
{ display: '无法连接到本地数据,显示示例数据', timestamp: Date.now() - 7200000, project: '/demo' }
];
this.projects = [
{ path: '/example/project1', name: 'Project 1', count: 1, lastAccess: Date.now() },
{ path: '/example/project2', name: 'Project 2', count: 1, lastAccess: Date.now() - 3600000 }
];
this.displayMessages = this.history;
this.totalMessages = this.history.length;
this.stats = {
totalMessages: 3,
totalProjects: 2,
firstMessage: this.history[2],
lastMessage: this.history[0]
};
},
// 格式化函数
formatDate(timestamp) {
if (!timestamp) return '未知';
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()}`;
},
formatDateTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
},
getProjectName(path) {
if (!path) return '未知项目';
const parts = path.split('/');
return parts[parts.length - 1] || 'Root';
}
},
mounted() {
console.log('Claude History Viewer (Java Backend) 已启动');
// 立即尝试获取数据
this.fetchHistory();
// 定期刷新
setInterval(() => {
if (this.connected) {
this.fetchHistory();
}
}, 60000); // 每分钟刷新
}
}).mount('#app');
</script>
</body>
</html>
@@ -0,0 +1,882 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Real History Viewer</title>
<!-- 引入 Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 引入 Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
background: #1a1a2e;
color: #eee;
height: 100vh;
overflow: hidden;
}
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
/* 顶部栏 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.header h1 {
font-size: 24px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.claude-logo {
width: 30px;
height: 30px;
}
.stats-bar {
display: flex;
gap: 30px;
margin-top: 10px;
font-size: 14px;
opacity: 0.95;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.stat-value {
font-weight: bold;
color: #ffd700;
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
/* 左侧项目列表 */
.sidebar {
width: 300px;
background: #16213e;
border-right: 1px solid #2a2a4e;
display: flex;
flex-direction: column;
}
.search-section {
padding: 15px;
border-bottom: 1px solid #2a2a4e;
}
.search-input {
width: 100%;
padding: 10px;
background: #0f3460;
border: 1px solid #2a2a4e;
border-radius: 6px;
color: white;
font-size: 14px;
}
.search-input::placeholder {
color: #888;
}
.filter-buttons {
display: flex;
gap: 5px;
margin-top: 10px;
}
.filter-btn {
flex: 1;
padding: 6px;
background: #0f3460;
border: 1px solid #2a2a4e;
border-radius: 4px;
color: white;
font-size: 12px;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn:hover {
background: #1e5f8e;
}
.filter-btn.active {
background: #667eea;
border-color: #667eea;
}
.project-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.project-item {
background: #0f3460;
border: 1px solid #2a2a4e;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s;
}
.project-item:hover {
background: #1e5f8e;
transform: translateX(3px);
}
.project-item.active {
background: #667eea;
border-color: #764ba2;
}
.project-name {
font-weight: bold;
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 5px;
}
.project-path {
font-size: 11px;
color: #aaa;
margin-bottom: 8px;
word-break: break-all;
}
.project-stats {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #ccc;
}
/* 中间消息列表 */
.message-area {
flex: 1;
background: #0f3460;
display: flex;
flex-direction: column;
}
.message-header {
padding: 15px;
background: #16213e;
border-bottom: 1px solid #2a2a4e;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.message-item {
background: #16213e;
border: 1px solid #2a2a4e;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
animation: slideIn 0.3s ease;
}
.message-content {
margin-bottom: 8px;
line-height: 1.5;
}
.message-meta {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #888;
}
.message-project {
color: #667eea;
}
/* 右侧详情面板 */
.detail-panel {
width: 350px;
background: #16213e;
border-left: 1px solid #2a2a4e;
padding: 20px;
overflow-y: auto;
}
.detail-section {
margin-bottom: 25px;
}
.detail-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
color: #667eea;
}
.detail-content {
font-size: 13px;
line-height: 1.6;
}
.chart-container {
height: 200px;
background: #0f3460;
border-radius: 8px;
padding: 10px;
margin-top: 10px;
}
/* 加载状态 */
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #2a2a4e;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.empty-icon {
font-size: 48px;
margin-bottom: 10px;
}
/* 错误状态 */
.error-message {
background: #e74c3c;
color: white;
padding: 10px;
border-radius: 6px;
margin: 10px;
}
/* 按钮样式 */
.btn {
padding: 8px 16px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.btn:hover {
background: #764ba2;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 工具栏 */
.toolbar {
display: flex;
gap: 10px;
padding: 10px;
background: #0f3460;
border-top: 1px solid #2a2a4e;
}
/* 动画 */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0f3460;
}
::-webkit-scrollbar-thumb {
background: #2a2a4e;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #667eea;
}
/* 标签样式 */
.tag {
display: inline-block;
padding: 2px 8px;
background: #667eea;
border-radius: 12px;
font-size: 11px;
margin-right: 5px;
}
.time-group {
margin-bottom: 20px;
}
.time-group-header {
font-size: 12px;
color: #667eea;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #2a2a4e;
}
</style>
</head>
<body>
<div id="app">
<!-- 顶部栏 -->
<div class="header">
<h1>
<span class="claude-logo">🤖</span>
Claude 本地历史记录查看器
</h1>
<div class="stats-bar">
<div class="stat-item">
<span>📝</span>
<span>总消息数:</span>
<span class="stat-value">{{ stats.totalMessages }}</span>
</div>
<div class="stat-item">
<span>📁</span>
<span>项目数:</span>
<span class="stat-value">{{ stats.totalProjects }}</span>
</div>
<div class="stat-item">
<span>📅</span>
<span>最后活动:</span>
<span class="stat-value">{{ lastActivityTime }}</span>
</div>
<div class="stat-item">
<span>🔄</span>
<span>状态:</span>
<span class="stat-value">{{ connectionStatus }}</span>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 左侧项目列表 -->
<div class="sidebar">
<div class="search-section">
<input
type="text"
class="search-input"
v-model="searchQuery"
placeholder="搜索项目或消息..."
@input="performSearch"
>
<div class="filter-buttons">
<button
class="filter-btn"
:class="{ active: filterType === 'all' }"
@click="setFilter('all')"
>
全部
</button>
<button
class="filter-btn"
:class="{ active: filterType === 'recent' }"
@click="setFilter('recent')"
>
最近
</button>
<button
class="filter-btn"
:class="{ active: filterType === 'active' }"
@click="setFilter('active')"
>
活跃
</button>
</div>
</div>
<div class="project-list">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="filteredProjects.length === 0" class="empty-state">
<div class="empty-icon">📭</div>
<p>暂无项目</p>
</div>
<div
v-else
v-for="project in filteredProjects"
:key="project.path"
:class="['project-item', { active: selectedProject === project }]"
@click="selectProject(project)"
>
<div class="project-name">
📁 {{ project.name }}
</div>
<div class="project-path">{{ project.path }}</div>
<div class="project-stats">
<span>💬 {{ project.count }} 条消息</span>
<span>{{ formatDate(project.lastAccess) }}</span>
</div>
</div>
</div>
</div>
<!-- 中间消息列表 -->
<div class="message-area">
<div class="message-header">
<h3>{{ selectedProject ? selectedProject.name + ' 的消息' : '所有消息' }}</h3>
<div style="margin-top: 5px; font-size: 12px; color: #888;">
共 {{ displayMessages.length }} 条消息
</div>
</div>
<div class="message-list">
<div v-if="displayMessages.length === 0" class="empty-state">
<div class="empty-icon">💭</div>
<p>暂无消息</p>
</div>
<!-- 按时间分组显示 -->
<div v-for="group in messageGroups" :key="group.date" class="time-group">
<div class="time-group-header">{{ group.date }}</div>
<div
v-for="(msg, index) in group.messages"
:key="index"
class="message-item"
>
<div class="message-content">{{ msg.display }}</div>
<div class="message-meta">
<span class="message-project">{{ getProjectName(msg.project) }}</span>
<span>{{ formatTime(msg.timestamp) }}</span>
</div>
</div>
</div>
</div>
<div class="toolbar">
<button class="btn" @click="refreshData">
🔄 刷新
</button>
<button class="btn" @click="exportData">
📥 导出数据
</button>
<button class="btn" @click="loadMore" :disabled="!hasMore">
⬇️ 加载更多
</button>
</div>
</div>
<!-- 右侧详情面板 -->
<div class="detail-panel">
<div class="detail-section">
<div class="detail-title">📊 统计概览</div>
<div class="detail-content">
<p>开始时间: {{ formatDate(stats.firstMessage?.timestamp) }}</p>
<p>最后时间: {{ formatDate(stats.lastMessage?.timestamp) }}</p>
<p>使用天数: {{ usageDays }}</p>
<p>日均消息: {{ avgMessagesPerDay }}</p>
</div>
</div>
<div class="detail-section">
<div class="detail-title">📈 活动趋势</div>
<div class="chart-container">
<canvas id="activityChart"></canvas>
</div>
</div>
<div class="detail-section">
<div class="detail-title">🏆 最活跃项目</div>
<div class="detail-content">
<div v-for="(project, index) in topProjects" :key="index" style="margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between;">
<span>{{ index + 1 }}. {{ project.name }}</span>
<span class="tag">{{ project.count }}</span>
</div>
</div>
</div>
</div>
<div class="detail-section">
<div class="detail-title">🔍 快速操作</div>
<div class="detail-content">
<button class="btn" style="width: 100%; margin-bottom: 8px;" @click="openInFinder">
📂 打开数据目录
</button>
<button class="btn" style="width: 100%; margin-bottom: 8px;" @click="showRawData">
📝 查看原始数据
</button>
<button class="btn" style="width: 100%;" @click="clearCache">
🗑️ 清除缓存
</button>
</div>
</div>
</div>
</div>
<!-- 错误提示 -->
<div v-if="error" class="error-message">
⚠️ {{ error }}
</div>
</div>
<script>
const { createApp } = Vue;
const API_BASE = 'http://localhost:3333';
createApp({
data() {
return {
// 数据
history: [],
projects: [],
stats: {
totalMessages: 0,
totalProjects: 0,
firstMessage: null,
lastMessage: null,
messagesByDay: {}
},
// UI 状态
loading: false,
error: null,
searchQuery: '',
filterType: 'all',
selectedProject: null,
displayLimit: 50,
connectionStatus: '未连接',
// 过滤后的数据
filteredProjects: [],
displayMessages: []
}
},
computed: {
lastActivityTime() {
if (this.stats.lastMessage) {
return this.formatDate(this.stats.lastMessage.timestamp);
}
return '无';
},
messageGroups() {
const groups = {};
this.displayMessages.forEach(msg => {
const date = this.formatDateGroup(msg.timestamp);
if (!groups[date]) {
groups[date] = [];
}
groups[date].push(msg);
});
return Object.keys(groups).map(date => ({
date,
messages: groups[date]
}));
},
hasMore() {
return this.displayMessages.length < this.history.length;
},
topProjects() {
return [...this.projects]
.sort((a, b) => b.count - a.count)
.slice(0, 5);
},
usageDays() {
if (this.stats.firstMessage && this.stats.lastMessage) {
const days = Math.floor(
(this.stats.lastMessage.timestamp - this.stats.firstMessage.timestamp) /
(1000 * 60 * 60 * 24)
);
return days + 1;
}
return 0;
},
avgMessagesPerDay() {
if (this.usageDays > 0) {
return (this.stats.totalMessages / this.usageDays).toFixed(1);
}
return 0;
}
},
methods: {
// 获取历史数据
async fetchHistory() {
this.loading = true;
this.error = null;
try {
const response = await axios.get(`${API_BASE}/api/history`);
if (response.data.success) {
this.history = response.data.history;
this.projects = response.data.projects;
this.displayMessages = this.history.slice(0, this.displayLimit);
this.filteredProjects = [...this.projects];
this.connectionStatus = '已连接';
// 获取统计信息
await this.fetchStats();
} else {
throw new Error('获取数据失败');
}
} catch (error) {
this.error = '无法连接到历史记录服务器。请确保服务器正在运行。';
this.connectionStatus = '连接失败';
console.error('Error fetching history:', error);
} finally {
this.loading = false;
}
},
// 获取统计信息
async fetchStats() {
try {
const response = await axios.get(`${API_BASE}/api/stats`);
if (response.data.success) {
this.stats = response.data;
}
} catch (error) {
console.error('Error fetching stats:', error);
}
},
// 搜索
async performSearch() {
if (!this.searchQuery) {
this.displayMessages = this.history.slice(0, this.displayLimit);
this.filteredProjects = [...this.projects];
return;
}
try {
const response = await axios.get(`${API_BASE}/api/search`, {
params: { q: this.searchQuery }
});
if (response.data.success) {
this.displayMessages = response.data.results;
}
} catch (error) {
console.error('Search error:', error);
}
},
// 设置过滤器
setFilter(type) {
this.filterType = type;
this.applyFilter();
},
// 应用过滤器
applyFilter() {
switch (this.filterType) {
case 'recent':
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
this.filteredProjects = this.projects.filter(p =>
p.lastAccess > oneWeekAgo
);
break;
case 'active':
this.filteredProjects = this.projects.filter(p => p.count > 10);
break;
default:
this.filteredProjects = [...this.projects];
}
},
// 选择项目
selectProject(project) {
this.selectedProject = project;
if (project) {
this.displayMessages = this.history.filter(msg =>
msg.project === project.path
);
} else {
this.displayMessages = this.history.slice(0, this.displayLimit);
}
},
// 加载更多
loadMore() {
this.displayLimit += 50;
if (this.selectedProject) {
this.displayMessages = this.history.filter(msg =>
msg.project === this.selectedProject.path
);
} else {
this.displayMessages = this.history.slice(0, this.displayLimit);
}
},
// 刷新数据
refreshData() {
this.fetchHistory();
},
// 导出数据
exportData() {
const dataStr = JSON.stringify({
history: this.history,
projects: this.projects,
stats: this.stats,
exportDate: new Date().toISOString()
}, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-history-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
},
// 打开数据目录
openInFinder() {
// 这需要通过Java后端实现
alert('请在终端中运行: open ~/.claude');
},
// 显示原始数据
showRawData() {
console.log('History:', this.history);
console.log('Projects:', this.projects);
console.log('Stats:', this.stats);
alert('原始数据已输出到控制台');
},
// 清除缓存
clearCache() {
if (confirm('确定要清除本地缓存吗?这不会影响Claude的原始数据。')) {
this.history = [];
this.projects = [];
this.displayMessages = [];
this.filteredProjects = [];
this.selectedProject = null;
alert('缓存已清除,请刷新重新加载');
}
},
// 格式化函数
formatDate(timestamp) {
if (!timestamp) return '未知';
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
},
formatDateGroup(timestamp) {
if (!timestamp) return '未知';
const date = new Date(timestamp);
const today = new Date();
if (date.toDateString() === today.toDateString()) {
return '今天';
}
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) {
return '昨天';
}
return date.toLocaleDateString('zh-CN');
},
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
},
getProjectName(path) {
if (!path) return '未知项目';
const parts = path.split('/');
return parts[parts.length - 1] || 'Root';
}
},
mounted() {
console.log('Claude 本地历史记录查看器已启动');
this.fetchHistory();
// 自动刷新
setInterval(() => {
this.fetchHistory();
}, 60000); // 每分钟刷新一次
}
}).mount('#app');
</script>
</body>
</html>
+638
View File
@@ -0,0 +1,638 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude History - Java Backend</title>
<!-- 引入 Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
background: #1a1a2e;
color: #eee;
height: 100vh;
overflow: hidden;
}
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 15px 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.header h1 {
font-size: 24px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.stats-bar {
display: flex;
gap: 30px;
margin-top: 10px;
font-size: 14px;
opacity: 0.95;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.stat-value {
font-weight: bold;
color: #ffd700;
}
.main-content {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 300px;
background: #16213e;
border-right: 1px solid #2a2a4e;
display: flex;
flex-direction: column;
}
.search-section {
padding: 15px;
border-bottom: 1px solid #2a2a4e;
}
.search-input {
width: 100%;
padding: 10px;
background: #0f3460;
border: 1px solid #2a2a4e;
border-radius: 6px;
color: white;
font-size: 14px;
}
.project-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.project-item {
background: #0f3460;
border: 1px solid #2a2a4e;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s;
}
.project-item:hover {
background: #1e5f8e;
transform: translateX(3px);
}
.project-item.active {
background: #667eea;
border-color: #764ba2;
}
.project-name {
font-weight: bold;
margin-bottom: 5px;
}
.project-path {
font-size: 11px;
color: #aaa;
margin-bottom: 8px;
word-break: break-all;
}
.project-stats {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #ccc;
}
.message-area {
flex: 1;
background: #0f3460;
display: flex;
flex-direction: column;
}
.message-header {
padding: 15px;
background: #16213e;
border-bottom: 1px solid #2a2a4e;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.message-item {
background: #16213e;
border: 1px solid #2a2a4e;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
animation: slideIn 0.3s ease;
}
.message-content {
margin-bottom: 8px;
line-height: 1.5;
}
.message-meta {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #888;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #2a2a4e;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: #888;
}
.empty-icon {
font-size: 48px;
margin-bottom: 10px;
}
.error-message {
background: #e74c3c;
color: white;
padding: 10px;
border-radius: 6px;
margin: 10px;
}
.btn {
padding: 8px 16px;
background: #667eea;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
margin: 5px;
}
.btn:hover {
background: #764ba2;
}
.connection-status {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
font-size: 12px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #4caf50;
animation: pulse 2s infinite;
}
.status-dot.error {
background: #f44336;
animation: none;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #0f3460;
}
::-webkit-scrollbar-thumb {
background: #2a2a4e;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #667eea;
}
</style>
</head>
<body>
<div id="app">
<div class="header">
<h1>
🤖 Claude 本地历史记录 (Java Backend)
<div class="connection-status">
<span class="status-dot" :class="{ error: !connected }"></span>
{{ connectionStatus }}
</div>
</h1>
<div class="stats-bar">
<div class="stat-item">
<span>📝</span>
<span>总消息:</span>
<span class="stat-value">{{ stats.totalMessages }}</span>
</div>
<div class="stat-item">
<span>📁</span>
<span>项目数:</span>
<span class="stat-value">{{ stats.totalProjects }}</span>
</div>
<div class="stat-item">
<span>📅</span>
<span>最后活动:</span>
<span class="stat-value">{{ lastActivityTime }}</span>
</div>
</div>
</div>
<div class="main-content">
<div class="sidebar">
<div class="search-section">
<input
type="text"
class="search-input"
v-model="searchQuery"
placeholder="搜索消息..."
@input="performSearch"
>
</div>
<div class="project-list">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<div v-else-if="projects.length === 0" class="empty-state">
<div class="empty-icon">📭</div>
<p>暂无项目</p>
</div>
<div
v-else
v-for="project in projects"
:key="project.path"
:class="['project-item', { active: selectedProject === project }]"
@click="selectProject(project)"
>
<div class="project-name">📁 {{ project.name }}</div>
<div class="project-path">{{ project.path }}</div>
<div class="project-stats">
<span>💬 {{ project.count }}</span>
<span>{{ formatDate(project.lastAccess) }}</span>
</div>
</div>
</div>
</div>
<div class="message-area">
<div class="message-header">
<h3>{{ selectedProject ? selectedProject.name + ' 的消息' : '最近消息' }}</h3>
<div style="margin-top: 5px; font-size: 12px; color: #888;">
显示 {{ displayMessages.length }} / {{ totalMessages }} 条
</div>
</div>
<div class="message-list">
<div v-if="displayMessages.length === 0" class="empty-state">
<div class="empty-icon">💭</div>
<p>暂无消息</p>
</div>
<div
v-for="(msg, index) in displayMessages"
:key="index"
class="message-item"
>
<div class="message-content">{{ msg.display }}</div>
<div class="message-meta">
<span>{{ getProjectName(msg.project) }}</span>
<span>{{ formatDateTime(msg.timestamp) }}</span>
</div>
</div>
<div style="text-align: center; padding: 20px;">
<button
class="btn"
@click="loadMore"
v-if="hasMore"
>
加载更多
</button>
</div>
</div>
<div style="padding: 10px; background: #16213e; border-top: 1px solid #2a2a4e;">
<button class="btn" @click="refreshData">🔄 刷新</button>
<button class="btn" @click="exportData">📥 导出</button>
<button class="btn" @click="clearSelection">❌ 清除选择</button>
</div>
</div>
</div>
<div v-if="error" class="error-message">
⚠️ {{ error }}
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
// 数据
history: [],
projects: [],
stats: {
totalMessages: 0,
totalProjects: 0,
firstMessage: null,
lastMessage: null
},
// UI 状态
loading: false,
error: null,
connected: false,
connectionStatus: '检查中...',
searchQuery: '',
selectedProject: null,
// 显示相关
displayMessages: [],
displayLimit: 50,
totalMessages: 0
}
},
computed: {
lastActivityTime() {
if (this.stats.lastMessage) {
return this.formatDate(this.stats.lastMessage.timestamp);
}
return '无';
},
hasMore() {
return this.displayMessages.length < this.totalMessages;
}
},
methods: {
// 使用 Java 后端获取数据
async fetchHistory() {
this.loading = true;
this.error = null;
try {
// 检查 ClaudeAPI 是否可用
if (typeof window.ClaudeAPI === 'undefined') {
// 等待 API 注入
await new Promise(resolve => setTimeout(resolve, 1000));
if (typeof window.ClaudeAPI === 'undefined') {
throw new Error('ClaudeAPI 未初始化,请确保插件正确加载');
}
}
// 使用 Java 后端 API
window.ClaudeAPI.fetchHistory((response) => {
if (response.success) {
this.history = response.history || [];
this.projects = response.projects || [];
this.stats = response.stats || this.stats;
this.totalMessages = response.total || this.history.length;
this.displayMessages = this.history.slice(0, this.displayLimit);
this.connected = true;
this.connectionStatus = 'Java Backend';
this.loading = false;
} else {
throw new Error(response.error || '获取数据失败');
}
});
} catch (error) {
console.error('Error:', error);
this.error = error.message;
this.connected = false;
this.connectionStatus = '连接失败';
this.loading = false;
// 使用模拟数据
this.useMockData();
}
},
// 搜索
performSearch() {
if (!this.searchQuery) {
this.displayMessages = this.history.slice(0, this.displayLimit);
return;
}
if (window.ClaudeAPI && window.ClaudeAPI.search) {
window.ClaudeAPI.search(this.searchQuery, (response) => {
if (response.success) {
this.displayMessages = response.data.results || [];
}
});
} else {
// 本地搜索
const query = this.searchQuery.toLowerCase();
this.displayMessages = this.history.filter(msg =>
msg.display && msg.display.toLowerCase().includes(query)
);
}
},
// 选择项目
selectProject(project) {
this.selectedProject = project;
if (project && project.messages) {
this.displayMessages = project.messages.slice(0, this.displayLimit);
this.totalMessages = project.messages.length;
} else {
this.displayMessages = this.history.slice(0, this.displayLimit);
this.totalMessages = this.history.length;
}
},
// 清除选择
clearSelection() {
this.selectedProject = null;
this.displayMessages = this.history.slice(0, this.displayLimit);
this.totalMessages = this.history.length;
},
// 加载更多
loadMore() {
this.displayLimit += 50;
if (this.selectedProject && this.selectedProject.messages) {
this.displayMessages = this.selectedProject.messages.slice(0, this.displayLimit);
} else {
this.displayMessages = this.history.slice(0, this.displayLimit);
}
},
// 刷新数据
refreshData() {
this.displayLimit = 50;
this.fetchHistory();
},
// 导出数据
exportData() {
const dataStr = JSON.stringify({
history: this.history,
projects: this.projects,
stats: this.stats,
exportDate: new Date().toISOString()
}, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-history-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
},
// 使用模拟数据
useMockData() {
this.history = [
{ display: '示例消息 1', timestamp: Date.now(), project: '/example/project1' },
{ display: '示例消息 2', timestamp: Date.now() - 3600000, project: '/example/project2' },
{ display: '无法连接到本地数据,显示示例数据', timestamp: Date.now() - 7200000, project: '/demo' }
];
this.projects = [
{ path: '/example/project1', name: 'Project 1', count: 1, lastAccess: Date.now() },
{ path: '/example/project2', name: 'Project 2', count: 1, lastAccess: Date.now() - 3600000 }
];
this.displayMessages = this.history;
this.totalMessages = this.history.length;
this.stats = {
totalMessages: 3,
totalProjects: 2,
firstMessage: this.history[2],
lastMessage: this.history[0]
};
},
// 格式化函数
formatDate(timestamp) {
if (!timestamp) return '未知';
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()}`;
},
formatDateTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
},
getProjectName(path) {
if (!path) return '未知项目';
const parts = path.split('/');
return parts[parts.length - 1] || 'Root';
}
},
mounted() {
console.log('Claude History Viewer (Java Backend) 已启动');
// 立即尝试获取数据
this.fetchHistory();
// 定期刷新
setInterval(() => {
if (this.connected) {
this.fetchHistory();
}
}, 60000); // 每分钟刷新
}
}).mount('#app');
</script>
</body>
</html>
+476
View File
@@ -0,0 +1,476 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Claude Plugin</title>
<!-- 引入 Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 引入 Axios 用于 API 调用 -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
height: 100vh;
overflow: auto;
}
#app {
width: 100%;
min-height: 100vh;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 30px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}
h1 {
font-size: 36px;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
animation: fadeIn 1s ease-in;
text-align: center;
}
.claude-section {
margin-top: 30px;
}
.api-key-section {
margin-bottom: 20px;
padding: 15px;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
}
.input-group {
margin-bottom: 15px;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-size: 14px;
opacity: 0.9;
}
.input-group input,
.input-group textarea {
width: 100%;
padding: 10px;
border: none;
border-radius: 8px;
font-size: 14px;
background: rgba(255, 255, 255, 0.9);
color: #333;
}
.input-group textarea {
min-height: 100px;
resize: vertical;
}
.button {
padding: 12px 30px;
font-size: 16px;
background: white;
color: #764ba2;
border: none;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: bold;
margin: 10px 5px;
}
.button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.button.secondary {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.response-section {
margin-top: 20px;
padding: 15px;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
}
.response-section h3 {
margin-bottom: 10px;
font-size: 18px;
}
.response-content {
padding: 15px;
background: rgba(255, 255, 255, 0.9);
color: #333;
border-radius: 8px;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
.loading {
text-align: center;
padding: 20px;
}
.spinner {
display: inline-block;
width: 30px;
height: 30px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error {
padding: 10px;
background: rgba(255, 0, 0, 0.2);
border-radius: 8px;
margin-top: 10px;
}
.success {
padding: 10px;
background: rgba(0, 255, 0, 0.2);
border-radius: 8px;
margin-top: 10px;
}
.vue-logo {
width: 40px;
height: 40px;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
animation: rotate 20s linear infinite;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<!-- 标题部分 -->
<h1>
<svg class="vue-logo" viewBox="0 0 261.76 226.69" xmlns="http://www.w3.org/2000/svg">
<path d="m161.096.001-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/>
<path d="m161.096.001-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/>
</svg>
Vue + Claude 集成插件
</h1>
<!-- Claude API 集成部分 -->
<div class="claude-section">
<!-- API Key 配置 -->
<div class="api-key-section">
<div class="input-group">
<label for="apiKey">Claude API Key (可选,使用代理则无需填写):</label>
<input
type="password"
id="apiKey"
v-model="apiKey"
placeholder="sk-ant-api03-..."
@input="saveApiKey"
>
</div>
<div class="success" v-if="apiKeySaved">
API Key 已保存到本地存储
</div>
</div>
<!-- 消息输入区 -->
<div class="input-group">
<label for="userMessage">向 Claude 提问:</label>
<textarea
id="userMessage"
v-model="userMessage"
placeholder="例如:请解释一下 Vue 3 的组合式 API..."
@keydown.enter.ctrl="sendMessage"
></textarea>
</div>
<!-- 按钮组 -->
<div>
<button
class="button"
@click="sendMessage"
:disabled="loading || !userMessage.trim()"
>
发送消息
</button>
<button
class="button secondary"
@click="clearConversation"
:disabled="loading"
>
清除对话
</button>
<button
class="button secondary"
@click="testConnection"
:disabled="loading"
>
测试连接
</button>
</div>
<!-- 加载状态 -->
<div class="loading" v-if="loading">
<div class="spinner"></div>
<p>正在与 Claude 通信...</p>
</div>
<!-- 错误提示 -->
<div class="error" v-if="error">
{{ error }}
</div>
<!-- 响应显示区 -->
<div class="response-section" v-if="conversation.length > 0">
<h3>对话历史:</h3>
<div v-for="(msg, index) in conversation" :key="index" style="margin-bottom: 15px;">
<div style="font-weight: bold; margin-bottom: 5px;">
{{ msg.role === 'user' ? '你' : 'Claude' }}:
</div>
<div class="response-content">{{ msg.content }}</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
// Claude API 代理服务器(用于绕过 CORS)
const PROXY_URL = 'https://api.anthropic.com/v1/messages';
createApp({
data() {
return {
apiKey: '',
apiKeySaved: false,
userMessage: '',
conversation: [],
loading: false,
error: null
}
},
methods: {
// 保存 API Key 到本地存储
saveApiKey() {
if (this.apiKey) {
localStorage.setItem('claude_api_key', this.apiKey);
this.apiKeySaved = true;
setTimeout(() => {
this.apiKeySaved = false;
}, 2000);
}
},
// 测试连接
async testConnection() {
this.loading = true;
this.error = null;
try {
// 简单的测试消息
const response = await this.callClaudeAPI('Hello Claude, please respond with "Connection successful!"');
if (response) {
this.error = null;
this.conversation = [{
role: 'assistant',
content: '连接测试成功!Claude API 已就绪。'
}];
}
} catch (err) {
this.error = '连接测试失败:' + err.message;
} finally {
this.loading = false;
}
},
// 发送消息到 Claude
async sendMessage() {
if (!this.userMessage.trim()) return;
this.loading = true;
this.error = null;
// 添加用户消息到对话
this.conversation.push({
role: 'user',
content: this.userMessage
});
try {
const response = await this.callClaudeAPI(this.userMessage);
if (response) {
this.conversation.push({
role: 'assistant',
content: response
});
}
// 清空输入框
this.userMessage = '';
} catch (err) {
this.error = '发送失败:' + err.message;
// 移除失败的用户消息
this.conversation.pop();
} finally {
this.loading = false;
}
},
// 调用 Claude API
async callClaudeAPI(message) {
const apiKey = this.apiKey || localStorage.getItem('claude_api_key');
// 如果没有 API Key,使用模拟响应
if (!apiKey) {
// 模拟 Claude 响应
await new Promise(resolve => setTimeout(resolve, 1000));
return this.getMockResponse(message);
}
try {
const response = await axios.post(
PROXY_URL,
{
model: 'claude-3-sonnet-20240229',
max_tokens: 1024,
messages: [
{
role: 'user',
content: message
}
]
},
{
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01'
}
}
);
if (response.data && response.data.content && response.data.content[0]) {
return response.data.content[0].text;
}
throw new Error('Invalid response format');
} catch (error) {
if (error.response && error.response.status === 401) {
throw new Error('API Key 无效,请检查您的密钥');
} else if (error.response && error.response.status === 429) {
throw new Error('请求过于频繁,请稍后再试');
} else {
// 如果真实 API 调用失败,返回模拟响应
console.warn('API 调用失败,使用模拟响应:', error);
return this.getMockResponse(message);
}
}
},
// 获取模拟响应
getMockResponse(message) {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes('hello') || lowerMessage.includes('你好')) {
return '你好!我是 Claude,很高兴为您服务。这是一个集成在 IDEA 插件中的演示版本。';
} else if (lowerMessage.includes('vue')) {
return 'Vue.js 是一个渐进式的 JavaScript 框架,用于构建用户界面。Vue 3 引入了组合式 API,提供了更好的逻辑复用和类型推导能力。';
} else if (lowerMessage.includes('test') || lowerMessage.includes('测试')) {
return '测试连接成功!Claude API 集成正常工作。';
} else if (lowerMessage.includes('help') || lowerMessage.includes('帮助')) {
return '我可以回答您关于编程、技术和各种主题的问题。请随时向我提问!';
} else {
return `我理解您说的:"${message}"。作为演示版本,我的响应是预设的。要获得真实的 Claude 响应,请配置有效的 API Key。`;
}
},
// 清除对话
clearConversation() {
this.conversation = [];
this.userMessage = '';
this.error = null;
}
},
mounted() {
console.log('Vue + Claude 应用已成功挂载!');
// 从本地存储加载 API Key
const savedKey = localStorage.getItem('claude_api_key');
if (savedKey) {
this.apiKey = savedKey;
}
// 显示欢迎消息
this.conversation.push({
role: 'assistant',
content: '欢迎使用 Vue + Claude 集成插件!您可以在上方输入 API Key(可选),然后向我提问。如果没有 API Key,我会提供模拟响应。'
});
}
}).mount('#app');
</script>
</body>
</html>
@@ -0,0 +1,941 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue Claude Plugin with History</title>
<!-- 引入 Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 引入 Axios 用于 API 调用 -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
height: 100vh;
overflow: hidden;
}
#app {
width: 100%;
height: 100vh;
display: flex;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
/* 侧边栏样式 */
.sidebar {
width: 250px;
background: rgba(0, 0, 0, 0.2);
padding: 20px;
overflow-y: auto;
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar h2 {
font-size: 18px;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.history-controls {
margin-bottom: 20px;
}
.history-controls button {
width: 100%;
margin-bottom: 8px;
padding: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.history-controls button:hover {
background: rgba(255, 255, 255, 0.2);
}
.search-box {
width: 100%;
padding: 8px;
margin-bottom: 15px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
color: white;
font-size: 12px;
}
.search-box::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.history-list {
max-height: calc(100vh - 300px);
overflow-y: auto;
}
.history-item {
padding: 10px;
margin-bottom: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 12px;
}
.history-item:hover {
background: rgba(255, 255, 255, 0.2);
}
.history-item.active {
background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.5);
}
.history-item-title {
font-weight: bold;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-item-preview {
opacity: 0.8;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.history-item-date {
opacity: 0.6;
font-size: 10px;
margin-top: 5px;
}
/* 主内容区样式 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
}
.container {
flex: 1;
display: flex;
flex-direction: column;
max-width: 900px;
margin: 0 auto;
width: 100%;
padding: 30px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}
h1 {
font-size: 32px;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
text-align: center;
}
.claude-section {
flex: 1;
display: flex;
flex-direction: column;
}
.api-key-section {
margin-bottom: 15px;
padding: 12px;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
}
.input-group {
margin-bottom: 12px;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-size: 13px;
opacity: 0.9;
}
.input-group input,
.input-group textarea {
width: 100%;
padding: 8px;
border: none;
border-radius: 6px;
font-size: 13px;
background: rgba(255, 255, 255, 0.9);
color: #333;
}
.input-group textarea {
min-height: 80px;
resize: vertical;
}
.button {
padding: 10px 20px;
font-size: 14px;
background: white;
color: #764ba2;
border: none;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: bold;
margin: 5px;
}
.button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.button.secondary {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.button.danger {
background: rgba(255, 100, 100, 0.3);
color: white;
}
.response-section {
flex: 1;
margin-top: 15px;
padding: 12px;
background: rgba(255, 255, 255, 0.15);
border-radius: 10px;
overflow-y: auto;
max-height: calc(100vh - 400px);
}
.response-section h3 {
margin-bottom: 10px;
font-size: 16px;
}
.conversation-stats {
font-size: 11px;
opacity: 0.8;
margin-bottom: 10px;
}
.response-content {
padding: 12px;
background: rgba(255, 255, 255, 0.9);
color: #333;
border-radius: 6px;
white-space: pre-wrap;
font-size: 13px;
margin-bottom: 10px;
}
.message-wrapper {
margin-bottom: 12px;
animation: slideIn 0.3s ease;
}
.message-role {
font-weight: bold;
margin-bottom: 5px;
font-size: 12px;
}
.message-time {
font-size: 10px;
opacity: 0.6;
margin-left: 10px;
}
.loading {
text-align: center;
padding: 15px;
}
.spinner {
display: inline-block;
width: 24px;
height: 24px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.error {
padding: 8px;
background: rgba(255, 0, 0, 0.2);
border-radius: 6px;
margin-top: 8px;
font-size: 12px;
}
.success {
padding: 8px;
background: rgba(0, 255, 0, 0.2);
border-radius: 6px;
margin-top: 8px;
font-size: 12px;
}
.vue-logo {
width: 30px;
height: 30px;
display: inline-block;
vertical-align: middle;
margin-right: 8px;
animation: rotate 20s linear infinite;
}
/* 文件上传样式 */
.file-input {
display: none;
}
.file-label {
display: inline-block;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.3s ease;
}
.file-label:hover {
background: rgba(255, 255, 255, 0.2);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
</head>
<body>
<div id="app">
<!-- 左侧历史记录侧边栏 -->
<div class="sidebar">
<h2>对话历史</h2>
<!-- 搜索框 -->
<input
type="text"
class="search-box"
v-model="searchQuery"
placeholder="搜索历史记录..."
@input="filterHistory"
>
<!-- 历史记录控制按钮 -->
<div class="history-controls">
<button @click="createNewSession">
➕ 新建对话
</button>
<button @click="exportAllHistory">
📥 导出所有历史
</button>
<label class="file-label">
📤 导入历史
<input
type="file"
class="file-input"
accept=".json"
@change="importHistory"
>
</label>
<button @click="clearAllHistory" class="button danger">
🗑️ 清除所有历史
</button>
</div>
<!-- 历史记录列表 -->
<div class="history-list">
<div
v-for="session in filteredSessions"
:key="session.id"
:class="['history-item', { active: currentSessionId === session.id }]"
@click="loadSession(session.id)"
>
<div class="history-item-title">
{{ session.title || '未命名对话' }}
</div>
<div class="history-item-preview">
{{ session.preview }}
</div>
<div class="history-item-date">
{{ formatDate(session.timestamp) }}
<span style="float: right;">💬 {{ session.messageCount }}</span>
</div>
</div>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="container">
<!-- 标题部分 -->
<h1>
<svg class="vue-logo" viewBox="0 0 261.76 226.69" xmlns="http://www.w3.org/2000/svg">
<path d="m161.096.001-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/>
<path d="m161.096.001-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/>
</svg>
Vue + Claude 历史记录版
</h1>
<!-- Claude API 集成部分 -->
<div class="claude-section">
<!-- API Key 配置 -->
<div class="api-key-section">
<div class="input-group">
<label for="apiKey">Claude API Key (可选):</label>
<input
type="password"
id="apiKey"
v-model="apiKey"
placeholder="sk-ant-api03-..."
@input="saveApiKey"
>
</div>
<div class="success" v-if="apiKeySaved">
API Key 已保存
</div>
</div>
<!-- 消息输入区 -->
<div class="input-group">
<label for="userMessage">向 Claude 提问:</label>
<textarea
id="userMessage"
v-model="userMessage"
placeholder="输入你的问题..."
@keydown.enter.ctrl="sendMessage"
></textarea>
</div>
<!-- 按钮组 -->
<div>
<button
class="button"
@click="sendMessage"
:disabled="loading || !userMessage.trim()"
>
发送消息
</button>
<button
class="button secondary"
@click="clearCurrentSession"
:disabled="loading"
>
清除当前对话
</button>
<button
class="button secondary"
@click="exportCurrentSession"
:disabled="!conversation.length"
>
导出当前对话
</button>
<button
class="button secondary"
@click="renameSession"
>
重命名对话
</button>
</div>
<!-- 加载状态 -->
<div class="loading" v-if="loading">
<div class="spinner"></div>
<p>正在与 Claude 通信...</p>
</div>
<!-- 错误提示 -->
<div class="error" v-if="error">
{{ error }}
</div>
<!-- 响应显示区 -->
<div class="response-section" v-if="conversation.length > 0">
<h3>当前对话</h3>
<div class="conversation-stats">
共 {{ conversation.length }} 条消息 |
会话ID: {{ currentSessionId }} |
开始时间: {{ formatDate(currentSessionTimestamp) }}
</div>
<div v-for="(msg, index) in conversation" :key="index" class="message-wrapper">
<div class="message-role">
{{ msg.role === 'user' ? '👤 你' : '🤖 Claude' }}:
<span class="message-time">{{ formatTime(msg.timestamp) }}</span>
</div>
<div class="response-content">{{ msg.content }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue;
// Claude API 配置
const PROXY_URL = 'https://api.anthropic.com/v1/messages';
const STORAGE_KEY = 'claude_history';
const API_KEY_STORAGE = 'claude_api_key';
createApp({
data() {
return {
// API 相关
apiKey: '',
apiKeySaved: false,
// 当前对话
userMessage: '',
conversation: [],
loading: false,
error: null,
// 会话管理
currentSessionId: null,
currentSessionTimestamp: null,
sessions: [],
// 搜索和过滤
searchQuery: '',
filteredSessions: []
}
},
methods: {
// 生成唯一ID
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
},
// 格式化日期
formatDate(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
},
// 格式化时间
formatTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return `${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
},
// 创建新会话
createNewSession() {
this.saveCurrentSession();
this.currentSessionId = this.generateId();
this.currentSessionTimestamp = Date.now();
this.conversation = [];
this.error = null;
},
// 加载会话
loadSession(sessionId) {
this.saveCurrentSession();
const session = this.sessions.find(s => s.id === sessionId);
if (session) {
this.currentSessionId = session.id;
this.currentSessionTimestamp = session.timestamp;
this.conversation = session.messages || [];
this.error = null;
}
},
// 保存当前会话
saveCurrentSession() {
if (!this.currentSessionId || this.conversation.length === 0) return;
const sessionIndex = this.sessions.findIndex(s => s.id === this.currentSessionId);
const sessionData = {
id: this.currentSessionId,
timestamp: this.currentSessionTimestamp,
title: this.getSessionTitle(),
preview: this.getSessionPreview(),
messages: this.conversation,
messageCount: this.conversation.length
};
if (sessionIndex >= 0) {
this.sessions[sessionIndex] = sessionData;
} else {
this.sessions.unshift(sessionData);
}
this.saveToStorage();
this.filterHistory();
},
// 获取会话标题
getSessionTitle() {
if (this.conversation.length > 0) {
const firstUserMessage = this.conversation.find(m => m.role === 'user');
if (firstUserMessage) {
return firstUserMessage.content.substring(0, 30) + (firstUserMessage.content.length > 30 ? '...' : '');
}
}
return '新对话';
},
// 获取会话预览
getSessionPreview() {
if (this.conversation.length > 1) {
const lastMessage = this.conversation[this.conversation.length - 1];
return lastMessage.content.substring(0, 50) + (lastMessage.content.length > 50 ? '...' : '');
}
return '无内容';
},
// 保存到本地存储
saveToStorage() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.sessions));
},
// 从本地存储加载
loadFromStorage() {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
this.sessions = JSON.parse(stored);
this.filteredSessions = [...this.sessions];
} catch (e) {
console.error('加载历史记录失败:', e);
}
}
},
// 搜索历史
filterHistory() {
if (!this.searchQuery) {
this.filteredSessions = [...this.sessions];
return;
}
const query = this.searchQuery.toLowerCase();
this.filteredSessions = this.sessions.filter(session => {
const titleMatch = session.title && session.title.toLowerCase().includes(query);
const messagesMatch = session.messages && session.messages.some(msg =>
msg.content.toLowerCase().includes(query)
);
return titleMatch || messagesMatch;
});
},
// 导出当前会话
exportCurrentSession() {
if (!this.conversation.length) return;
const sessionData = {
id: this.currentSessionId,
timestamp: this.currentSessionTimestamp,
title: this.getSessionTitle(),
messages: this.conversation,
exportDate: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(sessionData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-session-${this.currentSessionId}.json`;
a.click();
URL.revokeObjectURL(url);
},
// 导出所有历史
exportAllHistory() {
const exportData = {
sessions: this.sessions,
exportDate: new Date().toISOString(),
totalSessions: this.sessions.length,
totalMessages: this.sessions.reduce((sum, s) => sum + (s.messageCount || 0), 0)
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `claude-all-history-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
},
// 导入历史
async importHistory(event) {
const file = event.target.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
if (data.sessions) {
// 导入多个会话
this.sessions = [...data.sessions, ...this.sessions];
} else if (data.messages) {
// 导入单个会话
this.sessions.unshift(data);
}
this.saveToStorage();
this.filterHistory();
this.error = null;
alert('历史记录导入成功!');
} catch (e) {
this.error = '导入失败:文件格式错误';
console.error('导入失败:', e);
}
// 清除文件输入
event.target.value = '';
},
// 清除所有历史
clearAllHistory() {
if (confirm('确定要清除所有历史记录吗?此操作不可恢复。')) {
this.sessions = [];
this.filteredSessions = [];
this.conversation = [];
this.currentSessionId = null;
this.currentSessionTimestamp = null;
localStorage.removeItem(STORAGE_KEY);
this.createNewSession();
}
},
// 清除当前会话
clearCurrentSession() {
this.conversation = [];
this.userMessage = '';
this.error = null;
},
// 重命名会话
renameSession() {
if (!this.currentSessionId) return;
const newTitle = prompt('请输入新的会话标题:', this.getSessionTitle());
if (newTitle) {
const session = this.sessions.find(s => s.id === this.currentSessionId);
if (session) {
session.title = newTitle;
this.saveToStorage();
this.filterHistory();
}
}
},
// 保存 API Key
saveApiKey() {
if (this.apiKey) {
localStorage.setItem(API_KEY_STORAGE, this.apiKey);
this.apiKeySaved = true;
setTimeout(() => {
this.apiKeySaved = false;
}, 2000);
}
},
// 发送消息到 Claude
async sendMessage() {
if (!this.userMessage.trim()) return;
// 确保有当前会话
if (!this.currentSessionId) {
this.createNewSession();
}
this.loading = true;
this.error = null;
const messageTimestamp = Date.now();
// 添加用户消息到对话
this.conversation.push({
role: 'user',
content: this.userMessage,
timestamp: messageTimestamp
});
try {
const response = await this.callClaudeAPI(this.userMessage);
if (response) {
this.conversation.push({
role: 'assistant',
content: response,
timestamp: Date.now()
});
}
// 保存会话
this.saveCurrentSession();
// 清空输入框
this.userMessage = '';
} catch (err) {
this.error = '发送失败:' + err.message;
// 移除失败的用户消息
this.conversation.pop();
} finally {
this.loading = false;
}
},
// 调用 Claude API
async callClaudeAPI(message) {
const apiKey = this.apiKey || localStorage.getItem(API_KEY_STORAGE);
// 如果没有 API Key,使用模拟响应
if (!apiKey) {
await new Promise(resolve => setTimeout(resolve, 1000));
return this.getMockResponse(message);
}
try {
const response = await axios.post(
PROXY_URL,
{
model: 'claude-3-sonnet-20240229',
max_tokens: 1024,
messages: this.conversation.map(msg => ({
role: msg.role,
content: msg.content
}))
},
{
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01'
}
}
);
if (response.data && response.data.content && response.data.content[0]) {
return response.data.content[0].text;
}
throw new Error('Invalid response format');
} catch (error) {
console.warn('API 调用失败,使用模拟响应:', error);
return this.getMockResponse(message);
}
},
// 获取模拟响应
getMockResponse(message) {
const responses = [
'这是一个模拟响应。要获得真实的 Claude 响应,请配置有效的 API Key。',
'我理解你的问题。作为演示版本,我提供预设的回复。',
'很好的问题!在实际使用中,Claude 会提供更详细和个性化的回答。',
'感谢你的提问。这个历史记录功能可以保存所有的对话记录。'
];
return responses[Math.floor(Math.random() * responses.length)] + '\n\n你说的是:"' + message + '"';
}
},
mounted() {
console.log('Vue + Claude 历史记录版已挂载!');
// 加载 API Key
const savedKey = localStorage.getItem(API_KEY_STORAGE);
if (savedKey) {
this.apiKey = savedKey;
}
// 加载历史记录
this.loadFromStorage();
// 创建初始会话
this.createNewSession();
// 显示欢迎消息
this.conversation.push({
role: 'assistant',
content: '欢迎使用增强版 Claude 插件!现在支持完整的历史记录功能:\n\n✅ 保存所有对话历史\n✅ 搜索历史记录\n✅ 导入/导出对话\n✅ 多会话管理\n✅ 会话重命名\n\n你可以在左侧查看和管理所有历史对话。',
timestamp: Date.now()
});
}
}).mount('#app');
</script>
</body>
</html>
+4
View File
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 261.76 226.69" xmlns="http://www.w3.org/2000/svg">
<path d="m161.096.001-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/>
<path d="m161.096.001-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/>
</svg>

After

Width:  |  Height:  |  Size: 307 B