feat(oss-browser-link): 实现一键注册+自动定位补丁功能

1. 新增oss-browser自动定位包装层脚本与注册批处理
2. 优化粘贴行为:焦点在输入框时交给浏览器默认粘贴
3. 新增一批调试与构建辅助脚本
4. 更新oss-browser下载页说明与步骤
This commit is contained in:
yangxiangyuan
2026-08-12 13:29:11 +08:00
parent b21ef2ea3e
commit 4016636153
12 changed files with 583 additions and 10 deletions
@@ -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)