未分类

腾讯云旧版IDE Studio容器保活

1、创建腾讯云密钥

SecretId SecretId

cloud.tencent.com

登录 – 腾讯云

腾讯云为数百万的企业和开发者提供安全、稳定的云服务器、云主机、CDN、对象存储、域名注册、云存储、云数据库等云服务,帮助用户解决游戏、视频、移动、微信、互联网+等行业的架构难题,是全球领先的云计算服务商。

image

2、创建旧版IDE Studio 容器

注意创建免费版1c2g 8g存储 (用来跑自己的轻量服务,中转访问墙外站点都可以;当然他也有送每个月1w分钟的机时,你也可以通过api控制某个时间段启动关闭容器,当然我只用免费版,不过有个缺点就是只能源码部署,不支持docker)

image

image

设置永不休眠

进入容器中设置永不休眠

image

然后我发现这个免费容器虽然设置永不休眠,但是每次启动会持续运行1440分钟。

image

 就是这个心跳limit 会从1440开始倒计时

偶尔可能会中断几秒或者1分钟,没衔接上保活

Cloud studio

编辑程序自启动脚本

再次重启容器的话由于不支持docker, 想要控制容器内部程序启动的方式就是配置
.vscode/preview.yml 结合空间密钥 使用browserless api 无头浏览器访问你的ide空间地址, 触发相关程序启动

image

3、官方接口控制容器保活

cloud.tencent.com

登录 – 腾讯云

腾讯云为数百万的企业和开发者提供安全、稳定的云服务器、云主机、CDN、对象存储、域名注册、云存储、云数据库等云服务,帮助用户解决游戏、视频、移动、微信、互联网+等行业的架构难题,是全球领先的云计算服务商。

image

获取 spaceKey 相当于空间id

记住刚刚免费容器的spaceKey

image

创建free空间对应的空间密钥

空间密钥通过腾讯云密钥创建,空间密钥需要上面的spaceKey, 创建空间密钥的时候将过期时间设置长一点

image

保活脚本大致参考

