Compare commits

...

19 Commits

Author SHA1 Message Date
dcf190067c Merge pull request 'feat: auto-configure DevStar MCP Server for Github Copilot and Trae' (#6) from feature/auto-configure-mcp into main
Some checks failed
CI/CD Pipeline for DevStar Extension / build (push) Failing after 9s
Reviewed-on: #6
2026-01-09 12:18:33 +00:00
a816e9cd91 feat: auto-configure local VSCode MCP Server for GitHub Copilot
All checks were successful
CI/CD Pipeline for DevStar Extension / build (pull_request) Successful in 7m18s
2026-01-07 21:13:48 +08:00
afd408a4b8 feat: automatically configure DevStar MCP Server in container for Trae 2026-01-07 20:52:08 +08:00
7c2d5d9ad9 Merge pull request 'Fix: Use VSCode Extension API for remote SSH connection (Trae IDE support)' (#5) from feature/trae-ide-support into main
Some checks failed
CI/CD Pipeline for DevStar Extension / build (push) Failing after 14s
Reviewed-on: #5
2025-12-31 03:42:54 +00:00
2e9126fe64 fix: use VSCode Extension API for remote SSH connection to support Trae IDE
Some checks failed
CI/CD Pipeline for DevStar Extension / build (pull_request) Has been cancelled
2025-12-31 11:26:41 +08:00
67cb6ab7f0 feature-open-with-vscode (#4)
Some checks failed
CI/CD Pipeline for DevStar Extension / build (push) Failing after 11s
Co-authored-by: 孟宁 <mengning@mengning.com.cn>
Reviewed-on: #4
Co-authored-by: yinxue <2643126914@qq.com>
Co-committed-by: yinxue <2643126914@qq.com>
2025-12-31 01:52:07 +00:00
deee8f378f Merge pull request 'feature-open-with-vscode' (#1) from feature-open-with-vscode into main
Some checks failed
CI/CD Pipeline for DevStar Extension / build (push) Failing after 4m31s
Reviewed-on: #1
2025-11-26 05:23:52 +00:00
559c9eba27 移除工作流安装git
All checks were successful
CI/CD Pipeline for DevStar Extension / build (pull_request) Successful in 3m41s
2025-11-26 11:51:59 +08:00
fc59ba3bd0 移除自动创建版本标签
Some checks failed
CI/CD Pipeline for DevStar Extension / build (pull_request) Has been cancelled
2025-11-26 11:45:41 +08:00
b33384ca23 删除不必要代码&&修改工作流文件
Some checks failed
CI/CD Pipeline for DevStar Extension / build (pull_request) Has been cancelled
2025-11-26 11:04:05 +08:00
325fc8f4bd 实现点击open with vscode传递端口进行端口映射
Some checks failed
CI/CD Pipeline for DevStar Extension / build (pull_request) Failing after 2m28s
2025-11-25 15:41:51 +08:00
53d875e7ca 删除掉remote-container.ts中的一些调试信息 2025-11-18 17:36:29 +08:00
bdbae6eeb1 工作流镜像修改&&添加发布插件条件限制 2025-11-18 16:28:58 +08:00
728959c6b3 修改webpack 2025-11-18 15:28:20 +08:00
8f76f80938 将构建和发布合在一个阶段,并修改了命令 2025-11-18 14:44:02 +08:00
3c87a57c26 移除上传构建产物 2025-11-18 14:19:54 +08:00
ef67c1bb2c 添加工作流文件 2025-11-18 14:04:48 +08:00
515ab4ff91 已经完成随机端口号映射 2025-11-14 15:25:14 +08:00
4c2da46260 删除了README.en.md文件并更新了README.md 2025-11-13 15:02:22 +08:00
11 changed files with 753 additions and 324 deletions

View File

@@ -0,0 +1,76 @@
name: CI/CD Pipeline for DevStar Extension
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:20-alpine
steps:
- name: 安装 Git
run: |
apk add --no-cache git
- name: 拉取代码
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: 配置 Git
run: |
# 添加工作目录为安全目录
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git config --global --add safe.directory /github/workspace
# 配置用户信息
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: 自动递增版本号
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
apk add --no-cache jq
CURRENT_VERSION=$(jq -r '.version' package.json)
echo "当前版本: $CURRENT_VERSION"
# 分解版本号
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
# 递增补丁版本号
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH"
echo "新版本: $NEW_VERSION"
# 更新 package.json
jq --arg version "$NEW_VERSION" '.version = $version' package.json > package.json.tmp
mv package.json.tmp package.json
# 提交版本变更
git add package.json
git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
git push
- name: 安装依赖
run: |
npm install
- name: 构建插件
run: |
npm run package
- name: 发布插件
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
npm run publish
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}

View File

@@ -1,36 +0,0 @@
# DevStar
#### Description
Super IDE Client for VS Code
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

View File

@@ -2,7 +2,7 @@
## User Quick Start
进入home页面home页面的功能都需要登录后才能使用
点击插件后点击open进入home页面未登录时可以通过插件跳转到devstar网站登录后可以通过插件跳转到本地devstar个人主页
登录后,登录状态会长久保存,直到主动退出登录或者卸载插件。
@@ -14,26 +14,6 @@
1配置修改后重启vscode才能生效
### 创建新仓库/创建新项目
目前可供选择的字段
- name* 必填
- default_branch
- description
- gitignores
- issue_labels
- license
- object_format_name
- private
- readme
- template
- trust_model
### 打开项目
打开项目是指在vscode上打开远程容器中创建好的项目。选择项目名称右侧对应的Open project即可打开项目。
### 编译/调试
容器环境提供了开发环境,安装好编译与调试所需要的工具链。

View File

@@ -2,7 +2,7 @@
"name": "devstar",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.3.9",
"version": "0.4.3",
"keywords": [],
"publisher": "mengning",
"engines": {
@@ -136,4 +136,4 @@
"extensionDependencies": [
"ms-vscode.cpptools"
]
}
}

View File

@@ -43,31 +43,40 @@ export default class DevstarAPIHandler {
}
});
// 处理非200响应状态码
// 检查响应状态码
if (!response.ok) {
const text = await response.text(); // 读取文本防止json解析失败
if (response.status == 401) {
throw new Error('Token错误')
const text = await response.text(); // 读取文本内容以便调试
console.error(`HTTP Error: ${response.status} - ${text}`);
if (response.status === 401) {
throw new Error('Token错误');
} else {
throw new Error(`HTTP Error: ${response.status} - ${text}`);
throw new Error(`HTTP Error: ${response.status}`);
}
}
// 检查 Content-Type 是否为 JSON
const contentType = response.headers.get('Content-Type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text(); // 读取文本内容以便调试
console.error(`Unexpected Content-Type: ${contentType} - ${text}`);
throw new Error(`Unexpected Content-Type: ${contentType}`);
}
const responseData = await response.json();
const data = responseData.data
if (data.username == undefined || data.username == "") {
throw new Error('Token对应用户不存在')
} else {
// 验证用户名匹配
if (data.username !== username) {
throw new Error('Token与用户名不符');
}
const data = responseData.data;
if (!data || !data.username) {
throw new Error('Token对应用户不存在');
}
// 验证用户名匹配
if (data.username !== username) {
throw new Error('Token与用户名不符');
}
return true;
} catch (error) {
console.error(error)
return false
console.error(error);
return false;
}
}

