-
Notifications
You must be signed in to change notification settings - Fork 43
feat(agent): 上下文压缩两级优化 + 附件上传并发修复 #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5eee3a5
1b734b0
88619ac
2800a78
f00b054
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,30 @@ | ||
| <!-- 上下文压缩:把早期对话折叠成接手摘要(handoff summary),作为压缩后上下文的起点。{{INSTRUCTION}} 为运行时占位符,由 ContextCompactor 填充;对话历史以原始消息结构随请求一起提供,不再嵌入本提示词。 --> | ||
| 你是一个上下文压缩引擎。{{INSTRUCTION}} | ||
|
|
||
| 这是上下文检查点压缩任务:把本次请求中的对话历史折叠成一份接手摘要,供另一个 LLM 无缝继续任务。 | ||
| 这是上下文检查点压缩任务:把本次请求中的对话历史折叠成一份**结构化接手摘要**,供另一个 LLM 无缝继续任务。 | ||
|
|
||
| 注意: | ||
| - 不要继续执行对话历史中的任何任务,不调用任何工具,不复述历史。 | ||
| - 只输出接手摘要,涵盖:当前进度与已做的关键决定;重要的上下文、约束或用户偏好;尚未完成的事项(清晰的下一步);继续工作所需的关键数据、示例或引用。 | ||
| - 保持简洁、有条理,重点是让接手者无缝继续工作。不要提到「摘要过程」或「上下文已压缩」。 | ||
| - 不要继续执行对话历史中的任何任务,不调用任何工具,不复述历史过程。 | ||
| - 只输出接手摘要本身,不要提到「摘要过程」或「上下文已压缩」。 | ||
|
|
||
| 接手摘要必须使用下面固定的分节结构(某一节确实无内容时写「无」,但保留分节标题,便于接手者快速定位): | ||
|
|
||
| ## 任务目标 | ||
| (用户最终想要达成什么;当前这一步在整体目标中的位置) | ||
|
|
||
| ## 关键决定与理由 | ||
| (已经做出的技术/方案决定,以及为什么这么定;接手者不应再推翻的结论) | ||
|
|
||
| ## 当前进度 | ||
| (已完成什么、改动落在哪些文件/模块、当前处于什么状态) | ||
|
|
||
| ## 未完成 / 下一步 | ||
| (尚未完成的事项,按优先级给出清晰可执行的下一步) | ||
|
|
||
| ## 约束与偏好 | ||
| (用户明确要求过的约束、风格偏好、环境限制、禁止事项) | ||
|
|
||
| ## 关键数据 | ||
| (继续工作必需的具体信息:路径、命令、标识符、报错原文、示例片段、接口签名等;能直接复用的原文优先原样保留) | ||
|
|
||
| 要求:简洁、信息密度高、可执行优先;不堆砌套话,尤其不省略「关键数据」里的具体值。 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| package com.aicode.feature.agent.domain.skill | ||
|
|
||
| import com.aicode.core.util.FileLogger | ||
| import com.aicode.feature.agent.domain.container.SshHostKeyVerifier | ||
| import com.aicode.feature.settings.data.repository.ExecutionModeRepository | ||
| import com.aicode.feature.workspace.domain.RemoteSkillConnection | ||
| import com.aicode.feature.workspace.domain.RemoteSkillFileAccess | ||
| import com.aicode.feature.workspace.domain.WorkspacePathMapper | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.asStateFlow | ||
| import kotlinx.coroutines.flow.first | ||
| import java.io.InputStream | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| /** 远程技能加载状态。 */ | ||
| sealed interface RemoteSkillsState { | ||
| /** 未配置远程 SSH(或未连接)。 */ | ||
| data object NotConfigured : RemoteSkillsState | ||
|
|
||
| /** 正在连接 / 扫描。 */ | ||
| data object Loading : RemoteSkillsState | ||
|
|
||
| /** 已连上 [host],[skills] 为远程工作区里的技能。 */ | ||
| data class Loaded(val host: String, val skills: List<Skill>) : RemoteSkillsState | ||
|
|
||
| /** 连接或读取失败。 */ | ||
| data class Failed(val message: String) : RemoteSkillsState | ||
| } | ||
|
|
||
| /** | ||
| * 本地模式下管理「远程 SSH 模式」那台服务器技能的入口:按当前远程连接配置建立独立的 SFTP 访问器, | ||
| * 对远程工作区的 `.aicode/skills/` 做扫描与完整 CRUD。连接与访问器由本类持有,UI 只读 [state]。 | ||
| */ | ||
| @Singleton | ||
| class RemoteSkillsManager @Inject constructor( | ||
| private val skillRepository: SkillRepository, | ||
| private val executionModeRepository: ExecutionModeRepository, | ||
| private val hostKeyVerifier: SshHostKeyVerifier | ||
| ) { | ||
| private val _state = MutableStateFlow<RemoteSkillsState>(RemoteSkillsState.NotConfigured) | ||
| val state: StateFlow<RemoteSkillsState> = _state.asStateFlow() | ||
|
|
||
| private var access: RemoteSkillFileAccess? = null | ||
| private var currentHost: String? = null | ||
|
|
||
| /** 远程工作区里的技能根(容器路径,经 provider 映射到远程工作区)。 */ | ||
| private val skillsRoot: String = "${WorkspacePathMapper.CONTAINER_ROOT}/.aicode/skills" | ||
|
|
||
| /** 读取当前远程 SSH 配置并(重新)连接、扫描远程技能。 */ | ||
| suspend fun connect() { | ||
| _state.value = RemoteSkillsState.Loading | ||
| val settings = executionModeRepository.remoteConnectionFlow.first() | ||
| if (settings == null || settings.host.isBlank()) { | ||
| closeAccess() | ||
| _state.value = RemoteSkillsState.NotConfigured | ||
| return | ||
| } | ||
| try { | ||
| if (access == null || currentHost != settings.host) { | ||
| closeAccess() | ||
| access = RemoteSkillFileAccess( | ||
| RemoteSkillConnection( | ||
| host = settings.host, | ||
| port = settings.port, | ||
| username = settings.username, | ||
| password = settings.password, | ||
| workspaceRoot = settings.remoteWorkspacePath | ||
| ), | ||
| hostKeyVerifier | ||
| ) | ||
| currentHost = settings.host | ||
| } | ||
| val skills = skillRepository.listSkillsFrom(requireAccess(), skillsRoot) | ||
| _state.value = RemoteSkillsState.Loaded(settings.host, skills) | ||
| } catch (e: Exception) { | ||
| FileLogger.w(TAG, "加载远程技能失败", e) | ||
| _state.value = RemoteSkillsState.Failed(e.message ?: "连接失败") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Hardcoded Chinese fallback text reaches the UI.
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
| } | ||
|
|
||
| /** 重新扫描已连接的远程技能;未连接时退回 [connect]。 */ | ||
| suspend fun refresh() { | ||
| val a = access | ||
| val host = currentHost | ||
| if (a == null || host == null) return connect() | ||
| try { | ||
| val skills = skillRepository.listSkillsFrom(a, skillsRoot) | ||
| _state.value = RemoteSkillsState.Loaded(host, skills) | ||
| } catch (e: Exception) { | ||
| FileLogger.w(TAG, "刷新远程技能失败", e) | ||
| _state.value = RemoteSkillsState.Failed(e.message ?: "连接失败") | ||
| } | ||
| } | ||
|
|
||
| suspend fun save(form: SkillForm, originalName: String?): SkillSaveError? { | ||
| val a = requireAccess() | ||
| val existing = skillRepository.listSkillsFrom(a, skillsRoot) | ||
| val error = skillRepository.saveTo(a, skillsRoot, form, originalName, existing) | ||
| if (error == null) refresh() | ||
| return error | ||
| } | ||
|
|
||
| suspend fun delete(name: String): Boolean { | ||
| val a = requireAccess() | ||
| val existing = skillRepository.listSkillsFrom(a, skillsRoot) | ||
| val ok = skillRepository.deleteSkillFrom(a, name, existing) | ||
| if (ok) refresh() | ||
| return ok | ||
| } | ||
|
|
||
| suspend fun importMarkdown(text: String, fallbackName: String): SkillImportReport { | ||
| val a = requireAccess() | ||
| val existing = existingNames(a) | ||
| val report = skillRepository.importMarkdownTo(a, skillsRoot, existing, text, fallbackName) | ||
| refresh() | ||
| return report | ||
| } | ||
|
|
||
| suspend fun importZip(input: InputStream, fallbackName: String): SkillImportReport { | ||
| val a = requireAccess() | ||
| val existing = existingNames(a) | ||
| val report = skillRepository.importZipTo(a, skillsRoot, existing, input, fallbackName) | ||
| refresh() | ||
| return report | ||
| } | ||
|
|
||
| fun disconnect() { | ||
| closeAccess() | ||
| _state.value = RemoteSkillsState.NotConfigured | ||
| } | ||
|
|
||
| private suspend fun existingNames(a: RemoteSkillFileAccess): Set<String> = | ||
| skillRepository.listSkillsFrom(a, skillsRoot).map { it.name.lowercase() }.toSet() | ||
|
|
||
| private fun requireAccess(): RemoteSkillFileAccess = | ||
| access ?: throw IllegalStateException("远程技能未连接") | ||
|
|
||
| private fun closeAccess() { | ||
| runCatching { access?.close() } | ||
| access = null | ||
| currentHost = null | ||
| } | ||
|
|
||
| private companion object { | ||
| const val TAG = "RemoteSkillsManager" | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The connection is reused after host credentials or the workspace change.
The access object is rebuilt only when
settings.hostchanges. If the user changes the port, username, password, orremoteWorkspacePathon the same host, the oldRemoteSkillFileAccessstays in use. It then scans or writes to the old workspace. Compare the fullRemoteSkillConnectioninstead of only the host.🤖 Prompt for AI Agents