Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ public final class ExtensionRepository extends BaseRepository implements Extensi
private final SkillRepository skillRepository;

public ExtensionRepository(LineCodeDatabase database, ResourceProvider resourceProvider, SkillFileManager fileManager, SkillPromptProvider promptProvider) {
this(database, resourceProvider, fileManager, promptProvider, null, null);
}

public ExtensionRepository(LineCodeDatabase database, ResourceProvider resourceProvider, SkillFileManager fileManager, SkillPromptProvider promptProvider, cn.lineai.ssh.SshService sshService, cn.lineai.ipc.IpcProviderManager ipcProviderManager) {
super(database);
this.agentRepository = new AgentExtensionRepository(database);
this.mcpRepository = new McpExtensionRepository(database);
this.skillRepository = new SkillRepository(database, resourceProvider, fileManager, this.agentRepository, this.mcpRepository, promptProvider);
this.skillRepository = new SkillRepository(database, resourceProvider, fileManager, this.agentRepository, this.mcpRepository, promptProvider, sshService, ipcProviderManager);
}

@Override
Expand Down Expand Up @@ -105,6 +109,21 @@ public synchronized SkillRecord installSkillFromGitHub(String homePath, String l
return skillRepository.installSkillFromGitHub(homePath, location, githubUrl);
}

@Override
public synchronized SkillRecord installSkillFromSkillHub(String homePath, String location, String slug, String version) throws Exception {
return skillRepository.installSkillFromSkillHub(homePath, location, slug, version);
}

@Override
public synchronized SkillRecord installSkillFromSkillHubSsh(String location, String slug, String version) throws Exception {
return skillRepository.installSkillFromSkillHubSsh(location, slug, version);
}

@Override
public synchronized SkillRecord installSkillFromSkillHubIpc(String location, String slug, String version) throws Exception {
return skillRepository.installSkillFromSkillHubIpc(location, slug, version);
}

