feature-open-with-vscode #1

Merged
mengning merged 20 commits from feature-open-with-vscode into main 2025-11-26 05:23:52 +00:00
11 changed files with 933 additions and 345 deletions
Showing only changes of commit 325fc8f4bd - Show all commits

View File

@@ -2,7 +2,7 @@
"name": "devstar",
"displayName": "%displayName%",
"description": "%description%",
"version": "0.3.9",
"version": "0.4.0",
"keywords": [],
"publisher": "mengning",
"engines": {

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 {
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

@@ -110,7 +110,7 @@ export default class DSHome {
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);
this.remoteContainer.openRemoteFolder(data.host, data.port, data.username, data.path, this.context);
break;
case 'getDevstarDomain':
panel.webview.postMessage({ command: 'getDevstarDomain', data: { devstarDomain: this.devstarDomain } });

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';
@@ -67,7 +66,40 @@ export class DevStarExtension {
const path = params.get('path');
const accessToken = params.get('access_token');
const devstarUsername = params.get('devstar_username');
const devstarDomain = params.get('devstar_domain');
const rawDevstarDomain = params.get('devstar_domain');
let devstarDomain = rawDevstarDomain;
if (rawDevstarDomain) {
try {
const url = new URL(rawDevstarDomain);
devstarDomain = `${url.protocol}//${url.hostname}`;
// 从 rawDevstarDomain 的查询参数中提取 forwardPorts
const forwardPortsParam = url.searchParams.get('forwardPorts');
if (forwardPortsParam) {
const ports = forwardPortsParam.split(',').map(port => parseInt(port, 10)).filter(port => !isNaN(port));
console.log('解析到的 forwardPorts 参数:', ports);
context.globalState.update('forwardPorts', ports);
} else {
// 如果没有 forwardPorts 参数,清除 globalState 中的旧值
console.log('未找到 forwardPorts 参数,清除旧的 forwardPorts 配置');
context.globalState.update('forwardPorts', undefined);
}
} catch (error) {
console.error('Invalid devstar_domain URL:', error);
}
}
console.log('sanitized_devstar_domain:', devstarDomain);
// 使用修正后的 devstar_domain
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;

View File

@@ -77,7 +77,7 @@ export default class RemoteContainer {
await this.firstConnect(host, hostname, username, port, path)
.then((res) => {
if (res === 'success') {
this.openRemoteFolder(host, port, username, path);
this.openRemoteFolder(host, port, username, path, context);
} else {
vscode.window.showErrorMessage('首次连接容器失败,请检查网络和容器状态。');
}
@@ -96,7 +96,7 @@ export default class RemoteContainer {
/**
* local environment第一次连接其他项目
*/
async firstConnect(host: string, hostname: string, username: string, port: number, projectPath?: string): Promise<string> {
async firstConnect(host: string, hostname: string, _username: string, port: number, projectPath?: string): Promise<string> {
return new Promise(async (resolve, reject) => {
const ssh = new NodeSSH();
@@ -190,37 +190,6 @@ export default class RemoteContainer {
});
}
/**
* 从 devcontainer.json 中提取端口映射配置
*/
private async getPortsConfigFromDevContainer(ssh: any, containerPath: string): Promise<{
portsAttributes: any;
forwardPorts?: number[];
otherPortsAttributes?: any;
}> {
try {
const findResult = await ssh.execCommand(`find ${containerPath} -name "devcontainer.json" -type f`);
if (findResult.code === 0 && findResult.stdout.trim()) {
const devcontainerPath = findResult.stdout.trim().split('\n')[0];
const readResult = await ssh.execCommand(`cat ${devcontainerPath}`);
if (readResult.code === 0) {
const devcontainerConfig = JSON.parse(readResult.stdout);
return {
portsAttributes: devcontainerConfig.portsAttributes || {},
forwardPorts: devcontainerConfig.forwardPorts,
otherPortsAttributes: devcontainerConfig.otherPortsAttributes
};
}
}
} catch (error) {
}
return { portsAttributes: {} };
}
/**
* 查找可用的本地端口 - 优先使用相同端口
*/
@@ -279,74 +248,6 @@ export default class RemoteContainer {
});
}
/**
* 建立端口映射 - 根据 devcontainer.json 配置
*/
private async setupPortForwarding(hostname: string, port: number, containerPath: string): Promise<void> {
const ssh = new NodeSSH();
const portMappings: Array<{ containerPort: number, localPort: number, label: string, source: string }> = [];
try {
await ssh.connect({
host: hostname,
username: 'root',
port: port,
privateKeyPath: this.user.getUserPrivateKeyPath(),
readyTimeout: 30000,
});
const portsConfig = await this.getPortsConfigFromDevContainer(ssh, containerPath);
if (portsConfig.forwardPorts && portsConfig.forwardPorts.length > 0) {
for (const containerPort of portsConfig.forwardPorts) {
const localPort = await this.findAvailableLocalPort(containerPort);
await this.createSSHPortForward(hostname, port, containerPort, localPort);
portMappings.push({
containerPort,
localPort,
label: `Port ${containerPort}`,
source: 'forwardPorts'
});
}
}
for (const [containerPortStr, attributes] of Object.entries(portsConfig.portsAttributes)) {
const containerPort = parseInt(containerPortStr);
if (!isNaN(containerPort)) {
const alreadyMapped = portMappings.some(m => m.containerPort === containerPort);
if (!alreadyMapped) {
const localPort = await this.findAvailableLocalPort(containerPort);
await this.createSSHPortForward(hostname, port, containerPort, localPort);
const label = (attributes as any).label || `Port ${containerPort}`;
portMappings.push({
containerPort,
localPort,
label,
source: 'portsAttributes'
});
}
}
}
await ssh.dispose();
const mappingKey = `${hostname}:${port}`;
this.portMappings.set(mappingKey, portMappings);
if (portMappings.length > 0) {
this.showPortMappingsSummary(portMappings);
this.registerPortMappingsCommands(mappingKey, portMappings);
}
} catch (error) {
await ssh.dispose();
throw error;
}
}
/**
* 创建 SSH 端口转发
*/
@@ -369,10 +270,10 @@ export default class RemoteContainer {
reject(error);
});
sshProcess.stdout.on('data', (data: Buffer) => {
sshProcess.stdout.on('data', (_data: Buffer) => {
});
sshProcess.stderr.on('data', (data: Buffer) => {
sshProcess.stderr.on('data', (_data: Buffer) => {
});
if (!this.sshProcesses) {
@@ -535,12 +436,13 @@ export default class RemoteContainer {
/**
* local env
*/
async openRemoteFolder(host: string, port: number, username: string, path: string): Promise<void> {
async openRemoteFolder(host: string, port: number, _username: string, path: string, context: vscode.ExtensionContext): Promise<void> {
try {
const sshConfig = await this.getSSHConfig(host);
if (sshConfig) {
try {
await this.setupPortForwarding(sshConfig.hostname, port, path);
// 调用 setupPortForwardingFromGlobalState 方法
await this.setupPortForwardingFromGlobalState(sshConfig.hostname, port, context);
setTimeout(() => {
this.showPortMappingsPanel(sshConfig.hostname, port);
@@ -639,6 +541,47 @@ export default class RemoteContainer {
this.portMappings.clear();
}
}
/**
* 从 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);
this.registerPortMappingsCommands(mappingKey, portMappings);
}
// 使用完毕后立即清除 globalState 中的 forwardPorts避免影响下一个项目
console.log('端口映射完成,清除 globalState 中的 forwardPorts');
context.globalState.update('forwardPorts', undefined);
} else {
console.log('未找到 forwardPorts 参数,跳过端口映射设置。');
}
}
}
/**

View File

@@ -117,8 +117,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 +172,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 +182,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 +259,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;
}
}
}