feat(oss-browser-link): 实现一键注册+自动定位补丁功能
1. 新增oss-browser自动定位包装层脚本与注册批处理 2. 优化粘贴行为:焦点在输入框时交给浏览器默认粘贴 3. 新增一批调试与构建辅助脚本 4. 更新oss-browser下载页说明与步骤
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// CDP 运行时取证:监听 console + network,手动广播 goToOssAddress,观察列表请求
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
;(async () => {
|
||||
const list = await (await fetch('http://127.0.0.1:9222/json')).json()
|
||||
const page = (list || []).find(t => t.type === 'page' && /index\.html/.test(t.url || ''))
|
||||
if (!page) { console.log('no page'); process.exit(1) }
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl)
|
||||
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej })
|
||||
let mid = 0
|
||||
const send = (method, params) => ws.send(JSON.stringify({ id: ++mid, method, params: params || {} }))
|
||||
ws.onmessage = ev => {
|
||||
const m = JSON.parse(ev.data)
|
||||
if (m.method === 'Runtime.consoleAPICalled') {
|
||||
const args = (m.params.args || []).map(a => a.value || a.description || '').join(' ')
|
||||
if (/ossAddressChange|goToOssAddress|listFiles|AccessDenied|Error/i.test(args)) console.log('[console]', args.slice(0, 300))
|
||||
}
|
||||
if (m.method === 'Network.requestWillBeSent') {
|
||||
const u = m.params.request.url
|
||||
if (/aliyuncs/.test(u)) console.log('[req]', m.params.request.method, u.slice(0, 160))
|
||||
}
|
||||
if (m.method === 'Network.responseReceived') {
|
||||
const u = m.params.response.url
|
||||
if (/aliyuncs/.test(u)) console.log('[rsp]', m.params.response.status, u.slice(0, 160))
|
||||
}
|
||||
}
|
||||
send('Runtime.enable')
|
||||
send('Network.enable')
|
||||
await sleep(2000)
|
||||
console.log('--- 手动广播 goToOssAddress ---')
|
||||
send('Runtime.evaluate', { expression: `angular.element(document.querySelector('[ng-app]')).injector().get('$rootScope').$broadcast('goToOssAddress','oss://android-o1-images/trae-files-temp/'); 'ok'` })
|
||||
await sleep(8000)
|
||||
ws.close()
|
||||
process.exit(0)
|
||||
})()
|
||||
@@ -0,0 +1,27 @@
|
||||
// 仅连接 CDP 读取 __trae_dbg(配合手动启动的带调试端口实例)
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
;(async () => {
|
||||
let page = null
|
||||
for (let i = 0; i < 20 && !page; i++) {
|
||||
await sleep(1000)
|
||||
try {
|
||||
const list = await (await fetch('http://127.0.0.1:9222/json')).json()
|
||||
page = (list || []).find(t => t.type === 'page' && /index\.html/.test(t.url || ''))
|
||||
} catch {}
|
||||
}
|
||||
if (!page) { console.log('no page target'); process.exit(1) }
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl)
|
||||
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej })
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await sleep(2000)
|
||||
const r = await new Promise(resolve => {
|
||||
const id = 777 + i
|
||||
const onMsg = ev => { const m = JSON.parse(ev.data); if (m.id === id) { ws.removeEventListener('message', onMsg); resolve(m.result) } }
|
||||
ws.addEventListener('message', onMsg)
|
||||
ws.send(JSON.stringify({ id, method: 'Runtime.evaluate', params: { expression: 'JSON.stringify({dbg: window.__trae_dbg || null, hasNg: !!window.angular, url: location.href})', returnByValue: true } }))
|
||||
})
|
||||
console.log(`t+${(i + 1) * 2}s`, r.result && r.result.value)
|
||||
}
|
||||
ws.close()
|
||||
process.exit(0)
|
||||
})()
|
||||
@@ -0,0 +1,24 @@
|
||||
// 提取诊断版 main.js 中的 injection 字符串并做语法检查 + 直接通过 CDP 手动执行验证
|
||||
const fs = require('fs')
|
||||
const src = fs.readFileSync('temp\\ossbrowser_inspect\\oss-browser-win32-x64\\resources\\app\\main.js', 'utf8')
|
||||
const start = src.indexOf("const injection =")
|
||||
const end = src.indexOf("app.on('web-contents-created'")
|
||||
const chunk = src.slice(start, end)
|
||||
// 在 Node 里执行这段拼接代码,得到 injection 字符串
|
||||
const fn = new Function('CFG', chunk + '; return injection;')
|
||||
let injection
|
||||
try {
|
||||
injection = fn({ target: 'oss://android-o1-images/trae-files-temp/', region: 'oss-cn-shanghai' })
|
||||
} catch (e) {
|
||||
console.log('[FAIL] 拼接代码执行失败:', e.message)
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('injection length:', injection.length)
|
||||
try {
|
||||
new Function(injection)
|
||||
console.log('[OK] injection 语法正确')
|
||||
} catch (e) {
|
||||
console.log('[FAIL] injection 语法错误:', e.message)
|
||||
console.log(injection.slice(0, 500))
|
||||
}
|
||||
fs.writeFileSync('temp\\injection_extract.js', injection)
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 诊断 DOC_CLOUD_KEEPER 这对 Key 在 android-o1-images(cn-beijing)的权限边界:
|
||||
* 1) ListBuckets(账号级)
|
||||
* 2) ListObjectsV2 prefix=trae-files-temp/(bucket 级)
|
||||
* 3) GetBucketInfo
|
||||
* 只读探测,不做任何写/删操作。
|
||||
* 用法:node dev_test_scripts/debug/debug_oss_list_permission.js
|
||||
*/
|
||||
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-beijing.aliyuncs.com'
|
||||
|
||||
const loadEnv = () => {
|
||||
const out = {}
|
||||
try {
|
||||
const text = fs.readFileSync(path.join(os.homedir(), 'Toolbox_local_creds.env.local'), '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 key'); process.exit(1) }
|
||||
|
||||
// 只打印 env 里所有 OSS 相关 key 的“名字”(不打印值),看有没有其他可用凭据
|
||||
console.log('env 中 OSS 相关 key 名:', Object.keys(ENV).filter(k => /OSS|ALIYUN/i.test(k)).join(', '))
|
||||
|
||||
const sign = (method, date, resource, contentType = '') => {
|
||||
const stringToSign = `${method}\n\n${contentType}\n${date}\n${resource}`
|
||||
return crypto.createHmac('sha1', SK).update(stringToSign).digest('base64')
|
||||
}
|
||||
|
||||
const ossRequest = (method, hostname, urlPath, resource) => new Promise((resolve, reject) => {
|
||||
const date = new Date().toUTCString()
|
||||
const req = https.request({
|
||||
hostname, path: urlPath, method,
|
||||
headers: {
|
||||
Host: hostname,
|
||||
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, body: Buffer.concat(chunks).toString('utf8') }))
|
||||
})
|
||||
req.on('error', reject)
|
||||
req.end()
|
||||
})
|
||||
|
||||
const probe = async (name, hostname, urlPath, resource) => {
|
||||
try {
|
||||
const r = await ossRequest('GET', hostname, urlPath, resource)
|
||||
const codeMatch = r.body.match(/<Code>([^<]+)<\/Code>/)
|
||||
const msgMatch = r.body.match(/<Message>([^<]+)<\/Message>/)
|
||||
console.log(`[${name}] status=${r.status}${codeMatch ? ' code=' + codeMatch[1] : ''}${msgMatch ? ' msg=' + msgMatch[1].slice(0, 120) : ''}${r.status === 200 ? ' body前120=' + r.body.slice(0, 120).replace(/\s+/g, ' ') : ''}`)
|
||||
return r
|
||||
} catch (e) {
|
||||
console.log(`[${name}] ERR ${e.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
await probe('ListBuckets(账号级)', ENDPOINT, '/', '/')
|
||||
await probe('ListObjectsV2(prefix=trae-files-temp/)', `${BUCKET}.${ENDPOINT}`, '/?list-type=2&prefix=trae-files-temp%2F&max-keys=10', `/${BUCKET}/`)
|
||||
await probe('ListObjectsV2(无prefix,根)', `${BUCKET}.${ENDPOINT}`, '/?list-type=2&max-keys=5', `/${BUCKET}/`)
|
||||
await probe('GetBucketInfo', `${BUCKET}.${ENDPOINT}`, '/?bucketInfo', `/${BUCKET}/?bucketInfo`)
|
||||
})().catch(e => { console.error(e); process.exit(1) })
|
||||
@@ -0,0 +1,87 @@
|
||||
// 端到端实测:带包装层启动 oss-browser(CDP 远程调试),验证
|
||||
// 1) $rootScope.bucketMap['android-o1-images'].region === 'oss-cn-shanghai'
|
||||
// 2) 文件列表真实加载出对象(页面文本含已知对象名)
|
||||
// 3) 页面文本不含 AccessDenied
|
||||
// 用法:node dev_test_scripts/debug/debug_ossbrowser_e2e.js
|
||||
const { spawn } = require('child_process')
|
||||
const path = require('path')
|
||||
|
||||
const EXE = path.join(__dirname, '..', '..', 'temp', 'ossbrowser_inspect', 'oss-browser-win32-x64', 'oss-browser.exe')
|
||||
const PORT = 9222
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
const fetchJson = async url => {
|
||||
const res = await fetch(url)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
const cdpEval = (ws, expression) => new Promise((resolve, reject) => {
|
||||
const id = Math.floor(Math.random() * 1e6)
|
||||
const onMsg = ev => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if (msg.id === id) {
|
||||
ws.removeEventListener('message', onMsg)
|
||||
resolve(msg.result)
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
ws.addEventListener('message', onMsg)
|
||||
ws.send(JSON.stringify({ id, method: 'Runtime.evaluate', params: { expression, returnByValue: true } }))
|
||||
setTimeout(() => { ws.removeEventListener('message', onMsg); reject(new Error('eval timeout')) }, 8000)
|
||||
})
|
||||
|
||||
const EXPRESSION = `(function () {
|
||||
try {
|
||||
var inj = window.angular && window.angular.element(document.documentElement).injector();
|
||||
var rs = inj && inj.get('$rootScope');
|
||||
var input = document.querySelector('input[ng-model="address"]');
|
||||
return JSON.stringify({
|
||||
bucketMap: rs ? rs.bucketMap : null,
|
||||
address: input ? input.value : null,
|
||||
dbg: window.__trae_dbg || null,
|
||||
text: (document.body && document.body.innerText || '').slice(0, 6000)
|
||||
});
|
||||
} catch (e) { return JSON.stringify({ err: String(e) }); }
|
||||
})()`
|
||||
|
||||
;(async () => {
|
||||
const child = spawn(EXE, [`--remote-debugging-port=${PORT}`], { detached: true, stdio: 'ignore' })
|
||||
console.log('spawned pid', child.pid)
|
||||
child.unref()
|
||||
|
||||
let targets = null
|
||||
for (let i = 0; i < 40 && !targets; i++) {
|
||||
await sleep(1000)
|
||||
try {
|
||||
const list = await fetchJson(`http://127.0.0.1:${PORT}/json`)
|
||||
const page = (list || []).find(t => t.type === 'page' && /index\.html/.test(t.url || ''))
|
||||
if (page) targets = page
|
||||
} catch {}
|
||||
}
|
||||
if (!targets) { console.log('[FAIL] 未找到 CDP page target'); process.exit(1) }
|
||||
|
||||
const ws = new WebSocket(targets.webSocketDebuggerUrl)
|
||||
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej })
|
||||
|
||||
let ok = false
|
||||
for (let i = 0; i < 20 && !ok; i++) {
|
||||
await sleep(1500)
|
||||
try {
|
||||
const r = await cdpEval(ws, EXPRESSION)
|
||||
const v = JSON.parse(r.result.value || '{}')
|
||||
if (!v.bucketMap) continue
|
||||
const region = v.bucketMap['android-o1-images'] && v.bucketMap['android-o1-images'].region
|
||||
const hasFile = /jianpian\.rar|cover\.png/.test(v.text || '')
|
||||
const denied = /AccessDenied/.test(v.text || '')
|
||||
console.log(`probe#${i} address=${v.address} region=${region} hasFile=${hasFile} denied=${denied} dbg=${JSON.stringify(v.dbg)}`)
|
||||
if (region === 'oss-cn-shanghai' && hasFile && !denied && v.address === 'oss://android-o1-images/trae-files-temp/') ok = true
|
||||
} catch (e) { /* 页面可能还在加载 */ }
|
||||
}
|
||||
|
||||
ws.close()
|
||||
try { process.kill(child.pid, 'SIGTERM') } catch {}
|
||||
console.log(ok ? '[PASS] 地址栏定位 + region 正确 + 列表加载成功,无 AccessDenied' : '[FAIL] 未达到预期状态')
|
||||
process.exit(ok ? 0 : 1)
|
||||
})().catch(e => { console.error('[FAIL]', e.message); process.exit(1) })
|
||||
@@ -0,0 +1,13 @@
|
||||
// 探查 login 流程 AuthInfo.save 的字段结构
|
||||
const fs = require('fs')
|
||||
const s = fs.readFileSync('temp\\ossbrowser_inspect\\oss-browser-win32-x64\\resources\\app.asar').toString('latin1')
|
||||
let i = -1, n = 0
|
||||
while ((i = s.indexOf('AuthInfo.save(', i + 1)) !== -1 && n < 6) {
|
||||
console.log('--- hit ' + n)
|
||||
console.log(s.slice(Math.max(0, i - 900), i + 200))
|
||||
n++
|
||||
}
|
||||
// 再看 ossSvs2 如何取凭据构造 client
|
||||
let j = s.indexOf("factory('ossSvs2'")
|
||||
console.log('======== ossSvs2 head')
|
||||
console.log(s.slice(j, j + 1500))
|
||||
@@ -0,0 +1,29 @@
|
||||
// 校验 register_ossbrowser_launch.bat:关键行 + base64 回环解码与源码一致
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { execFileSync } = require('child_process')
|
||||
|
||||
const ROOT = path.join(__dirname, '..', '..')
|
||||
const BAT = path.join(ROOT, 'public', 'tools', 'oss_browser_link', 'assets', 'register_ossbrowser_launch.bat')
|
||||
const SRC = path.join(ROOT, 'public', 'tools', 'oss_browser_link', 'assets', 'ossbrowser_wrapper_main.js')
|
||||
|
||||
const s = fs.readFileSync(BAT, 'utf8')
|
||||
if (s.includes('\r\n') === false) { console.log('[FAIL] bat 不是 CRLF'); process.exit(1) }
|
||||
const lines = s.split(/\r\n/)
|
||||
console.log('total lines:', lines.length)
|
||||
console.log('reg cmd line ok:', !!lines.find(l => l.includes('shell\\open\\command') && l.includes('\\"%EXE%\\"')))
|
||||
console.log('mkdir line ok:', !!lines.find(l => l.includes('mkdir "%~dp0resources\\app"')))
|
||||
console.log('txt region line ok:', !!lines.find(l => l.includes('echo oss-cn-shanghai>>')))
|
||||
|
||||
const marker = '>> "%~dp0_wrapper.b64"'
|
||||
const b64 = lines.filter(l => l.startsWith('echo ') && l.endsWith(marker)).map(l => l.slice(5, l.length - marker.length)).join('')
|
||||
console.log('b64 len:', b64.length)
|
||||
const b64File = path.join(ROOT, 'temp', 'w2.b64')
|
||||
const outFile = path.join(ROOT, 'temp', 'w2.js')
|
||||
fs.writeFileSync(b64File, b64)
|
||||
try { fs.unlinkSync(outFile) } catch {}
|
||||
execFileSync('certutil', ['-decode', b64File, outFile], { stdio: 'pipe' })
|
||||
const a = fs.readFileSync(SRC)
|
||||
const b = fs.readFileSync(outFile)
|
||||
console.log('roundtrip identical:', a.equals(b))
|
||||
if (!a.equals(b)) process.exit(1)
|
||||
@@ -0,0 +1,81 @@
|
||||
// 构建 register_ossbrowser_launch.bat:模板 + 包装层 base64 块
|
||||
// 源:public/tools/oss_browser_link/assets/ossbrowser_wrapper_main.js
|
||||
// 产物:public/tools/oss_browser_link/assets/register_ossbrowser_launch.bat(CRLF)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const ROOT = path.join(__dirname, '..', '..')
|
||||
const SRC = path.join(ROOT, 'public', 'tools', 'oss_browser_link', 'assets', 'ossbrowser_wrapper_main.js')
|
||||
const OUT = path.join(ROOT, 'public', 'tools', 'oss_browser_link', 'assets', 'register_ossbrowser_launch.bat')
|
||||
|
||||
const b = fs.readFileSync(SRC).toString('base64')
|
||||
const echoLines = []
|
||||
for (let i = 0; i < b.length; i += 96) {
|
||||
echoLines.push(`echo ${b.slice(i, i + 96)}>> "%~dp0_wrapper.b64"`)
|
||||
}
|
||||
|
||||
const head = `@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal
|
||||
rem ==========================================
|
||||
rem oss-browser 一键注册脚本(协议拉起 + 启动自动定位补丁)
|
||||
rem 用法:把本文件放到与 oss-browser.exe 同一目录,双击运行
|
||||
rem 作用:
|
||||
rem 1. 在当前用户注册表(HKCU)注册 ossbrowser:// 协议,无需管理员权限
|
||||
rem 2. 安装 resources\\app 包装层:启动时自动把默认 OSS 路径填进地址栏并定位(含正确 region)
|
||||
rem 3. 生成 trae_default_oss_path.txt(第1行 oss 路径,第2行可选 region)
|
||||
rem 卸载补丁:删除 resources\\app 目录与 trae_default_oss_path.txt 即恢复原版
|
||||
rem ==========================================
|
||||
set "EXE=%~dp0oss-browser.exe"
|
||||
if not exist "%EXE%" (
|
||||
echo.
|
||||
echo [错误] 未在当前目录找到 oss-browser.exe
|
||||
echo 请把本脚本放入解压后的 oss-browser-win32-x64 文件夹内(与 oss-browser.exe 同级)再运行。
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
reg add "HKCU\\Software\\Classes\\ossbrowser" /ve /d "URL:ossbrowser Protocol" /f >nul
|
||||
reg add "HKCU\\Software\\Classes\\ossbrowser" /v "URL Protocol" /t REG_SZ /d "" /f >nul
|
||||
reg add "HKCU\\Software\\Classes\\ossbrowser\\shell\\open\\command" /ve /d "\\"%EXE%\\" \\"%%1\\"" /f >nul
|
||||
if errorlevel 1 (
|
||||
echo [失败] 注册表写入出错。
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo [完成] 已注册 ossbrowser:// 协议。
|
||||
|
||||
rem ---- 安装启动自动定位包装层 ----
|
||||
if not exist "%~dp0resources\\app" mkdir "%~dp0resources\\app"
|
||||
echo {"name":"oss-browser","version":"1.18.0","main":"main.js"}> "%~dp0resources\\app\\package.json"
|
||||
if exist "%~dp0_wrapper.b64" del "%~dp0_wrapper.b64" >nul
|
||||
`
|
||||
|
||||
const tail = `if exist "%~dp0resources\\app\\main.js" del "%~dp0resources\\app\\main.js" >nul
|
||||
certutil -decode "%~dp0_wrapper.b64" "%~dp0resources\\app\\main.js" >nul
|
||||
if errorlevel 1 (
|
||||
echo [警告] 包装层解码失败,跳过自动定位补丁(协议拉起仍可用)。
|
||||
) else (
|
||||
echo [完成] 已安装启动自动定位包装层(resources\\app)。
|
||||
)
|
||||
del "%~dp0_wrapper.b64" >nul
|
||||
|
||||
rem ---- 默认路径配置(已存在则不覆盖,尊重手动修改) ----
|
||||
if not exist "%~dp0trae_default_oss_path.txt" (
|
||||
echo oss://android-o1-images/trae-files-temp/> "%~dp0trae_default_oss_path.txt"
|
||||
echo oss-cn-shanghai>> "%~dp0trae_default_oss_path.txt"
|
||||
echo [完成] 已生成默认路径配置 trae_default_oss_path.txt
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 现在可回到网页点击「打开 oss-browser」。
|
||||
echo 首次拉起时浏览器会弹确认框,勾选“始终允许”后即为无感打开。
|
||||
echo 启动后地址栏会自动定位到 trae_default_oss_path.txt 中的路径。
|
||||
echo 该文件第1行为 oss 路径,第2行可选 region(默认 oss-cn-shanghai)。
|
||||
echo.
|
||||
pause
|
||||
`
|
||||
|
||||
const bat = (head + echoLines.join('\r\n') + '\r\n' + tail).replace(/\r\n/g, '\n').replace(/\n/g, '\r\n')
|
||||
fs.writeFileSync(OUT, bat)
|
||||
console.log('bat written, echo lines =', echoLines.length)
|
||||
Reference in New Issue
Block a user