@Override
public synchronized void setSkillEnabled(String id, boolean enabled) {
skillRepository.setSkillEnabled(id, enabled);
Expand Down
237 changes: 229 additions & 8 deletions app/src/main/java/cn/lineai/data/repository/SkillRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
import cn.lineai.data.db.LineCodeDatabase;
import cn.lineai.data.service.GitHubSkillInstaller;
import cn.lineai.data.service.SkillFileManager;
import cn.lineai.data.service.SkillHubClient;
import cn.lineai.ipc.IpcProviderManager;
import cn.lineai.ipc.IpcProviderType;
import cn.lineai.ipc.terminal.TerminalIpcProvider;
import cn.lineai.model.ExtensionAgentConfig;
import cn.lineai.model.ExtensionMcpConfig;
import cn.lineai.model.McpToolSummary;
Expand Down Expand Up @@ -35,14 +39,22 @@ public final class SkillRepository extends BaseRepository {
private final SkillPromptProvider promptProvider;
private final AgentExtensionRepository agentRepository;
private final McpExtensionRepository mcpRepository;
private final cn.lineai.ssh.SshService sshService;
private final IpcProviderManager ipcProviderManager;

public SkillRepository(LineCodeDatabase database, ResourceProvider resourceProvider, SkillFileManager fileManager, AgentExtensionRepository agentRepository, McpExtensionRepository mcpRepository, SkillPromptProvider promptProvider) {
this(database, resourceProvider, fileManager, agentRepository, mcpRepository, promptProvider, null, null);
}

public SkillRepository(LineCodeDatabase database, ResourceProvider resourceProvider, SkillFileManager fileManager, AgentExtensionRepository agentRepository, McpExtensionRepository mcpRepository, SkillPromptProvider promptProvider, cn.lineai.ssh.SshService sshService, IpcProviderManager ipcProviderManager) {
super(database);
this.resourceProvider = resourceProvider;
this.fileManager = fileManager;
this.promptProvider = promptProvider;
this.agentRepository = agentRepository;
this.mcpRepository = mcpRepository;
this.sshService = sshService;
this.ipcProviderManager = ipcProviderManager;
}

public synchronized List<SkillRecord> getSkills(String homePath) {
Expand Down Expand Up @@ -106,11 +118,12 @@ public synchronized SkillRecord installSkillFromUri(String homePath, String loca
File tempDir = fileManager.uniqueChild(tempRoot, fileManager.stripExtension(fileName));
File tempFile = new File(tempDir, fileName.toLowerCase(Locale.ROOT).endsWith(".zip") ? fileName : "SKILL.md");
fileManager.copyUriToFile(uri, tempFile);
try {
return installSkill(homePath, location, tempFile.getAbsolutePath(), fileManager.stripExtension(fileName));
} finally {
fileManager.deleteRecursive(tempDir);
}
return installTemporarySkill(
homePath,
location,
tempFile,
fileManager.stripExtension(fileName),
tempDir);
}

public synchronized SkillRecord installSkillFromGitHub(String homePath, String location, String githubUrl) throws Exception {
Expand All @@ -121,10 +134,216 @@ public synchronized SkillRecord installSkillFromGitHub(String homePath, String l
fileManager
);
File downloaded = installer.downloadToTemp(githubUrl);
return installTemporarySkill(
homePath,
location,
downloaded,
downloaded.getName(),
downloaded);
}

public synchronized SkillRecord installSkillFromSkillHub(
String homePath,
String location,
String slug,
String version
) throws Exception {
fileManager.ensureSkillRoots(homePath);
File tempRoot = new File(fileManager.getWorkspacePaths().getLinecodeRoot(), "tmp/skills-skillhub");
File tempDir = fileManager.uniqueChild(tempRoot, fileManager.sanitizeFileName(slug));
File archive = new File(tempDir, "skill.zip");
byte[] bytes = new SkillHubClient().download(slug, version);
try {
fileManager.writeBytes(archive, bytes);
} catch (RuntimeException error) {
fileManager.deleteRecursive(tempDir);
throw error;
}
return installTemporarySkill(homePath, location, archive, slug, tempDir);
}

public synchronized SkillRecord installSkillFromSkillHubSsh(
String location,
String slug,
String version
) throws Exception {
if (sshService == null) {
throw new IllegalStateException("SSH 服务未配置。");
}
return installSkillToSsh(slug, version);
}

public synchronized SkillRecord installSkillFromSkillHubIpc(
String location,
String slug,
String version
) throws Exception {
if (ipcProviderManager == null) {
throw new IllegalStateException("IPC 终端服务未配置。");
}
return installSkillToIpc(slug, version);
}

private SkillRecord installSkillToSsh(String slug, String version) throws Exception {
File tempDir = prepareSkillHubArchive(slug, version);
try {
File skillMd = fileManager.findSkillMd(tempDir, 0);
if (skillMd == null) {
throw new IllegalArgumentException("Skill 包缺少 SKILL.md。");
}
File skillRoot = skillMd.getParentFile();
SkillRecord local = fileManager.parseSkill(skillRoot, skillMd, SkillRecord.LOCATION_SSH);
String baseName = fileManager.sanitizeFileName(slug);
String remoteRoot = sshService.withSftp(sftp -> {
sftp.cd("~");
mkdirIfMissing(sftp, ".linecode");
sftp.cd(".linecode");
mkdirIfMissing(sftp, "skills");
sftp.cd("skills");
String targetName = baseName;
if (exists(sftp, targetName)) {
targetName += "_" + System.currentTimeMillis();
}
sftp.mkdir(targetName);
sftp.cd(targetName);
uploadSshDirectory(sftp, skillRoot);
return "~/.linecode/skills/" + targetName;
}, 120000);
SkillRecord record = remoteRecord(local, remoteRoot, remoteRoot + "/SKILL.md", SkillRecord.LOCATION_SSH);
upsertDiscoveredSkills(Collections.singletonList(record));
return record;
} finally {
fileManager.deleteRecursive(tempDir);
}
}

private SkillRecord installSkillToIpc(String slug, String version) throws Exception {
File tempDir = prepareSkillHubArchive(slug, version);
try {
File skillMd = fileManager.findSkillMd(tempDir, 0);
if (skillMd == null) {
throw new IllegalArgumentException("Skill 包缺少 SKILL.md。");
}
File skillRoot = skillMd.getParentFile();
SkillRecord local = fileManager.parseSkill(skillRoot, skillMd, SkillRecord.LOCATION_IPC);
TerminalIpcProvider provider = (TerminalIpcProvider) ipcProviderManager.getProviderByType(IpcProviderType.TERMINAL);
if (provider == null) {
throw new IllegalStateException("IPC 终端服务未绑定。");
}
String home = provider.getHomePath();
if (home == null || home.trim().length() == 0) {
throw new IllegalStateException("IPC 终端未返回主目录。");
}
String root = home + "/.linecode/skills";
provider.executeShell("mkdir -p " + remoteShellQuote(root), null, 30000, null);
String target = root + "/" + fileManager.sanitizeFileName(slug);
if (provider.fileExists(target)) {
target += "_" + System.currentTimeMillis();
}
provider.executeShell("mkdir -p " + remoteShellQuote(target), null, 30000, null);
uploadIpcDirectory(provider, skillRoot, target);
SkillRecord record = remoteRecord(local, target, target + "/SKILL.md", SkillRecord.LOCATION_IPC);
upsertDiscoveredSkills(Collections.singletonList(record));
return record;
} finally {
fileManager.deleteRecursive(tempDir);
}
}

private File prepareSkillHubArchive(String slug, String version) throws Exception {
File tempRoot = new File(fileManager.getWorkspacePaths().getLinecodeRoot(), "tmp/skills-skillhub-remote");
File tempDir = fileManager.uniqueChild(tempRoot, fileManager.sanitizeFileName(slug));
try {
File archive = new File(tempDir, "skill.zip");
fileManager.writeBytes(archive, new SkillHubClient().download(slug, version));
fileManager.unzip(archive, tempDir);
archive.delete();
return tempDir;
} catch (Exception error) {
fileManager.deleteRecursive(tempDir);
throw error;
}
}

private SkillRecord remoteRecord(SkillRecord local, String rootPath, String skillMdPath, String location) {
return new SkillRecord(
fileManager.skillId(location, skillMdPath), local.getName(), local.getDescription(),
rootPath, skillMdPath, location, true, local.getDiscoveredAt(), System.currentTimeMillis());
}

private void mkdirIfMissing(com.jcraft.jsch.ChannelSftp sftp, String name) throws Exception {
try {
sftp.stat(name);
} catch (Exception ignored) {
sftp.mkdir(name);
}
}

private boolean exists(com.jcraft.jsch.ChannelSftp sftp, String name) {
try {
sftp.stat(name);
return true;
} catch (Exception ignored) {
return false;
}
}

private void uploadSshDirectory(com.jcraft.jsch.ChannelSftp sftp, File directory) throws Exception {
File[] children = directory.listFiles();
if (children == null) {
return;
}
for (File child : children) {
if (child.isDirectory()) {
mkdirIfMissing(sftp, child.getName());
sftp.cd(child.getName());
uploadSshDirectory(sftp, child);
sftp.cd("..");
} else {
java.io.FileInputStream input = new java.io.FileInputStream(child);
try {
sftp.put(input, child.getName(), com.jcraft.jsch.ChannelSftp.OVERWRITE);
} finally {
input.close();
}
}
}
}

private void uploadIpcDirectory(TerminalIpcProvider provider, File directory, String remoteDirectory) throws Exception {
File[] children = directory.listFiles();
if (children == null) {
return;
}
for (File child : children) {
String remotePath = remoteDirectory + "/" + child.getName();
if (child.isDirectory()) {
provider.executeShell("mkdir -p " + remoteShellQuote(remotePath), null, 30000, null);
uploadIpcDirectory(provider, child, remotePath);
} else {
byte[] bytes = java.nio.file.Files.readAllBytes(child.toPath());
if (!provider.writeFile(remotePath, bytes)) {
throw new IllegalStateException("IPC 写入 Skill 文件失败: " + remotePath);
}
}
}
}

private String remoteShellQuote(String value) {
return "'" + (value == null ? "" : value).replace("'", "'\\\"'\\\"'") + "'";
}

private SkillRecord installTemporarySkill(
String homePath,
String location,
File source,
String name,
File cleanupTarget
) throws Exception {
try {
return installSkill(homePath, location, downloaded.getAbsolutePath(), downloaded.getName());
return installSkill(homePath, location, source.getAbsolutePath(), name);
} finally {
fileManager.deleteRecursive(downloaded);
fileManager.deleteRecursive(cleanupTarget);
}
}

Expand All @@ -144,7 +363,9 @@ public synchronized void setSkillEnabled(String id, boolean enabled) {
public synchronized void deleteSkill(String id) {
SkillRecord target = findSkill(id);
database.getWritableDatabase().delete("skills", "id = ?", new String[] {safe(id)});
if (target != null && !SkillRecord.LOCATION_SSH.equals(target.getLocation()) && target.getRootPath().length() > 0) {
if (target != null && !SkillRecord.LOCATION_SSH.equals(target.getLocation())
&& !SkillRecord.LOCATION_IPC.equals(target.getLocation())
&& target.getRootPath().length() > 0) {
fileManager.deleteRecursive(new File(target.getRootPath()));
}
}
Expand Down
Loading