// https://console.cloud.tencent.com/cam/capi// 启动 POST https://workerAppName.yourCfSpaceName.workers.dev/start/{spaceKey} // 停止 POST https://workerAppName.yourCfSpaceName.workers.dev/stop/{spaceKey} // 保活 POST https://workerAppName.yourCfSpaceName.workers.dev/heart/{spaceKey}   // 通过cfworker 地址重定向访问网页ide  https://workerAppName.yourCfSpaceName.workers.dev/ide/{spaceKey} // 工作空间临时访问凭证 https://console.cloud.tencent.com/api/explorer?Product=cloudstudio&Version=2023-05-08&Action=CreateWorkspaceToken// { [spaceKey]: token }// *********************************************************************************************************const foreverToken = {  '{你的spaceKey}': "{你的空间密钥}", // 提前生成空间对应的几十年有效期token}const SECRET_ID = '{你的腾讯云账号SecretId}'const SECRET_KEY = '{你的腾讯云账号SecretKey}'const workerHost = 'https://workerAppName.yourCfSpaceName.workers.dev' // 改成你的域名// 按日期轮询const browserlessKeys = [  '{你的broserless key}', // 可以创建两三个,可能一个也够用了,或者你用cf 自带的浏览器自动化api 也行, 用于触发 .vscode/preview.yml 程序自启动]const freeIds = ['{你的spaceKey}']// *********************************************************************************************************// 访问网页等待终端加载const init = (spaceKey, token) => {  return fetch("https://production-sfo.browserless.io/function?token=" + browserlessKeys[new Date().getDay()], {      "headers": {        "Content-Type": `application/javascript`,      },      "body": `export default async({page})=>{await page.goto("https://ide.cloud.tencent.com/tty/${spaceKey}/?report_open_type=list_open&token=`+ token +'",{waitUntil:"networkidle2"});await page.waitForSelector(`.xterm-link-layer`, { timeout: 180000 });}',      "method": "POST"  });}export default {  scheduled: scheduled,  fetch: fetchHandler};// * * * * * 使用定时任务面板触发定时async function scheduled(event, env, ctx) {  const now = new Date();  // console.log("当前不在指定执行窗口内");  // 1. 核心修正:手动将 UTC 时间转换为 东八区 (UTC+8) 时间  //    getUTCHours() 获取 UTC 小时  //    加 8 小时,并对 24 取模防止超过 23 点  const utcHour = now.getUTCHours();  let beijingHour = (utcHour + 8) % 24;   let beijingMinute = now.getUTCMinutes();  console.log(`UTC 时间: ${utcHour.toString().padStart(2, '0')}:${beijingMinute.toString().padStart(2, '0')} | 东八区推算: ${beijingHour.toString().padStart(2, '0')}:${beijingMinute.toString().padStart(2, '0')}`);  // 2. 判断逻辑优化  // worker 需要启用兼容性标志才能自己调用自己 global_fetch_strictly_public  await Promise.allSettled(freeIds.map(spaceKey => fetchHandler(`${workerHost}/heart/${spaceKey}`/**, { "body": null, "method": "POST" } */)))  // 凌晨4点重启  if (beijingHour !== 4 ) return  // beijingMinute = 0  for await (const [index, spaceKey] of freeIds.entries()) {    // 分别再4:00  4:03  4:06  4:09 ... 间隔开启动应用    if (beijingMinute === index * 3) {      try {        // 机器可能会自动停,可能都不需要这个操作        // await fetchHandler(`${workerHost}/stop/${spaceKey}`/**, { "body": null, "method": "POST" } */)      } catch (error) {        console.log(error)      }      // 确保没开机的机器开机      await fetchHandler(`${workerHost}/start/${spaceKey}`/**, { "body": null, "method": "POST" } */)    }  }  // 提供给cron 反馈用  return}async function fetchHandler(request, env, ctx) {  const url = new URL(typeof request === 'string' ? request : request.url);  let spaceKey = url.pathname.replace(/(.*/)/,'')    if (url.pathname.includes('/scheduled')) {    await scheduled(request, env, ctx)    return new Response(JSON.stringify('completed', null, 2), {       status: 200,      headers: { 'Content-Type': 'application/json' }    });  }  if (url.pathname.includes('/start/all')) {    for await (const [index, spaceKey] of freeIds.entries()) {      // 确保没开机的机器开机await fetchHandler(`${workerHost}/start/${spaceKey}`/**, { "body": null, "method": "POST" } */)    }    return new Response(JSON.stringify('ok', null, 2), {       status: 200,      headers: { 'Content-Type': 'application/json' }    });  }  if (!(url.pathname.includes('/start') || url.pathname.includes('/stop') || url.pathname.includes('/heart') || url.pathname.includes('/ide') || url.pathname.includes('/mockserver'))) return;  if(request.method !== 3) {    let action = ''    if (url.pathname.includes('/start')) action = 'RunWorkspace'    if (url.pathname.includes('/stop')) action = 'StopWorkspace'    if (url.pathname.includes('/heart') || url.pathname.includes('/ide') || url.pathname.includes('/mockserver')) action = 'CreateWorkspaceToken'    try {      // const { spaceKey } = await request.json();      if (!spaceKey) {        return new Response(JSON.stringify({ Error: "Missing spaceKey" }), {           status: 400,           headers: { 'Content-Type': 'application/json' }         });      }      const response = await runTencentCloudAPI(env, spaceKey, action);      // !!!如果是启动应用, 需要打开网页进行初始化程序      if (action === 'RunWorkspace') {        const response = await runTencentCloudAPI(env, spaceKey, 'CreateWorkspaceToken');        const token = response.Response.Token        const res = await init(spaceKey, token)        console.log(await res.text())      }      if (action === 'CreateWorkspaceToken') {        const token = response.Response.Token        if (url.pathname.includes('/heart')) {                    return fetch("https://ide.cloud.tencent.com/api/workspace/"+ spaceKey +"/heartbeat", {            "headers": {              "authorization": `Bearer ${token}`,              "x-ide-workspace-session-id": `baa4d673-2hg6-4014-861a-${(Date.now()+'').slice(1,13)}`,            },            "body": null,            "method": "GET"          });        }        // 获取ide 访问路径        if (          url.pathname.includes('/ide')        ) {                    return Response.redirect(`https://ide.cloud.tencent.com/tty/${spaceKey}/?report_open_type=list_open&token=`+ token, 301);        }      }      return new Response(JSON.stringify({ Success: response }), {         status: 200,        headers: { 'Content-Type': 'application/json' }      });    } catch (error) {      return new Response(JSON.stringify({ Error: error.message }), {         status: 500,        headers: { 'Content-Type': 'application/json' }       });    }  }  return new Response('Not Found', { status: 404 });}// --- 工具函数 ---async function getHash(message) {  const encoder = new TextEncoder();  const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(message));  return Array.from(new Uint8Array(hashBuffer))    .map(b => b.toString(16).padStart(2, '0'))    .join('');}/** * 核心修正点:支持字符串和 ArrayBuffer 两种类型的密钥 * @param {string | ArrayBuffer} key - 密钥 (可以是原始字符串或二进制数据) * @param {string} message - 要签名的消息 * @returns {ArrayBuffer} - 签名的二进制数据 */async function hmacSha256(key, message) {  const encoder = new TextEncoder();    // 1. 准备密钥数据  let keyData;  if (typeof key === 'string') {    // 如果是字符串,直接编码    keyData = encoder.encode(key);  } else {    // 如果是 ArrayBuffer (来自上一轮 HMAC 输出),直接使用    keyData = key;  }  // 2. 导入密钥 (关键步骤:必须先导入才能用于 sign)  // 注意:这里使用 extractable: false 是标准做法  const cryptoKey = await crypto.subtle.importKey(    'raw', // 密钥数据格式    keyData, // 密钥数据 (Uint8Array)    { name: 'HMAC', hash: { name: 'SHA-256' } }, // 算法    false, // 是否可导出    ['sign'] // 用途  );  // 3. 生成签名  return await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message));}function arrayBufferToHex(buffer) {  return Array.from(new Uint8Array(buffer))    .map(b => b.toString(16).padStart(2, '0'))    .join('');}function getDate(timestamp) {  const date = new Date(timestamp * 1000);  const year = date.getUTCFullYear();  const month = String(date.getUTCMonth() + 1).padStart(2, '0');  const day = String(date.getUTCDate()).padStart(2, '0');  return `${year}-${month}-${day}`;}/** * 调用腾讯云 API */async function runTencentCloudAPI(env, spaceKey, action = 'RunWorkspace') {  if (foreverToken[spaceKey] && action === 'CreateWorkspaceToken') {    return await Promise.resolve({      Response: {        Token: foreverToken[spaceKey]/** 永久tokend */      }    })  }  // 请务必在环境变量中配置 如果你是多个free 空间这边得改下,自行调整  if (!SECRET_ID || !SECRET_KEY) {    throw new Error("Missing Tencent Cloud credentials");  }  const host = "cloudstudio.tencentcloudapi.com";  const service = "cloudstudio";  const region = "ap-shanghai";  // action = "RunWorkspace";// "RunWorkspace"  StopWorkspace  CreateWorkspaceToken  const version = "2023-05-08";  const timestamp = Math.floor(Date.now() / 1000);  const date = getDate(timestamp);  const payload = JSON.stringify({ SpaceKey: spaceKey });  // --- 1. 规范请求串 ---  const canonicalHeaders = `content-type:application/json; charset=utf-8nhost:${host}n`;  const signedHeaders = "content-type;host";  const hashedRequestPayload = await getHash(payload);    const canonicalRequest = [    'POST', '/', '', canonicalHeaders, signedHeaders, hashedRequestPayload  ].join('n');  // --- 2. 待签名字符串 ---  const algorithm = "TC3-HMAC-SHA256";  const credentialScope = `${date}/${service}/tc3_request`;  const hashedCanonicalRequest = await getHash(canonicalRequest);    const stringToSign = [    algorithm, timestamp, credentialScope, hashedCanonicalRequest  ].join('n');  // --- 3. 计算签名 (链式调用) ---  // 注意:这里的 kDate, kService, kSigning 都是 ArrayBuffer  const kDate = await hmacSha256("TC3" + SECRET_KEY, date);   const kService = await hmacSha256(kDate, service);   const kSigning = await hmacSha256(kService, "tc3_request");   // 最后一步:将二进制签名转为 Hex 字符串  const signature = arrayBufferToHex(await hmacSha256(kSigning, stringToSign));  // --- 4. 构造请求 ---  const authorization = `${algorithm} Credential=${SECRET_ID}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;  const url = `https://${host}`;  const headers = {    'Content-Type': 'application/json; charset=utf-8',    'Host': host,    'X-TC-Action': action,    'X-TC-Timestamp': timestamp.toString(),    'X-TC-Version': version,    'X-TC-Region': region,    'Authorization': authorization,  };  const fetchResponse = await fetch(url, {    method: 'POST',    headers,    body: payload,  });  if (!fetchResponse.ok) {    const errText = await fetchResponse.text();    throw new Error(`API Error: ${fetchResponse.status} ${errText}`);  }  return await fetchResponse.json();}```

5、其他说明worker 自调用需要设置下global_fetch_strictly_publicworker 需要启用兼容性标志才能自己调用自己 global_fetch_strictly_public然后我发现同一个实名用户创建的多个账户,每个账户都可以创建免费ide空间实例,我目前跑了5个 free space, 稳定运行几个月了大致是这样子,笔记丑陋凑活看看,等佬友完善补充下

88119645

这位作者很神秘,什么都没有留下。

留下你的想法