feat: 新增多项功能并添加新工具

- 私人剪贴板新增限时直通URL生成功能
- doc_cloud_keeper新增标题复制按钮
- 新增贪蛇出动游戏工具
- 新增OSS中转直链工具
- 添加批量抽帧测试脚本
This commit is contained in:
yangxiangyuan
2026-08-11 10:22:31 +08:00
parent 91148fe064
commit 6dc79a2c12
28 changed files with 3169 additions and 6 deletions
@@ -0,0 +1,168 @@
/**
* OSS 中转脚本:把 oss://android-o1-images/trae-files-temp/oss-browser-win32-x64.zip
* 下载到本地 public/download_files/,成功后立即删除 OSS 对象。
*
* 密钥来源:~/Toolbox_local_creds.env.local(DOC_CLOUD_KEEPER_OSS_ACCESS_KEY_ID / SECRET)
* 用法:node dev_test_scripts/runners/run_oss_browser_zip_transit.js [--keep-oss]
* --keep-oss 仅下载不删除 OSS 对象(调试用,默认下载成功后删除)
*/
const fs = require('fs')
const os = require('os')
const path = require('path')
const crypto = require('crypto')
const https = require('https')
const BUCKET = 'android-o1-images'
const ENDPOINT = 'oss-cn-shanghai.aliyuncs.com'
const OSS_PREFIX = 'trae-files-temp/'
const TARGET_NAME = 'oss-browser-win32-x64.zip'
const LOCAL_DIR = path.join(process.cwd(), 'public', 'download_files')
const LOCAL_FILE = path.join(LOCAL_DIR, TARGET_NAME)
const loadEnv = () => {
const out = {}
try {
const p = path.join(os.homedir(), 'Toolbox_local_creds.env.local')
const text = fs.readFileSync(p, 'utf8')
for (const line of text.split(/\r?\n/)) {
const t = line.trim()
if (!t || t.startsWith('#')) continue
const eq = t.indexOf('=')
if (eq <= 0) continue
out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim()
}
} catch {}
return out
}
const ENV = loadEnv()
const AK = ENV.DOC_CLOUD_KEEPER_OSS_ACCESS_KEY_ID || ''
const SK = ENV.DOC_CLOUD_KEEPER_OSS_ACCESS_KEY_SECRET || ''
if (!AK || !SK) {
console.error('[FAIL] 缺少 DOC_CLOUD_KEEPER_OSS_ACCESS_KEY_ID / SECRET(~/Toolbox_local_creds.env.local)')
process.exit(1)
}
const sign = (method, date, resource, contentType = '', canonicalHeaders = '') => {
const stringToSign = `${method}\n\n${contentType}\n${date}\n${canonicalHeaders ? canonicalHeaders + '\n' : ''}${resource}`
return crypto.createHmac('sha1', SK).update(stringToSign).digest('base64')
}
const ossRequest = (method, urlPath, resource) => new Promise((resolve, reject) => {
const date = new Date().toUTCString()
const req = https.request({
hostname: `${BUCKET}.${ENDPOINT}`,
path: urlPath,
method,
headers: {
Host: `${BUCKET}.${ENDPOINT}`,
Date: date,
Authorization: `OSS ${AK}:${sign(method, date, resource)}`
}
}, (res) => {
const chunks = []
res.on('data', c => chunks.push(c))
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }))
})
req.on('error', reject)
req.setTimeout(120000, () => req.destroy(new Error('timeout')))
req.end()
})
const listPrefix = async (prefix) => {
const resp = await ossRequest('GET', `/?prefix=${encodeURIComponent(prefix)}&max-keys=100`, `/${BUCKET}/`)
if (resp.status !== 200) throw new Error(`OSS list 失败: ${resp.status} ${resp.body.toString('utf8').slice(0, 200)}`)
const xml = resp.body.toString('utf8')
const keys = []
let idx = 0
while (true) {
const s = xml.indexOf('<Key>', idx)
if (s < 0) break
const e = xml.indexOf('</Key>', s)
if (e < 0) break
keys.push(xml.slice(s + 5, e))
idx = e
}
return keys
}
const downloadToFile = (key, localPath) => new Promise((resolve, reject) => {
const date = new Date().toUTCString()
const resource = `/${BUCKET}/${key}`
const req = https.request({
hostname: `${BUCKET}.${ENDPOINT}`,
path: `/${key.split('/').map(encodeURIComponent).join('/')}`,
method: 'GET',
headers: {
Host: `${BUCKET}.${ENDPOINT}`,
Date: date,
Authorization: `OSS ${AK}:${sign('GET', date, resource)}`
}
}, (res) => {
if (res.statusCode !== 200) {
const chunks = []
res.on('data', c => chunks.push(c))
res.on('end', () => reject(new Error(`OSS 下载失败: ${res.statusCode} ${Buffer.concat(chunks).toString('utf8').slice(0, 200)}`)))
return
}
const ws = fs.createWriteStream(localPath)
let received = 0
res.on('data', c => { received += c.length })
res.pipe(ws)
ws.on('finish', () => resolve(received))
ws.on('error', reject)
})
req.on('error', reject)
req.setTimeout(600000, () => req.destroy(new Error('下载超时')))
req.end()
})
const deleteObject = async (key) => {
const resource = `/${BUCKET}/${key}`
const urlPath = `/${key.split('/').map(encodeURIComponent).join('/')}`
const resp = await ossRequest('DELETE', urlPath, resource)
if (resp.status !== 204 && resp.status !== 200) {
throw new Error(`OSS 删除失败: ${resp.status} ${resp.body.toString('utf8').slice(0, 200)}`)
}
return true
}
const bjNow = () => new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai' })
;(async () => {
console.log(`[${bjNow()}] 列举 ${BUCKET}/${OSS_PREFIX} ...`)
const keys = await listPrefix(OSS_PREFIX)
console.log(` 前缀下共 ${keys.length} 个对象`)
const zipKey = keys.find(k => k.endsWith('/' + TARGET_NAME) || k === TARGET_NAME)
if (!zipKey) {
console.error(`[FAIL] 未找到 ${OSS_PREFIX}${TARGET_NAME},现有对象:`)
keys.forEach(k => console.error(' - ' + k))
process.exit(1)
}
console.log(` 找到对象: ${zipKey}`)
if (!fs.existsSync(LOCAL_DIR)) fs.mkdirSync(LOCAL_DIR, { recursive: true })
const tmpFile = LOCAL_FILE + '.part'
console.log(`[${bjNow()}] 开始下载到 ${LOCAL_FILE} ...`)
const received = await downloadToFile(zipKey, tmpFile)
const size = fs.statSync(tmpFile).size
if (size < 1024 * 1024) {
fs.unlinkSync(tmpFile)
throw new Error(`下载文件过小(${size} 字节),疑似异常,已丢弃临时文件`)
}
if (fs.existsSync(LOCAL_FILE)) fs.unlinkSync(LOCAL_FILE)
fs.renameSync(tmpFile, LOCAL_FILE)
console.log(` 下载完成: ${(size / 1024 / 1024).toFixed(2)} MB`)
if (process.argv.includes('--keep-oss')) {
console.log('[SKIP] --keep-oss:保留 OSS 对象不删除')
} else {
console.log(`[${bjNow()}] 删除 OSS 对象 ${zipKey} ...`)
await deleteObject(zipKey)
console.log(' OSS 对象已删除(中转完成)')
}
console.log(`[${bjNow()}] 中转成功:本地 ${LOCAL_FILE}`)
})().catch(e => {
console.error('[FAIL]', e.message || e)
process.exit(1)
})