diff --git a/index.html b/index.html new file mode 100644 index 0000000..d1ae82e --- /dev/null +++ b/index.html @@ -0,0 +1,395 @@ + + + + + + CodePlanGUI - 你的 IDEA AI 助手 + + + + + + + + +
+
+
内测开放中
+

不被厂商定义的
IDE AI 工作站

+

用你已有的 Coding Plan,在 IDEA 里获得完整的 AI 编码体验 — 流式对话、代码补全、Commit 生成、多步 Agent 工作流,不绑定任何模型厂商。

+ +

无需注册账号 · 你的 Key 你做主 · MIT 开源协议

+
+
+ + +
+
+ +

为 IDEA 而生的 AI 体验

+

以下所有功能均已实现,开箱即用。不画饼,只交付。

+
+ +
+
💬
+

流式 Chat

+

SSE 逐 token 输出,Markdown 渲染 + 代码高亮。

+
+ +
+
+

Commit Message

+

一键读取暂存区 diff,生成 Conventional Commits。

+
+ +
+
🔌
+

多 Provider

+

任意 OpenAI 兼容接口,支持连接测试。

+
+ +
+
🖱️
+

Ask AI

+

右键选中代码直接提问,告别复制粘贴。

+
+ +
+
🛡️
+

命令权限控制

+

白名单 + 用户审批 + 工作区路径保护。

+
+ +
+
🔒
+

安全存储

+

API Key 存入 PasswordSafe,明文不落盘。

+
+ + +
+
✏️
+ 开发中 +

内联补全

+

光标停留触发 AI 建议,Tab 接受。

+
+ +
+
🤖
+ 开发中 +

异构 Agent

+

每个节点绑定不同 Provider,按需分配模型。

+
+ +
+
🔌
+ 开发中 +

MCP Server

+

管理 MCP Servers,AI 自动发现外部工具。

+
+ +
+
📊
+ 开发中 +

用量统计

+

Token 用量与费用估算,辅助 Provider 选型。

+
+ +
+
+
+ + +
+
+ +

不被厂商绑架的 IDE AI 助手

+

通义灵码有 IDE 集成但锁定自家模型;Claude Code GUI 能力强但绑定 Anthropic 协议。CodePlanGUI 兼得两者之长。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodePlanGUI通义灵码 / CopilotClaude Code GUI
接入任意 OpenAI 兼容模型 DeepSeek / 千问 / 豆包 / Ollama… 绑定自家模型 仅 Anthropic 协议
Chat 侧边栏 + 代码高亮
Commit Message 生成
命令执行 + 权限审批
本地 Ollama / 私有部署
零服务器 / Key 不上传
异构多节点 Agent(开发中)
+
+
+
+ + +
+
+ +

你的 API,你做主

+

支持所有 OpenAI 兼容接口,已有 Key 即可使用,无需重新注册。

+
+
OpenAI
+
DeepSeek
+
阿里百炼(千问)
+
字节豆包
+
本地 Ollama
+
任何 OpenAI 兼容接口...
+
+
+┌─────────────────────────────────────────────┐ +│ IntelliJ IDEA │ +│ ├── CodePlanGUI Tool Window (Chat) │ +│ ├── Settings → Providers 管理 │ +│ ├── Git Commit → ✨ 生成提交信息 │ +│ └── JCEF Browser (React 19 + Ant Design 5) │ +│ ↕ Kotlin ↔ JS Bridge │ +│ ↕ OkHttp SSE │ +└─────────────────────────────────────────────┘ + ↓ + 你自己的 AI API (OpenAI 兼容) + 阿里百炼 / 豆包 / DeepSeek / OpenAI / Ollama
+
+
+ + +
+
+ +

三步上手

+

无需注册,无需绑定账号,几分钟即可开始使用。

+
+
+
1
+

安装插件

+

从 GitHub 下载 zip,Settings → Plugins → Install from Disk

+
+
+
2
+

配置 Provider

+

Settings → Tools → CodePlanGUI,填入你的 API Endpoint 和 Key

+
+
+
3
+

开始对话

+

打开右侧 Tool Window,开始和 AI 畅聊你的代码

+
+
+
+
+ + +
+
+