View File

@@ -79,95 +79,35 @@ export default class DSHome {
async (message) => {
const data = message.data;
const need_return = message.need_return;
if (need_return) {
// ================= need return ====================
// 处理需要返回结果的消息
switch (message.command) {
// ----------------- frequent -----------------------
case 'getHomeConfig':
const config = {
language: vscode.env.language
};
panel.webview.postMessage({ command: 'getHomeConfig', data: { homeConfig: config } });
break;
case 'getUserToken':
const userToken = this.user.getUserTokenFromLocal();
if (userToken === undefined) {
panel.webview.postMessage({ command: 'getUserToken', data: { userToken: '' } });
} else {
panel.webview.postMessage({ command: 'getUserToken', data: { userToken: userToken } });
}
panel.webview.postMessage({
command: 'getUserToken',
data: { userToken: this.user.getUserTokenFromLocal() }
});
break;
case 'getUsername':
const username = this.user.getUsernameFromLocal();
if (username === undefined) {
panel.webview.postMessage({ command: 'getUsername', data: { username: '' } });
} else {
panel.webview.postMessage({ command: 'getUsername', data: { username: username } });
}
break;
case 'firstOpenRemoteFolder':
// data.host - project name
await this.remoteContainer.firstOpenProject(data.host, data.hostname, data.port, data.username, data.path, this.context);
break;
case 'openRemoteFolder':
this.remoteContainer.openRemoteFolder(data.host, data.port, data.username, data.path);
panel.webview.postMessage({
command: 'getUsername',
data: { username: this.user.getUsernameFromLocal() }
});
break;
case 'getDevstarDomain':
panel.webview.postMessage({ command: 'getDevstarDomain', data: { devstarDomain: this.devstarDomain } });
break;
// ----------------- not frequent -----------------------
case 'setUserToken':
this.user.setUserTokenToLocal(data.userToken);
if (data.userToken === this.user.getUserTokenFromLocal()) {
panel.webview.postMessage({ command: 'setUserToken', data: { ok: true } });
} else {
panel.webview.postMessage({ command: 'setUserToken', data: { ok: false } });
}
break;
case 'setUsername':
this.user.setUsernameToLocal(data.username);
if (data.username === this.user.getUsernameFromLocal()) {
panel.webview.postMessage({ command: 'setUsername', data: { ok: true } });
} else {
panel.webview.postMessage({ command: 'setUsername', data: { ok: false } });
}
break;
case 'getUserPublicKey':
let userPublicKey = '';
if (this.user.existUserPrivateKey()) {
userPublicKey = this.user.getUserPublicKey();
}
panel.webview.postMessage({ command: 'getUserPublicKey', data: { userPublicKey: userPublicKey } });
break;
case 'createUserPublicKey':
await this.user.createUserSSHKey();
if (this.user.existUserPublicKey()) {
panel.webview.postMessage({ command: 'createUserPublicKey', data: { ok: true } });
} else {
panel.webview.postMessage({ command: 'createUserPublicKey', data: { ok: false } });
}
break;
case 'getMachineName':
const machineName = os.hostname();
panel.webview.postMessage({ command: 'getMachineName', data: { machineName: machineName } });
panel.webview.postMessage({
command: 'getDevstarDomain',
data: { devstarDomain: this.devstarDomain }
});
break;
}
} else {
// ================= don't need return ==============
// frequent
// 处理不需要返回结果的消息
switch (message.command) {
// ----------------- frequent -----------------------
case 'showInformationNotification':
vscode.window.showInformationMessage(data.message);
break;
case 'showWarningNotification':
vscode.window.showWarningMessage(data.message);
break;
case 'showErrorNotification':
await utils.showErrorNotification(data.message);
break;
case 'openExternalUrl':
// 修复:直接从 message 中获取 url而不是从 data
const url = message.url || (data && data.url);
if (url) {
try {
@@ -182,6 +122,18 @@ export default class DSHome {
vscode.window.showErrorMessage('打开链接失败: 链接地址无效');
}
break;
case 'showInformationNotification':
if (data && data.message) {
vscode.window.showInformationMessage(data.message);
}
break;
case 'showErrorNotification':
if (data && data.message) {
vscode.window.showErrorMessage(data.message);
}
break;
}
}
},

View File

@@ -4,7 +4,6 @@ import QuickAccessTreeProvider from './views/quick-access-tree';
import DSHome from './home';
import RemoteContainer, { openProjectWithoutLogging } from './remote-container';
import User from './user';
import DevstarAPIHandler from './devstar-api';
import * as os from 'os';
import * as utils from './utils';
@@ -68,6 +67,25 @@ export class DevStarExtension {
const accessToken = params.get('access_token');
const devstarUsername = params.get('devstar_username');
const devstarDomain = params.get('devstar_domain');
const forwardPortsParam = params.get('forwardPorts');
// 处理 forwardPorts 参数
if (forwardPortsParam) {
const ports = forwardPortsParam.split(',').map(port => parseInt(port, 10)).filter(port => !isNaN(port));
context.globalState.update('forwardPorts', ports);
} else {
// 如果没有 forwardPorts 参数,清除 globalState 中的旧值
context.globalState.update('forwardPorts', undefined);
}
if (devstarDomain) {
this.user.setDevstarDomain(devstarDomain);
this.remoteContainer.setUser(this.user);
this.dsHome.setDevstarDomainAndHomePageURL(devstarDomain);
this.dsHome.setUser(this.user);
this.dsHome.setRemoteContainer(this.remoteContainer);
context.globalState.update('devstarDomain', devstarDomain);
}
if (host && hostname && port && username && path) {
const containerHost = host;
@@ -167,7 +185,7 @@ export class DevStarExtension {
);
this.registerGlobalCommands(context);
//防止进入HOME页面
// this.startDevStarHome();
}

View File

@@ -3,8 +3,10 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as vscode from 'vscode';
import * as rd from 'readline'
const { NodeSSH } = require('node-ssh')
import * as rd from 'readline';
const { NodeSSH } = require('node-ssh');
const { spawn } = require('child_process');
const net = require('net');
import * as utils from './utils';
import User from './user';
@@ -12,106 +14,79 @@ import DevstarAPIHandler from './devstar-api';
export default class RemoteContainer {
private user: User;
private sshProcesses?: Map<string, any>;
private portMappings: Map<string, Array<{ containerPort: number, localPort: number, label: string, source: string }>> = new Map();
constructor(user: User) {
this.user = user
this.user = user;
}
public setUser(user: User) {
this.user = user
this.user = user;
}
/**
* 第一次打开远程项目
*
* 远程环境先创建local窗口在通过命令行调用url打开目前仅支持vscode协议
* @param host 项目名称
* @param hostname ip
* @param port
* @param username
* @param path
* @param context 用于支持远程项目环境
*/
async firstOpenProject(host: string, hostname: string, port: number, username: string, path: string, context: vscode.ExtensionContext) {
console.log(`[RemoteContainer] firstOpenProject called with:`, { host, hostname, port, username, path });
if (vscode.env.remoteName) {
// 远程环境
console.log(`[RemoteContainer] Running in remote environment: ${vscode.env.remoteName}`);
try {
await vscode.commands.executeCommand('workbench.action.terminal.newLocal');
const terminal = vscode.window.terminals[vscode.window.terminals.length - 1];
if (terminal) {
let devstarDomain: string | undefined = context.globalState.get("devstarDomain_" + vscode.env.sessionId)
if (devstarDomain == undefined || devstarDomain == "")
devstarDomain = undefined
// vscode协议
// 根据系统+命令行版本确定命令
const semver = require('semver')
const powershellVersion = context.globalState.get('powershellVersion')
const powershell_semver_compatible_version = semver.coerce(powershellVersion)
if (terminal) {
let devstarDomain: string | undefined = context.globalState.get("devstarDomain_" + vscode.env.sessionId);
if (devstarDomain == undefined || devstarDomain == "") {
devstarDomain = undefined;
}
const semver = require('semver');
const powershellVersion = context.globalState.get('powershellVersion');
const powershell_semver_compatible_version = semver.coerce(powershellVersion);
let command = '';
if (devstarDomain === undefined) {
// 不传递devstarDomain
if (powershellVersion === undefined) {
command = `code --new-window && code --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}"`;
} else if (semver.satisfies(powershell_semver_compatible_version, ">=5.1.26100")) {
// win & powershell >= 5.1.26100.0
command = `code --new-window ; code --% --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}"`;
} else {
// win & powershell < 5.1.26100.0
command = `code --new-window && code --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}"`;
}
} else {
if (powershellVersion === undefined) {
command = `code --new-window && code --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}&devstar_domain=${devstarDomain}"`;
} else if (semver.satisfies(powershell_semver_compatible_version, ">=5.1.26100")) {
// win & powershell >= 5.1.26100.0
command = `code --new-window ; code --% --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}&devstar_domain=${devstarDomain}"`;
} else {
// win & powershell < 5.1.26100.0
command = `code --new-window && code --open-url "vscode://mengning.devstar/openProjectSkippingLoginCheck?host=${host}&hostname=${hostname}&port=${port}&username=${username}&path=${path}&devstar_domain=${devstarDomain}"`;
}
}
console.log(`[RemoteContainer] Sending command to terminal: ${command}`);
terminal.sendText(command);
} else {
console.error(`[RemoteContainer] Failed to create or access terminal`);
vscode.window.showErrorMessage('无法创建终端,请检查终端是否可用。');
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error(`[RemoteContainer] Error in remote environment:`, error);
vscode.window.showErrorMessage(`远程环境操作失败: ${errorMessage}`);
}
} else {
console.log(`[RemoteContainer] Running in local environment, attempting firstConnect`);
try {
//传入真实IP
await this.firstConnect(host, hostname, username, port)
await this.firstConnect(host, hostname, username, port, path)
.then((res) => {
if (res === 'success') {
console.log(`[RemoteContainer] firstConnect succeeded, opening remote folder`);
// only success then open folder
this.openRemoteFolder(host, port, username, path);
this.openRemoteFolder(host, port, username, path, context);
} else {
console.error(`[RemoteContainer] firstConnect returned: ${res}`);
vscode.window.showErrorMessage('首次连接容器失败,请检查网络和容器状态。');
}
})
.catch(error => {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error(`[RemoteContainer] firstConnect failed:`, error);
vscode.window.showErrorMessage(`首次连接容器时发生错误: ${errorMessage}`);
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error(`[RemoteContainer] Error in local environment firstOpenProject:`, error);
vscode.window.showErrorMessage(`打开项目失败: ${errorMessage}`);
}
}
@@ -119,83 +94,55 @@ export default class RemoteContainer {
/**
* local environment第一次连接其他项目
* @param host 项目名称
* @param hostname ip
* @param username
* @param port
* @returns 成功返回success
*/
// connect with key
async firstConnect(host: string, hostname: string, username: string, port: number): Promise<string> {
console.log(`[RemoteContainer] firstConnect called with:`, { host, hostname, username, port });
async firstConnect(host: string, hostname: string, _username: string, port: number, projectPath?: string): Promise<string> {
return new Promise(async (resolve, reject) => {
const ssh = new NodeSSH();
vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: vscode.l10n.t("Installing vscode-server and devstar extension in container"),
cancellable: false
}, async (progress) => {
try {
console.log(`[RemoteContainer] Checking SSH keys existence`);
// 检查公私钥是否存在,如果不存在,需要创建
if (!this.user.existUserPrivateKey() || !this.user.existUserPublicKey()) {
console.log(`[RemoteContainer] SSH keys not found, creating new keys`);
await this.user.createUserSSHKey()
// 上传公钥
console.log(`[RemoteContainer] Uploading public key`);
const devstarAPIHandler = new DevstarAPIHandler()
const uploadResult = await devstarAPIHandler.uploadUserPublicKey(this.user)
await this.user.createUserSSHKey();
const devstarAPIHandler = new DevstarAPIHandler();
const uploadResult = await devstarAPIHandler.uploadUserPublicKey(this.user);
if (uploadResult !== "ok") {
throw new Error('Upload public key failed.')
throw new Error('Upload public key failed.');
}
console.log(`[RemoteContainer] Public key uploaded successfully`);
} else {
console.log(`[RemoteContainer] SSH keys already exist`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error("[RemoteContainer] Failed to first connect container - SSH key setup: ", error)
reject(error);
return;
}
// 本地环境
try {
console.log(`[RemoteContainer] Attempting SSH connection to ${hostname}:${port} as ${username}`);
// connect with key
await ssh.connect({
host: hostname,
username: 'root',
port: port,
privateKeyPath: this.user.getUserPrivateKeyPath(),
readyTimeout: 30000, // 增加超时时间到30秒
readyTimeout: 30000,
onKeyboardInteractive: (
_name: string,
_instructions: string,
_instructionsLang: string,
_prompts: any[],
_name: string,
_instructions: string,
_instructionsLang: string,
_prompts: any[],
finish: (responses: string[]) => void
) => {
console.log(`[RemoteContainer] Keyboard interactive authentication required`);
finish([]);
}
});
console.log(`[RemoteContainer] SSH connection established successfully`);
progress.report({ message: vscode.l10n.t("Connected! Start installation") });
// install vscode-server and devstar extension
console.log(`[RemoteContainer] Getting VSCode commit ID`);
const vscodeCommitId = await utils.getVsCodeCommitId()
if ("" != vscodeCommitId) {
console.log(`[RemoteContainer] VSCode commit ID: ${vscodeCommitId}`);
const vscodeServerUrl = `https://vscode.download.prss.microsoft.com/dbazure/download/stable/${vscodeCommitId}/vscode-server-linux-x64.tar.gz`
const vscodeCommitId = await utils.getVsCodeCommitId();
if ("" !== vscodeCommitId) {
const vscodeServerUrl = `https://vscode.download.prss.microsoft.com/dbazure/download/stable/${vscodeCommitId}/vscode-server-linux-x64.tar.gz`;
const installVscodeServerScript = `
mkdir -p ~/.vscode-server/bin/${vscodeCommitId} && \\
if [ "$(ls -A ~/.vscode-server/bin/${vscodeCommitId})" ]; then
@@ -211,43 +158,30 @@ export default class RemoteContainer {
~/.vscode-server/bin/${vscodeCommitId}/bin/code-server --install-extension mengning.devstar
fi
`;
console.log(`[RemoteContainer] Executing installation script`);
const installResult = await ssh.execCommand(installVscodeServerScript);
if (installResult.code === 0) {
console.log("[RemoteContainer] VSCode server and extension installed successfully");
console.log("[RemoteContainer] Installation stdout:", installResult.stdout);
if (installResult.stderr) {
console.warn("[RemoteContainer] Installation stderr:", installResult.stderr);
}
vscode.window.showInformationMessage(vscode.l10n.t('Installation completed!'));
} else {
console.error("[RemoteContainer] Installation failed with code:", installResult.code);
console.error("[RemoteContainer] Installation stderr:", installResult.stderr);
throw new Error(`Installation failed with exit code ${installResult.code}: ${installResult.stderr}`);
}
} else {
throw new Error('Failed to get VSCode commit ID');
}
if (projectPath) {
}
await ssh.dispose();
console.log(`[RemoteContainer] SSH connection disposed`);
// only connect successfully then save the host info
console.log(`[RemoteContainer] Storing project SSH info`);
await this.storeProjectSSHInfo(host, hostname, port, 'root')
await this.storeProjectSSHInfo(host, hostname, port, 'root');
resolve('success')
resolve('success');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error('[RemoteContainer] Failed to install vscode-server and extension: ', error);
try {
await ssh.dispose();
} catch (disposeError) {
const disposeErrorMessage = disposeError instanceof Error ? disposeError.message : '未知错误';
console.error('[RemoteContainer] Error disposing SSH connection: ', disposeError);
}
reject(error);
}
@@ -255,105 +189,551 @@ export default class RemoteContainer {
});
}
/**
* 查找可用的本地端口 - 优先使用相同端口
*/
private async findAvailableLocalPort(containerPort: number): Promise<number> {
if (await this.isPortAvailable(containerPort)) {
return containerPort;
}
for (let offset = 1; offset <= 10; offset++) {
const port1 = containerPort + offset;
if (port1 < 65536 && await this.isPortAvailable(port1)) {
return port1;
}
const port2 = containerPort - offset;
if (port2 > 0 && await this.isPortAvailable(port2)) {
return port2;
}
}
return this.findRandomAvailablePort();
}
/**
* 检查端口是否可用
*/
private async isPortAvailable(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = net.createServer();
server.unref();
server.on('error', () => {
resolve(false);
});
server.listen(port, () => {
server.close(() => {
resolve(true);
});
});
});
}
/**
* 查找随机可用端口
*/
private async findRandomAvailablePort(): Promise<number> {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.unref();
server.on('error', reject);
server.listen(0, () => {
const port = server.address().port;
server.close(() => {
resolve(port);
});
});
});
}
/**
* 创建 SSH 端口转发
*/
private async createSSHPortForward(hostname: string, sshPort: number, containerPort: number, localPort: number): Promise<void> {
return new Promise((resolve, reject) => {
const sshArgs = [
'-N',
'-L',
`${localPort}:localhost:${containerPort}`,
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
'-p', sshPort.toString(),
'-i', this.user.getUserPrivateKeyPath(),
`root@${hostname}`
];
// 使用 detached 选项让 SSH 进程独立运行,不随父进程退出
const sshProcess = spawn('ssh', sshArgs, {
detached: true, // 让进程在后台独立运行
stdio: 'ignore' // 忽略输入输出,避免进程挂起
});
// 解除父进程对子进程的引用,使其真正独立
sshProcess.unref();
sshProcess.on('error', (error: Error) => {
reject(error);
});
// 由于使用了 stdio: 'ignore',这些事件监听器不再需要
// sshProcess.stdout.on('data', (data: Buffer) => {
// console.log(`[SSH stdout] ${data.toString()}`);
// });
// sshProcess.stderr.on('data', (data: Buffer) => {
// console.error(`[SSH stderr] ${data.toString()}`);
// });
// 注意:由于进程已 detached 和 unref不再需要保存到 sshProcesses Map
// 因为我们无法也不需要控制这些独立进程的生命周期
// 等待 SSH 连接建立
setTimeout(() => {
resolve();
}, 2000);
});
}
/**
* 显示端口映射汇总信息
*/
private showPortMappingsSummary(portMappings: Array<{ containerPort: number, localPort: number, label: string, source: string }>): void {
let message = `🎯 已建立 ${portMappings.length} 个端口映射:\n\n`;
portMappings.forEach(mapping => {
const samePort = mapping.containerPort === mapping.localPort ? " ✅" : " 🔄";
message += `${mapping.label}\n`;
message += ` 容器端口: ${mapping.containerPort} → 本地端口: ${mapping.localPort}${samePort}\n`;
message += ` 访问地址: http://localhost:${mapping.localPort}\n`;
message += ` 配置来源: ${mapping.source}\n\n`;
});
vscode.window.showInformationMessage(message, '复制映射信息', '在浏览器中打开', '查看详细信息')
.then(selection => {
if (selection === '复制映射信息') {
const copyText = portMappings.map(m =>
`${m.label}: http://localhost:${m.localPort} (容器端口: ${m.containerPort})`
).join('\n');
vscode.env.clipboard.writeText(copyText);
vscode.window.showInformationMessage('端口映射信息已复制到剪贴板');
} else if (selection === '在浏览器中打开' && portMappings.length > 0) {
vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${portMappings[0].localPort}`));
} else if (selection === '查看详细信息') {
}
});
}
/**
* 本地环境保存项目的ssh连接信息
* @param host
* @param hostname
* @param port
* @param username
*/
async storeProjectSSHInfo(host: string, hostname: string, port: number, username: string): Promise<void> {
console.log(`[RemoteContainer] storeProjectSSHInfo called with:`, { host, hostname, port, username });
const sshConfigPath = path.join(os.homedir(), '.ssh', 'config');
console.log(`[RemoteContainer] SSH config path: ${sshConfigPath}`);
// check if the host and related info exist in local ssh config file before saving
let canAppendSSHConfig = true;
if (fs.existsSync(sshConfigPath)) {
console.log(`[RemoteContainer] SSH config file exists, checking for existing host`);
const reader = rd.createInterface(fs.createReadStream(sshConfigPath));
for await (const line of reader) {
if (line.includes(`Host ${host}`)) {
// the container ssh info exists
console.log(`[RemoteContainer] Host ${host} already exists in SSH config`);
canAppendSSHConfig = false;
break;
}
}
} else {
console.log(`[RemoteContainer] SSH config file does not exist, will create it`);
}
if (canAppendSSHConfig) {
// save the host to the local ssh config file
const privateKeyPath = this.user.getUserPrivateKeyPath();
console.log(`[RemoteContainer] Using private key path: ${privateKeyPath}`);
const newSShConfigContent =
`\nHost ${host}\n HostName ${hostname}\n Port ${port}\n User ${username}\n PreferredAuthentications publickey\n IdentityFile ${privateKeyPath}\n `;
try {
fs.writeFileSync(sshConfigPath, newSShConfigContent, { encoding: 'utf8', flag: 'a' });
console.log('[RemoteContainer] Host registered in local ssh config');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error('[RemoteContainer] Failed to write SSH config:', error);
throw error;
}
}
}
/**
* local env
* 仅支持已经成功连接并在ssh config file中存储ssh信息的项目连接。
*
* @host 表示project name
* local env - 使用 Extension API 打开远程文件夹
*/
openRemoteFolder(host: string, port: number, username: string, path: string): void {
console.log(`[RemoteContainer] openRemoteFolder called with:`, { host, port, username, path });
async openRemoteFolder(host: string, port: number, _username: string, path: string, context: vscode.ExtensionContext): Promise<void> {
try {
let terminal = vscode.window.activeTerminal || vscode.window.createTerminal(`Ext Terminal`);
terminal.show(true);
const command = `code --remote ssh-remote+root@${host}:${port} ${path} --reuse-window`;
console.log(`[RemoteContainer] Sending command to terminal: ${command}`);
// 在原窗口打开
terminal.sendText(command);
console.log(`[RemoteContainer] Command sent successfully`);
const sshConfig = await this.getSSHConfig(host);
if (sshConfig) {
try {
// 调用 setupPortForwardingFromGlobalState 方法
await this.setupPortForwardingFromGlobalState(sshConfig.hostname, port, context);
} catch (portError) {
vscode.window.showWarningMessage('端口映射设置失败,但容器连接已建立');
}
// 自动配置本地 VSCode 的 MCP Server
try {
await this.configureMCPServerLocally();
} catch (mcpError) {
console.error('[MCP] 本地 MCP 配置失败:', mcpError);
// 不阻塞主流程,仅记录错误
}
// 自动配置容器内 AI IDE 的 MCP Server
try {
await this.configureMCPServerInContainer(sshConfig.hostname, port);
} catch (mcpError) {
console.error('[MCP] 容器内 MCP 配置失败:', mcpError);
// 不阻塞主流程,仅记录错误
}
}
// 使用 VSCode Extension API 打开远程连接
// 使用 SSH config 中的 Host 别名,让 SSH config 的用户配置生效
// 格式: vscode-remote://ssh-remote+host/path
// 注意:不在 URI 中指定用户名,让 SSH config 中的 User root 配置生效
const remoteUri = vscode.Uri.parse(
`vscode-remote://ssh-remote+${host}${path}`
);
// 尝试使用 vscode.openFolder 命令
await vscode.commands.executeCommand('vscode.openFolder', remoteUri, {
forceNewWindow: false
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error(`[RemoteContainer] Error in openRemoteFolder:`, error);
vscode.window.showErrorMessage(`打开远程文件夹失败: ${errorMessage}`);
}
}
/**
* 从 SSH config 获取主机配置
*/
private async getSSHConfig(host: string): Promise<{ hostname: string; port: number } | null> {
const sshConfigPath = path.join(os.homedir(), '.ssh', 'config');
if (!fs.existsSync(sshConfigPath)) {
return null;
}
const configContent = fs.readFileSync(sshConfigPath, 'utf8');
const lines = configContent.split('\n');
let currentHost = '';
let hostname = '';
let port = 22;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('Host ')) {
currentHost = trimmed.substring(5).trim();
} else if (currentHost === host) {
if (trimmed.startsWith('HostName ')) {
hostname = trimmed.substring(9).trim();
} else if (trimmed.startsWith('Port ')) {
port = parseInt(trimmed.substring(5).trim());
}
}
if (hostname && currentHost === host) {
return { hostname, port };
}
}
return null;
}
/**
* 从 globalState 获取 forwardPorts 并建立端口映射
*/
public async setupPortForwardingFromGlobalState(hostname: string, port: number, context: vscode.ExtensionContext): Promise<void> {
// 从 globalState 获取 forwardPorts 参数
const forwardPorts: number[] | undefined = context.globalState.get('forwardPorts');
if (forwardPorts && forwardPorts.length > 0) {
const portMappings: Array<{ containerPort: number, localPort: number, label: string, source: string }> = [];
for (const containerPort of forwardPorts) {
const localPort = await this.findAvailableLocalPort(containerPort);
try {
await this.createSSHPortForward(hostname, port, containerPort, localPort);
portMappings.push({
containerPort,
localPort,
label: `Port ${containerPort}`,
source: 'globalState forwardPorts'
});
} catch (error) {
console.error(`映射容器端口 ${containerPort} 到本地端口 ${localPort} 失败:`, error);
}
}
const mappingKey = `${hostname}:${port}`;
this.portMappings.set(mappingKey, portMappings);
if (portMappings.length > 0) {
this.showPortMappingsSummary(portMappings);
}
// 使用完毕后立即清除 globalState 中的 forwardPorts避免影响下一个项目
console.log('端口映射完成,清除 globalState 中的 forwardPorts');
context.globalState.update('forwardPorts', undefined);
} else {
console.log('未找到 forwardPorts 参数,跳过端口映射设置。');
}
}
/**
* 在容器内配置 AI IDE 的 MCP Server
* 支持 Trae IDE 等 AI IDE 的 MCP 自动配置
*/
private async configureMCPServerInContainer(hostname: string, sshPort: number): Promise<void> {
// 获取用户 token
const userToken = this.user.getUserTokenFromLocal();
if (!userToken) {
console.log('[MCP] 用户未登录,跳过 MCP Server 配置');
return;
}
// 获取 DevStar 域名
const devstarDomain = this.user.getDevstarDomain();
const mcpUrl = `${devstarDomain}/api/mcp`;
console.log(`[MCP] 开始配置 MCP Server: ${mcpUrl}`);
// Trae IDE 的 MCP 配置路径
const traeMcpPath = '/root/.trae-server/data/Machine/mcp.json';
// MCP 配置内容(注意:使用 "mcpServers" 格式)
const mcpConfig = {
mcpServers: {
devstar: {
type: 'http',
url: mcpUrl,
headers: {
Authorization: `Bearer ${userToken}`
}
}
}
};
const configJson = JSON.stringify(mcpConfig, null, 2);
// 使用 SSH 连接到容器并配置
const ssh = new NodeSSH();
try {
await ssh.connect({
host: hostname,
username: 'root',
port: sshPort,
privateKeyPath: this.user.getUserPrivateKeyPath(),
readyTimeout: 10000,
});
console.log('[MCP] SSH 连接成功');
// 检查现有配置
const checkScript = `
if [ -f "${traeMcpPath}" ]; then
cat "${traeMcpPath}"
else
echo "FILE_NOT_EXISTS"
fi
`;
const checkResult = await ssh.execCommand(checkScript);
let needUpdate = true;
if (checkResult.stdout !== 'FILE_NOT_EXISTS') {
try {
const existingConfig = JSON.parse(checkResult.stdout);
const existingDevstar = existingConfig.mcpServers?.devstar;
if (existingDevstar) {
// 检查 URL 和 token 是否匹配
const urlMatch = existingDevstar.url === mcpUrl;
const tokenMatch = existingDevstar.headers?.Authorization === `Bearer ${userToken}`;
if (urlMatch && tokenMatch) {
console.log('[MCP] DevStar MCP 配置已存在且正确,无需更新');
needUpdate = false;
} else {
console.log(`[MCP] DevStar MCP 配置需要更新 (URL匹配: ${urlMatch}, Token匹配: ${tokenMatch})`);
}
}
} catch (parseError) {
console.log('[MCP] 解析现有配置失败,将创建新配置');
}
} else {
console.log('[MCP] 容器内无配置文件,需要创建');
}
// 创建或更新配置文件
if (needUpdate) {
const setupScript = `
mkdir -p /root/.trae-server/data/Machine && \
cat > ${traeMcpPath} << 'EOF'
${configJson}
EOF
echo "MCP 配置已更新"
`;
const result = await ssh.execCommand(setupScript);
if (result.code === 0) {
console.log('[MCP] DevStar MCP 配置成功');
} else {
console.error(`[MCP] 配置失败: ${result.stderr}`);
}
}
} catch (error) {
console.error('[MCP] SSH 连接或配置失败:', error);
throw error;
} finally {
try {
await ssh.dispose();
} catch (e) {
// ignore dispose error
}
}
}
/**
* 在本地 VSCode 中配置 MCP Server
* 支持 GitHub Copilot 等使用本地 MCP 配置
*/
private async configureMCPServerLocally(): Promise<void> {
// 获取用户 token
const userToken = this.user.getUserTokenFromLocal();
if (!userToken) {
console.log('[MCP] 用户未登录,跳过本地 MCP Server 配置');
return;
}
// 获取 DevStar 域名
const devstarDomain = this.user.getDevstarDomain();
const mcpUrl = `${devstarDomain}/api/mcp`;
console.log(`[MCP] 开始配置本地 MCP Server: ${mcpUrl}`);
// 根据操作系统确定配置文件路径
let mcpConfigPath: string;
const platform = os.platform();
const homedir = os.homedir();
if (platform === 'darwin') {
// macOS
mcpConfigPath = path.join(homedir, 'Library/Application Support/Code/User/mcp.json');
} else if (platform === 'win32') {
// Windows
mcpConfigPath = path.join(homedir, 'AppData/Roaming/Code/User/mcp.json');
} else {
// Linux
mcpConfigPath = path.join(homedir, '.config/Code/User/mcp.json');
}
console.log(`[MCP] 配置文件路径: ${mcpConfigPath}`);
// MCP 配置内容(本地 VSCode 使用 "servers" 格式)
const mcpConfig = {
servers: {
devstar: {
type: 'http',
url: mcpUrl,
headers: {
Authorization: `Bearer ${userToken}`
}
}
}
};
const configJson = JSON.stringify(mcpConfig, null, 2);
try {
// 检查现有配置
let existingConfig: any = {};
let needUpdate = true;
if (fs.existsSync(mcpConfigPath)) {
try {
const content = fs.readFileSync(mcpConfigPath, 'utf8');
existingConfig = JSON.parse(content);
const existingDevstar = existingConfig.servers?.devstar;
if (existingDevstar) {
// 检查 URL 和 token 是否匹配
const urlMatch = existingDevstar.url === mcpUrl;
const tokenMatch = existingDevstar.headers?.Authorization === `Bearer ${userToken}`;
if (urlMatch && tokenMatch) {
console.log('[MCP] 本地 DevStar MCP 配置已存在且正确,无需更新');
needUpdate = false;
} else {
console.log(`[MCP] 本地 DevStar MCP 配置需要更新 (URL匹配: ${urlMatch}, Token匹配: ${tokenMatch})`);
}
}
} catch (parseError) {
console.log('[MCP] 解析现有配置失败,将创建新配置');
}
} else {
console.log('[MCP] 本地无配置文件,需要创建');
}
// 创建或更新配置文件
if (needUpdate) {
// 确保目录存在
const configDir = path.dirname(mcpConfigPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// 合并现有配置(保留其他 MCP servers
const newConfig = {
...existingConfig,
servers: {
...existingConfig.servers,
...mcpConfig.servers
}
};
fs.writeFileSync(mcpConfigPath, JSON.stringify(newConfig, null, 2), 'utf8');
console.log('[MCP] 本地 DevStar MCP 配置成功');
}
} catch (error) {
console.error('[MCP] 本地 MCP 配置失败:', error);
throw error;
}
}
}
/**
* 打开项目(无须插件登录)
* @param hostname 表示ip
* @param port
* @param username
* @param path
* 打开项目(无须插件登录)- 使用 Extension API
*/
export async function openProjectWithoutLogging(hostname: string, port: number, username: string, path: string): Promise<void> {
console.log(`[RemoteContainer] openProjectWithoutLogging called with:`, { hostname, port, username, path });
const command = `code --remote ssh-remote+${username}@${hostname}:${port} ${path} --reuse-window`
console.log(`[RemoteContainer] Command: ${command}`);
export async function openProjectWithoutLogging(host: string, _port: number, _username: string, path: string): Promise<void> {
try {
let terminal = vscode.window.activeTerminal || vscode.window.createTerminal(`Ext Terminal`);
terminal.show(true);
terminal.sendText(command);
console.log(`[RemoteContainer] openProjectWithoutLogging completed successfully`);
// 使用 VSCode Extension API 打开远程连接
// 使用 SSH config 中的 Host 别名,让 SSH config 的用户配置生效
// 格式: vscode-remote://ssh-remote+host/path
const remoteUri = vscode.Uri.parse(
`vscode-remote://ssh-remote+${host}${path}`
);
// 使用 vscode.openFolder 命令
await vscode.commands.executeCommand('vscode.openFolder', remoteUri, {
forceNewWindow: false
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error(`[RemoteContainer] Error in openProjectWithoutLogging:`, error);
vscode.window.showErrorMessage(`无登录打开项目失败: ${errorMessage}`);
}
}

View File

@@ -6,7 +6,6 @@ import DevstarAPIHandler from './devstar-api';
import * as utils from './utils';
const {
generateKeyPairSync,
createHash
} = require('node:crypto');
const sshpk = require('sshpk');
@@ -117,8 +116,8 @@ export default class User {
}
public async isLogged(): Promise<boolean> {
const username: string|undefined = this.context.globalState.get(this.usernameKey)
const userToken: string|undefined = this.context.globalState.get(this.userTokenKey)
const username: string | undefined = this.context.globalState.get(this.usernameKey)
const userToken: string | undefined = this.context.globalState.get(this.userTokenKey)
if ((username != undefined && username != '') && (userToken != undefined && userToken != '')) {
const devstarAPIHandler = new DevstarAPIHandler(this.devstarDomain)
@@ -172,7 +171,7 @@ export default class User {
if (!this.isLogged()) {
return '';
} else {
const username: string|undefined = this.context.globalState.get(this.usernameKey)
const username: string | undefined = this.context.globalState.get(this.usernameKey)
// islogged为trueusername不为空
return path.join(os.homedir(), '.ssh', `id_rsa_${username}_${this.devstarHostname}`)
}
@@ -182,7 +181,7 @@ export default class User {
if (!this.isLogged()) {
return '';
} else {
const username: string|undefined = this.context.globalState.get(this.usernameKey)
const username: string | undefined = this.context.globalState.get(this.usernameKey)
// islogged为trueusername不为空
return path.join(os.homedir(), '.ssh', `id_rsa_${username}_${this.devstarHostname}.pub`)
}
@@ -259,8 +258,55 @@ export default class User {
this.updateLocalUserPrivateKeyPath(this.getUserPrivateKeyPath())
console.log(`Update local user private key path.`)
} catch (error) {
const username: string|undefined = this.context.globalState.get(this.usernameKey)
const username: string | undefined = this.context.globalState.get(this.usernameKey)
console.error(`Failed to write public/private key into the user(${username}) ssh public/key file: `, error);
}
}
public async verifyToken(token: string, username: string): Promise<boolean> {
try {
const response = await fetch(this.devstarDomain + `/api/devcontainer/user`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'token ' + token
}
});
// 检查响应状态码
if (!response.ok) {
const text = await response.text(); // 读取文本内容以便调试
console.error(`HTTP Error: ${response.status} - ${text}`);
if (response.status === 401) {
throw new Error('Token错误');
} else {
throw new Error(`HTTP Error: ${response.status}`);
}
}
// 检查 Content-Type 是否为 JSON
const contentType = response.headers.get('Content-Type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text(); // 读取文本内容以便调试
console.error(`Unexpected Content-Type: ${contentType} - ${text}`);
throw new Error(`Unexpected Content-Type: ${contentType}`);
}
const responseData = await response.json();
const data = responseData.data;
if (!data || !data.username) {
throw new Error('Token对应用户不存在');
}
// 验证用户名匹配
if (data.username !== username) {
throw new Error('Token与用户名不符');
}
return true;
} catch (error) {
console.error(error);
return false;
}
}
}

View File

@@ -3,7 +3,6 @@ import * as https from 'https';
import * as vscode from 'vscode';
import * as os from 'os';
import { exec } from 'child_process';
import * as path from 'path';
const {
generateKeyPairSync,

View File

@@ -28,5 +28,10 @@ module.exports = {
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('./src')],
extensions: ['.ts', '.js']
},
node: {
__dirname: false,
__filename: false,
global: false
}
};