欢迎加入内测

+

CodePlanGUI 正在内测中,我们期待你的反馈与建议。在 GitHub 上 Star、提 Issue 或提交 PR,一起打造更好的 IDEA AI 插件。

+ +
+
+ + + + + + + + + diff --git a/src/main/kotlin/com/github/codeplangui/ChatService.kt b/src/main/kotlin/com/github/codeplangui/ChatService.kt index f13bf23..c12c3fb 100644 --- a/src/main/kotlin/com/github/codeplangui/ChatService.kt +++ b/src/main/kotlin/com/github/codeplangui/ChatService.kt @@ -667,6 +667,8 @@ $selection if (session.getMessages().any { it.role != MessageRole.SYSTEM }) { return } + val ttlDays = PluginSettings.getInstance().state.sessionTtlDays + sessionStore.evictExpiredSessions(ttlDays) val data = sessionStore.loadSession() ?: return session = ChatSession(data.threadId) data.messages.forEach { session.add(it) } diff --git a/src/main/kotlin/com/github/codeplangui/settings/PluginSettings.kt b/src/main/kotlin/com/github/codeplangui/settings/PluginSettings.kt index b95994b..93394a2 100644 --- a/src/main/kotlin/com/github/codeplangui/settings/PluginSettings.kt +++ b/src/main/kotlin/com/github/codeplangui/settings/PluginSettings.kt @@ -32,7 +32,8 @@ data class SettingsState( var memoryText: String = "", var commandExecutionEnabled: Boolean = true, var commandWhitelist: MutableList = ShellPlatform.current().defaultWhitelist().toMutableList(), - var commandTimeoutSeconds: Int = 30 + var commandTimeoutSeconds: Int = 30, + var sessionTtlDays: Int = 30 ) @State( diff --git a/src/main/kotlin/com/github/codeplangui/settings/PluginSettingsConfigurable.kt b/src/main/kotlin/com/github/codeplangui/settings/PluginSettingsConfigurable.kt index 8439be1..cbbf877 100644 --- a/src/main/kotlin/com/github/codeplangui/settings/PluginSettingsConfigurable.kt +++ b/src/main/kotlin/com/github/codeplangui/settings/PluginSettingsConfigurable.kt @@ -60,6 +60,7 @@ class PluginSettingsConfigurable : Configurable { private lateinit var commandTimeoutSpinner: JSpinner private lateinit var commandWhitelistModel: DefaultListModel private lateinit var commandWhitelistList: JList + private lateinit var sessionTtlDaysSpinner: JSpinner private val client = OkHttpSseClient() private val pendingApiKeyUpdates = linkedMapOf() @@ -208,6 +209,8 @@ class PluginSettingsConfigurable : Configurable { wrapStyleWord = true } + sessionTtlDaysSpinner = JSpinner(SpinnerNumberModel(settings.sessionTtlDays, 1, 365, 1)) + val chatCommitPanel = FormBuilder.createFormBuilder() .addLabeledComponent("Temperature:", temperatureSpinner) .addLabeledComponent("Max Tokens:", maxTokensSpinner) @@ -240,6 +243,10 @@ class PluginSettingsConfigurable : Configurable { JBLabel("AI 记忆(注入所有对话的系统提示词):"), JScrollPane(memoryTextArea) ) + .addLabeledComponent( + "Session 过期天数 (0 = 永不过期):", + sessionTtlDaysSpinner + ) .panel val execSettings = SettingsFormState.fromSettingsState(PluginSettings.getInstance().getState()) @@ -345,6 +352,7 @@ class PluginSettingsConfigurable : Configurable { commitMaxFilesSpinner.value = settings.commitMaxFiles commitDiffLineLimitSpinner.value = settings.commitDiffLineLimit memoryTextArea.text = settings.memoryText + sessionTtlDaysSpinner.value = settings.sessionTtlDays val execState = SettingsFormState.fromSettingsState(PluginSettings.getInstance().getState()) commandExecutionCheckbox.isSelected = execState.commandExecutionEnabled commandTimeoutSpinner.value = execState.commandTimeoutSeconds @@ -375,6 +383,7 @@ class PluginSettingsConfigurable : Configurable { commandWhitelistModel.getElementAt(it) }.toMutableList(), commandTimeoutSeconds = (commandTimeoutSpinner.value as Number).toInt(), + sessionTtlDays = (sessionTtlDaysSpinner.value as Number).toInt(), ) private fun selectedProviderId(): String? = diff --git a/src/main/kotlin/com/github/codeplangui/settings/SettingsFormState.kt b/src/main/kotlin/com/github/codeplangui/settings/SettingsFormState.kt index cabeee5..794f47d 100644 --- a/src/main/kotlin/com/github/codeplangui/settings/SettingsFormState.kt +++ b/src/main/kotlin/com/github/codeplangui/settings/SettingsFormState.kt @@ -17,7 +17,8 @@ data class SettingsFormState( var memoryText: String = "", var commandExecutionEnabled: Boolean = true, var commandWhitelist: MutableList = ShellPlatform.current().defaultWhitelist().toMutableList(), - var commandTimeoutSeconds: Int = 30 + var commandTimeoutSeconds: Int = 30, + var sessionTtlDays: Int = 30 ) { fun toSettingsState(): SettingsState = SettingsState( providers = providers.toMutableList(), @@ -35,6 +36,7 @@ data class SettingsFormState( commandExecutionEnabled = commandExecutionEnabled, commandWhitelist = commandWhitelist.toMutableList(), commandTimeoutSeconds = commandTimeoutSeconds, + sessionTtlDays = sessionTtlDays, ) companion object { @@ -54,6 +56,7 @@ data class SettingsFormState( commandExecutionEnabled = state.commandExecutionEnabled, commandWhitelist = state.commandWhitelist.toMutableList(), commandTimeoutSeconds = state.commandTimeoutSeconds, + sessionTtlDays = state.sessionTtlDays, ) } } diff --git a/src/main/kotlin/com/github/codeplangui/storage/SessionStore.kt b/src/main/kotlin/com/github/codeplangui/storage/SessionStore.kt index 8069b93..8901cc9 100644 --- a/src/main/kotlin/com/github/codeplangui/storage/SessionStore.kt +++ b/src/main/kotlin/com/github/codeplangui/storage/SessionStore.kt @@ -10,11 +10,14 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption import java.security.MessageDigest +import java.time.Instant +import java.time.temporal.ChronoUnit @Serializable data class SessionData( val threadId: String, - val messages: List + val messages: List, + val lastAccessedAt: Long = System.currentTimeMillis() ) class SessionStore(private val projectId: String) { @@ -41,7 +44,7 @@ class SessionStore(private val projectId: String) { fun saveSession(threadId: String, messages: List) { try { - val data = SessionData(threadId, messages) + val data = SessionData(threadId, messages, lastAccessedAt = System.currentTimeMillis()) val content = json.encodeToString(data) Files.writeString(tempFile, content) try { @@ -66,6 +69,38 @@ class SessionStore(private val projectId: String) { } } + /** + * Evict sessions that haven't been accessed within [ttlDays] days. + * Scans all project session directories under the sessions root. + */ + fun evictExpiredSessions(ttlDays: Int) { + if (ttlDays <= 0) return + try { + val sessionsRoot = dataDir.parent + if (!Files.isDirectory(sessionsRoot)) return + val cutoff = Instant.now().minus(ttlDays.toLong(), ChronoUnit.DAYS) + Files.list(sessionsRoot).use { dirs -> + dirs.filter { Files.isDirectory(it) }.forEach { projectDir -> + val file = projectDir.resolve("session.json") + if (!Files.exists(file)) return@forEach + try { + val content = Files.readString(file) + val data = json.decodeFromString(content) + val lastAccess = Instant.ofEpochMilli(data.lastAccessedAt) + if (lastAccess.isBefore(cutoff)) { + Files.deleteIfExists(file) + logger.warn("Evicted expired session: $projectDir (last accessed $lastAccess)") + } + } catch (e: Exception) { + logger.warn("Failed to check session expiry for $projectDir", e) + } + } + } + } catch (e: Exception) { + logger.warn("Failed to evict expired sessions", e) + } + } + fun clearSession() { try { Files.deleteIfExists(sessionFile)